import { useState } from 'react' import { api, runJob } from '../api' import { useJob, ProgressBox, ErrorBox } from '../hooks' import PromptPanel from '../components/PromptPanel' export default function TopicsStep({ data, refreshProject, projectId, selectReel }) { const [count, setCount] = useState(8) const [picked, setPicked] = useState(null) const [editing, setEditing] = useState(null) const [editTopic, setEditTopic] = useState(null) // {title, reason, popularity_signal} const [prompt, setPrompt] = useState(null) const [creating, setCreating] = useState(false) const job = useJob() const topics = data.topics?.topics || [] const generate = async () => { try { await job.start(`/api/projects/${projectId}/topics/generate`, { n: Number(count), prompt }) await refreshProject() setPicked(null) } catch { /* shown */ } } // A legacy single-string signal looks like: // "Source ('quote'), Other ('q1', 'q2'), r/sub ('quote')" // Split only on ")," boundaries between sources, so commas *inside* the // parenthetical quotes don't split. Falls back to the whole string. const splitLegacy = (str) => { const parts = str.split(/\)\s*,\s*/) return parts .map((p, i) => (i < parts.length - 1 ? p + ')' : p).trim()) .filter(Boolean) } // Signals may arrive as the new list, a one-item list still holding a // combined string, or the legacy single string. Split EVERY entry on the // "), " source boundaries (idempotent: already-clean items pass through). const signalsOf = (t) => { const raw = (t.signals && t.signals.length) ? t.signals : (t.popularity_signal ? [t.popularity_signal] : []) return raw.flatMap((s) => splitLegacy(String(s))) } const startEdit = (i) => { setEditing(i); setEditTopic({ ...topics[i], signals: signalsOf(topics[i]) }) } const saveEdit = async (i) => { // Clean up signal lines (drop blanks/whitespace) only at save time. const clean = { ...editTopic, signals: (editTopic.signals || []).map((s) => s.trim()).filter(Boolean) } const updated = topics.map((t, j) => (j === i ? { ...t, ...clean } : t)) await api.put(`/api/projects/${projectId}/topics`, { topics: updated }) await refreshProject() setEditing(null) } const addCustom = async () => { const fresh = { title: "New topic", reason: "added manually", signals: [] } await api.put(`/api/projects/${projectId}/topics`, { topics: [fresh, ...topics] }) await refreshProject() setEditing(0) setEditTopic({ ...fresh }) } const removeTopic = async (i) => { const updated = topics.filter((_, j) => j !== i) await api.put(`/api/projects/${projectId}/topics`, { topics: updated }) await refreshProject() setPicked(null) } // Create a reel for the chosen topic and open it on the Script step, // where the user can QC/edit the script prompt before generating. const useTopic = async () => { setCreating(true) try { const reel = await api.post(`/api/projects/${projectId}/reels`, { topic: topics[picked].title }) await refreshProject() selectReel(reel.id) } catch (e) { job.setError(e.message) } finally { setCreating(false) } } return (

Topics

Ranked from your reference channels' top Shorts plus news and Reddit signals. Edit any topic inline, then turn one into a reel.

(await runJob(`/api/projects/${projectId}/topics/prompt`, { n: Number(count) })).prompt} />
{job.running && } {(topics.length > 0 || data.topics) && (
Topic pool ({topics.length})
{topics.map((t, i) => (
setPicked(i)} style={{ marginTop: 5 }} />
{editing === i ? (