mirror of
https://github.com/JezzWTF/vibepod.git
synced 2026-06-13 03:58:07 +00:00
feat: add studio roadmap and streaming cleanup
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
"use client";
|
||||
|
||||
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;
|
||||
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<ServerStatus, "online">,
|
||||
{ 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." },
|
||||
};
|
||||
|
||||
export default function GenerationControls({
|
||||
speaker,
|
||||
availableVoices,
|
||||
onSpeakerChange,
|
||||
cfgScale,
|
||||
onCfgScaleChange,
|
||||
inferenceSteps,
|
||||
onInferenceStepsChange,
|
||||
onGenerate,
|
||||
onStop,
|
||||
onPauseStream,
|
||||
onResumeStream,
|
||||
isStreamPaused,
|
||||
isGenerating,
|
||||
genElapsed,
|
||||
genPct,
|
||||
wordCount,
|
||||
serverStatus,
|
||||
downloadProgress,
|
||||
}: GenerationControlsProps) {
|
||||
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 (
|
||||
<div
|
||||
className="rounded-xl border p-5 flex flex-col gap-5"
|
||||
style={{ background: "var(--card-bg)", borderColor: "var(--border)" }}
|
||||
>
|
||||
<h2
|
||||
className="text-sm font-semibold uppercase tracking-wider"
|
||||
style={{ color: "var(--accent-teal)" }}
|
||||
>
|
||||
Generation Settings
|
||||
</h2>
|
||||
|
||||
{/* Voice selector */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium" style={{ color: "var(--foreground)" }}>
|
||||
Voice
|
||||
</label>
|
||||
<select
|
||||
value={speaker}
|
||||
onChange={(e) => onSpeakerChange(e.target.value)}
|
||||
disabled={!serverReady}
|
||||
className="w-full px-3 py-2 rounded-lg text-sm font-medium appearance-none cursor-pointer disabled:cursor-not-allowed"
|
||||
style={{
|
||||
background: "var(--background)",
|
||||
border: "1px solid var(--border)",
|
||||
color: serverReady ? "var(--foreground)" : "var(--muted)",
|
||||
}}
|
||||
>
|
||||
{voices.map((v) => (
|
||||
<option key={v} value={v}>
|
||||
{v.charAt(0).toUpperCase() + v.slice(1)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* CFG Scale slider */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm font-medium" style={{ color: "var(--foreground)" }}>
|
||||
Voice Expressiveness
|
||||
</label>
|
||||
<span
|
||||
className="text-sm font-mono px-2 py-0.5 rounded"
|
||||
style={{ background: "var(--background)", color: "var(--accent-teal)" }}
|
||||
>
|
||||
{cfgScale.toFixed(1)}
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min={0.5}
|
||||
max={4.0}
|
||||
step={0.1}
|
||||
value={cfgScale}
|
||||
onChange={(e) => onCfgScaleChange(parseFloat(e.target.value))}
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="flex items-center justify-between text-xs" style={{ color: "var(--muted)" }}>
|
||||
<span>Flat (0.5)</span>
|
||||
<span>CFG Scale</span>
|
||||
<span>Expressive (4.0)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Inference Steps slider */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm font-medium" style={{ color: "var(--foreground)" }}>
|
||||
Quality vs Speed
|
||||
</label>
|
||||
<span
|
||||
className="text-sm font-mono px-2 py-0.5 rounded"
|
||||
style={{ background: "var(--background)", color: "var(--accent-violet)" }}
|
||||
>
|
||||
{inferenceSteps}
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min={5}
|
||||
max={20}
|
||||
step={1}
|
||||
value={inferenceSteps}
|
||||
onChange={(e) => onInferenceStepsChange(parseInt(e.target.value, 10))}
|
||||
className="w-full"
|
||||
style={{ "--thumb-color": "var(--accent-violet)" } as React.CSSProperties}
|
||||
/>
|
||||
<div className="flex items-center justify-between text-xs" style={{ color: "var(--muted)" }}>
|
||||
<span>Faster (5)</span>
|
||||
<span>Diffusion Steps</span>
|
||||
<span>Better (20)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Server status banner */}
|
||||
{!serverReady && (
|
||||
<div
|
||||
className="flex flex-col gap-2 px-3 py-3 rounded-lg text-sm"
|
||||
style={{ background: "var(--background)", border: "1px solid var(--border)" }}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={`w-2 h-2 rounded-full flex-shrink-0 ${serverStatus === "offline" || serverStatus === "error" ? "" : "animate-pulse"}`}
|
||||
style={{ background: STATUS_CONFIG[serverStatus].color }}
|
||||
/>
|
||||
<span style={{ color: STATUS_CONFIG[serverStatus].color }}>
|
||||
{STATUS_CONFIG[serverStatus].label(downloadProgress)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{serverStatus === "downloading" && (
|
||||
<div className="w-full rounded-full h-1.5 overflow-hidden" style={{ background: "var(--border)" }}>
|
||||
<div
|
||||
className="h-1.5 rounded-full transition-all duration-500"
|
||||
style={{
|
||||
width: `${downloadPct}%`,
|
||||
background: "linear-gradient(90deg, #60a5fa, var(--accent-teal))",
|
||||
minWidth: downloadPct > 0 ? "4px" : "0",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{serverStatus === "loading" && (
|
||||
<div className="w-full rounded-full h-1.5 overflow-hidden" style={{ background: "var(--border)" }}>
|
||||
<div
|
||||
className="h-1.5 rounded-full animate-pulse"
|
||||
style={{ width: "60%", background: "linear-gradient(90deg, #fbbf24, var(--accent-teal))" }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Generation progress bar */}
|
||||
{isGenerating && (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="flex items-center justify-between text-xs" style={{ color: "var(--muted)" }}>
|
||||
<span>{genElapsed}s elapsed</span>
|
||||
<span>{genPct !== null ? `${genPct}%` : "starting..."}</span>
|
||||
</div>
|
||||
<div className="w-full rounded-full h-1.5 overflow-hidden" style={{ background: "var(--border)" }}>
|
||||
<div
|
||||
className="h-1.5 rounded-full transition-all duration-500"
|
||||
style={{
|
||||
width: genPct !== null ? `${genPct}%` : "0%",
|
||||
background: "linear-gradient(90deg, var(--accent-teal), var(--accent-violet))",
|
||||
minWidth: genPct !== null && genPct > 0 ? "4px" : "0",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Generate / Stop buttons */}
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={onGenerate}
|
||||
disabled={buttonDisabled}
|
||||
className="flex-1 py-3 rounded-xl font-semibold text-sm transition-all cursor-pointer disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||
style={
|
||||
buttonDisabled
|
||||
? { background: "var(--border)", color: "var(--muted)" }
|
||||
: {
|
||||
background: "linear-gradient(135deg, var(--accent-teal-dim), var(--accent-violet-dim))",
|
||||
color: "#fff",
|
||||
boxShadow: "0 4px 15px rgba(45, 212, 191, 0.2)",
|
||||
}
|
||||
}
|
||||
>
|
||||
{isGenerating ? (
|
||||
<>
|
||||
<svg className="animate-spin w-4 h-4" viewBox="0 0 24 24" fill="none">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
Generating...
|
||||
</>
|
||||
) : !serverReady ? (
|
||||
<>
|
||||
<svg className="animate-spin w-4 h-4" viewBox="0 0 24 24" fill="none">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
{serverStatus === "downloading" ? "Downloading model..." : "Waiting for server..."}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<polygon points="5 3 19 12 5 21 5 3" />
|
||||
</svg>
|
||||
Generate Audio
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{isGenerating && (
|
||||
<>
|
||||
<button
|
||||
onClick={isStreamPaused ? onResumeStream : onPauseStream}
|
||||
className="px-4 py-3 rounded-xl font-semibold text-sm transition-all cursor-pointer flex items-center justify-center gap-1.5"
|
||||
style={{
|
||||
background: "var(--background)",
|
||||
border: `1px solid ${isStreamPaused ? "var(--accent-teal)" : "#fbbf24"}`,
|
||||
color: isStreamPaused ? "var(--accent-teal)" : "#fbbf24",
|
||||
}}
|
||||
>
|
||||
{isStreamPaused ? (
|
||||
<>
|
||||
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
|
||||
<polygon points="5 3 19 12 5 21 5 3" />
|
||||
</svg>
|
||||
Resume
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
|
||||
<rect x="6" y="4" width="4" height="16" />
|
||||
<rect x="14" y="4" width="4" height="16" />
|
||||
</svg>
|
||||
Pause
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={onStop}
|
||||
className="px-4 py-3 rounded-xl font-semibold text-sm transition-all cursor-pointer flex items-center justify-center gap-1.5"
|
||||
style={{
|
||||
background: "var(--background)",
|
||||
border: "1px solid var(--error)",
|
||||
color: "var(--error)",
|
||||
}}
|
||||
>
|
||||
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
|
||||
<rect x="4" y="4" width="16" height="16" rx="2" />
|
||||
</svg>
|
||||
Stop
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user