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:
1 parent
81625b2e6f
commit
c6048aeea0
37 files changed
+7037
-225
No files matched your search
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
|
||||
@@ -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,'"').replace(/\n/g,'\\n');
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 }}
|
||||
|
|
||||
<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 %}
|
||||
<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 %}
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
|
||||
@@ -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 текст... Пост 2 текст... Пост 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="Владельцы МСП... IT-специалисты... Активная молодёжь..."></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) Энергичная уверенность 2) Структурированная спонтанность 3) Искренность 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="История пути... Закулисье... Общественная деятельность..."></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="Экспертные разборы... Практические советы... Кейсы..."></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="Опросы... Прямые эфиры... Интерактивные челленджи..."></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 | Статистика канала Ежемесячный охват | 250000 | Аналитика соцсетей"></textarea>
|
||||
</label>
|
||||
<label>Качественные метрики <span class="form-hint">Тот же формат</span>
|
||||
<textarea name="metrics_qualitative" rows="5" placeholder="Уровень вовлеченности (ER) | > 7% | Формула Количество приглашений как спикера | 5-7 в год | Календарь"></textarea>
|
||||
</label>
|
||||
<label>Коммерческие метрики <span class="form-hint">Тот же формат</span>
|
||||
<textarea name="metrics_commercial" rows="5" placeholder="Заявки с пометкой «от Кирилла» | 5 в месяц | CRM Конверсия из лида в продажу | 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="Выгорание от ежедневного контента | Заранее создать банк контента... Конфликт образов | Связать через общую миссию..."></textarea>
|
||||
</label>
|
||||
<label>Рекомендации <span class="form-hint">По строкам</span>
|
||||
<textarea name="recommendations" rows="5" placeholder="Быть консистентным... Делегировать техническую часть... Измерять и адаптироваться..."></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="Миссия: ... Роли: ... Действия: ... Принцип: ... Визуальные коды: ..."></textarea>
|
||||
</label>
|
||||
<h3>Вспомогательные архетипы</h3>
|
||||
<label>Вспомогательные архетипы <span class="form-hint">По строкам, формат: Название | Суть | Проявления</span>
|
||||
<textarea name="archetype_supporting" rows="5" placeholder="Творец | Инноватор... | Создание экосистемы... Заботливый | Альтруист... | Помощь молодым..."></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="Выступление на форуме | Ведущий-Герой | Чёткая структура... Разбор провала | Творец+Герой | Анализ ошибок..."></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 и бизнес... Предпринимательство... Развитие региона... Личностное развитие..."></textarea>
|
||||
</label>
|
||||
<label>Форматы и шаблоны <textarea name="prompt_formats" rows="5" placeholder="Экспертный пост... Личный пост... Вовлекающий пост..."></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 %}
|
||||
@@ -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 %}
|
||||
Reference in new issue
Block a user