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
262 lines
16 KiB
HTML
262 lines
16 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ru">
|
|
<head>
|
|
<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=3">
|
|
<script>
|
|
(function () {
|
|
function isDark() {
|
|
return document.documentElement.getAttribute('data-theme') === 'dark';
|
|
}
|
|
function setIcon(btn) {
|
|
if (btn) btn.textContent = isDark() ? '\u2600\uFE0F' : '\uD83C\uDF19';
|
|
}
|
|
try {
|
|
var saved = localStorage.getItem('theme');
|
|
if (saved !== 'light') {
|
|
document.documentElement.setAttribute('data-theme', 'dark');
|
|
}
|
|
} catch (e) {}
|
|
document.addEventListener('click', function (e) {
|
|
var btn = e.target.closest('[data-theme-toggle]');
|
|
if (!btn) return;
|
|
if (isDark()) {
|
|
document.documentElement.removeAttribute('data-theme');
|
|
try { localStorage.setItem('theme', 'light'); } catch (e) {}
|
|
} else {
|
|
document.documentElement.setAttribute('data-theme', 'dark');
|
|
try { localStorage.setItem('theme', 'dark'); } catch (e) {}
|
|
}
|
|
setIcon(btn);
|
|
});
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
setIcon(document.querySelector('[data-theme-toggle]'));
|
|
});
|
|
})();
|
|
</script>
|
|
</head>
|
|
<body>
|
|
<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>
|
|
|
|
{% 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 %}
|
|
{% if errors %}
|
|
<div class="errors">
|
|
<ul>
|
|
{% for error in errors %}
|
|
<li>{{ error }}</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</div>
|
|
{% 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 ? '\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() {
|
|
var ta = document.createElement('textarea');
|
|
ta.value = text;
|
|
ta.style.position = 'fixed';
|
|
ta.style.opacity = '0';
|
|
document.body.appendChild(ta);
|
|
ta.select();
|
|
var ok = false;
|
|
try { ok = document.execCommand('copy'); } catch (e) { ok = false; }
|
|
document.body.removeChild(ta);
|
|
done(ok);
|
|
}
|
|
if (navigator.clipboard && window.isSecureContext) {
|
|
navigator.clipboard.writeText(text).then(function () { done(true); }, fallback);
|
|
} else {
|
|
fallback();
|
|
}
|
|
}
|
|
document.addEventListener('click', function (e) {
|
|
var btn = e.target.closest('[data-copy-target]');
|
|
if (!btn) return;
|
|
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>
|