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

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

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>