/* Studio 76 — The Imaginarium. Talk to "Eve", Studio 76's designer, using the browser's built-in speech (no external voice key needed): the browser listens, Gemini is Eve's brain (imaginarium.php?action=chat), and the browser speaks her replies. She renders the piece live with Nano Banana 2. Typed flow stays as a fallback. */ const IMAG_EXAMPLES = [ 'A fine rose-gold pendant with a single pearl — modern and minimal', 'Delicate diamond ear-climbers, contemporary, barely-there', 'A slim stacking ring with a tiny emerald, new-age and clean', 'A thread-thin gold chain with one floating sapphire', ]; const IMAG_TONES = [ { k: 'original', label: 'As designed' }, { k: 'yellow', label: 'Yellow gold' }, { k: 'white', label: 'White gold' }, { k: 'rose', label: 'Rose gold' }, ]; async function imagApi(action, payload) { const r = await fetch('imaginarium.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(Object.assign({ action: action }, payload)), }); let j = {}; try { j = await r.json(); } catch (e) {} if (!r.ok || j.error) throw new Error(j.error || ('Something went wrong (' + r.status + ')')); return j; } function getEveVoice() { const vs = (window.speechSynthesis && window.speechSynthesis.getVoices()) || []; if (!vs.length) return null; const inFemale = vs.find(v => /en-IN/i.test(v.lang) && /female|woman|priya|veena|raveena|aditi|heera|neerja/i.test(v.name)); const inAny = vs.find(v => /en-IN/i.test(v.lang)); const enFemale = vs.find(v => /^en/i.test(v.lang) && /female|samantha|victoria|karen|tessa|moira|fiona|serena|zira|google uk english female/i.test(v.name)); const enAny = vs.find(v => /^en/i.test(v.lang)); return inFemale || inAny || enFemale || enAny || vs[0]; } function SpecRow({ label, value }) { if (!value) return null; return
{label}{value}
; } function Imaginarium({ onBegin }) { const [mode, setMode] = React.useState('console'); // console | voice const [voiceReady, setVoiceReady] = React.useState(false); const [vstate, setV] = React.useState('idle'); // idle | connecting | live | ended | error const [speaking, setSpeaking] = React.useState(false); const [listening, setListening] = React.useState(false); const [interim, setInterim] = React.useState(''); const [transcript, setTranscript] = React.useState([]); const [design, setDesign] = React.useState(null); const [hero, setHero] = React.useState(null); const [variants, setVariants] = React.useState({}); const [wire, setWire] = React.useState(null); const [active, setActive] = React.useState('original'); const [view, setView] = React.useState('render'); const [err, setErr] = React.useState(''); const [dream, setDream] = React.useState(''); const [phase, setPhase] = React.useState('idle'); const heroRef = React.useRef(null); const tEndRef = React.useRef(null); const taRef = React.useRef(null); const histRef = React.useRef([]); const recogRef = React.useRef(null); const endedRef = React.useRef(false); const audioRef = React.useRef(null); const speakingRef = React.useRef(false); React.useEffect(() => { heroRef.current = hero; }, [hero]); React.useEffect(() => { if (tEndRef.current) tEndRef.current.scrollTop = tEndRef.current.scrollHeight; }, [transcript, interim]); React.useEffect(() => { const SR = window.SpeechRecognition || window.webkitSpeechRecognition; setVoiceReady(!!(SR && window.speechSynthesis)); if (window.speechSynthesis) { try { window.speechSynthesis.getVoices(); window.speechSynthesis.onvoiceschanged = () => window.speechSynthesis.getVoices(); } catch (e) {} } return () => { endedRef.current = true; try { if (recogRef.current) recogRef.current.abort(); } catch (e) {} try { window.speechSynthesis && window.speechSynthesis.cancel(); } catch (e) {} }; }, []); const fmtStone = (s) => s ? [s.carat_estimate, s.cut, s.color, s.type].filter(Boolean).join(' ') : ''; const fmtAccents = (a) => (a && a.length) ? a.map(x => (typeof x === 'string' ? x : [x.cut, x.color, x.type].filter(Boolean).join(' '))).filter(Boolean).join(', ') : ''; const push = (role, text) => setTranscript(t => t.concat([{ role, text }])); /* ---------- render helpers ---------- */ async function renderExtras(id) { setWire('loading'); try { const w = await imagApi('wireframe', { id }); setWire(w.dataUrl); } catch (e) { setWire(null); } for (const t of ['yellow', 'white', 'rose']) { setVariants(v => Object.assign({}, v, { [t]: 'loading' })); try { const r = await imagApi('variant', { id, tone: t }); setVariants(v => Object.assign({}, v, { [t]: r.dataUrl })); } catch (e) { setVariants(v => { const n = Object.assign({}, v); delete n[t]; return n; }); } } } async function renderHero(spec) { setHero(null); setVariants({}); setWire(null); setActive('original'); setView('render'); const h = await imagApi('hero', { spec }); setHero(h); setVariants({ original: h.dataUrl }); return h.id; } function applyVariant(tone) { const id = heroRef.current && heroRef.current.id; const t = ({ rose: 'rose', yellow: 'yellow', white: 'white' })[tone] || ''; if (!id || !t) return; setActive(t); setView('render'); setVariants(v => Object.assign({}, v, { [t]: 'loading' })); imagApi('variant', { id, tone: t }).then(r => setVariants(v => Object.assign({}, v, { [t]: r.dataUrl }))).catch(() => {}); } /* ---------- browser speech ---------- */ function browserSpeak(text) { return new Promise(res => { if (!window.speechSynthesis || !text) return res(); try { const u = new SpeechSynthesisUtterance(text); const v = getEveVoice(); if (v) { u.voice = v; u.lang = v.lang; } else { u.lang = 'en-IN'; } u.rate = 1.0; u.pitch = 1.06; u.onend = () => res(); u.onerror = () => res(); window.speechSynthesis.cancel(); window.speechSynthesis.speak(u); } catch (e) { res(); } }); } function playAudio(dataUrl) { return new Promise(res => { try { const a = audioRef.current || new Audio(); audioRef.current = a; a.muted = false; a.src = dataUrl; a.onended = () => res('ok'); a.onerror = () => res('err'); const p = a.play(); if (p && p.catch) p.catch(() => res('err')); } catch (e) { res('err'); } }); } async function speak(text) { if (!text) return; speakingRef.current = true; setSpeaking(true); let done = false; try { const r = await imagApi('say', { text }); if (r && r.audio) { const out = await playAudio(r.audio); done = (out === 'ok'); } } catch (e) {} if (!done && !endedRef.current) { await browserSpeak(text); } // fallback: browser voice speakingRef.current = false; setSpeaking(false); } function listenOnce() { return new Promise(res => { const SR = window.SpeechRecognition || window.webkitSpeechRecognition; if (!SR) return res(''); const r = new SR(); recogRef.current = r; r.lang = 'en-IN'; r.interimResults = true; r.maxAlternatives = 1; r.continuous = false; let finalText = ''; r.onresult = (e) => { let intr = ''; for (let i = e.resultIndex; i < e.results.length; i++) { const seg = e.results[i]; if (seg.isFinal) finalText += seg[0].transcript; else intr += seg[0].transcript; } setInterim(intr); }; r.onerror = () => {}; r.onend = () => { setListening(false); setInterim(''); res(finalText.trim()); }; setListening(true); try { r.start(); } catch (e) { setListening(false); res(''); } }); } function waitSpeak() { return new Promise(res => { const c = () => { if (endedRef.current || !speakingRef.current) res(); else setTimeout(c, 150); }; c(); }); } async function runConversation() { let empties = 0; while (!endedRef.current) { await waitSpeak(); // let Eve finish before we listen (no echo) if (endedRef.current) break; const said = await listenOnce(); if (endedRef.current) break; if (!said) { empties++; if (empties >= 3) { const n = 'I’m still here whenever you’re ready.'; push('eve', n); await speak(n); empties = 0; } continue; } empties = 0; push('you', said); histRef.current.push({ role: 'you', text: said }); let r; try { r = await imagApi('chat', { history: histRef.current }); } catch (e) { const m = 'Sorry, I lost that for a moment — could you say it again?'; push('eve', m); await speak(m); continue; } push('eve', r.reply); histRef.current.push({ role: 'eve', text: r.reply }); if (r.ready && r.spec) { setDesign({ concept: r.concept || { name: 'Your piece', description: '' }, spec: r.spec }); (async () => { try { const id = await renderHero(r.spec); renderExtras(id); } catch (e) {} })(); } if (r.variant) applyVariant(r.variant); if (endedRef.current) break; await speak(r.reply); } } async function startVoice() { setErr(''); const SR = window.SpeechRecognition || window.webkitSpeechRecognition; if (!SR || !window.speechSynthesis) { setVoiceReady(false); if (taRef.current) { taRef.current.focus(); taRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' }); } return; } setMode('voice'); setV('connecting'); setTranscript([]); setInterim(''); endedRef.current = false; setDesign(null); setHero(null); setVariants({}); setWire(null); // Unlock