'use client'; import { useState, useEffect, useCallback } from 'react'; import { Plus, Sparkles, RefreshCw, Loader2, Trash2, Edit3, Save, X, ChevronDown, ChevronRight, Play, Settings, Calendar, Clock, BookOpen, Zap, Tag, RotateCcw, Check, AlertCircle, } from 'lucide-react'; // ─── Константы ──────────────────────────────────────────────────────────────── const COLORS = [ { key: 'indigo', cls: 'bg-indigo-50 dark:bg-indigo-950 border-indigo-200 dark:border-indigo-800 text-indigo-700 dark:text-indigo-300' }, { key: 'emerald', cls: 'bg-emerald-50 dark:bg-emerald-950 border-emerald-200 dark:border-emerald-800 text-emerald-700 dark:text-emerald-300' }, { key: 'amber', cls: 'bg-amber-50 dark:bg-amber-950 border-amber-200 dark:border-amber-800 text-amber-700 dark:text-amber-300' }, { key: 'red', cls: 'bg-red-50 dark:bg-red-950 border-red-200 dark:border-red-800 text-red-700 dark:text-red-300' }, { key: 'purple', cls: 'bg-purple-50 dark:bg-purple-950 border-purple-200 dark:border-purple-800 text-purple-700 dark:text-purple-300' }, { key: 'blue', cls: 'bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800 text-blue-700 dark:text-blue-300' }, { key: 'cyan', cls: 'bg-cyan-50 dark:bg-cyan-950 border-cyan-200 dark:border-cyan-800 text-cyan-700 dark:text-cyan-300' }, { key: 'orange', cls: 'bg-orange-50 dark:bg-orange-950 border-orange-200 dark:border-orange-800 text-orange-700 dark:text-orange-300' }, { key: 'rose', cls: 'bg-rose-50 dark:bg-rose-950 border-rose-200 dark:border-rose-800 text-rose-700 dark:text-rose-300' }, { key: 'neutral', cls: 'bg-neutral-50 dark:bg-neutral-900 border-neutral-200 dark:border-neutral-700 text-neutral-700 dark:text-neutral-300' }, ]; const colorCls = (k) => (COLORS.find(c => c.key === k) || COLORS[0]).cls; const GENRE_LABELS = { tutorial: { label: 'Туториал', color: 'bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-300' }, comparison: { label: 'Сравнение', color: 'bg-purple-100 text-purple-700 dark:bg-purple-950 dark:text-purple-300' }, opinion: { label: 'Мнение', color: 'bg-amber-100 text-amber-700 dark:bg-amber-950 dark:text-amber-300' }, digest: { label: 'Дайджест', color: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-950 dark:text-emerald-300' }, case: { label: 'Кейс', color: 'bg-rose-100 text-rose-700 dark:bg-rose-950 dark:text-rose-300' }, news: { label: 'Новость', color: 'bg-cyan-100 text-cyan-700 dark:bg-cyan-950 dark:text-cyan-300' }, }; function detectGenre(topic) { const m = topic?.match(/^\[([^\]]+)\]/); if (!m) return null; const map = { 'ТУТОРИАЛ':'tutorial','СРАВНЕНИЕ':'comparison','МНЕНИЕ':'opinion','ДАЙДЖЕСТ':'digest','КЕЙС':'case','НОВОСТЬ':'news' }; return map[m[1].toUpperCase()] || null; } function GenreBadge({ genre }) { if (!genre) return null; const g = GENRE_LABELS[genre]; if (!g) return null; return {g.label}; } // ─── Главный компонент ──────────────────────────────────────────────────────── export default function AutogenTab({ channel }) { const [settings, setSettings] = useState(null); const [categories, setCategories] = useState([]); const [todayDrafts, setTodayDrafts] = useState([]); const [rotation, setRotation] = useState([]); const [loading, setLoading] = useState(true); const [toast, setToast] = useState(null); // Состояние создания категории const [showNewCat, setShowNewCat] = useState(false); const [newCat, setNewCat] = useState({ slug:'', name:'', icon:'📝', color:'indigo', description:'' }); const [savingCat, setSavingCat] = useState(false); // Развёрнутая категория (темы) const [expandedCatId, setExpandedCatId] = useState(null); // Настройки autogen const [showSettings, setShowSettings] = useState(false); const [autogenForm, setAutogenForm] = useState({ enabled: false, posts_per_day: 3, run_hour: 10, run_minute: 0 }); const [savingSettings, setSavingSettings] = useState(false); // Запуск генерации const [running, setRunning] = useState(false); const flash = (msg, type='success') => { setToast({ msg, type }); setTimeout(() => setToast(null), 3500); }; const load = useCallback(async () => { setLoading(true); try { const [statusRes, catsRes, draftsRes, rotRes] = await Promise.allSettled([ fetch(`/api/engine/channels/${channel.id}/autogen`), fetch(`/api/engine/channels/${channel.id}/categories`), fetch(`/api/engine/channels/${channel.id}/autogen/today`), fetch(`/api/engine/channels/${channel.id}/autogen/rotation?days=7`), ]); if (statusRes.status === 'fulfilled' && statusRes.value.ok) { const d = await statusRes.value.json(); setSettings(d); setAutogenForm({ enabled: !!d.enabled, posts_per_day: d.posts_per_day||3, run_hour: d.run_hour||10, run_minute: d.run_minute||0 }); } if (catsRes.status === 'fulfilled' && catsRes.value.ok) { const d = await catsRes.value.json(); setCategories(d.items || []); } if (draftsRes.status === 'fulfilled' && draftsRes.value.ok) { const d = await draftsRes.value.json(); setTodayDrafts(d.drafts || []); } if (rotRes.status === 'fulfilled' && rotRes.value.ok) { const d = await rotRes.value.json(); setRotation(d.preview || []); } } catch (e) { flash('Ошибка загрузки: ' + e.message, 'error'); } setLoading(false); }, [channel.id]); useEffect(() => { load(); }, [load]); // ── Сохранить настройки autogen ────────────────────────────────────────── async function saveAutogenSettings() { setSavingSettings(true); try { const r = await fetch(`/api/engine/channels/${channel.id}/autogen/enable`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(autogenForm), }); if (!r.ok) throw new Error((await r.json()).error || 'fail'); flash('Настройки сохранены'); setShowSettings(false); await load(); } catch (e) { flash(e.message, 'error'); } setSavingSettings(false); } // ── Создать категорию ───────────────────────────────────────────────────── async function createCategory() { if (!newCat.name.trim() || !newCat.slug.trim()) return; setSavingCat(true); try { const r = await fetch(`/api/engine/channels/${channel.id}/categories`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(newCat), }); const d = await r.json(); if (!r.ok) throw new Error(d.error || 'fail'); flash(`Категория «${newCat.name}» создана`); setNewCat({ slug:'', name:'', icon:'📝', color:'indigo', description:'' }); setShowNewCat(false); await load(); } catch (e) { flash(e.message, 'error'); } setSavingCat(false); } // ── Запустить генерацию вручную ──────────────────────────────────────────── async function runGeneration(categoryId = null) { setRunning(true); try { const r = await fetch(`/api/engine/channels/${channel.id}/autogen/run`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(categoryId ? { category_id: categoryId } : {}), }); if (!r.ok) throw new Error((await r.json()).error || 'fail'); flash('Генерация запущена — черновики появятся через ~30 сек'); setTimeout(() => load(), 35000); } catch (e) { flash(e.message, 'error'); } setRunning(false); } if (loading) return (
); const todaySet = new Set(settings?.categories?.filter(c => c.today_active).map(c => c.id) || []); const postsPerDay = settings?.posts_per_day || autogenForm.posts_per_day || 3; return (
{toast && (
{toast.msg}
)} {/* ── Хедер с настройками ── */}

Автогенерация

{settings?.enabled ? `Включена · ${postsPerDay} поста/день · ${String(settings.run_hour||10).padStart(2,'0')}:${String(settings.run_minute||0).padStart(2,'0')}` : 'Выключена — настрой и включи'}

{/* Форма настроек */} {showSettings && (
)}
{/* ── Планируется сегодня ── */}

Планируется сегодня

{todayDrafts.length > 0 ? `${todayDrafts.length} из ${postsPerDay} готово` : `Генерация в ${String(settings?.run_hour||10).padStart(2,'0')}:${String(settings?.run_minute||0).padStart(2,'0')}`}
{todayDrafts.length === 0 ? (
Черновики появятся после генерации
) : (
{todayDrafts.map(d => (
{d.category_icon || '📝'}
{d.content?.split('\n')[0]?.slice(0,70) || '...'}
{d.category_name && {d.category_name}} {d.genre && }
Открыть
))}
)}
{/* ── Категории ── */}

Категории контента ({categories.filter(c => c.is_active).length} активных)

{/* Форма новой категории */} {showNewCat && (
setNewCat(f => ({ ...f, icon: e.target.value }))} maxLength={4} className="input text-center text-2xl w-full py-1.5" />
setNewCat(f => ({ ...f, name: e.target.value, slug: f.slug || e.target.value.toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-|-$/g,''), }))} className="input text-sm" placeholder="Туториалы" />
setNewCat(f => ({ ...f, slug: e.target.value.toLowerCase().replace(/[^a-z0-9-]/g,'') }))} className="input text-sm font-mono" placeholder="tutorials" />
{COLORS.map(c => ( ))}
setNewCat(f => ({ ...f, description: e.target.value }))} className="input text-sm w-full" placeholder="Пошаговые гайды по AI-инструментам" />
)} {/* Список категорий */}
{categories.length === 0 && (
Категорий нет — добавь первую
)} {categories.map(cat => ( setExpandedCatId(expandedCatId === cat.id ? null : cat.id)} onRunNow={() => runGeneration(cat.id)} running={running} flash={flash} onRefresh={load} /> ))}
{/* ── Ротация на 7 дней ── */} {rotation.length > 0 && (

Ротация категорий на 7 дней

{rotation.map((day, i) => { const isToday = i === 0; return (
{isToday ? 'Сегодня' : new Date(day.date+'T12:00:00').toLocaleDateString('ru-RU',{day:'numeric',month:'short'})}
{day.categories.map(c => ( {c.icon} {c.name} ))}
); })}

Скользящее окно {postsPerDay} из {categories.filter(c=>c.is_active).length} категорий — каждый день другой набор.

)}
); } // ─── CategoryRow ─────────────────────────────────────────────────────────────── function CategoryRow({ cat, channelId, todayActive, expanded, onToggle, onRunNow, running, flash, onRefresh }) { const [topics, setTopics] = useState([]); const [loadingTopics, setLoadingTopics] = useState(false); const [newTopic, setNewTopic] = useState(''); const [addingTopic, setAddingTopic] = useState(false); const [generating, setGenerating] = useState(false); const [genCount, setGenCount] = useState(15); useEffect(() => { if (expanded && topics.length === 0) loadTopics(); }, [expanded]); async function loadTopics() { setLoadingTopics(true); try { const r = await fetch(`/api/engine/channels/${channelId}/categories/${cat.id}/topics?free=false`); const d = await r.json(); setTopics(d.topics || []); } catch {} setLoadingTopics(false); } async function addTopic() { if (!newTopic.trim()) return; setAddingTopic(true); try { const r = await fetch(`/api/engine/channels/${channelId}/categories/${cat.id}/topics`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ topic: newTopic.trim() }), }); const d = await r.json(); if (!r.ok) throw new Error(d.error); setNewTopic(''); await loadTopics(); onRefresh(); flash('Тема добавлена'); } catch (e) { flash(e.message, 'error'); } setAddingTopic(false); } async function generateTopics() { setGenerating(true); try { await fetch(`/api/engine/channels/${channelId}/categories/${cat.id}/topics/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ count: genCount }), }); flash(`AI генерирует ${genCount} тем — появятся через ~15 сек`); setTimeout(() => { loadTopics(); onRefresh(); }, 18000); } catch (e) { flash(e.message, 'error'); } setGenerating(false); } async function deleteTopic(id) { await fetch(`/api/engine/channels/${channelId}/categories/${cat.id}/topics/${id}`, { method: 'DELETE' }); setTopics(ts => ts.filter(t => t.id !== id)); onRefresh(); } async function toggleUsed(t) { await fetch(`/api/engine/channels/${channelId}/categories/${cat.id}/topics/${t.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ is_used: !t.is_used }), }); await loadTopics(); onRefresh(); } const freeCount = topics.filter(t => !t.is_used).length; return (
{/* Заголовок строки */}
{cat.icon || '📝'}
{cat.name} {cat.slug} {todayActive ? сегодня : не сегодня }
{todayActive && ( )}
{/* Раскрытая панель тем */} {expanded && (
{/* AI генерация */}
{/* Добавить тему вручную */}
setNewTopic(e.target.value)} onKeyDown={e => e.key === 'Enter' && addTopic()} placeholder="Новая тема... ([ТУТОРИАЛ] Как сделать X)" className="input text-sm flex-1" />
{/* Список тем */} {loadingTopics ? (
) : (
{topics.length === 0 && (
Тем нет — добавь или сгенерируй
)} {topics.map(t => { const genre = t.genre || detectGenre(t.topic); const cleanTopic = t.topic.replace(/^\[[^\]]+\]\s*/,''); return (
{genre && } {cleanTopic}
); })}
)}
{freeCount} свободных · {topics.length - freeCount} использованных
)}
); }