"use client"; import { useMemo, useState, useTransition } from "react"; import { useRouter } from "next/navigation"; import { toast } from "sonner"; import { Loader2, Save, RefreshCw, AudioLines, Clipboard, Clock } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { updateScriptAction, regenerateAction, regenerateSectionAction, } from "@/app/(app)/episodes/actions"; interface Turn { speakerKey: string; text: string; } interface Section { id: string; title: string; turns: Turn[]; } interface Script { title: string; sections: Section[]; } // Average speaking pace used to estimate spoken duration from word count. const WORDS_PER_MINUTE = 150; function wordCount(text: string): number { const t = text.trim(); return t ? t.split(/\s+/).length : 0; } function estimateDuration(words: number): string { const totalSec = Math.round((words / WORDS_PER_MINUTE) * 60); const m = Math.floor(totalSec / 60); const s = totalSec % 60; return m > 0 ? `${m}m ${s}s` : `${s}s`; } function sectionWords(section: Section): number { return section.turns.reduce((n, t) => n + wordCount(t.text), 0); } export function ScriptEditor({ episodeId, script, speakerNames, }: { episodeId: string; script: Script; speakerNames: Record; }) { const router = useRouter(); const [sections, setSections] = useState(script.sections); const [dirty, setDirty] = useState(false); const [saving, startSave] = useTransition(); const [busySection, setBusySection] = useState(null); const [rerecording, setRerecording] = useState(false); const totalWords = useMemo( () => sections.reduce((n, s) => n + sectionWords(s), 0), [sections] ); function updateTurn(si: number, ti: number, text: string) { setSections((prev) => prev.map((s, i) => i === si ? { ...s, turns: s.turns.map((t, j) => (j === ti ? { ...t, text } : t)) } : s ) ); setDirty(true); } function save() { startSave(async () => { const res = await updateScriptAction(episodeId, { title: script.title, sections }); if (res.ok) { toast.success("Script saved"); setDirty(false); } else { toast.error(res.error ?? "Could not save"); } }); } async function regenSection(id: string) { setBusySection(id); const res = await regenerateSectionAction(episodeId, id); setBusySection(null); if (!res.ok || !res.section) { toast.error(res.error ?? "Could not regenerate"); return; } setSections((prev) => prev.map((s) => (s.id === id ? res.section! : s))); setDirty(false); toast.success("Section regenerated"); } async function rerecord() { setRerecording(true); if (dirty) { const saved = await updateScriptAction(episodeId, { title: script.title, sections }); if (!saved.ok) { toast.error(saved.error ?? "Save failed"); setRerecording(false); return; } setDirty(false); } const res = await regenerateAction(episodeId, "audio"); if (res.ok) { toast.success("Re-recording audio…"); router.refresh(); } else { toast.error(res.error ?? "Could not re-record"); setRerecording(false); } } function copyTranscript() { const lines: string[] = [script.title, ""]; for (const section of sections) { lines.push(section.title, ""); for (const turn of section.turns) { const name = speakerNames[turn.speakerKey] ?? turn.speakerKey; lines.push(`${name}: ${turn.text}`, ""); } } navigator.clipboard .writeText(lines.join("\n").trimEnd()) .then(() => toast.success("Transcript copied")) .catch(() => toast.error("Could not copy")); } return (
{/* Sticky save bar — dirty-aware. */}

Script

{totalWords.toLocaleString()} words · ~{estimateDuration(totalWords)} {dirty && ( Unsaved changes )}
{sections.map((section, si) => { const words = sectionWords(section); return (
{section.title}

{words.toLocaleString()} words · ▶ {estimateDuration(words)}

{section.turns.map((turn, ti) => (
{speakerNames[turn.speakerKey] ?? turn.speakerKey}