import { useState, type FormEvent, type KeyboardEvent } from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faCircleNotch, faWandMagicSparkles, faFire, faMicrophone, faMusic, } from "@fortawesome/free-solid-svg-icons"; import type { InputValues, Language, Vocals } from "../lib/types"; import { formatElapsed, useElapsed } from "../lib/useElapsed"; import { randomStyle } from "../lib/llm"; import { useI18n } from "../lib/i18n"; export type { InputValues }; const LANGUAGES: { value: Language; label: string }[] = [ { value: "English", label: "English" }, { value: "Deutsch", label: "Deutsch" }, { value: "Español", label: "Español" }, { value: "Français", label: "Français" }, { value: "Italiano", label: "Italiano" }, { value: "Português", label: "Português" }, { value: "Polski", label: "Polski" }, { value: "Czech", label: "Čeština" }, { value: "Other", label: "Other…" }, ]; // Curated "Surprise me" pairs — coherent idea + matching style const SURPRISE_PAIRS: { idea: string; style: string; mood: string; vocals: Vocals }[] = [ { idea: "A melancholic lo-fi house beat for a rainy Sunday morning", style: "lo-fi, downtempo, warm vinyl crackle, 85 BPM", mood: "melancholic", vocals: "instrumental" }, { idea: "An epic orchestral fantasy theme for a dragon rider", style: "cinematic orchestra, soaring strings, choir, 120 BPM", mood: "epic", vocals: "instrumental" }, { idea: "A dreamy synthwave love song about neon city nights", style: "synthwave, retro arpeggios, lush reverb, 100 BPM", mood: "dreamy", vocals: "vocals" }, { idea: "A summer beach pop anthem with a massive singalong chorus", style: "upbeat pop, electric guitar, catchy hooks, 128 BPM", mood: "euphoric", vocals: "vocals" }, { idea: "A tense cinematic underscore for a space station emergency", style: "dark ambient, pulsing synths, tension pads, 90 BPM", mood: "tense", vocals: "instrumental" }, { idea: "A jazzy bossa nova track about sipping coffee in Paris", style: "bossa nova, acoustic guitar, warm brass, 110 BPM", mood: "relaxed", vocals: "vocals" }, { idea: "A dark folk ballad about a lighthouse keeper lost at sea", style: "dark folk, acoustic guitar, haunting vocals, 72 BPM", mood: "longing", vocals: "vocals" }, { idea: "A bouncy chiptune adventure theme for a retro platformer", style: "chiptune, 8-bit, NES style, upbeat, 140 BPM", mood: "playful", vocals: "instrumental" }, ]; // Curated "Go crazy" pairs — wild, absurd, unexpected combos const CRAZY_PAIRS: { idea: string; style: string; mood: string; vocals: Vocals }[] = [ { idea: "A death metal ode to a sentient washing machine who dreams of becoming a DJ", style: "death metal, heavy blast beats, distorted guitar, 180 BPM", mood: "aggressive", vocals: "vocals" }, { idea: "An operatic trap banger about a medieval wizard discovering TikTok", style: "operatic trap, 808s, harpsichord, 140 BPM", mood: "chaotic", vocals: "vocals" }, { idea: "A romantic waltz between a robot and a cactus on Mars", style: "classical waltz, electronic glitch, orchestral, 90 BPM", mood: "bittersweet", vocals: "instrumental" }, { idea: "A gospel choir celebrating the invention of bubble wrap", style: "gospel, full choir, claps, euphoric brass, 120 BPM", mood: "joyful", vocals: "vocals" }, { idea: "An avant-garde jazz piece where all instruments are kitchen utensils", style: "experimental jazz, freeform, atonal, 0 BPM (undefined)", mood: "chaotic", vocals: "instrumental" }, { idea: "A synthpop ballad about a time-traveling accountant in ancient Rome", style: "80s synthpop, catchy arpeggios, reverb, 118 BPM", mood: "quirky", vocals: "vocals" }, { idea: "A Viking drinking song remixed by an alien DJ", style: "folk metal, hypnotic alien synths, epic choir, 160 BPM", mood: "wild", vocals: "vocals" }, { idea: "A lullaby for a grumpy dragon who cannot fall asleep", style: "ambient lullaby, gentle bells, warm pads, 60 BPM", mood: "tender", vocals: "vocals" }, ]; interface InputPanelProps { values: InputValues; onChange: (next: InputValues) => void; onSubmit: () => void; loading: boolean; disabled?: boolean; cancelButton?: React.ReactNode; } export function InputPanel({ values, onChange, onSubmit, loading, disabled, cancelButton, }: InputPanelProps) { const [styleLoading, setStyleLoading] = useState(null); const elapsed = useElapsed(loading); const { t } = useI18n(); const set = (key: K, value: InputValues[K]) => onChange({ ...values, [key]: value }); const canSubmit = !loading && !disabled && values.idea.trim().length > 0; const submit = (e?: FormEvent) => { e?.preventDefault(); if (canSubmit) onSubmit(); }; const onCmdEnter = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key === "Enter") { e.preventDefault(); submit(); } }; // "Surprise me" fills style. If idea is empty, fills a random idea too. const handleSurpriseMe = async () => { if (styleLoading) return; const hasIdea = values.idea.trim().length > 0; const ex = SURPRISE_PAIRS[Math.floor(Math.random() * SURPRISE_PAIRS.length)]; const ideaToUse = hasIdea ? values.idea : ex.idea; const ctrl = new AbortController(); setStyleLoading("normal"); try { const style = await randomStyle("normal", ctrl.signal, ideaToUse); onChange({ ...values, idea: ideaToUse, style_hint: style, mood: hasIdea ? values.mood : ex.mood, vocals: hasIdea ? values.vocals : ex.vocals }); } catch (err) { if (err instanceof DOMException && err.name === "AbortError") return; onChange({ ...values, idea: ideaToUse, style_hint: hasIdea ? values.style_hint : ex.style, mood: hasIdea ? values.mood : ex.mood, vocals: hasIdea ? values.vocals : ex.vocals }); } finally { setStyleLoading((curr) => (curr === "normal" ? null : curr)); } }; // "Go crazy" fills style wildly. If idea is empty, fills a wild idea too. const handleGoCrazy = async () => { if (styleLoading) return; const hasIdea = values.idea.trim().length > 0; const ex = CRAZY_PAIRS[Math.floor(Math.random() * CRAZY_PAIRS.length)]; const ideaToUse = hasIdea ? values.idea : ex.idea; const ctrl = new AbortController(); setStyleLoading("crazy"); try { const style = await randomStyle("crazy", ctrl.signal, ideaToUse); onChange({ ...values, idea: ideaToUse, style_hint: style, mood: hasIdea ? values.mood : ex.mood, vocals: hasIdea ? values.vocals : ex.vocals }); } catch (err) { if (err instanceof DOMException && err.name === "AbortError") return; onChange({ ...values, idea: ideaToUse, style_hint: hasIdea ? values.style_hint : ex.style, mood: hasIdea ? values.mood : ex.mood, vocals: hasIdea ? values.vocals : ex.vocals }); } finally { setStyleLoading((curr) => (curr === "crazy" ? null : curr)); } }; const anyLoading = styleLoading !== null; return (
{/* Music Idea */}