ab4e340db9
/onboarding: 3-шаговый вайзард (платформа → название/ниша → готово)
login/page.js: новый пользователь → /onboarding, существующий → /
TopicBank.js: просмотр/пополнение/добавление/удаление тем
ChannelEdit AI-стиль: TopicBank компонент внизу вкладки
channels/new: при 402 CHANNEL_LIMIT_REACHED → ошибка + redirect /plans
lib/engine.js: ENGINE_URL дефолт 3040 → 3030
API routes: /api/topics-bank/[channelId]/{refill,add}, /item/[id]
127 lines
4.7 KiB
JavaScript
127 lines
4.7 KiB
JavaScript
'use client';
|
|
import { useState, useEffect } from 'react';
|
|
import { RefreshCw, Plus, Trash2, Loader2, Lightbulb, X } from 'lucide-react';
|
|
|
|
export default function TopicBank({ channelId }) {
|
|
const [topics, setTopics] = useState([]);
|
|
const [total, setTotal] = useState(0);
|
|
const [loading, setLoading] = useState(true);
|
|
const [refilling,setRefilling]= useState(false);
|
|
const [newTopic, setNewTopic] = useState('');
|
|
const [adding, setAdding] = useState(false);
|
|
const [showAdd, setShowAdd] = useState(false);
|
|
|
|
async function load() {
|
|
setLoading(true);
|
|
try {
|
|
const res = await fetch(`/api/topics-bank/${channelId}`).then(r => r.json());
|
|
setTopics(res.topics || []);
|
|
setTotal(res.total_unused || 0);
|
|
} catch {}
|
|
setLoading(false);
|
|
}
|
|
|
|
useEffect(() => { load(); }, [channelId]);
|
|
|
|
async function refill() {
|
|
setRefilling(true);
|
|
await fetch(`/api/topics-bank/${channelId}/refill`, { method: 'POST' });
|
|
await load();
|
|
setRefilling(false);
|
|
}
|
|
|
|
async function addManual() {
|
|
if (!newTopic.trim()) return;
|
|
setAdding(true);
|
|
await fetch(`/api/topics-bank/${channelId}/add`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ topics: [newTopic.trim()] }),
|
|
});
|
|
setNewTopic('');
|
|
setShowAdd(false);
|
|
await load();
|
|
setAdding(false);
|
|
}
|
|
|
|
async function deleteTopic(id) {
|
|
await fetch(`/api/topics-bank/item/${id}`, { method: 'DELETE' });
|
|
setTopics(t => t.filter(x => x.id !== id));
|
|
setTotal(n => Math.max(0, n - 1));
|
|
}
|
|
|
|
const unusedTopics = topics.filter(t => !t.is_used);
|
|
const stockColor = total >= 5 ? 'text-green-400' : total > 0 ? 'text-yellow-400' : 'text-red-400';
|
|
|
|
return (
|
|
<div className="card p-5 space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h3 className="font-semibold text-sm flex items-center gap-2">
|
|
<Lightbulb className="w-4 h-4 text-accent" /> Банк тем
|
|
</h3>
|
|
<p className={`text-xs mt-0.5 ${stockColor}`}>
|
|
{total === 0 ? 'Темы закончились — нужно пополнить' : `${total} тем в запасе`}
|
|
</p>
|
|
</div>
|
|
<div className="flex gap-1.5">
|
|
<button onClick={() => setShowAdd(v => !v)} className="btn-ghost p-2" title="Добавить тему">
|
|
<Plus className="w-4 h-4" />
|
|
</button>
|
|
<button onClick={refill} disabled={refilling} className="btn-ghost p-2" title="Сгенерировать темы AI">
|
|
<RefreshCw className={`w-4 h-4 ${refilling ? 'animate-spin text-accent' : ''}`} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Добавить вручную */}
|
|
{showAdd && (
|
|
<div className="flex gap-2">
|
|
<input
|
|
value={newTopic}
|
|
onChange={e => setNewTopic(e.target.value)}
|
|
onKeyDown={e => e.key === 'Enter' && addManual()}
|
|
placeholder="Введите тему поста..."
|
|
className="input flex-1 text-sm py-1.5"
|
|
autoFocus
|
|
/>
|
|
<button onClick={addManual} disabled={adding || !newTopic.trim()} className="btn-primary px-3 py-1.5 text-sm">
|
|
{adding ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : 'Добавить'}
|
|
</button>
|
|
<button onClick={() => { setShowAdd(false); setNewTopic(''); }} className="btn-ghost p-2">
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{loading && <div className="py-4 text-center"><Loader2 className="w-4 h-4 animate-spin mx-auto text-accent" /></div>}
|
|
|
|
{!loading && unusedTopics.length === 0 && (
|
|
<div className="py-4 text-center text-sm text-gray-500">
|
|
Тем нет. Нажмите ↻ чтобы сгенерировать AI
|
|
</div>
|
|
)}
|
|
|
|
{!loading && unusedTopics.length > 0 && (
|
|
<ul className="space-y-1.5 max-h-64 overflow-y-auto">
|
|
{unusedTopics.map(t => (
|
|
<li key={t.id} className="flex items-start gap-2 text-sm group">
|
|
<span className="flex-1 text-gray-300 leading-snug">{t.topic}</span>
|
|
<button
|
|
onClick={() => deleteTopic(t.id)}
|
|
className="opacity-0 group-hover:opacity-100 btn-ghost p-1 shrink-0 text-gray-500 hover:text-red-400 transition-opacity"
|
|
>
|
|
<Trash2 className="w-3 h-3" />
|
|
</button>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
|
|
<p className="text-xs text-gray-500">
|
|
Темы используются при автоматической генерации постов. При запасе <5 — AI пополняет автоматически.
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|