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
This commit is contained in:
apuc committed 2026-08-27 15:24:15 +03:00
1 parent 81625b2e6f
commit c6048aeea0
37 files changed
+7037 -225

No files matched your search

+174 -27
View File
@@ -4,18 +4,17 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Личный кабинет{% endblock %}</title>
<link rel="stylesheet" href="/static/style.css?v=2">
<link rel="stylesheet" href="/static/style.css?v=3">
<script>
(function () {
function isDark() {
return document.documentElement.getAttribute('data-theme') === 'dark';
}
function setIcon(btn) {
if (btn) btn.textContent = isDark() ? '☀️' : '🌙';
if (btn) btn.textContent = isDark() ? '\u2600\uFE0F' : '\uD83C\uDF19';
}
try {
var saved = localStorage.getItem('theme');
// Тёмная тема по умолчанию: dark включается всегда, кроме явного выбора 'light'
if (saved !== 'light') {
document.documentElement.setAttribute('data-theme', 'dark');
}
@@ -39,30 +38,97 @@
</script>
</head>
<body>
<header class="topbar">
<a class="brand" href="{{ '/dashboard' if user else '/login' }}">🗂 Личный кабинет</a>
<nav>
<button type="button" class="theme-toggle" data-theme-toggle aria-label="Переключить тему">🌙</button>
{% if user %}
<a href="/dashboard">Главная</a>
<a href="/generate">Пост</a>
<a href="/plan">Контент-план</a>
<a href="/history">История</a>
<a href="/images">Картинки</a>
<a href="/stats">Статистика</a>
<a href="/profile/edit">Профиль</a>
<form action="/logout" method="post" class="inline-form">
<button type="submit" class="link-button">Выйти</button>
</form>
</nav>
{% else %}
<a href="/login">Вход</a>
<a href="/register">Регистрация</a>
</nav>
{% endif %}
</header>
<aside class="sidebar" id="sidebar">
<a class="sidebar-brand" href="{{ '/dashboard' if user else '/login' }}">
<svg class="sidebar-brand-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>
<span>ИИ Пресс-секретарь</span>
</a>
<main class="container">
{% if user %}
<nav class="sidebar-nav">
<a href="/dashboard" class="sidebar-link {% if request.path == '/dashboard' %}active{% endif %}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
<span>Главная</span>
</a>
<a href="/generate" class="sidebar-link {% if request.path == '/generate' %}active{% endif %}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
<span>Создать пост</span>
</a>
<a href="/plan" class="sidebar-link {% if request.path == '/plan' %}active{% endif %}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
<span>План</span>
</a>
<a href="/history" class="sidebar-link {% if request.path == '/history' %}active{% endif %}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
<span>Мои посты</span>
</a>
<a href="/images" class="sidebar-link {% if request.path == '/images' %}active{% endif %}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
<span>Картинки</span>
</a>
<a href="/channels" class="sidebar-link {% if request.path == '/channels' %}active{% endif %}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z"/></svg>
<span>Каналы</span>
</a>
<a href="/surveys" class="sidebar-link {% if '/surveys' in request.path %}active{% endif %}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><rect x="8" y="2" width="8" height="4" rx="1" ry="1"/><path d="M9 14l2 2 4-4"/></svg>
<span>Медиа-образы</span>
</a>
<a href="/stats" class="sidebar-link {% if request.path == '/stats' %}active{% endif %}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>
<span>Статистика</span>
</a>
<div class="sidebar-divider"></div>
<a href="/provider" class="sidebar-link {% if request.path == '/provider' %}active{% endif %}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="2" ry="2"/><rect x="9" y="9" width="6" height="6"/><line x1="9" y1="1" x2="9" y2="4"/><line x1="15" y1="1" x2="15" y2="4"/><line x1="9" y1="20" x2="9" y2="23"/><line x1="15" y1="20" x2="15" y2="23"/><line x1="20" y1="9" x2="23" y2="9"/><line x1="20" y1="14" x2="23" y2="14"/><line x1="1" y1="9" x2="4" y2="9"/><line x1="1" y1="14" x2="4" y2="14"/></svg>
<span>ИИ</span>
</a>
<a href="/profile/edit" class="sidebar-link {% if request.path == '/profile/edit' %}active{% endif %}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
<span>Профиль</span>
</a>
</nav>
<div class="sidebar-footer">
<button type="button" class="sidebar-link" data-theme-toggle aria-label="Переключить тему">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
<span>Тема</span>
</button>
<form action="/logout" method="post" class="sidebar-form">
<button type="submit" class="sidebar-link">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
<span>Выйти</span>
</button>
</form>
</div>
{% else %}
<nav class="sidebar-nav">
<a href="/login" class="sidebar-link {% if request.path == '/login' %}active{% endif %}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><polyline points="10 17 15 12 10 7"/><line x1="15" y1="12" x2="3" y2="12"/></svg>
<span>Вход</span>
</a>
<a href="/register" class="sidebar-link {% if request.path == '/register' %}active{% endif %}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="8.5" cy="7" r="4"/><line x1="20" y1="8" x2="20" y2="14"/><line x1="23" y1="11" x2="17" y2="11"/></svg>
<span>Регистрация</span>
</a>
</nav>
<div class="sidebar-footer">
<button type="button" class="sidebar-link" data-theme-toggle aria-label="Переключить тему">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
<span>Тема</span>
</button>
</div>
{% endif %}
</aside>
<button class="sidebar-toggle" id="sidebarToggle" aria-label="Меню">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
</button>
<main class="main-content">
{% if flash_message %}
<div class="flash">{{ flash_message }}</div>
{% endif %}
@@ -77,12 +143,27 @@
{% endif %}
{% block content %}{% endblock %}
</main>
<script>
(function () {
var toggle = document.getElementById('sidebarToggle');
var sidebar = document.getElementById('sidebar');
if (toggle && sidebar) {
toggle.addEventListener('click', function () {
sidebar.classList.toggle('open');
});
document.addEventListener('click', function (e) {
if (sidebar.classList.contains('open') && !sidebar.contains(e.target) && e.target !== toggle) {
sidebar.classList.remove('open');
}
});
}
})();
function copyText(text, btn) {
function done(ok) {
if (!btn) return;
var old = btn.textContent;
btn.textContent = ok ? '✅ Скопировано' : '❌ Ошибка';
btn.textContent = ok ? '\u2705 \u0421\u043a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u043d\u043e' : '\u274c \u041e\u0448\u0438\u0431\u043a\u0430';
setTimeout(function () { btn.textContent = old; }, 1500);
}
function fallback() {
@@ -109,6 +190,72 @@
var el = document.getElementById(btn.getAttribute('data-copy-target'));
if (el) copyText(el.textContent, btn);
});
/* === Background Task Engine === */
window.TaskEngine = {
start: function(formEl, opts) {
var fd = new FormData(formEl);
var loaderEl = document.getElementById(opts.loaderId || 'taskLoader');
var resultEl = document.getElementById(opts.resultId || 'taskResult');
var formContainer = document.getElementById(opts.formId || 'taskForm');
var btn = formEl.querySelector('button[type="submit"]');
var errEl = document.getElementById(opts.errorId || 'taskError');
// Hide form, show loader
if (formContainer) formContainer.style.display = 'none';
if (resultEl) resultEl.style.display = 'none';
if (errEl) { errEl.style.display = 'none'; errEl.textContent = ''; }
if (loaderEl) loaderEl.style.display = 'flex';
fetch('/api/tasks', { method: 'POST', body: fd })
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.status === 'error' || data.error) {
if (loaderEl) loaderEl.style.display = 'none';
if (formContainer) formContainer.style.display = '';
if (errEl) { errEl.style.display = 'block'; errEl.textContent = data.error || 'Ошибка запуска задачи'; }
return;
}
TaskEngine.poll(data.task_id, opts);
})
.catch(function(err) {
if (loaderEl) loaderEl.style.display = 'none';
if (formContainer) formContainer.style.display = '';
if (errEl) { errEl.style.display = 'block'; errEl.textContent = 'Ошибка сети: ' + err.message; }
});
},
poll: function(taskId, opts) {
var loaderEl = document.getElementById(opts.loaderId || 'taskLoader');
var resultEl = document.getElementById(opts.resultId || 'taskResult');
var formContainer = document.getElementById(opts.formId || 'taskForm');
var errEl = document.getElementById(opts.errorId || 'taskError');
fetch('/api/tasks/' + taskId)
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.status === 'running' || data.status === 'pending') {
setTimeout(function() { TaskEngine.poll(taskId, opts); }, 1500);
return;
}
if (loaderEl) loaderEl.style.display = 'none';
if (data.status === 'error') {
if (formContainer) formContainer.style.display = '';
if (errEl) { errEl.style.display = 'block'; errEl.textContent = data.error || 'Ошибка генерации'; }
return;
}
if (data.status === 'done' && opts.onDone) {
opts.onDone(data.result || {});
}
})
.catch(function(err) {
if (loaderEl) loaderEl.style.display = 'none';
if (formContainer) formContainer.style.display = '';
if (errEl) { errEl.style.display = 'block'; errEl.textContent = 'Ошибка сети: ' + err.message; }
});
}
};
</script>
</body>
</html>
@@ -0,0 +1,19 @@
{% extends "base.html" %}
{% block title %}Добавить канал — Личный кабинет{% endblock %}
{% block content %}
<h1>Добавить канал</h1>
<div class="card">
<p class="form-hint">
Введите <strong>@username</strong> канала/чата или числовой ID.<br>
Бот должен быть добавлен в админы канала, иначе публикация не сработает.
</p>
</div>
<form method="post" action="/channels/add" class="form">
<label>@username или ID
<input type="text" name="chat_input" value="{{ form.chat_input if form else '' }}"
placeholder="@my_channel или -1001234567890" required>
</label>
<button type="submit">Проверить и добавить</button>
</form>
<p><a href="/channels">← Назад к каналам</a></p>
{% endblock %}
+37
View File
@@ -0,0 +1,37 @@
{% extends "base.html" %}
{% block title %}Каналы публикации — Личный кабинет{% endblock %}
{% block content %}
<h1>Каналы публикации</h1>
<p class="form-hint">Добавьте Telegram-каналы и чаты, куда бот будет публиковать посты. Бот должен быть админом в канале.</p>
<div class="grid">
<a class="tile" href="/channels/add">➕ Добавить канал</a>
</div>
{% if channels %}
<table class="table">
<thead>
<tr><th>Название</th><th>Тип</th><th>Username</th><th>Добавлен</th><th></th></tr>
</thead>
<tbody>
{% for ch in channels %}
<tr>
<td>{{ ch.title }}</td>
<td>{{ ch.chat_type }}</td>
<td>{% if ch.username %}@{{ ch.username }}{% else %}—{% endif %}</td>
<td>{{ ch.created_at }}</td>
<td>
<form action="/channels/{{ ch.id }}/delete" method="post" class="inline-form" onsubmit="return confirm('Удалить канал?')">
<button type="submit" class="button-link button-link-danger">🗑</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="card">
<p>Нет добавленных каналов. <a href="/channels/add">Добавьте первый →</a></p>
</div>
{% endif %}
{% endblock %}
+24 -3
View File
@@ -4,15 +4,36 @@
<h1>Привет, {{ user.name }}!</h1>
<div class="card">
<p><strong>Телефон:</strong> {{ user.phone }}</p>
<p><strong>Вид бизнеса:</strong> {{ business_name }}</p>
<p><strong>Описание:</strong> {{ user.description }}</p>
<p><strong>Email:</strong> {{ user.email }}</p>
<p><strong>В кабинете с:</strong> {{ user.created_at }}</p>
</div>
<div class="card">
<h2>Медиа-образ</h2>
{% if latest_survey %}
<p><strong>Активный:</strong> <a href="/surveys/{{ latest_survey.id }}">{{ latest_survey.title }}</a></p>
<p class="form-hint">Посты генерируются на основе этого медиа-образа.</p>
<div class="post-actions">
<a href="/surveys/{{ latest_survey.id }}" class="copy-btn">Открыть</a>
<a href="/survey" class="copy-btn">➕ Создать новый</a>
<a href="/survey/import" class="copy-btn">📤 Загрузить из файла</a>
</div>
{% else %}
<p>Медиа-образ ещё не создан. Создайте его — посты будут генерироваться на его основе.</p>
<div class="post-actions">
<a href="/survey" class="copy-btn">➕ Создать медиа-образ</a>
<a href="/survey/import" class="copy-btn">📤 Загрузить из файла</a>
</div>
{% endif %}
</div>
<div class="grid">
<a class="tile" href="/generate">💡 Сгенерировать пост</a>
<a class="tile" href="/plan">📅 Контент-план</a>
<a class="tile" href="/history">📜 История постов</a>
<a class="tile" href="/history">📜 Мои посты</a>
<a class="tile" href="/images">🖼 Медиа-библиотека</a>
<a class="tile" href="/channels">📢 Каналы публикации</a>
<a class="tile" href="/surveys">📋 Мои медиа-образы</a>
<a class="tile" href="/stats">📈 Статистика</a>
<a class="tile" href="/profile/edit">✏️ Редактировать профиль</a>
<a class="tile tile-danger" href="/delete-account">🗑 Удалить аккаунт</a>
+90 -16
View File
@@ -2,25 +2,99 @@
{% block title %}Сгенерировать пост — Личный кабинет{% endblock %}
{% block content %}
<h1>Пост в своём стиле</h1>
<form method="post" action="/generate" class="form">
<label>Платформа
<select name="platform" required>
{% for key, label in platforms %}
<option value="{{ key }}" {% if form and form.platform == key %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</label>
<label>Стиль (например: дружелюбный, с юмором, экспертный)
<input type="text" name="style" value="{{ form.style if form else '' }}" required>
</label>
<label>Пожелания к посту
<textarea name="wishes" rows="3">{{ form.wishes if form else '' }}</textarea>
</label>
<button type="submit">Сгенерировать</button>
</form>
{% if latest_survey %}
<div class="card">
<p><strong>Медиа-образ:</strong> <a href="/surveys/{{ latest_survey.id }}">{{ latest_survey.title }}</a></p>
<p class="form-hint">Пост сгенерируется на основе этого медиа-образа (личность, УТП, Tone of Voice, структура мысли).</p>
</div>
{% else %}
<div class="card">
<p>⚠️ Медиа-образ не найден. Посты генерируются на основе медиа-образа.</p>
<a href="/survey" class="copy-btn">➕ Создать медиа-образ</a>
</div>
{% endif %}
<div id="taskError" class="task-error"></div>
<div id="taskLoader" class="task-loader">
<div class="spinner"></div>
<div class="task-loader-text">Генерирую пост...</div>
<div class="task-loader-sub">Это может занять 15–60 секунд</div>
</div>
<div id="taskResult" class="task-result"></div>
<div id="taskForm">
<form method="post" action="/generate" class="form" id="generateForm" onsubmit="return startPostGen(event)">
{% if surveys and surveys|length > 1 %}
<label>Медиа-образ
<select name="survey_id">
<option value="">Последний (по умолчанию)</option>
{% for s in surveys %}
<option value="{{ s.id }}">{{ s.title }} ({{ s.created_at }})</option>
{% endfor %}
</select>
</label>
{% endif %}
<label>Платформа
<select name="platform" required>
{% for key, label in platforms %}
<option value="{{ key }}" {% if (prefill and prefill.platform == key) or (form and form.platform == key) %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</label>
<label>Стиль (например: дружелюбный, с юмором, экспертный)
<input type="text" name="style" value="{{ form.style if form else '' }}" required>
</label>
<label>Пожелания к посту
<textarea name="wishes" rows="3">{{ prefill.idea if prefill else (form.wishes if form else '') }}</textarea>
</label>
<input type="hidden" name="task_type" value="generate_post">
{% if prefill %}<input type="hidden" name="plan_id" value="{{ prefill.plan_id }}">{% endif %}
<button type="submit" {% if not latest_survey %}disabled{% endif %}>Сгенерировать</button>
</form>
</div>
{% if result %}
<h2>Готовый пост</h2>
<div class="card result">{{ result }}</div>
<div class="post-actions">
<button type="button" class="copy-btn" data-copy-target="post-text">📋 Копировать</button>
</div>
<div id="post-text" hidden>{{ result }}</div>
{% endif %}
<p><a href="/dashboard">← Назад в кабинет</a></p>
<script>
function startPostGen(e) {
e.preventDefault();
var form = document.getElementById('generateForm');
TaskEngine.start(form, {
loaderId: 'taskLoader',
formId: 'taskForm',
errorId: 'taskError',
resultId: 'taskResult',
onDone: function(result) {
var html = '<h2>Готовый пост</h2>' +
'<div class="card result">' + escHtml(result.content) + '</div>' +
'<div class="post-actions">' +
'<button type="button" class="copy-btn" onclick="copyText(\'' + escAttr(result.content).replace(/'/g, "\\'") + '\', this)">📋 Копировать</button>' +
' <a href="/posts/' + result.post_id + '" class="copy-btn">Открыть пост →</a>' +
'</div>';
document.getElementById('taskResult').innerHTML = html;
document.getElementById('taskResult').style.display = 'block';
}
});
return false;
}
function escHtml(s) {
var d = document.createElement('div');
d.textContent = s || '';
return d.innerHTML;
}
function escAttr(s) {
return (s || '').replace(/\\/g,'\\\\').replace(/'/g,"\\'").replace(/"/g,'&quot;').replace(/\n/g,'\\n');
}
</script>
{% endblock %}
+12 -3
View File
@@ -1,13 +1,22 @@
{% extends "base.html" %}
{% block title %}История постов — Личный кабинет{% endblock %}
{% block title %}Мои посты — Личный кабинет{% endblock %}
{% block content %}
<h1>История постов</h1>
<h1>Мои посты</h1>
{% if posts %}
<ul class="post-list">
{% for post in posts %}
<li class="card">
<div class="post-meta">{{ platform_label(post.platform) }} · {{ post.created_at }}</div>
<div class="post-meta">{{ platform_label(post.platform) }} · {{ post.created_at }}
{% if post.published %} · ✅ Опубликован{% endif %}
</div>
<div class="post-content" id="post-content-{{ post.id }}">{{ post.content }}</div>
{% if post.images %}
<div class="post-images">
{% for img in post.images %}
<img src="/media/{{ img.filename }}" class="post-thumb" alt="">
{% endfor %}
</div>
{% endif %}
<div class="post-actions">
<button type="button" class="copy-btn" data-copy-target="post-content-{{ post.id }}">📋 Копировать</button>
<a class="button-link" href="/posts/{{ post.id }}">👁 Открыть</a>
+168 -21
View File
@@ -2,26 +2,173 @@
{% block title %}Сгенерировать картинку — Личный кабинет{% endblock %}
{% block content %}
<h1>Генерация картинки</h1>
<form method="post" action="/images/generate" class="form">
<label>Что нарисовать?
<textarea name="prompt" rows="4" required placeholder="Например: современный кофейный стакан на белом фоне, логотип для кофейни">{{ form.prompt if form else '' }}</textarea>
</label>
<label>Размер
<select name="size" required>
{% for key, (value, label) in sizes.items() %}
<option value="{{ key }}" {% if form and form.size == key %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</label>
<label>Стиль
<select name="style">
{% for key, (label, suffix) in styles.items() %}
<option value="{{ key }}" {% if form and form.style == key %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</label>
<button type="submit">🎨 Сгенерировать</button>
<p class="form-hint">Генерация занимает 10–60 секунд. Картинка сохранится в медиа-библиотеку.</p>
</form>
<div id="taskError" class="task-error"></div>
<div id="taskLoader" class="task-loader">
<div class="spinner"></div>
<div class="task-loader-text">Генерирую картинку...</div>
<div class="task-loader-sub">Это может занять 15–60 секунд</div>
</div>
<div id="taskResult" class="task-result"></div>
<div id="taskForm">
<!-- Выбор шаблона -->
<h2>Выбери шаблон</h2>
<div class="template-grid" id="templateGrid">
{% 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 }}" data-hint="{{ t.hint }}">
<span class="template-icon">{{ t.icon }}</span>
<span class="template-name">{{ t.name }}</span>
<span class="template-hint">{{ t.hint }}</span>
</button>
{% endfor %}
<button type="button" class="template-card template-card-custom" data-key="custom">
<span class="template-icon">✏️</span>
<span class="template-name">Свой вариант</span>
<span class="template-hint">Напиши промпт сам</span>
</button>
</div>
<div id="selectedTemplateHint" class="selected-hint" style="display:none;"></div>
<!-- Форма генерации -->
<form method="post" action="/images/generate" class="form" id="imageForm" onsubmit="return startImageGen(event)" enctype="multipart/form-data">
<label>Что нарисовать?
<textarea name="prompt" id="promptInput" rows="4" required
placeholder="Например: современный кофейный стакан на белом фоне, логотип для кофейни">{{ form.prompt if form else '' }}</textarea>
</label>
<label>Исходное изображение <span class="form-hint">(необязательно) — ИИ использует его как основу</span>
<input type="file" name="reference_image" id="referenceImage" accept="image/*">
</label>
<div id="refImagePreview" style="display:none; margin: 5px 0 10px;">
<img id="refImageThumb" style="max-height:120px; border-radius:8px; border:1px solid var(--border);">
<button type="button" class="button-link button-link-danger" onclick="clearRefImage()" style="margin-left:8px">✕ Убрать</button>
</div>
<label>Размер
<select name="size" id="sizeInput" required>
{% for key, (value, label) in sizes.items() %}
<option value="{{ key }}" {% if form and form.size == key %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</label>
<label>Стиль
<select name="style" id="styleInput">
{% for key, (label, suffix) in styles.items() %}
<option value="{{ key }}" {% if form and form.style == key %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</label>
<input type="hidden" name="task_type" value="generate_image">
<button type="submit" id="submitBtn" disabled>🎨 Сгенерировать</button>
<p class="form-hint">Генерация занимает 10–60 секунд. Картинка сохранится в медиа-библиотеку.</p>
</form>
</div>
<p><a href="/images">← Назад в библиотеку</a></p>
<script>
function clearRefImage() {
document.getElementById('referenceImage').value = '';
document.getElementById('refImagePreview').style.display = 'none';
}
document.getElementById('referenceImage').addEventListener('change', function(e) {
var file = e.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function(ev) {
document.getElementById('refImageThumb').src = ev.target.result;
document.getElementById('refImagePreview').style.display = '';
};
reader.readAsDataURL(file);
});
</script>
<script>
(function() {
var grid = document.getElementById('templateGrid');
var promptInput = document.getElementById('promptInput');
var sizeInput = document.getElementById('sizeInput');
var styleInput = document.getElementById('styleInput');
var hintEl = document.getElementById('selectedTemplateHint');
var submitBtn = document.getElementById('submitBtn');
var selectedKey = null;
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');
selectedKey = card.dataset.key;
if (selectedKey === 'custom') {
hintEl.style.display = 'none';
promptInput.value = '';
promptInput.focus();
promptInput.removeAttribute('readonly');
submitBtn.disabled = !promptInput.value.trim();
return;
}
var prompt = card.dataset.prompt;
var style = card.dataset.style;
var size = card.dataset.size;
var hint = card.dataset.hint;
promptInput.value = prompt;
if (styleInput.querySelector('option[value="' + style + '"]')) {
styleInput.value = style;
}
if (sizeInput.querySelector('option[value="' + size + '"]')) {
sizeInput.value = size;
}
hintEl.textContent = '💡 ' + hint;
hintEl.style.display = 'block';
submitBtn.disabled = false;
});
// Разрешить ввод вручную при изменении текста
promptInput.addEventListener('input', function() {
if (selectedKey && selectedKey !== 'custom') {
// Пользователь изменил текст — переключаем на "свой вариант"
grid.querySelectorAll('.template-card').forEach(function(c) { c.classList.remove('selected'); });
var customCard = grid.querySelector('[data-key="custom"]');
if (customCard) customCard.classList.add('selected');
selectedKey = 'custom';
hintEl.style.display = 'none';
}
submitBtn.disabled = !promptInput.value.trim();
});
})();
</script>
<script>
function startImageGen(e) {
e.preventDefault();
var form = document.getElementById('imageForm');
TaskEngine.start(form, {
loaderId: 'taskLoader',
formId: 'taskForm',
errorId: 'taskError',
resultId: 'taskResult',
onDone: function(result) {
var html = '<div class="card">' +
'<p>✅ Картинка сгенерирована!</p>' +
'<div class="media-grid"><div class="media-card">' +
'<a href="/media/' + result.filename + '" class="media-link" target="_blank">' +
'<img class="media-thumb" src="/media/' + result.filename + '" alt="Картинка">' +
'</a></div></div>' +
'<p><a href="/images" class="copy-btn">Перейти в библиотеку →</a> ' +
'<a href="/images/generate" class="copy-btn">Ещё одна →</a></p></div>';
document.getElementById('taskResult').innerHTML = html;
document.getElementById('taskResult').style.display = 'block';
}
});
return false;
}
</script>
{% endblock %}
+41 -7
View File
@@ -24,6 +24,7 @@
</div>
<div class="plan-idea">{{ item.idea }}</div>
<div class="post-actions">
<a href="/generate?plan_id={{ item.id }}&platform={{ item.platform }}&idea={{ item.idea | urlencode }}" class="copy-btn">✍️ Создать пост</a>
<form method="post" action="/plan/{{ item.id }}/status" class="inline-form">
<select name="status" class="status-select">
<option value="planned" {% if item.status == 'planned' %}selected{% endif %}>📝 В плане</option>
@@ -47,12 +48,45 @@
{% endif %}
<h2>{% if plan %}Пересоставить план{% else %}Составить план{% endif %}</h2>
<form method="post" action="/plan" class="form"
onsubmit="{% if plan %}return confirm('Новый план заменит текущий (отметки статусов сбросятся). Продолжить?');{% endif %}">
<label>На сколько дней (1-14)
<input type="number" name="days" min="1" max="14" value="7" required>
</label>
<button type="submit">📅 Составить план</button>
</form>
<div id="taskError" class="task-error"></div>
<div id="taskLoader" class="task-loader">
<div class="spinner"></div>
<div class="task-loader-text">Составляю контент-план...</div>
<div class="task-loader-sub">Генерация занимает 20–60 секунд</div>
</div>
<div id="taskResult" class="task-result"></div>
<div id="taskForm">
<form method="post" action="/plan" class="form" id="planForm" onsubmit="return startPlanGen(event)">
<label>На сколько дней (1-14)
<input type="number" name="days" min="1" max="14" value="7" required>
</label>
<input type="hidden" name="task_type" value="generate_plan">
<button type="submit">📅 Составить план</button>
</form>
</div>
<p><a href="/dashboard">← Назад в кабинет</a></p>
<script>
function startPlanGen(e) {
e.preventDefault();
var form = document.getElementById('planForm');
TaskEngine.start(form, {
loaderId: 'taskLoader',
formId: 'taskForm',
errorId: 'taskError',
resultId: 'taskResult',
onDone: function(result) {
var html = '<div class="card"><p>✅ План на ' + result.days + ' дн. составлен! (' + result.items + ' пунктов)</p>' +
'<p><a href="/plan" class="copy-btn">Обновить страницу →</a></p></div>';
document.getElementById('taskResult').innerHTML = html;
document.getElementById('taskResult').style.display = 'block';
}
});
return false;
}
</script>
{% endblock %}
+429 -4
View File
@@ -7,18 +7,443 @@
{{ platform_label(post.platform) }} · {{ post.created_at }}
{% if post.style %} · Стиль: {{ post.style }}{% endif %}
</div>
<div class="post-content" id="post-content-{{ post.id }}">{{ post.content }}</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">
<button type="button" class="copy-btn" data-copy-target="post-content-{{ post.id }}">📋 Копировать</button>
<a class="button-link" href="/posts/{{ post.id }}/edit">✏️ Изменить</a>
<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>
<p><a href="/history">← Назад к постам</a></p>
{% endblock %}
+11 -10
View File
@@ -9,17 +9,18 @@
<label>Телефон
<input type="text" name="phone" value="{{ user.phone }}" required>
</label>
<label>Вид бизнеса
<select name="business_type" required>
{% for key, value in business_types.items() %}
<option value="{{ key }}" {% if key == user.business_type|string %}selected{% endif %}>{{ value.name }}</option>
{% endfor %}
</select>
</label>
<label>Описание бизнеса
<textarea name="description" rows="4" required>{{ user.description }}</textarea>
</label>
<button type="submit">Сохранить</button>
</form>
<div class="card">
<h2>Провайдер ИИ</h2>
{% if provider %}
<p><strong>Текущий:</strong> {{ provider_presets[provider.provider_type].name if provider.provider_type in provider_presets else provider.provider_type }}</p>
{% else %}
<p>Используются глобальные настройки (PiAPI).</p>
{% endif %}
<a href="/provider" class="copy-btn">⚙ Настроить провайдера</a>
</div>
<p><a href="/dashboard">← Назад в кабинет</a></p>
{% endblock %}
+192
View File
@@ -0,0 +1,192 @@
{% extends "base.html" %}
{% block title %}Провайдеры ИИ — Личный кабинет{% endblock %}
{% block content %}
<h1>Провайдеры ИИ</h1>
<p class="form-hint">Настройте провайдеров для генерации текста и изображений независимо. API ключи хранятся только у вас.</p>
{% if provider %}
<div class="card">
<p class="form-hint" style="margin:0">
<strong>Текст:</strong> {{ provider_presets[provider.text_provider_type].name if provider.text_provider_type in provider_presets else provider.text_provider_type }} → {{ provider.text_model }}
&nbsp;|&nbsp;
<strong>Изображения:</strong>
{% if provider.image_api_key %}
{{ provider_presets[provider.image_provider_type].name if provider.image_provider_type in provider_presets else provider.image_provider_type }} → {{ provider.image_model or '(не указана)' }}
{% else %}
<em>глобальный PiAPI</em>
{% endif %}
&nbsp;<span class="form-hint">· {{ provider.updated_at }}</span>
</p>
</div>
{% else %}
<div class="card">
<p style="margin:0">Используются глобальные настройки (PiAPI из .env). Настройте свои провайдеры для независимости.</p>
</div>
{% endif %}
<!-- Табы -->
<div class="provider-tabs">
<button type="button" class="provider-tab active" onclick="switchTab('text')">✍️ Генерация текста</button>
<button type="button" class="provider-tab" onclick="switchTab('image')">🎨 Генерация изображений</button>
</div>
<form method="post" action="/provider" class="form" id="providerForm">
<!-- === ТАБ: ТЕКСТ === -->
<div class="provider-tab-content active" id="tabText">
<h2>✍️ Генерация текста</h2>
<label>Провайдер
<select name="text_provider_type" id="textProviderType" required onchange="updatePresets('text')">
<option value="" disabled {% if not provider and not form %}selected{% endif %}>Выберите провайдера</option>
{% for key, preset in provider_presets.items() %}
<option value="{{ key }}"
{% if (provider and provider.text_provider_type == key) or (form and form.text_provider_type == key) %}selected{% endif %}>
{{ preset.name }}
</option>
{% endfor %}
</select>
</label>
<label>API ключ
<input type="password" name="text_api_key" id="textApiKey"
value="{{ form.text_api_key if form else '' }}"
placeholder="Ваш API ключ">
</label>
<label style="flex-direction: row; align-items: center; gap: 8px;">
<input type="checkbox" name="text_use_global" value="1" id="textUseGlobal"
onchange="toggleGlobal('text')">
<span>Использовать глобальные настройки (PiAPI из .env)</span>
</label>
<label>Base URL
<input type="text" name="text_base_url" id="textBaseUrl"
value="{{ form.text_base_url if form else (provider.text_base_url if provider else '') }}"
placeholder="https://...">
</label>
<label>Модель
<input type="text" name="text_model" id="textModel"
value="{{ form.text_model if form else (provider.text_model if provider else '') }}"
placeholder="напр. gpt-4o, glm-5.2, claude-sonnet-5">
</label>
<div class="card" style="margin-top:15px; background: var(--bg-secondary);">
<p style="margin:0; font-size: 13px;"><strong>Популярные модели для текста:</strong></p>
<ul style="margin:5px 0 0 18px; font-size: 13px;">
<li><code>gpt-4o</code> — OpenAI, OpenRouter, PiAPI</li>
<li><code>claude-sonnet-5</code> — Anthropic через OpenRouter</li>
<li><code>glm-5.2</code> — OpenCode Zen (платная)</li>
<li><code>deepseek-v4-flash-free</code> — OpenCode Zen (бесплатная)</li>
<li><code>hy3-free</code> — OpenCode Zen (бесплатная, reasoning)</li>
</ul>
</div>
</div>
<!-- === ТАБ: ИЗОБРАЖЕНИЯ === -->
<div class="provider-tab-content" id="tabImage">
<h2>🎨 Генерация изображений</h2>
<label>Провайдер
<select name="image_provider_type" id="imageProviderType" required onchange="updatePresets('image')">
<option value="" disabled {% if not provider and not form %}selected{% endif %}>Выберите провайдера</option>
{% for key, preset in provider_presets.items() %}
{% if preset.supports_images %}
<option value="{{ key }}"
{% if (provider and provider.image_provider_type == key) or (form and form.image_provider_type == key) %}selected{% endif %}>
{{ preset.name }}
</option>
{% endif %}
{% endfor %}
</select>
</label>
<label>API ключ
<input type="password" name="image_api_key" id="imageApiKey"
value="{{ form.image_api_key if form else '' }}"
placeholder="Ваш API ключ">
</label>
<label style="flex-direction: row; align-items: center; gap: 8px;">
<input type="checkbox" name="image_use_global" value="1" id="imageUseGlobal"
onchange="toggleGlobal('image')">
<span>Использовать глобальные настройки (PiAPI из .env)</span>
</label>
<label>Base URL
<input type="text" name="image_base_url" id="imageBaseUrl"
value="{{ form.image_base_url if form else (provider.image_base_url if provider else '') }}"
placeholder="https://...">
</label>
<label>Модель
<input type="text" name="image_model" id="imageModel"
value="{{ form.image_model if form else (provider.image_model if provider else '') }}"
placeholder="напр. gpt-image-1, dall-e-3">
</label>
<div class="card" style="margin-top:15px; background: var(--bg-secondary);">
<p style="margin:0; font-size: 13px;"><strong>Популярные модели для изображений:</strong></p>
<ul style="margin:5px 0 0 18px; font-size: 13px;">
<li><code>gpt-image-1</code> — OpenAI, PiAPI (рекомендуется)</li>
<li><code>gpt-image-1-mini</code> — OpenAI (быстрая, дешевле)</li>
<li><code>dall-e-3</code> — OpenAI (старая, через прямой API)</li>
</ul>
</div>
</div>
<button type="submit">Сохранить настройки</button>
</form>
<p style="margin-top:20px"><a href="/profile/edit">← Назад в профиль</a> · <a href="/dashboard">← На главную</a></p>
<script>
const PRESETS = {
{% for key, preset in provider_presets.items() %}
"{{ key }}": {"base_url": "{{ preset.base_url }}", "text_model": "{{ preset.text_model }}", "image_model": "{{ preset.image_model }}"},
{% endfor %}
};
function switchTab(tab) {
// Переключаем кнопки
document.querySelectorAll('.provider-tab').forEach(function(btn) { btn.classList.remove('active'); });
document.querySelectorAll('.provider-tab-content').forEach(function(c) { c.classList.remove('active'); });
// Активируем нужный таб
if (tab === 'text') {
document.querySelectorAll('.provider-tab')[0].classList.add('active');
document.getElementById('tabText').classList.add('active');
} else {
document.querySelectorAll('.provider-tab')[1].classList.add('active');
document.getElementById('tabImage').classList.add('active');
}
}
function updatePresets(kind) {
var typeEl = document.getElementById(kind + "ProviderType");
var preset = PRESETS[typeEl.value];
if (!preset) return;
document.getElementById(kind + "BaseUrl").value = preset.base_url;
if (kind === "text") document.getElementById(kind + "Model").value = preset.text_model;
if (kind === "image") document.getElementById(kind + "Model").value = preset.image_model;
var globalCb = document.getElementById(kind + "UseGlobal");
if (globalCb && globalCb.checked) {
globalCb.checked = false;
toggleGlobal(kind);
}
}
function toggleGlobal(kind) {
var cb = document.getElementById(kind + "UseGlobal");
var key = document.getElementById(kind + "ApiKey");
var baseUrl = document.getElementById(kind + "BaseUrl");
var model = document.getElementById(kind + "Model");
if (cb.checked) {
key.disabled = true;
key.placeholder = "Используются глобальные настройки";
key.value = "";
baseUrl.value = "https://api.piapi.ai/v1";
model.value = (kind === "text") ? "gpt-4o" : "gpt-image-1";
} else {
key.disabled = false;
key.placeholder = "Ваш API ключ";
}
}
</script>
{% endblock %}
+43
View File
@@ -0,0 +1,43 @@
{% extends "base.html" %}
{% block title %}Опубликовать пост — Личный кабинет{% endblock %}
{% block content %}
<h1>Публикация поста</h1>
<div class="card">
<p><strong>Платформа:</strong> {{ platform_label(post.platform) }}</p>
<p><strong>Создан:</strong> {{ post.created_at }}</p>
{% if post.published %}
<p><strong>Статус:</strong> ✅ Опубликован в «{{ post.published_to }}» {{ post.published_at }}</p>
{% else %}
<p><strong>Статус:</strong> ⏳ Не опубликован</p>
{% endif %}
</div>
<div class="card">
<h2>Текст поста</h2>
<div class="result">{{ post.content }}</div>
</div>
{% if channels %}
<form method="post" action="/posts/{{ post.id }}/publish" class="form">
<label>Канал для публикации
<select name="channel_id" required>
<option value="" disabled selected>Выберите канал или чат</option>
{% for ch in channels %}
<option value="{{ ch.id }}">{{ ch.title }} ({{ ch.chat_type }}){% if ch.username %} @{{ ch.username }}{% endif %}</option>
{% endfor %}
</select>
</label>
<button type="submit">📤 Опубликовать</button>
</form>
{% else %}
<div class="card">
<p>⚠️ Нет добавленных каналов. <a href="/channels/add">Добавьте канал →</a></p>
</div>
{% endif %}
<div class="post-actions">
<a href="/posts/{{ post.id }}" class="copy-btn">← Назад к посту</a>
<a href="/channels" class="copy-btn">⚙ Управление каналами</a>
</div>
{% endblock %}
-11
View File
@@ -9,17 +9,6 @@
<label>Телефон
<input type="text" name="phone" value="{{ form.phone if form else '' }}" required>
</label>
<label>Вид бизнеса
<select name="business_type" required>
<option value="" disabled {% if not form %}selected{% endif %}>Выбери вид бизнеса</option>
{% for key, value in business_types.items() %}
<option value="{{ key }}" {% if form and form.business_type == key %}selected{% endif %}>{{ value.name }}</option>
{% endfor %}
</select>
</label>
<label>Описание бизнеса
<textarea name="description" rows="4" required>{{ form.description if form else '' }}</textarea>
</label>
<label>Email
<input type="email" name="email" value="{{ form.email if form else '' }}" required>
</label>
+660
View File
@@ -0,0 +1,660 @@
{% extends "base.html" %}
{% block title %}Опрос медиа-образа — Личный кабинет{% endblock %}
{% block content %}
<h1>Опрос для создания медиа-образа</h1>
<p class="form-hint">Заполните поля по шагам. На последнем шаге можно сохранить и сгенерировать документ стратегии.</p>
<!-- === АНАЛИЗ ПРОФИЛЯ СОЦСЕТИ === -->
<div class="card" id="socialAnalyzer">
<h2 style="margin-top:0">🔍 Автозаполнение из профиля соцсети</h2>
<p class="form-hint">Вставьте ссылку на профиль или канал — ИИ проанализирует посты и заполнит поля медиа-образа. Поддерживается Telegram (автоматически), VK, Tenchat, Instagram (вручную).</p>
<div id="socialTaskError" class="task-error"></div>
<div id="socialTaskLoader" class="task-loader" style="display:none; padding:15px">
<div class="spinner"></div>
<div class="task-loader-text">Анализирую посты...</div>
<div class="task-loader-sub">Это может занять 20–60 секунд</div>
</div>
<div id="socialTaskResult" class="task-result" style="display:none"></div>
<div id="socialForm">
<label>Ссылка на профиль/канал
<input type="url" id="socialUrl" placeholder="https://t.me/channel, https://vk.com/username, https://tenchat.ru/username">
</label>
<div id="manualPostsSection" style="display:none">
<label>Тексты постов <span class="form-hint">Вставьте 3-10 постов. Разделяйте пустой строкой.</span>
<textarea id="manualPosts" rows="8" placeholder="Пост 1 текст...&#10;&#10;Пост 2 текст...&#10;&#10;Пост 3 текст..."></textarea>
</label>
<p class="form-hint" id="manualHint"></p>
</div>
<button type="button" class="copy-btn" onclick="startSocialAnalysis()" id="socialAnalyzeBtn">🔍 Анализировать профиль</button>
</div>
</div>
<script>
document.getElementById('socialUrl').addEventListener('input', function() {
var url = this.value.trim().toLowerCase();
var isTG = /t\.me\//.test(url);
var section = document.getElementById('manualPostsSection');
var hint = document.getElementById('manualHint');
if (url && !isTG) {
section.style.display = '';
hint.textContent = 'Для VK, Tenchat и других платформ: откройте профиль, скопируйте 3-10 постов и вставьте сюда.';
} else {
section.style.display = 'none';
}
});
function startSocialAnalysis() {
var url = document.getElementById('socialUrl').value.trim();
if (!url) { alert('Вставьте ссылку на профиль'); return; }
var manualPosts = document.getElementById('manualPosts') ? document.getElementById('manualPosts').value : '';
var fd = new FormData();
fd.append('task_type', 'analyze_social_profile');
fd.append('url', url);
fd.append('manual_posts', manualPosts);
document.getElementById('socialForm').style.display = 'none';
document.getElementById('socialTaskLoader').style.display = 'flex';
document.getElementById('socialTaskError').style.display = 'none';
document.getElementById('socialTaskResult').style.display = 'none';
fetch('/api/tasks', {method: 'POST', body: fd})
.then(function(r) { return r.json(); })
.then(function(data) {
var taskId = data.task_id;
var poll = setInterval(function() {
fetch('/api/tasks/' + taskId)
.then(function(r) { return r.json(); })
.then(function(t) {
if (t.status === 'done') {
clearInterval(poll);
document.getElementById('socialTaskLoader').style.display = 'none';
onSocialAnalysisDone(t.result);
} else if (t.status === 'error') {
clearInterval(poll);
document.getElementById('socialTaskLoader').style.display = 'none';
document.getElementById('socialTaskError').textContent = t.error || 'Ошибка анализа';
document.getElementById('socialTaskError').style.display = 'block';
document.getElementById('socialForm').style.display = '';
}
});
}, 1500);
})
.catch(function(e) {
document.getElementById('socialTaskLoader').style.display = 'none';
document.getElementById('socialTaskError').textContent = 'Ошибка сети: ' + e;
document.getElementById('socialTaskError').style.display = 'block';
document.getElementById('socialForm').style.display = '';
});
}
function onSocialAnalysisDone(result) {
var info = result.platform_info || {};
var count = result.posts_count || 0;
var html = '<div class="card"><p>✅ Проанализировано <strong>' + count + ' постов</strong> из ' + (info.platform || 'профиля') + ' (@' + (info.channel || '') + ')</p>';
html += '<button type="button" class="copy-btn" onclick="applySocialData()">✅ Применить к медиа-образу</button>';
html += ' <button type="button" class="button-link" onclick="resetSocialForm()">↩ Отмена</button></div>';
document.getElementById('socialTaskResult').innerHTML = html;
document.getElementById('socialTaskResult').style.display = '';
window._socialData = result;
}
function resetSocialForm() {
document.getElementById('socialForm').style.display = '';
document.getElementById('socialTaskResult').style.display = 'none';
document.getElementById('socialTaskResult').innerHTML = '';
}
function applySocialData() {
var d = window._socialData;
if (!d) return;
// Заполняем поля формы
setVal('client_name', d.client_name);
setVal('client_role', d.client_role);
setVal('brand_essence', d.brand_essence);
setVal('current_situation', d.current_situation);
setVal('positioning_who', d.positioning_who);
setVal('positioning_problem', d.positioning_problem);
setVal('positioning_result', d.positioning_result);
setVal('tov_description', d.tov_description);
setVal('tov_references', d.tov_references);
setVal('tov_axes', d.tov_axes);
setVal('prompt_directions', d.prompt_directions);
setVal('prompt_structure', d.prompt_structure);
setVal('prompt_formats', d.prompt_formats);
// Сворачиваем анализатор
document.getElementById('socialAnalyzer').style.display = 'none';
// Переходим к шагу 1
showStep(1);
}
function setVal(name, value) {
if (!value) return;
// AI может вернуть массив — превращаем в строки через перенос
if (Array.isArray(value)) {
value = value.join('\n');
}
var el = document.querySelector('[name="' + name + '"]');
if (el) {
el.value = value;
el.style.borderColor = 'var(--accent)';
setTimeout(function() { el.style.borderColor = ''; }, 2000);
}
}
</script>
<!-- === КОНЕЦ АНАЛИЗАТОРА === -->
<div class="survey-wizard">
<!-- Прогресс -->
<div class="survey-progress">
<div class="survey-progress-bar" id="progressBar"></div>
</div>
<div class="survey-steps-indicator" id="stepsIndicator"></div>
<form id="surveyForm">
<!-- Шаг 1: Основная информация -->
<div class="survey-step" data-step="1">
<h2>Шаг 1 — Основная информация</h2>
<div class="form">
<label>Тип медиа-образа
<select name="brand_type" id="brandType" onchange="toggleBrandType()">
<option value="personal">Личный бренд</option>
<option value="organization">Компания / бренд</option>
<option value="community">Общественная организация</option>
</select>
</label>
<!-- Личный бренд -->
<div id="fieldsPersonal">
<label>Имя клиента <input type="text" name="client_name" placeholder="Напр. Кирилл Бойко"></label>
<label>Профессия / роль <input type="text" name="client_role" placeholder="Напр. IT-эксперт, предприниматель"></label>
</div>
<!-- Организация -->
<div id="fieldsOrganization" style="display:none">
<label>Название организации <input type="text" name="org_name" placeholder="Напр. ТехноСтарт"></label>
<label>Сфера деятельности <input type="text" name="org_industry" placeholder="Напр. IT-консалтинг, разработка ПО"></label>
<label>Миссия организации <textarea name="org_mission" rows="2" placeholder="Напр. Помогаем бизнесу расти через технологии"></textarea></label>
<label>Ключевые продукты / услуги <textarea name="org_products" rows="2" placeholder="Напр. Разработка мобильных приложений, IT-аудит"></textarea></label>
<label>Количество сотрудников <input type="text" name="org_team_size" placeholder="Напр. 15-30"></label>
<label>Год основания <input type="text" name="org_founded" placeholder="Напр. 2018"></label>
</div>
<!-- Общественная организация -->
<div id="fieldsCommunity" style="display:none">
<label>Название организации <input type="text" name="org_name" placeholder="Напр. Сообщество молодых предпринимателей"></label>
<label>Сфера деятельности <input type="text" name="org_industry" placeholder="Напр. Бизнес-сообщество, поддержка молодых предпринимателей"></label>
<label>Миссия организации <textarea name="org_mission" rows="2" placeholder="Напр. Объединять молодых предпринимателей для обмена опытом и совместного роста"></textarea></label>
<label>Ключевые направления деятельности <textarea name="org_products" rows="2" placeholder="Напр. Мероприятия, менторство, нетворкинг, образовательные программы"></textarea></label>
<label>Количество участников <input type="text" name="org_team_size" placeholder="Напр. 200+"></label>
<label>Год основания <input type="text" name="org_founded" placeholder="Напр. 2022"></label>
<label>Формат участия <input type="text" name="org_format" placeholder="Напр. Бесплатное членство, ежемесячные встречи"></label>
</div>
<label>Город / регион <input type="text" name="client_location" placeholder="Напр. Донецк"></label>
<label>Описание текущей ситуации <textarea name="current_situation" rows="5" placeholder="Кто он/оно, чем занимается, какой уникальный опыт, текущий статус..."></textarea></label>
</div>
</div>
<!-- Шаг 2: Цели -->
<div class="survey-step" data-step="2" hidden>
<h2>Шаг 2 — Цели на 12 месяцев</h2>
<div class="form">
<label>Целевое число подписчиков <input type="number" name="goal_audience_number" placeholder="10000"></label>
<label>Основная платформа <input type="text" name="goal_audience_platform" placeholder="Напр. Telegram-канал"></label>
<label>Описание целевой аудитории <textarea name="goal_audience_description" rows="3" placeholder="Кто активная аудитория..."></textarea></label>
<label>Область экспертизы <input type="text" name="goal_expert_field" placeholder="Напр. цифровизация МСП"></label>
<label>Целевое кол-во приглашений на выступления в год <input type="number" name="goal_expert_speaking" placeholder="5"></label>
<label>Целевое кол-во заявок в месяц <input type="number" name="goal_commercial_leads" placeholder="5"></label>
<label>Какие услуги <textarea name="goal_commercial_services" rows="2" placeholder="Напр. IT-аудит, внедрение"></textarea></label>
<label>Описание контент-системы <textarea name="goal_content_system" rows="3" placeholder="Напр. Ежедневный контент-план на трёх столпах..."></textarea></label>
</div>
</div>
<!-- Шаг 3: Позиционирование -->
<div class="survey-step" data-step="3" hidden>
<h2>Шаг 3 — Позиционирование и УТП</h2>
<div class="form">
<label>Сущность бренда (одной строкой) <input type="text" name="brand_essence" placeholder="Напр. Цифровой архитектор, строящий IT-устойчивость бизнеса"></label>
<!-- Личный бренд -->
<div class="brand-fields-personal">
<label>Кому я помогаю? <textarea name="positioning_who" rows="2"></textarea></label>
<label>Какую проблему решаю? <textarea name="positioning_problem" rows="2"></textarea></label>
<label>Какой результат получает клиент? <textarea name="positioning_result" rows="2"></textarea></label>
</div>
<!-- Организация -->
<div class="brand-fields-organization" style="display:none">
<label>Какую проблему решает наш продукт/услуга? <textarea name="positioning_problem" rows="2"></textarea></label>
<label>Ценность для клиента <textarea name="positioning_result" rows="2" placeholder="Какую ценность получает клиент от нашей организации?"></textarea></label>
<label>Кому мы помогаем (целевой сегмент)? <textarea name="positioning_who" rows="2" placeholder="B2B / B2C, отрасли, размер бизнеса..."></textarea></label>
<label>Почему выбирают нас, а не конкурентов? <textarea name="positioning_difference" rows="2"></textarea></label>
</div>
<!-- Общественная организация -->
<div class="brand-fields-community" style="display:none">
<label>Какую проблему решает наше сообщество? <textarea name="positioning_problem" rows="2" placeholder="Изоляция молодых предпринимателей, отсутствие наставничества, слабый нетворкинг..."></textarea></label>
<label>Ценность для участника <textarea name="positioning_result" rows="2" placeholder="Что получает каждый участник сообщества?"></textarea></label>
<label>Для кого создано сообщество? <textarea name="positioning_who" rows="2" placeholder="Молодые предприниматели, стартаперы, начинающие бизнесмены..."></textarea></label>
<label>Чем отличаемся от других сообществ и ассоциаций? <textarea name="positioning_difference" rows="2"></textarea></label>
<label>Форматы взаимодействия <textarea name="community_formats" rows="2" placeholder="Митапы, оффлайн-встречи, онлайн-чаты, менторские сессии..."></textarea></label>
</div>
<label>УТП <textarea name="usp" rows="3" placeholder="Уникальное торговое предложение..."></textarea></label>
</div>
</div>
<!-- Шаг 4: Аудитории и каналы -->
<div class="survey-step" data-step="4" hidden>
<h2>Шаг 4 — Аудитории и каналы</h2>
<div class="form">
<label>Идеальные подписчики / клиенты <span class="form-hint">По строкам</span>
<textarea name="ideal_audiences" rows="5" placeholder="Владельцы МСП...&#10;IT-специалисты...&#10;Активная молодёжь..."></textarea>
</label>
<div>
<strong>Каналы коммуникации</strong>
<table class="table" id="channelsTable">
<thead><tr><th>Платформа</th><th>Тип</th><th>Частота</th><th>Описание</th><th></th></tr></thead>
<tbody id="channelsBody"></tbody>
</table>
<button type="button" class="copy-btn" onclick="addChannel()">+ Добавить канал</button>
</div>
</div>
</div>
<!-- Шаг 5: Визуальная идентичность -->
<div class="survey-step" data-step="5" hidden>
<h2>Шаг 5 — Визуальная идентичность</h2>
<div class="form">
<label>Концепция <input type="text" name="visual_concept" placeholder="Напр. Цифровой архитектор"></label>
<h3>Цветовая палитра</h3>
<label>Основной цвет — название <input type="text" name="color_main_name" placeholder="Напр. Глубокий антрацитово-синий"></label>
<label>Основной цвет — HEX <input type="text" name="color_main_hex" placeholder="#0A1A2F"></label>
<label>Акцент 1 — название <input type="text" name="color_accent1_name" placeholder="Напр. Электрический бирюзовый"></label>
<label>Акцент 1 — HEX <input type="text" name="color_accent1_hex" placeholder="#00E5CC"></label>
<label>Акцент 2 — название <input type="text" name="color_accent2_name" placeholder="Напр. Терракотовый"></label>
<label>Акцент 2 — HEX <input type="text" name="color_accent2_hex" placeholder="#FF6B4A"></label>
<label>Нейтральная база <input type="text" name="color_neutral" placeholder="Напр. Светло-серый, белый, графит"></label>
<h3>Типографика</h3>
<label>Заголовки <input type="text" name="typography_headings" placeholder="Напр. Montserrat / Gilroy"></label>
<label>Текст <input type="text" name="typography_body" placeholder="Напр. Open Sans / Inter"></label>
<label>Акценты <input type="text" name="typography_accents" placeholder="Напр. Roboto Mono"></label>
</div>
</div>
<!-- Шаг 6: Tone of Voice -->
<div class="survey-step" data-step="6" hidden>
<h2>Шаг 6 — Tone of Voice</h2>
<div class="form">
<label>Описание тона <input type="text" name="tov_description" placeholder="Напр. Стратег-прагматик с душой"></label>
<label>Референсы / вдохновители <textarea name="tov_references" rows="3" placeholder="Напр. Уверенность Лаврова, глубина Гео, яркость Минаева..."></textarea></label>
<label>Оси ToV <span class="form-hint">По строкам</span>
<textarea name="tov_axes" rows="4" placeholder="1) Энергичная уверенность&#10;2) Структурированная спонтанность&#10;3) Искренность&#10;4) Ответственный взгляд лидера"></textarea>
</label>
</div>
</div>
<!-- Шаг 7: Контент-стратегия -->
<div class="survey-step" data-step="7" hidden>
<h2>Шаг 7 — Контент-стратегия: Три столпа</h2>
<div class="form">
<!-- Личный бренд -->
<div class="brand-fields-personal">
<h3>Столп 1 — «Кто я» (Личность, Доверие)</h3>
<label>Название <input type="text" name="pillar1_name" value="Кто я"></label>
</div>
<div class="brand-fields-organization" style="display:none">
<h3>Столп 1 — «О продукте» (Компания, Доверие)</h3>
<label>Название <input type="text" name="pillar1_name" value="О продукте"></label>
</div>
<div class="brand-fields-community" style="display:none">
<h3>Столп 1 — «О миссии» (Сообщество, Ценности)</h3>
<label>Название <input type="text" name="pillar1_name" value="О миссии"></label>
</div>
<label>Цель <textarea name="pillar1_goal" rows="2" placeholder="Создать эмоциональную связь..."></textarea></label>
<label>Ключевые форматы <span class="form-hint">По строкам</span>
<textarea name="pillar1_formats" rows="3" class="pillar1-formats-hint" placeholder="История пути...&#10;Закулисье...&#10;Общественная деятельность..."></textarea>
</label>
<label>Частота <input type="text" name="pillar1_frequency" value="30%"></label>
<!-- Личный бренд -->
<div class="brand-fields-personal">
<h3>Столп 2 — «Решаю боль» (Экспертиза, Польза)</h3>
<label>Название <input type="text" name="pillar2_name" value="Решаю боль"></label>
</div>
<div class="brand-fields-organization" style="display:none">
<h3>Столп 2 — «Экспертиза» (Знания, Авторитет)</h3>
<label>Название <input type="text" name="pillar2_name" value="Экспертиза"></label>
</div>
<div class="brand-fields-community" style="display:none">
<h3>Столп 2 — «Мероприятия» (Активности, Результат)</h3>
<label>Название <input type="text" name="pillar2_name" value="Мероприятия"></label>
</div>
<label>Цель <textarea name="pillar2_goal" rows="2" placeholder="Закрепить статус эксперта..."></textarea></label>
<label>Ключевые форматы <span class="form-hint">По строкам</span>
<textarea name="pillar2_formats" rows="3" class="pillar2-formats-hint" placeholder="Экспертные разборы...&#10;Практические советы...&#10;Кейсы..."></textarea>
</label>
<label>Частота <input type="text" name="pillar2_frequency" value="50%"></label>
<!-- Личный бренд -->
<div class="brand-fields-personal">
<h3>Столп 3 — «Втягиваю» (Вовлечение, Диалог)</h3>
<label>Название <input type="text" name="pillar3_name" value="Втягиваю"></label>
</div>
<div class="brand-fields-organization" style="display:none">
<h3>Столп 3 — «Вовлечение» (Сообщество, Лояльность)</h3>
<label>Название <input type="text" name="pillar3_name" value="Вовлечение"></label>
</div>
<div class="brand-fields-community" style="display:none">
<h3>Столп 3 — «Участники» (Истории, Вовлечение)</h3>
<label>Название <input type="text" name="pillar3_name" value="Участники"></label>
</div>
<label>Цель <textarea name="pillar3_goal" rows="2" placeholder="Превратить пассивную аудиторию в активное сообщество..."></textarea></label>
<label>Ключевые форматы <span class="form-hint">По строкам</span>
<textarea name="pillar3_formats" rows="3" class="pillar3-formats-hint" placeholder="Опросы...&#10;Прямые эфиры...&#10;Интерактивные челленджи..."></textarea>
</label>
<label>Частота <input type="text" name="pillar3_frequency" value="20%"></label>
<label>Особые форматы <textarea name="special_formats" rows="2" placeholder="Напр. Живые видеокружочки..."></textarea></label>
</div>
</div>
<!-- Шаг 8: Воронка и продукты -->
<div class="survey-step" data-step="8" hidden>
<h2>Шаг 8 — Воронка и продуктовый ряд</h2>
<div class="form">
<h3>Воронка вовлечения</h3>
<div id="funnelContainer">
<!-- 5 этапов -->
</div>
<h3>Продуктовый ряд</h3>
<label>Входная точка (Lead Magnet) <textarea name="product_lead_magnet" rows="2" placeholder="Напр. Бесплатный PDF..."></textarea></label>
<label>Самая простая услуга <textarea name="product_simple" rows="2"></textarea></label>
<label>Услуга-конвертер <textarea name="product_converter" rows="2"></textarea></label>
<label>Комплексные решения <textarea name="product_complex" rows="2"></textarea></label>
<label>Экспертные продукты <textarea name="product_expert" rows="2"></textarea></label>
</div>
</div>
<!-- Шаг 9: Метрики -->
<div class="survey-step" data-step="9" hidden>
<h2>Шаг 9 — Метрики</h2>
<div class="form">
<label>Количественные метрики <span class="form-hint">По строкам, формат: Название | Целевое значение | Измерение</span>
<textarea name="metrics_quantitative" rows="5" placeholder="Подписчики в Telegram | 10000 | Статистика канала&#10;Ежемесячный охват | 250000 | Аналитика соцсетей"></textarea>
</label>
<label>Качественные метрики <span class="form-hint">Тот же формат</span>
<textarea name="metrics_qualitative" rows="5" placeholder="Уровень вовлеченности (ER) | > 7% | Формула&#10;Количество приглашений как спикера | 5-7 в год | Календарь"></textarea>
</label>
<label>Коммерческие метрики <span class="form-hint">Тот же формат</span>
<textarea name="metrics_commercial" rows="5" placeholder="Заявки с пометкой «от Кирилла» | 5 в месяц | CRM&#10;Конверсия из лида в продажу | 20% | Отчёт"></textarea>
</label>
</div>
</div>
<!-- Шаг 10: Риски и рекомендации -->
<div class="survey-step" data-step="10" hidden>
<h2>Шаг 10 — Риски и рекомендации</h2>
<div class="form">
<label>Потенциальные риски <span class="form-hint">По строкам, формат: Риск | Решение</span>
<textarea name="risks" rows="5" placeholder="Выгорание от ежедневного контента | Заранее создать банк контента...&#10;Конфликт образов | Связать через общую миссию..."></textarea>
</label>
<label>Рекомендации <span class="form-hint">По строкам</span>
<textarea name="recommendations" rows="5" placeholder="Быть консистентным...&#10;Делегировать техническую часть...&#10;Измерять и адаптироваться..."></textarea>
</label>
</div>
</div>
<!-- Шаг 11: Архетипы -->
<div class="survey-step" data-step="11" hidden>
<h2>Шаг 11 — Архетипический образ</h2>
<div class="form">
<h3>Ключевой архетип</h3>
<label>Название <input type="text" name="archetype_key_name" placeholder="Напр. Ведущий-Герой"></label>
<label>Суть <textarea name="archetype_key_essence" rows="3"></textarea></label>
<label>Проявления <span class="form-hint">По строкам</span>
<textarea name="archetype_key_manifestations" rows="5" placeholder="Миссия: ...&#10;Роли: ...&#10;Действия: ...&#10;Принцип: ...&#10;Визуальные коды: ..."></textarea>
</label>
<h3>Вспомогательные архетипы</h3>
<label>Вспомогательные архетипы <span class="form-hint">По строкам, формат: Название | Суть | Проявления</span>
<textarea name="archetype_supporting" rows="5" placeholder="Творец | Инноватор... | Создание экосистемы...&#10;Заботливый | Альтруист... | Помощь молодым..."></textarea>
</label>
<label>Синтез архетипов <textarea name="archetype_synthesis" rows="3" placeholder="Итоговый образ..."></textarea></label>
<label>Таблица проявлений <span class="form-hint">По строкам, формат: Ситуация | Архетип | Как выглядит</span>
<textarea name="archetype_table" rows="5" placeholder="Выступление на форуме | Ведущий-Герой | Чёткая структура...&#10;Разбор провала | Творец+Герой | Анализ ошибок..."></textarea>
</label>
</div>
</div>
<!-- Шаг 12: Промпт -->
<div class="survey-step" data-step="12" hidden>
<h2>Шаг 12 — Промпт для генерации контента</h2>
<div class="form">
<div class="brand-fields-personal">
<label>Контекст и личность <textarea name="prompt_context" rows="5" placeholder="Кто я, мой бренд, мои роли..."></textarea></label>
</div>
<div class="brand-fields-organization" style="display:none">
<label>Контекст организации <textarea name="prompt_context" rows="5" placeholder="Наша компания, миссия, ключевые ценности, роль в отрасли..."></textarea></label>
</div>
<div class="brand-fields-community" style="display:none">
<label>Контекст сообщества <textarea name="prompt_context" rows="5" placeholder="Миссия, ценности и форматы деятельности нашего сообщества..."></textarea></label>
</div>
<label>Tone of Voice <textarea name="prompt_tov" rows="4" placeholder="Мой голос — это..."></textarea></label>
<label>Структура мысли <textarea name="prompt_structure" rows="4" placeholder="Зацепка → Тезис → Аргументация → Личный угол → Призыв..."></textarea></label>
<label>Контент-направления <span class="form-hint">По строкам</span>
<textarea name="prompt_directions" rows="4" placeholder="IT и бизнес...&#10;Предпринимательство...&#10;Развитие региона...&#10;Личностное развитие..."></textarea>
</label>
<label>Форматы и шаблоны <textarea name="prompt_formats" rows="5" placeholder="Экспертный пост...&#10;Личный пост...&#10;Вовлекающий пост..."></textarea></label>
<label>Технические требования <textarea name="prompt_technical" rows="4" placeholder="Для Telegram: 3-7 абзацев..."></textarea></label>
</div>
</div>
<!-- Навигация -->
<div class="survey-nav">
<button type="button" id="btnPrev" onclick="prevStep()" hidden>← Назад</button>
<span id="stepCounter"></span>
<button type="button" id="btnNext" onclick="nextStep()">Далее →</button>
<button type="submit" id="btnSubmit" hidden>💾 Сохранить и сгенерировать документ</button>
</div>
</form>
</div>
<script>
const TOTAL_STEPS = 12;
let currentStep = 1;
const FUNNEL_STAGES = [
{name: "Этап 0: ОСВЕДОМЛЕННОСТЬ", content: "", cta: ""},
{name: "Этап 1: ИНТЕРЕС", content: "", cta: ""},
{name: "Этап 2: РАССМОТРЕНИЕ", content: "", cta: ""},
{name: "Этап 3: РЕШЕНИЕ", content: "", cta: ""},
{name: "КЛИЕНТ (Покупка)", content: "", cta: ""},
];
// --- Переключение типа бренда ---
function toggleBrandType() {
var type = document.getElementById("brandType").value;
var isPersonal = (type === "personal");
var isOrg = (type === "organization");
var isCommunity = (type === "community");
document.querySelectorAll(".brand-fields-personal").forEach(function(el) {
el.style.display = isPersonal ? "" : "none";
});
document.querySelectorAll(".brand-fields-organization").forEach(function(el) {
el.style.display = isOrg ? "" : "none";
});
document.querySelectorAll(".brand-fields-community").forEach(function(el) {
el.style.display = isCommunity ? "" : "none";
});
document.getElementById("fieldsPersonal").style.display = isPersonal ? "" : "none";
document.getElementById("fieldsOrganization").style.display = isOrg ? "" : "none";
document.getElementById("fieldsCommunity").style.display = isCommunity ? "" : "none";
updatePillarHints(type);
}
function updatePillarHints(type) {
var p1 = document.querySelector(".pillar1-formats-hint");
var p2 = document.querySelector(".pillar2-formats-hint");
var p3 = document.querySelector(".pillar3-formats-hint");
if (type === "community") {
if (p1) p1.placeholder = "Миссия сообщества...\\nИстория создания...\\nЦенности и принципы...";
if (p2) p2.placeholder = "Мероприятия и активности...\\nОбзоры встреч...\\nИтоги и анонсы...";
if (p3) p3.placeholder = "Истории участников...\\nОтзывы и кейсы...\\nПриглашения к участию...";
} else if (type === "organization") {
if (p1) p1.placeholder = "Обзор продукта...\\nКейсы клиентов...\\nНовинки компании...";
if (p2) p2.placeholder = "Экспертные статьи...\\nАналитика рынка...\\nРазборы технологий...";
if (p3) p3.placeholder = "Опросы...\\nРозыгрыши...\\nОтзывы клиентов...";
} else {
if (p1) p1.placeholder = "История пути...\\nЗакулисье...\\nОбщественная деятельность...";
if (p2) p2.placeholder = "Экспертные разборы...\\nПрактические советы...\\nКейсы...";
if (p3) p3.placeholder = "Опросы...\\nПрямые эфиры...\\nИнтерактивные челленджи...";
}
}
// --- Инициализация воронки ---
function initFunnel() {
const container = document.getElementById("funnelContainer");
container.innerHTML = "";
FUNNEL_STAGES.forEach((stage, i) => {
container.innerHTML += `
<div class="funnel-stage">
<label><strong>${stage.name}</strong></label>
<label>Контент <input type="text" name="funnel_${i}_content" placeholder="Какой контент на этом этапе"></label>
<label>CTA <input type="text" name="funnel_${i}_cta" placeholder="Призыв к действию"></label>
<input type="hidden" name="funnel_${i}_name" value="${stage.name}">
</div>`;
});
}
// --- Динамические каналы ---
let channelIdx = 0;
function addChannel(platform = "", type = "", frequency = "", description = "") {
const tbody = document.getElementById("channelsBody");
const tr = document.createElement("tr");
tr.innerHTML = `
<td><input type="text" name="channels_${channelIdx}_platform" value="${platform}" placeholder="Напр. Telegram-канал"></td>
<td><input type="text" name="channels_${channelIdx}_type" value="${type}" placeholder="Основной / доп."></td>
<td><input type="text" name="channels_${channelIdx}_frequency" value="${frequency}" placeholder="Напр. 1 пост в день"></td>
<td><input type="text" name="channels_${channelIdx}_description" value="${description}" placeholder="Описание"></td>
<td><button type="button" class="button-link button-link-danger" onclick="this.closest('tr').remove()">✕</button></td>`;
tbody.appendChild(tr);
channelIdx++;
}
// --- Навигация ---
function showStep(n) {
document.querySelectorAll(".survey-step").forEach(el => el.hidden = true);
const el = document.querySelector(`.survey-step[data-step="${n}"]`);
if (el) el.hidden = false;
document.getElementById("btnPrev").hidden = (n === 1);
document.getElementById("btnNext").hidden = (n === TOTAL_STEPS);
document.getElementById("btnSubmit").hidden = (n !== TOTAL_STEPS);
document.getElementById("stepCounter").textContent = `Шаг ${n} из ${TOTAL_STEPS}`;
// Прогресс
const pct = ((n - 1) / (TOTAL_STEPS - 1)) * 100;
document.getElementById("progressBar").style.width = pct + "%";
// Индикатор
const ind = document.getElementById("stepsIndicator");
ind.innerHTML = "";
for (let i = 1; i <= TOTAL_STEPS; i++) {
const dot = document.createElement("span");
dot.className = "step-dot" + (i === n ? " active" : "") + (i < n ? " done" : "");
dot.textContent = i;
dot.onclick = () => showStep(i);
ind.appendChild(dot);
}
window.scrollTo(0, 0);
}
function nextStep() {
if (currentStep < TOTAL_STEPS) { currentStep++; showStep(currentStep); }
}
function prevStep() {
if (currentStep > 1) { currentStep--; showStep(currentStep); }
}
// --- Сборка данных ---
function collectData() {
const fd = new FormData(document.getElementById("surveyForm"));
const data = {};
for (const [key, val] of fd.entries()) {
data[key] = val;
}
// Каналы
const channels = [];
for (let i = 0; i < channelIdx + 10; i++) {
const p = data[`channels_${i}_platform`];
if (p && p.trim()) {
channels.push({
platform: p,
type: data[`channels_${i}_type`] || "",
frequency: data[`channels_${i}_frequency`] || "",
description: data[`channels_${i}_description`] || "",
});
}
}
data["channels"] = channels;
// Воронка
const funnel = [];
for (let i = 0; i < FUNNEL_STAGES.length; i++) {
funnel.push({
name: data[`funnel_${i}_name`] || FUNNEL_STAGES[i].name,
content: data[`funnel_${i}_content`] || "",
cta: data[`funnel_${i}_cta`] || "",
});
}
data["funnel"] = funnel;
return data;
}
// --- Отправка ---
document.getElementById("surveyForm").addEventListener("submit", async function(e) {
e.preventDefault();
const data = collectData();
var brandType = data.brand_type || "personal";
var title = (brandType === "organization")
? (data.org_name || "Без названия")
: (data.client_name || "Без названия");
const btn = document.getElementById("btnSubmit");
btn.disabled = true;
btn.textContent = "⏳ Генерация...";
try {
const resp = await fetch("/survey/submit", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({title: title, data: data}),
});
if (resp.ok) {
const result = await resp.json();
window.location.href = "/surveys/" + result.id;
} else {
alert("Ошибка при сохранении: " + resp.status);
btn.disabled = false;
btn.textContent = "💾 Сохранить и сгенерировать документ";
}
} catch (err) {
alert("Ошибка сети: " + err);
btn.disabled = false;
btn.textContent = "💾 Сохранить и сгенерировать документ";
}
});
// --- Старт ---
initFunnel();
addChannel("Telegram-канал", "Основной", "1 пост в день", "Глубокий контент, экспертиза");
addChannel("ВКвидео / Рутуб", "Доп.", "1 видео в неделю", "Длинные разборы, интервью");
showStep(1);
</script>
{% endblock %}
@@ -0,0 +1,26 @@
{% extends "base.html" %}
{% block title %}Загрузить медиа-образ — Личный кабинет{% endblock %}
{% block content %}
<h1>Загрузить медиа-образ</h1>
<div class="card">
<p>Загрузите готовый медиа-образ файлом — он будет автоматически разобран и сохранён.</p>
<p class="form-hint">Поддерживаются два формата:</p>
<ul>
<li><strong>.docx</strong> — документ стратегии (как образец). Разбирается через ИИ: извлекаются личность, УТП, Tone of Voice, столпы контента, архетипы и все поля опроса.</li>
<li><strong>.json</strong> — структурированные данные опроса (прямой импорт полей).</li>
</ul>
</div>
<form method="post" action="/survey/import" enctype="multipart/form-data" class="form">
<label>Файл медиа-образа
<input type="file" name="file" accept=".docx,.json" required>
</label>
<button type="submit">📤 Загрузить</button>
</form>
<div class="post-actions">
<a href="/survey" class="copy-btn">➕ Заполнить форму вручную</a>
<a href="/surveys" class="copy-btn">← К списку медиа-образов</a>
</div>
{% endblock %}
@@ -0,0 +1,35 @@
{% extends "base.html" %}
{% block title %}Медиа-образы — Личный кабинет{% endblock %}
{% block content %}
<h1>Медиа-образы</h1>
<div class="post-actions" style="margin-bottom:15px">
<a class="tile" href="/survey">➕ Создать новый</a>
<a class="tile" href="/survey/import">📤 Загрузить из файла</a>
</div>
{% if surveys %}
<table class="table">
<thead>
<tr><th>Название</th><th>Дата</th><th></th></tr>
</thead>
<tbody>
{% for s in surveys %}
<tr>
<td><a href="/surveys/{{ s.id }}">{{ s.title }}</a></td>
<td>{{ s.created_at }}</td>
<td>
<a href="/surveys/{{ s.id }}/export" class="button-link">📄 .docx</a>
<form action="/surveys/{{ s.id }}/delete" method="post" class="inline-form" onsubmit="return confirm('Удалить медиа-образ?')">
<button type="submit" class="button-link button-link-danger">🗑</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="card">
<p>Пока нет ни одного медиа-образа. <a href="/survey">Создайте первый →</a></p>
</div>
{% endif %}
{% endblock %}
+201
View File
@@ -0,0 +1,201 @@
{% extends "base.html" %}
{% block title %}{{ survey.title }} — Личный кабинет{% endblock %}
{% block content %}
<h1>{{ survey.title }}</h1>
<div class="post-actions">
<a href="/surveys/{{ survey.id }}/export" class="copy-btn">📄 Скачать .docx</a>
<a href="/survey" class="copy-btn">➕ Новый опрос</a>
<a href="/surveys" class="copy-btn">← К списку</a>
<form action="/surveys/{{ survey.id }}/delete" method="post" class="inline-form" onsubmit="return confirm('Удалить медиа-образ?')">
<button type="submit" class="button-link button-link-danger">🗑 Удалить</button>
</form>
</div>
<div class="card">
<p><strong>Дата создания:</strong> {{ survey.created_at }}</p>
</div>
{% set d = survey.data %}
<!-- 1. Резюме и цели -->
<div class="card">
<h2>1. Резюме и цели</h2>
<p><strong>Клиент:</strong> {{ d.client_name }} ({{ d.client_role }}), {{ d.client_location }}</p>
<p><strong>Текущая ситуация:</strong> {{ d.current_situation }}</p>
<h3>Цели на 12 месяцев:</h3>
<ul>
<li><strong>Аудитория:</strong> {{ d.goal_audience_number }} подписчиков в {{ d.goal_audience_platform }} ({{ d.goal_audience_description }})</li>
<li><strong>Экспертный статус:</strong> {{ d.goal_expert_field }}, {{ d.goal_expert_speaking }} выступлений/год</li>
<li><strong>Коммерциализация:</strong> {{ d.goal_commercial_leads }} заявок/мес ({{ d.goal_commercial_services }})</li>
<li><strong>Контент:</strong> {{ d.goal_content_system }}</li>
</ul>
</div>
<!-- 2. Позиционирование -->
<div class="card">
<h2>2. Позиционирование и УТП</h2>
<p><strong>Сущность бренда:</strong> {{ d.brand_essence }}</p>
<p><strong>Кому помогаю:</strong> {{ d.positioning_who }}</p>
<p><strong>Проблема:</strong> {{ d.positioning_problem }}</p>
<p><strong>Результат:</strong> {{ d.positioning_result }}</p>
<p><strong>УТП:</strong> {{ d.usp }}</p>
</div>
<!-- 3. Аудитории и каналы -->
<div class="card">
<h2>3. Аудитории и каналы</h2>
<h3>Идеальные подписчики:</h3>
<ul>
{% for line in (d.ideal_audiences or '').split('\n') if line.strip() %}
<li>{{ line.strip() }}</li>
{% endfor %}
</ul>
{% if d.channels %}
<h3>Каналы:</h3>
<table class="table">
<thead><tr><th>Платформа</th><th>Тип</th><th>Частота</th><th>Описание</th></tr></thead>
<tbody>
{% for ch in d.channels %}
<tr><td>{{ ch.platform }}</td><td>{{ ch.type }}</td><td>{{ ch.frequency }}</td><td>{{ ch.description }}</td></tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
<!-- 4. Визуальная идентичность -->
<div class="card">
<h2>4. Визуальная идентичность</h2>
<p><strong>Концепция:</strong> {{ d.visual_concept }}</p>
<h3>Цвета:</h3>
<ul>
<li>Основной: {{ d.color_main_name }} ({{ d.color_main_hex }})</li>
<li>Акцент 1: {{ d.color_accent1_name }} ({{ d.color_accent1_hex }})</li>
<li>Акцент 2: {{ d.color_accent2_name }} ({{ d.color_accent2_hex }})</li>
<li>Нейтральная база: {{ d.color_neutral }}</li>
</ul>
<h3>Типографика:</h3>
<ul>
<li>Заголовки: {{ d.typography_headings }}</li>
<li>Текст: {{ d.typography_body }}</li>
<li>Акценты: {{ d.typography_accents }}</li>
</ul>
</div>
<!-- 5. ToV -->
<div class="card">
<h2>5. Tone of Voice</h2>
<p><strong>Описание:</strong> {{ d.tov_description }}</p>
<p><strong>Референсы:</strong> {{ d.tov_references }}</p>
{% if d.tov_axes %}
<h3>Оси ToV:</h3>
<ul>
{% for line in d.tov_axes.split('\n') if line.strip() %}
<li>{{ line.strip() }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
<!-- 6. Контент-стратегия -->
<div class="card">
<h2>6. Контент-стратегия: Три столпа</h2>
<table class="table">
<thead><tr><th>Столп</th><th>Цель</th><th>Форматы</th><th>Частота</th></tr></thead>
<tbody>
<tr><td>{{ d.pillar1_name }}</td><td>{{ d.pillar1_goal }}</td><td>{{ d.pillar1_formats }}</td><td>{{ d.pillar1_frequency }}</td></tr>
<tr><td>{{ d.pillar2_name }}</td><td>{{ d.pillar2_goal }}</td><td>{{ d.pillar2_formats }}</td><td>{{ d.pillar2_frequency }}</td></tr>
<tr><td>{{ d.pillar3_name }}</td><td>{{ d.pillar3_goal }}</td><td>{{ d.pillar3_formats }}</td><td>{{ d.pillar3_frequency }}</td></tr>
</tbody>
</table>
{% if d.special_formats %}
<p><strong>Особые форматы:</strong> {{ d.special_formats }}</p>
{% endif %}
</div>
<!-- 7. Воронка и продукты -->
<div class="card">
<h2>7. Воронка и продуктовый ряд</h2>
{% if d.funnel %}
<table class="table">
<thead><tr><th>Этап</th><th>Контент</th><th>CTA</th></tr></thead>
<tbody>
{% for s in d.funnel %}
<tr><td>{{ s.name }}</td><td>{{ s.content }}</td><td>{{ s.cta }}</td></tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<h3>Продуктовый ряд:</h3>
<ul>
<li><strong>Lead Magnet:</strong> {{ d.product_lead_magnet }}</li>
<li><strong>Простая услуга:</strong> {{ d.product_simple }}</li>
<li><strong>Услуга-конвертер:</strong> {{ d.product_converter }}</li>
<li><strong>Комплексные решения:</strong> {{ d.product_complex }}</li>
<li><strong>Экспертные продукты:</strong> {{ d.product_expert }}</li>
</ul>
</div>
<!-- 8. Метрики -->
<div class="card">
<h2>8. Метрики</h2>
{% if d.metrics_quantitative %}
<h3>Количественные:</h3>
<pre class="result">{{ d.metrics_quantitative }}</pre>
{% endif %}
{% if d.metrics_qualitative %}
<h3>Качественные:</h3>
<pre class="result">{{ d.metrics_qualitative }}</pre>
{% endif %}
{% if d.metrics_commercial %}
<h3>Коммерческие:</h3>
<pre class="result">{{ d.metrics_commercial }}</pre>
{% endif %}
</div>
<!-- 9. Риски -->
<div class="card">
<h2>9. Риски и рекомендации</h2>
{% if d.risks %}
<h3>Риски:</h3>
<pre class="result">{{ d.risks }}</pre>
{% endif %}
{% if d.recommendations %}
<h3>Рекомендации:</h3>
<pre class="result">{{ d.recommendations }}</pre>
{% endif %}
</div>
<!-- 10. Архетипы -->
<div class="card">
<h2>10. Архетипический образ</h2>
<p><strong>Ключевой архетип:</strong> {{ d.archetype_key_name }}</p>
<p><strong>Суть:</strong> {{ d.archetype_key_essence }}</p>
{% if d.archetype_key_manifestations %}
<pre class="result">{{ d.archetype_key_manifestations }}</pre>
{% endif %}
{% if d.archetype_supporting %}
<h3>Вспомогательные:</h3>
<pre class="result">{{ d.archetype_supporting }}</pre>
{% endif %}
{% if d.archetype_synthesis %}
<h3>Синтез:</h3>
<p>{{ d.archetype_synthesis }}</p>
{% endif %}
{% if d.archetype_table %}
<h3>Таблица проявлений:</h3>
<pre class="result">{{ d.archetype_table }}</pre>
{% endif %}
</div>
<!-- 11. Промпт -->
<div class="card">
<h2>11. Промпт для генерации контента</h2>
{% if d.prompt_context %}<p><strong>Контекст:</strong> {{ d.prompt_context }}</p>{% endif %}
{% if d.prompt_tov %}<p><strong>ToV:</strong> {{ d.prompt_tov }}</p>{% endif %}
{% if d.prompt_structure %}<p><strong>Структура:</strong> {{ d.prompt_structure }}</p>{% endif %}
{% if d.prompt_directions %}<p><strong>Направления:</strong></p><pre class="result">{{ d.prompt_directions }}</pre>{% endif %}
{% if d.prompt_formats %}<p><strong>Форматы:</strong></p><pre class="result">{{ d.prompt_formats }}</pre>{% endif %}
{% if d.prompt_technical %}<p><strong>Тех. требования:</strong></p><pre class="result">{{ d.prompt_technical }}</pre>{% endif %}
</div>
{% endblock %}