"use client"; import { useState } from "react"; import type { ServerStatus, DownloadProgress } from "@/app/page"; const FALLBACK_VOICES = ["carter", "davis", "emma", "frank", "grace", "mike"]; interface GenerationControlsProps { speaker: string; availableVoices: string[]; onSpeakerChange: (v: string) => void; cfgScale: number; onCfgScaleChange: (v: number) => void; inferenceSteps: number; onInferenceStepsChange: (v: number) => void; prebufferSecs: number; onPrebufferSecsChange: (v: number) => void; rebufferThresholdSecs: number; onRebufferThresholdChange: (v: number) => void; resumeThresholdSecs: number; onResumeThresholdChange: (v: number) => void; onGenerate: () => void; onStop: () => void; onPauseStream: () => void; onResumeStream: () => void; isStreamPaused: boolean; isGenerating: boolean; genElapsed: number; genPct: number | null; wordCount: number; serverStatus: ServerStatus; downloadProgress: DownloadProgress | null; } const STATUS_CONFIG: Record< Exclude, { color: string; label: (p: DownloadProgress | null) => string } > = { offline: { color: "var(--error)", label: () => "Server offline — waiting for connection..." }, downloading: { color: "#60a5fa", label: (p) => p && p.total > 0 ? `Downloading model... (${p.done} / ${p.total} files)` : "Downloading model (~1 GB)..." }, loading: { color: "#fbbf24", label: () => "Loading model into memory..." }, error: { color: "var(--error)", label: () => "Server error — check the terminal for details." }, }; function SpinnerIcon() { return ( ); } export default function GenerationControls({ speaker, availableVoices, onSpeakerChange, cfgScale, onCfgScaleChange, inferenceSteps, onInferenceStepsChange, prebufferSecs, onPrebufferSecsChange, rebufferThresholdSecs, onRebufferThresholdChange, resumeThresholdSecs, onResumeThresholdChange, onGenerate, onStop, onPauseStream, onResumeStream, isStreamPaused, isGenerating, genElapsed, genPct, wordCount, serverStatus, downloadProgress, }: GenerationControlsProps) { const [showAdvanced, setShowAdvanced] = useState(false); const voices = availableVoices.length > 0 ? availableVoices : FALLBACK_VOICES; const serverReady = serverStatus === "online"; const buttonDisabled = isGenerating || wordCount === 0 || !serverReady; const downloadPct = downloadProgress && downloadProgress.total > 0 ? Math.round((downloadProgress.done / downloadProgress.total) * 100) : 0; return (

Generation Settings

{/* Voice selector */}
{/* CFG Scale slider */}
{cfgScale.toFixed(1)}
onCfgScaleChange(parseFloat(e.target.value))} className="w-full" />
Flat (0.5) CFG Scale Expressive (4.0)
{/* Inference Steps slider */}
{inferenceSteps}
onInferenceStepsChange(parseInt(e.target.value, 10))} className="w-full" style={{ "--thumb-color": "var(--accent-violet)" } as React.CSSProperties} />
Faster (5) Diffusion Steps Better (20)
{/* Advanced Buffering toggle */}
{showAdvanced && (
{/* Pre-buffer */}
{prebufferSecs.toFixed(1)}s
onPrebufferSecsChange(parseFloat(e.target.value))} className="w-full h-1" />
{/* Re-buffer threshold */}
{rebufferThresholdSecs.toFixed(1)}s
{ const next = parseFloat(e.target.value); onRebufferThresholdChange(next); if (resumeThresholdSecs <= next) { onResumeThresholdChange(parseFloat((next + 0.5).toFixed(1))); } }} className="w-full h-1" />
{/* Resume threshold */}
{resumeThresholdSecs.toFixed(1)}s
{ const next = parseFloat(e.target.value); if (next <= rebufferThresholdSecs) return; onResumeThresholdChange(next); }} className="w-full h-1" />
)} {/* Server status banner */} {!serverReady && (
{STATUS_CONFIG[serverStatus].label(downloadProgress)}
{serverStatus === "downloading" && (
0 ? "4px" : "0", }} />
)} {serverStatus === "loading" && (
)}
)} {/* Generation progress bar */} {isGenerating && (
{genElapsed}s elapsed {genPct !== null ? `${genPct}%` : "starting..."}
0 ? "4px" : "0", }} />
)} {/* Generate / Stop buttons */}
{isGenerating && ( <> )}
); }