'use client'; import { motion } from 'framer-motion'; import { AudioLines, Box, Download, Mic, MoreHorizontal, Pencil, Server, Sparkles, Speaker, Star, Trash2, Volume2, Wand2, } from 'lucide-react'; import { useCallback, useEffect, useRef, useState } from 'react'; import { LandingAudioPlayer, unlockAudioContext } from './LandingAudioPlayer'; // ─── Data ─────────────────────────────────────────────────────────────────── // Edit this section to customise all the content shown in the ControlUI demo. interface VoiceProfile { name: string; description: string; language: string; hasEffects: boolean; } /** Voice profiles shown in the grid / scroll strip. Index matters — DemoScript references profiles by index. */ const PROFILES: VoiceProfile[] = [ { name: 'Jarvis', description: 'Dry wit, composed British AI assistant', language: 'en', hasEffects: true, }, { name: 'Samuel L. Jackson', description: 'Commanding intensity with sharp, punchy delivery', language: 'en', hasEffects: true, }, { name: 'Bob Ross', description: 'Gentle, soothing voice full of quiet encouragement', language: 'en', hasEffects: false, }, { name: 'Sam Altman', description: 'Measured, thoughtful Silicon Valley cadence', language: 'en', hasEffects: false, }, { name: 'Morgan Freeman', description: 'Rich, warm baritone with gravitas and calm authority', language: 'en', hasEffects: false, }, { name: 'Linus Tech Tips', description: 'Enthusiastic, fast-paced tech explainer energy', language: 'en', hasEffects: false, }, { name: 'Fireship', description: 'Rapid-fire, deadpan tech humor with zero filler', language: 'en', hasEffects: false, }, { name: 'Scarlett Johansson', description: 'Smooth, low alto with understated warmth', language: 'en', hasEffects: false, }, { name: 'Dario Amodei', description: 'Calm, precise articulation with academic depth', language: 'en', hasEffects: false, }, { name: 'David Attenborough', description: 'Warm, reverent narration with wonder and precision', language: 'en', hasEffects: false, }, { name: 'Zendaya', description: 'Relaxed, modern delivery with effortless cool', language: 'en', hasEffects: false, }, { name: 'Barack Obama', description: 'Measured cadence with rhythmic pauses and gravitas', language: 'en', hasEffects: false, }, ]; /** Each entry is one cycle of the demo animation: select a profile → type text → generate → play audio. */ interface DemoStep { profileIndex: number; text: string; audioUrl: string; engine: string; duration: string; effect?: string; } const DEMO_SCRIPT: DemoStep[] = [ { profileIndex: 0, text: 'Sir, I have completed the analysis. Your code has twelve critical vulnerabilities, your coffee is cold, and frankly your commit messages could use some work.', audioUrl: '/audio/jarvis.webm', engine: 'Qwen 1.7B', duration: '0:10', effect: 'Robot', }, { profileIndex: 4, text: "I've narrated penguins, galaxies, and the entire history of mankind. But nothing prepared me for the moment a computer learned to do my job from a five second audio clip.", audioUrl: '/audio/morganfreeman.webm', engine: 'Qwen 1.7B', duration: '0:11', effect: 'Radio', }, { profileIndex: 3, text: "Open source? [laugh] What's that?", audioUrl: '/audio/samaltman.webm', engine: 'Chatterbox', duration: '0:03', }, { profileIndex: 1, text: "So let me get this straight. You downloaded an app, pressed a button, and now there's two of me? The world was not ready for one", audioUrl: '/audio/samjackson.webm', engine: 'Qwen 1.7B', duration: '0:10', }, { profileIndex: 5, text: "So we got this voice cloning software and honestly it's kind of terrifying. Like, my wife could not tell the difference. Voicebox dot s h, link in the description!", audioUrl: '/audio/linus.webm', engine: 'Qwen 1.7B', duration: '0:11', }, { profileIndex: 6, text: 'This is Voicebox in one hundred seconds. It clones voices locally, it runs on your GPU, and no, OpenAI cannot hear you. Lets go.', audioUrl: '/audio/fireship.webm', engine: 'Qwen 0.6B', duration: '0:09', }, ]; /** History rows pre-populated on first load. Oldest first visually (array index 0 = top row). */ interface Generation { id: number; profileName: string; text: string; language: string; engine: string; duration: string; timeAgo: string; favorited: boolean; versions: number; } const INITIAL_GENERATIONS: Generation[] = [ { id: 1, profileName: 'Morgan Freeman', text: 'The neural pathways of human speech contain more complexity than any language model can fully capture, yet we keep pushing the boundaries of what is possible.', language: 'en', engine: 'Qwen 1.7B', duration: '0:08', timeAgo: '2 minutes ago', favorited: true, versions: 3, }, { id: 2, profileName: 'Samuel L. Jackson', text: 'In a world increasingly shaped by artificial intelligence, the human voice remains our most powerful tool for connection and storytelling.', language: 'en', engine: 'Qwen 1.7B', duration: '0:07', timeAgo: '15 minutes ago', favorited: false, versions: 1, }, { id: 3, profileName: 'Jarvis', text: 'The architecture of modern text-to-speech systems reveals an elegant interplay between transformer models and acoustic feature prediction.', language: 'en', engine: 'Qwen 0.6B', duration: '0:09', timeAgo: '1 hour ago', favorited: false, versions: 2, }, { id: 4, profileName: 'Bob Ross', text: 'Welcome to the next chapter. Every great story begins with a single voice, and today that voice can be yours.', language: 'en', engine: 'Chatterbox', duration: '0:06', timeAgo: '3 hours ago', favorited: true, versions: 1, }, { id: 5, profileName: 'Linus Tech Tips', text: 'Local inference gives you complete control over your voice data. No cloud, no subscriptions, no compromises.', language: 'en', engine: 'Qwen 1.7B', duration: '0:05', timeAgo: '5 hours ago', favorited: false, versions: 1, }, ]; const SIDEBAR_ITEMS = [ { icon: Volume2, label: 'Generate' }, { icon: AudioLines, label: 'Stories' }, { icon: Mic, label: 'Voices' }, { icon: Wand2, label: 'Effects' }, { icon: Speaker, label: 'Audio' }, { icon: Box, label: 'Models' }, { icon: Server, label: 'Server' }, ]; // ─── Phase system ─────────────────────────────────────────────────────────── type Phase = 'idle' | 'selecting' | 'typing' | 'generating' | 'complete' | 'playing'; const PHASE_DURATIONS: Record = { idle: 2500, selecting: 800, typing: 6000, generating: 2800, complete: 1200, playing: 4000, }; // ─── Typewriter ───────────────────────────────────────────────────────────── function TypewriterText({ text, speed }: { text: string; speed?: number }) { // Default: fill the typing phase duration, leaving 500ms buffer at the end const resolvedSpeed = speed ?? Math.max(20, Math.floor((PHASE_DURATIONS.typing - 500) / text.length)); const [displayed, setDisplayed] = useState(''); const indexRef = useRef(0); useEffect(() => { indexRef.current = 0; setDisplayed(''); const interval = setInterval(() => { indexRef.current += 1; if (indexRef.current <= text.length) { setDisplayed(text.slice(0, indexRef.current)); } else { clearInterval(interval); } }, resolvedSpeed); return () => clearInterval(interval); }, [text, resolvedSpeed]); return ( <> {displayed} ); } // ─── Loading bars (simplified react-loaders replacement) ──────────────────── function LoadingBars({ mode }: { mode: 'idle' | 'generating' | 'playing' }) { const barColor = mode !== 'idle' ? 'bg-accent' : 'bg-muted-foreground/40'; return (
{[0, 1, 2, 3, 4].map((i) => ( ))}
); } // ─── Profile Card ─────────────────────────────────────────────────────────── const ProfileCard = ({ profile, selected, selecting, cardRef, }: { profile: VoiceProfile; selected: boolean; selecting: boolean; cardRef?: React.Ref; }) => { return (
{profile.name}
{profile.description}
{profile.language} {profile.hasEffects && }
); }; // ─── History Row ──────────────────────────────────────────────────────────── function HistoryRow({ gen, mode, isNew, }: { gen: Generation; mode: 'idle' | 'generating' | 'playing'; isNew: boolean; }) { return (
{/* Status icon */}
{/* Meta info */}
{gen.profileName}
{gen.language} {gen.engine} {mode !== 'generating' && {gen.duration}}
{mode === 'generating' ? ( Generating... ) : ( gen.timeAgo )}
{/* Transcript */}
{gen.text}
{/* Action buttons */}
{gen.versions > 1 && ( )}
); } // ─── Floating Generate Box ────────────────────────────────────────────────── function FloatingGenerateBox({ phase, typingText, selectedProfile, engine, effect, }: { phase: Phase; typingText: string; selectedProfile: VoiceProfile | null; engine: string; effect?: string; }) { const isFocused = phase === 'typing' || phase === 'generating'; const isGenerating = phase === 'generating'; return ( {/* Text area + generate button */}
{phase === 'typing' ? ( ) : phase === 'generating' ? ( {typingText} ) : ( {selectedProfile ? `Generate speech using ${selectedProfile.name}...` : 'Select a voice profile above...'} )}
{/* Generate button */}
{/* Bottom selectors */}
English {engine} {effect || 'Effect'}
); } // ─── Main ControlUI ───────────────────────────────────────────────────────── export function ControlUI() { const [phase, setPhase] = useState('idle'); const [selectedIndex, setSelectedIndex] = useState(DEMO_SCRIPT[0].profileIndex); const [cycle, setCycle] = useState(0); const [newGenId, setNewGenId] = useState(null); const [generations, setGenerations] = useState([...INITIAL_GENERATIONS]); const [isMuted, setIsMuted] = useState(true); const [isVisible, setIsVisible] = useState(true); const [pageHidden, setPageHidden] = useState(false); const containerRef = useRef(null); const phaseRef = useRef(phase); const mobileCardRefs = useRef>(new Map()); const desktopCardRefs = useRef>(new Map()); const profileGridRef = useRef(null); const [scrollLeft, setScrollLeft] = useState(0); phaseRef.current = phase; const step = DEMO_SCRIPT[cycle % DEMO_SCRIPT.length]; const selectedProfile = PROFILES[selectedIndex]; // Scroll to selected profile card — accounts for generate box overlay on desktop useEffect(() => { const isMobile = window.innerWidth < 768; if (isMobile) { const el = mobileCardRefs.current.get(selectedIndex); if (el) el.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }); return; } // Desktop const el = desktopCardRefs.current.get(selectedIndex); const scrollContainer = profileGridRef.current; if (!el || !scrollContainer) return; const containerTop = scrollContainer.getBoundingClientRect().top; const elTop = el.getBoundingClientRect().top; const elRelTop = elTop - containerTop + scrollContainer.scrollTop; const rowHeight = 145; const generateBoxHeight = 200; const visibleTop = scrollContainer.scrollTop; const visibleBottom = visibleTop + scrollContainer.clientHeight - generateBoxHeight; const elRelBottom = elRelTop + el.offsetHeight; if (elRelTop >= visibleTop && elRelBottom <= visibleBottom) { return; } const target = elRelTop - rowHeight; scrollContainer.scrollTo({ top: Math.max(0, target), behavior: 'smooth' }); }, [selectedIndex]); // Visibility detection useEffect(() => { const observer = new IntersectionObserver(([entry]) => setIsVisible(entry.isIntersecting), { threshold: 0, }); if (containerRef.current) observer.observe(containerRef.current); const handleVisibility = () => setPageHidden(document.visibilityState !== 'visible'); document.addEventListener('visibilitychange', handleVisibility); return () => { observer.disconnect(); document.removeEventListener('visibilitychange', handleVisibility); }; }, []); const paused = !isVisible || pageHidden; // Phase cycling — `playing` phase is driven by audio finish, not a timeout useEffect(() => { if (paused || phase === 'playing') return; const duration = PHASE_DURATIONS[phase]; const timer = setTimeout(() => { console.log( '[ControlUI] phase transition', phase, '→ next, cycle:', cycle, 'step profile:', PROFILES[step.profileIndex].name, ); switch (phase) { case 'idle': { setSelectedIndex(step.profileIndex); setPhase('selecting'); break; } case 'selecting': setPhase('typing'); break; case 'typing': { const profile = PROFILES[step.profileIndex]; const newGen: Generation = { id: Date.now(), profileName: profile.name, text: step.text, language: profile.language, engine: step.engine, duration: step.duration, timeAgo: 'just now', favorited: false, versions: 1, }; setGenerations((prev) => [newGen, ...prev.slice(0, 5)]); setNewGenId(newGen.id); setPhase('generating'); break; } case 'generating': setPhase('playing'); break; } }, duration); return () => clearTimeout(timer); }, [phase, paused, step, cycle]); const handleAudioFinish = useCallback(() => { if (phaseRef.current !== 'playing') return; setPhase('idle'); setCycle((c) => c + 1); setNewGenId(null); }, []); const isGenerating = phase === 'generating'; return (
{/* Unmute button with handwritten hint */}
{/* Handwritten hint — absolutely positioned above the button */} {isMuted && ( try me! {/* Curved arrow from text down-right toward the button */} )}
{/* ── Sidebar (hidden on mobile) ─────────────────────────── */}
{/* Logo */}
{/* Nav items */}
{SIDEBAR_ITEMS.map((item, i) => { const Icon = item.icon; const active = i === 0; return (
); })}
{/* Version */}
v0.2.0
{/* ── Main content ──────────────────────────────────────── */}
{/* Left: Profiles + Generate box */}
{/* Gradient fade overlay — sits between header and scroll content */}
{/* Header — floats above everything */}

Voicebox

{/* Scrollable profile cards — scrolls behind header + gradient */}
{/* Mobile: horizontal scroll strip with edge fade */}
{scrollLeft > 0 && (
)}
setScrollLeft(e.currentTarget.scrollLeft)} > {PROFILES.map((profile, i) => (
{ if (el) mobileCardRefs.current.set(i, el); }} >
))}
{/* Desktop: 3-col grid */}
{PROFILES.map((profile, i) => ( { if (el) desktopCardRefs.current.set(i, el); }} /> ))}
{/* Floating generate box — desktop: absolute overlay, mobile: inline */}
{/* Right/Below: History */}
{generations.map((gen) => { const isThisNew = gen.id === newGenId; const rowMode: 'idle' | 'generating' | 'playing' = isThisNew && isGenerating ? 'generating' : isThisNew && phase === 'playing' ? 'playing' : 'idle'; return ; })}
{/* Audio player */} {}} />
); }