import { useEffect, useMemo, useRef, useState } from "react"; import type { ModelInfo } from "@/lib/api"; import { type VoiceRecord } from "@/lib/idb"; import ParamsPanel from "@/components/ParamsPanel"; import SpeakerSlot from "@/components/SpeakerSlot"; import TagBar from "@/components/TagBar"; export type DialogSubmit = { text: string; engineId: string; language?: string; params: Record; speakers: { letter: "A" | "B" | "C" | "D"; voice: VoiceRecord }[]; }; type Props = { models: ModelInfo[]; engineId: string; onEngineChange: (id: string) => void; onSubmit: (input: DialogSubmit) => void; loadingModel: boolean; busy: boolean; libraryRefreshKey?: number; }; const ALL_LETTERS = ["A", "B", "C", "D"] as const; export default function DialogComposer({ models, engineId, onEngineChange, onSubmit, loadingModel, busy, libraryRefreshKey, }: Props) { const [count, setCount] = useState(2); const [speakers, setSpeakers] = useState>({}); const [text, setText] = useState("SPEAKER A: \nSPEAKER B: \n"); const [language, setLanguage] = useState(undefined); const [params, setParams] = useState>({}); const textRef = useRef(null); const engine = useMemo(() => models.find((m) => m.id === engineId), [models, engineId]); useEffect(() => { setParams( Object.fromEntries((engine?.params ?? []).map((p) => [p.name, p.default])), ); setLanguage(engine?.languages[0]?.code); }, [engine?.id]); function setSpeaker(letter: string, v: VoiceRecord | undefined) { setSpeakers((s) => ({ ...s, [letter]: v })); } function addSpeaker() { setCount((c) => Math.min(4, c + 1)); } function removeSpeaker(letter: string) { setSpeakers((s) => ({ ...s, [letter]: undefined })); setCount((c) => Math.max(2, c - 1)); } function insertPrefix(letter: string) { const el = textRef.current; if (!el) return; const tag = `SPEAKER ${letter}: `; const start = el.selectionStart ?? el.value.length; const end = el.selectionEnd ?? start; const before = el.value.slice(0, start); const after = el.value.slice(end); const native = Object.getOwnPropertyDescriptor( window.HTMLTextAreaElement.prototype, "value", )?.set; native?.call(el, before + tag + after); el.dispatchEvent(new Event("input", { bubbles: true })); const cursor = start + tag.length; el.setSelectionRange(cursor, cursor); el.focus(); } function handleSubmit() { if (!engine) return; const speakerList: DialogSubmit["speakers"] = []; for (let i = 0; i < count; i++) { const letter = ALL_LETTERS[i]; const v = speakers[letter]; if (v) speakerList.push({ letter, voice: v }); } onSubmit({ text, engineId: engine.id, language, params, speakers: speakerList, }); } const visibleLetters = ALL_LETTERS.slice(0, count); const canSubmit = !!engine && !busy && !loadingModel && text.trim().length > 0; return (

Speakers

{visibleLetters.map((letter) => ( setSpeaker(letter, v)} onRemove={count > 2 ? () => removeSpeaker(letter) : undefined} refreshKey={libraryRefreshKey} /> ))}
{count < 4 && ( )}

Engine

{models.map((m) => ( ))}
{engine?.languages && engine.languages.length > 1 && (
)}

Script