Files
apuc c6048aeea0 feat: AI Press Secretary - content generation and publishing platform
Features:
- AI-powered post generation from brand profiles
- Image generation with templates
- Content planning
- Telegram publishing
- Social profile analyzer
- Community organization support
- Markdown to HTML conversion for Telegram
- Inline AI chat for post editing

Deploy:
- Add deployment scripts for Linux servers
- Add PowerShell deploy script for Windows
- Add Nginx and SSL setup scripts
- Add backup and restore scripts
2026-08-27 15:24:15 +03:00

450 lines
21 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}Пост — Личный кабинет{% endblock %}
{% block content %}
<h1>Пост</h1>
<div class="card">
<div class="post-meta">
{{ platform_label(post.platform) }} · {{ post.created_at }}
{% if post.style %} · Стиль: {{ post.style }}{% endif %}
</div>
{% if post.published %}
<div class="post-meta" style="color: var(--success-text);">
✅ Опубликован в «{{ post.published_to }}» · {{ post.published_at }}
</div>
{% endif %}
{% if post.wishes %}
<div class="post-notes"><strong>Пожелания:</strong> {{ post.wishes }}</div>
{% endif %}
</div>
<!-- === ТЕКУЩИЙ ТЕКСТ ПОСТА === -->
<div class="card" id="postCard">
<h2>📄 Текст поста</h2>
<div class="post-content" id="postContent">{{ post.content }}</div>
<div class="post-editor-actions" style="margin-top: 12px;">
<button type="button" class="copy-btn" onclick="copyPostText()">📋 Копировать</button>
<a class="copy-btn" href="/posts/{{ post.id }}/publish">📤 Опубликовать</a>
<a class="button-link" href="/posts/{{ post.id }}/edit">✏️ Редактировать вручную</a>
</div>
</div>
<!-- === ЧАТ С НЕЙРОСЕТЬЮ === -->
<div class="card">
<h2>🤖 Редактировать с помощью ИИ</h2>
<p class="form-hint">Напишите, что нужно изменить — нейросеть перепишет пост. Например: «сократи вдвое», «сделай официальнее», «добавь призыв к действию».</p>
<!-- История чата -->
<div class="ai-chat" id="aiChat">
<div class="ai-chat-msg ai-chat-msg--system">
<div class="ai-chat-bubble ai-chat-bubble--system">Пост сгенерирован. Напишите инструкцию, чтобы изменить его.</div>
</div>
</div>
<!-- Лоадер -->
<div id="aiChatLoader" class="task-loader" style="display:none; padding: 15px;">
<div class="spinner"></div>
<div class="task-loader-text">Нейросеть переписывает пост...</div>
</div>
<div id="aiChatError" class="task-error"></div>
<!-- Поле ввода -->
<div class="ai-chat-input">
<textarea id="aiChatInput" class="ai-chat-textarea" rows="2"
placeholder="Например: сократи вдвое, сделай веселее, добавь эмодзи..."
onkeydown="if(event.key==='Enter' && !event.shiftKey){event.preventDefault(); sendAiInstruction();}"></textarea>
<button type="button" class="copy-btn" onclick="sendAiInstruction()" id="aiChatSendBtn">→ Отправить</button>
</div>
</div>
<!-- === СКРИПТ ЧАТА === -->
<script>
var _currentPostContent = {{ post.content | tojson }};
function copyPostText() {
navigator.clipboard.writeText(document.getElementById('postContent').textContent);
}
function sendAiInstruction() {
var input = document.getElementById('aiChatInput');
var instruction = input.value.trim();
if (!instruction) return;
var errEl = document.getElementById('aiChatError');
var loader = document.getElementById('aiChatLoader');
var sendBtn = document.getElementById('aiChatSendBtn');
errEl.style.display = 'none';
loader.style.display = 'flex';
sendBtn.disabled = true;
input.disabled = true;
// Добавляем сообщение пользователя в чат
addChatMsg('user', instruction);
input.value = '';
var fd = new FormData();
fd.append('task_type', 'modify_post');
fd.append('post_id', '{{ post.id }}');
fd.append('instruction', instruction);
fetch('/api/tasks', { method: 'POST', body: fd })
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.error) { showAiError(errEl, data.error, loader, sendBtn, input); return; }
pollModifyTask(data.task_id, errEl, loader, sendBtn, input, instruction);
})
.catch(function(e) { showAiError(errEl, 'Ошибка сети: ' + e, loader, sendBtn, input); });
}
function pollModifyTask(taskId, errEl, loader, sendBtn, input, instruction) {
fetch('/api/tasks/' + taskId)
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.status === 'running' || data.status === 'pending') {
setTimeout(function() { pollModifyTask(taskId, errEl, loader, sendBtn, input, instruction); }, 1500);
return;
}
loader.style.display = 'none';
sendBtn.disabled = false;
input.disabled = false;
input.focus();
if (data.status === 'error') {
showAiError(errEl, data.error || 'Ошибка модификации', null, null, null);
return;
}
if (data.status === 'done' && data.result) {
var newContent = data.result.content;
_currentPostContent = newContent;
// Обновляем текст поста на странице
document.getElementById('postContent').textContent = newContent;
// Добавляем ответ ИИ в чат
addChatMsg('ai', newContent, instruction);
// Убираем ошибку
errEl.style.display = 'none';
}
})
.catch(function(e) { showAiError(errEl, 'Ошибка сети: ' + e, null, null, null); });
}
function addChatMsg(role, text, instruction) {
var chat = document.getElementById('aiChat');
var now = new Date();
var dateStr = now.toLocaleTimeString('ru-RU', {hour:'2-digit', minute:'2-digit'});
var html = '';
if (role === 'user') {
html = '<div class="ai-chat-msg ai-chat-msg--user">' +
'<div class="ai-chat-bubble ai-chat-bubble--user">' + escH(text) + '</div>' +
'<div class="ai-chat-time">' + dateStr + '</div></div>';
} else {
var preview = text.substring(0, 200) + (text.length > 200 ? '…' : '');
var fullId = 'aifull-' + Date.now();
html = '<div class="ai-chat-msg ai-chat-msg--ai">' +
'<div class="ai-chat-meta">🤖 ИИ изменил пост по запросу: «' + escH(instruction || '') + '»</div>' +
'<div class="ai-chat-bubble ai-chat-bubble--ai">' +
'<div class="ai-chat-preview" id="aiprev-' + fullId + '">' + escH(preview) + '</div>' +
'<div class="ai-chat-full" id="' + fullId + '" style="display:none"><pre>' + escH(text) + '</pre></div>' +
'<button type="button" class="button-link" onclick="toggleAiFull(\'' + fullId + '\')" style="margin-top:6px;">Показать весь текст</button>' +
'</div>' +
'<div class="ai-chat-time">' + dateStr + '</div></div>';
}
chat.insertAdjacentHTML('beforeend', html);
chat.scrollTop = chat.scrollHeight;
}
function toggleAiFull(id) {
var prev = document.getElementById('aiprev-' + id);
var full = document.getElementById(id);
if (!prev || !full) return;
if (full.style.display === 'none') {
prev.style.display = 'none';
full.style.display = '';
} else {
full.style.display = 'none';
prev.style.display = '';
}
}
function showAiError(el, msg, loader, sendBtn, input) {
if (loader) loader.style.display = 'none';
if (sendBtn) { sendBtn.disabled = false; }
if (input) { input.disabled = false; input.focus(); }
el.textContent = msg;
el.style.display = 'block';
}
function escH(s) { var d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML; }
</script>
<!-- === ГАЛЕРЕЯ ИЗОБРАЖЕНИЙ === -->
<div class="card">
<h2>🖼 Изображения</h2>
{% if images %}
<div class="media-grid">
{% for img in images %}
<div class="media-card">
<a href="/media/{{ img.filename }}" class="media-link" target="_blank">
<img src="/media/{{ img.filename }}" class="media-thumb" alt="">
</a>
<div class="media-body">
<span class="form-hint">{{ img.source }} · {{ img.created_at }}</span>
<form action="/posts/{{ post.id }}/images/{{ img.id }}/delete" method="post" class="inline-form"
onsubmit="return confirm('Удалить изображение?')">
<button type="submit" class="button-link button-link-danger">🗑 Удалить</button>
</form>
</div>
</div>
{% endfor %}
</div>
{% else %}
<p class="form-hint">Изображений пока нет.</p>
{% endif %}
<!-- Загрузка файлов -->
<h3>📎 Загрузить изображения</h3>
<form method="post" action="/posts/{{ post.id }}/images/upload" enctype="multipart/form-data" class="form">
<label>Файлы (можно несколько)
<input type="file" name="files" accept="image/*" multiple required>
</label>
<button type="submit">📎 Загрузить</button>
</form>
<!-- Генерация изображения -->
<h3>🎨 Сгенерировать изображение для поста</h3>
<div id="postImgError" class="task-error"></div>
<!-- Лоадер анализа -->
<div id="postImgAnalyzeLoader" class="task-loader" style="padding: 20px;">
<div class="spinner"></div>
<div class="task-loader-text">Анализирую пост...</div>
<div class="task-loader-sub">ИИ подбирает лучшие варианты</div>
</div>
<!-- Лоадер генерации -->
<div id="postImgGenLoader" class="task-loader" style="padding: 20px;">
<div class="spinner"></div>
<div class="task-loader-text">Генерирую изображение...</div>
<div class="task-loader-sub">Это может занять 15–60 секунд</div>
</div>
<div id="postImgResult" class="task-result"></div>
<!-- Шаг 1: Выбор концепции -->
<div id="postImgConceptsSection">
<p class="form-hint" style="margin-bottom: 10px;">ИИ проанализирует пост и предложит варианты:</p>
<button type="button" class="copy-btn" id="postImgAnalyzeBtn" onclick="startAnalyzeConcepts()">🤖 ИИ подберёт вариант</button>
<div style="margin-top: 12px;">
<p class="form-hint" style="margin-bottom: 8px;">Или выбери шаблон вручную:</p>
<div class="template-grid" id="postImgGrid">
{% for t in templates %}
<button type="button" class="template-card" data-key="{{ t.key }}"
data-prompt="{{ t.prompt }}" data-style="{{ t.style }}" data-size="{{ t.size }}">
<span class="template-icon">{{ t.icon }}</span>
<span class="template-name">{{ t.name }}</span>
<span class="template-hint">{{ t.hint }}</span>
</button>
{% endfor %}
</div>
</div>
</div>
<!-- Шаг 2: Концепции от ИИ (скрыт до анализа) -->
<div id="postImgConceptsList" style="display:none;">
<h4 style="margin-bottom: 10px;">Выбери вариант:</h4>
<div id="postImgConcepts" class="concept-grid"></div>
<button type="button" class="button-link" style="margin-top: 10px;" onclick="resetConcepts()">← Назад к шаблонам</button>
</div>
<!-- Скрытая форма для отправки -->
<form method="post" action="/posts/{{ post.id }}/images/generate" class="form" id="postImgForm" style="display:none;">
<input type="hidden" name="task_type" id="postImgTaskType" value="generate_post_image">
<input type="hidden" name="post_id" value="{{ post.id }}">
<input type="hidden" name="template_key" id="postImgTemplateKey" value="">
<input type="hidden" name="concept_prompt" id="postImgConceptPrompt" value="">
<input type="hidden" name="concept_style" id="postImgConceptStyle" value="">
<input type="hidden" name="concept_size" id="postImgConceptSize" value="">
<button type="submit" id="postImgSubmitBtn">🎨 Сгенерировать</button>
</form>
<script>
(function() {
var form = document.getElementById('postImgForm');
var grid = document.getElementById('postImgGrid');
if (!form || !grid) return;
// Клик по шаблону — сразу генерация
grid.addEventListener('click', function(e) {
var card = e.target.closest('.template-card');
if (!card) return;
grid.querySelectorAll('.template-card').forEach(function(c) { c.classList.remove('selected'); });
card.classList.add('selected');
document.getElementById('postImgTaskType').value = 'generate_post_image';
document.getElementById('postImgTemplateKey').value = card.dataset.key || '';
document.getElementById('postImgConceptPrompt').value = '';
document.getElementById('postImgConceptStyle').value = '';
document.getElementById('postImgConceptSize').value = '';
runPostImgGeneration('postImgConceptsSection');
});
})();
function startAnalyzeConcepts() {
var errEl = document.getElementById('postImgError');
var loader = document.getElementById('postImgAnalyzeLoader');
var section = document.getElementById('postImgConceptsSection');
var conceptsList = document.getElementById('postImgConceptsList');
errEl.style.display = 'none';
section.style.display = 'none';
conceptsList.style.display = 'none';
loader.style.display = 'flex';
var fd = new FormData();
fd.append('task_type', 'analyze_post_image');
fd.append('post_id', '{{ post.id }}');
fetch('/api/tasks', { method: 'POST', body: fd })
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.error) { showError(errEl, data.error, section, loader); return; }
pollAnalyze(data.task_id, errEl, loader, section, conceptsList);
})
.catch(function(err) { showError(errEl, 'Ошибка сети: ' + err.message, section, loader); });
}
function pollAnalyze(taskId, errEl, loader, section, conceptsList) {
fetch('/api/tasks/' + taskId)
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.status === 'running' || data.status === 'pending') {
setTimeout(function() { pollAnalyze(taskId, errEl, loader, section, conceptsList); }, 1500);
return;
}
loader.style.display = 'none';
if (data.status === 'error') { showError(errEl, data.error, section, null); return; }
if (data.status === 'done' && data.result && data.result.concepts) {
showConcepts(data.result.concepts, conceptsList, section);
}
})
.catch(function(err) { showError(errEl, 'Ошибка сети: ' + err.message, section, null); });
}
function showConcepts(concepts, container, hiddenSection) {
var html = '';
concepts.forEach(function(c, i) {
var styleLabels = {photo:'Фото',illustration:'Иллюстрация',minimal:'Минимализм','3d':'3D',flat:'Плоская графика'};
var sizeLabels = {square:'Квадрат',wide:'Широкий',tall:'Вертикальный'};
html += '<button type="button" class="concept-card" onclick="selectConcept(' + i + ')">' +
'<div class="concept-title">' + escH(c.title) + '</div>' +
'<div class="concept-desc">' + escH(c.description) + '</div>' +
'<div class="concept-meta">' +
'<span class="concept-tag">' + (styleLabels[c.style] || c.style) + '</span> ' +
'<span class="concept-tag">' + (sizeLabels[c.size] || c.size) + '</span>' +
'</div></button>';
});
document.getElementById('postImgConcepts').innerHTML = html;
document.getElementById('postImgConceptsList').style.display = 'block';
// Сохраняем концепции для выбора
window._postImgConcepts = concepts;
}
function selectConcept(idx) {
var c = window._postImgConcepts[idx];
if (!c) return;
document.getElementById('postImgTaskType').value = 'generate_post_image_with_concept';
document.getElementById('postImgTemplateKey').value = '';
document.getElementById('postImgConceptPrompt').value = c.description;
document.getElementById('postImgConceptStyle').value = c.style || 'none';
document.getElementById('postImgConceptSize').value = c.size || 'wide';
runPostImgGeneration('postImgConceptsList');
}
function runPostImgGeneration(hideSectionId) {
var form = document.getElementById('postImgForm');
var hideSection = document.getElementById(hideSectionId);
var genLoader = document.getElementById('postImgGenLoader');
if (hideSection) hideSection.style.display = 'none';
genLoader.style.display = 'flex';
var fd = new FormData(form);
fetch('/api/tasks', { method: 'POST', body: fd })
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.error) { showError(document.getElementById('postImgError'), data.error, hideSection, genLoader); return; }
pollGenerate(data.task_id);
})
.catch(function(err) { showError(document.getElementById('postImgError'), 'Ошибка сети: ' + err.message, hideSection, genLoader); });
}
function pollGenerate(taskId) {
var genLoader = document.getElementById('postImgGenLoader');
var errEl = document.getElementById('postImgError');
fetch('/api/tasks/' + taskId)
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.status === 'running' || data.status === 'pending') {
setTimeout(function() { pollGenerate(taskId); }, 1500);
return;
}
genLoader.style.display = 'none';
if (data.status === 'error') {
showError(errEl, data.error, document.getElementById('postImgConceptsSection'), null);
return;
}
if (data.status === 'done' && data.result) {
var html = '<div class="media-grid"><div class="media-card">' +
'<a href="/media/' + data.result.filename + '" class="media-link" target="_blank">' +
'<img src="/media/' + data.result.filename + '" class="media-thumb" alt=""></a>' +
'<div class="media-body"><span class="form-hint">generated · только что</span></div></div></div>' +
'<p style="margin-top:10px;"><button type="button" class="copy-btn" onclick="resetConcepts()">← Ещё вариант</button> ' +
'<a href="/images" class="copy-btn">В библиотеку →</a></p>';
document.getElementById('postImgResult').innerHTML = html;
document.getElementById('postImgResult').style.display = 'block';
}
})
.catch(function(err) { showError(errEl, 'Ошибка сети: ' + err.message, document.getElementById('postImgConceptsSection'), null); });
}
function resetConcepts() {
document.getElementById('postImgResult').style.display = 'none';
document.getElementById('postImgConceptsList').style.display = 'none';
document.getElementById('postImgConceptsSection').style.display = '';
document.getElementById('postImgError').style.display = 'none';
document.getElementById('postImgGenLoader').style.display = 'none';
document.getElementById('postImgAnalyzeLoader').style.display = 'none';
}
function showError(el, msg, showSection, hideLoader) {
if (hideLoader) hideLoader.style.display = 'none';
if (showSection) showSection.style.display = '';
el.textContent = msg;
el.style.display = 'block';
}
function escH(s) { var d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML; }
</script>
</div>
<div class="post-actions">
<a class="copy-btn" href="/posts/{{ post.id }}/publish">📤 Опубликовать</a>
<a class="button-link" href="/posts/{{ post.id }}/edit">✏️ Полное редактирование</a>
<form method="post" action="/posts/{{ post.id }}/delete" class="inline-form"
onsubmit="return confirm('Удалить этот пост? Действие необратимо.');">
<button type="submit" class="button-link button-link-danger">🗑 Удалить</button>
</form>
</div>
<p><a href="/history">← Назад к постам</a></p>
{% endblock %}