import { useEffect, useMemo, useState } from 'react'; import type { DemoLocale } from './types'; type VoiceScene = { id: 'listen' | 'route' | 'cards'; durationMs: number; }; type VoiceFeatureDemoProps = { locale: DemoLocale; isBusy: boolean; }; const COPY = { en: { title: 'Voice Demo', busy: 'Running', hidden: 'Hidden', play: 'Play', pause: 'Pause', replay: 'Replay', hide: 'Hide', show: 'Show', videoName: 'voice_demo.webm', step: 'Step', lanes: { listen: 'Listen', route: 'Route', cards: 'Cards', }, routeWords: { nodes: ['Parse', 'Plan', 'Call'], chips: ['Stock', 'Price', 'Store', 'Safe'], }, captions: { listen: 'Capture voice and intent', route: 'Plan tool sequence', cards: 'Return voice cards', }, stepCopy: { listen: { title: 'Live Voice Capture', body: 'Microphone audio is sampled in real time, normalized for volume variance, and segmented into phrase-sized chunks for stable transcript quality.', detail: 'Noise suppression, language hinting, and intent candidate extraction run before any tool is called.', }, route: { title: 'Tool Route Planning', body: 'The planner maps your spoken goal into executable actions, selects the best tool chain, and prepares argument payloads with guardrails.', detail: 'Validation checks include category filters, price constraints, availability context, and fallback routing when confidence is low.', }, cards: { title: 'Structured Voice Response', body: 'Results are transformed into concise spoken output plus visual cards so users can confirm key data without opening raw payloads.', detail: 'Action chips are attached for immediate follow-up tasks such as editing stock, opening product details, or triggering the next workflow.', }, }, cardVisual: { summaryTitle: 'Assistant Summary', summaryBody: 'I updated stock to 48, synced pickup details, and prepared two follow-up actions.', actions: ['Open Product', 'Edit Again', 'Share Update'], statsTitle: 'Result Snapshot', stats: [ { label: 'Confidence', value: '96%' }, { label: 'Tool Calls', value: '3' }, { label: 'Latency', value: '1.2s' }, ], statsNote: 'Structured payload attached to voice response card.', }, }, 'zh-TW': { title: '語音導覽', busy: '執行中', hidden: '已隱藏', play: '播放', pause: '暫停', replay: '重播', hide: '隱藏', show: '顯示', videoName: '語音導覽影片.webm', step: '步驟', lanes: { listen: '收音', route: '路由', cards: '結果', }, routeWords: { nodes: ['解析', '規劃', '呼叫'], chips: ['庫存', '價格', '店家', '安全'], }, captions: { listen: '接收語音與意圖', route: '規劃工具流程', cards: '回傳語音卡片', }, stepCopy: { listen: { title: '即時語音收音', body: '系統會即時擷取麥克風音訊,先做音量與分段標準化,再輸出更穩定的語音文字片段。', detail: '在呼叫工具前,會先完成降噪、語系判斷與初步意圖分類。', }, route: { title: '工具路由規劃', body: '規劃器會把語音目標轉成可執行步驟,選擇最佳工具鏈並組裝對應參數。', detail: '同時檢查分類、價格、庫存與容錯路由,確保低信心情況也能安全回應。', }, cards: { title: '語音結果封裝', body: '最終回覆會整合口語摘要與視覺卡片,讓使用者快速理解結果而不必直接讀 JSON。', detail: '卡片會附帶後續操作按鈕,例如更新庫存、查看商品頁、或直接進入下一步。', }, }, cardVisual: { summaryTitle: '語音摘要', summaryBody: '已更新庫存為 48,補上取貨資訊,並整理兩個後續操作。', actions: ['查看商品', '再次編輯', '分享更新'], statsTitle: '結果快照', stats: [ { label: '信心值', value: '96%' }, { label: '工具數', value: '3' }, { label: '延遲', value: '1.2s' }, ], statsNote: '已將結構化載荷附加到語音卡片。', }, }, } as const; const SCENES: VoiceScene[] = [ { id: 'listen', durationMs: 1700 }, { id: 'route', durationMs: 1750 }, { id: 'cards', durationMs: 1700 }, ]; export default function VoiceFeatureDemo({ locale, isBusy }: VoiceFeatureDemoProps) { const copy = COPY[locale]; const scenes = useMemo(() => SCENES, []); const [sceneIndex, setSceneIndex] = useState(0); const [elapsedMs, setElapsedMs] = useState(0); const [isPlaying, setIsPlaying] = useState(true); const [isCollapsed, setIsCollapsed] = useState(false); const [reducedMotion, setReducedMotion] = useState(false); useEffect(() => { const media = window.matchMedia('(prefers-reduced-motion: reduce)'); const apply = () => setReducedMotion(media.matches); apply(); if (media.addEventListener) { media.addEventListener('change', apply); return () => media.removeEventListener('change', apply); } media.addListener(apply); return () => media.removeListener(apply); }, []); useEffect(() => { if (reducedMotion) setIsPlaying(false); }, [reducedMotion]); useEffect(() => { setSceneIndex(0); setElapsedMs(0); setIsCollapsed(false); if (!reducedMotion) setIsPlaying(true); }, [locale, reducedMotion]); useEffect(() => { if (!isBusy) return; setIsCollapsed(true); setIsPlaying(false); }, [isBusy]); useEffect(() => { if (!isPlaying || isCollapsed || isBusy || reducedMotion) return; const timer = window.setInterval(() => { setElapsedMs((prev) => { const scene = scenes[sceneIndex]; if (!scene) return 0; const next = prev + 120; if (next < scene.durationMs) return next; setSceneIndex((current) => (current + 1) % scenes.length); return 0; }); }, 120); return () => window.clearInterval(timer); }, [isPlaying, isCollapsed, isBusy, reducedMotion, scenes, sceneIndex]); const activeScene = scenes[sceneIndex] ?? scenes[0]; const totalDuration = scenes.reduce((sum, scene) => sum + scene.durationMs, 0); const previousDuration = scenes.slice(0, sceneIndex).reduce((sum, scene) => sum + scene.durationMs, 0); const progress = totalDuration > 0 ? Math.round(((previousDuration + elapsedMs) / totalDuration) * 100) : 0; const sceneCaption = copy.captions[activeScene.id]; const replay = () => { setSceneIndex(0); setElapsedMs(0); if (!reducedMotion && !isBusy) setIsPlaying(true); }; if (isCollapsed) { return (

{isBusy ? copy.busy : copy.hidden}

{!isBusy ? ( ) : null}
); } return (

{copy.title}

{copy.step} {sceneIndex + 1}/{scenes.length}
{copy.videoName}
1 {copy.lanes.listen}
{Array.from({ length: 10 }).map((_, index) => ( ))}
{copy.stepCopy.listen.title}

{copy.stepCopy.listen.body}

{copy.stepCopy.listen.detail}
2 {copy.lanes.route}
{copy.routeWords.nodes.map((word) => ( {word} ))}
{copy.routeWords.chips.map((word) => ( {word} ))}
{copy.stepCopy.route.title}

{copy.stepCopy.route.body}

{copy.stepCopy.route.detail}
3 {copy.lanes.cards}
{copy.cardVisual.summaryTitle}

{copy.cardVisual.summaryBody}

{copy.cardVisual.actions.map((action) => ( {action} ))}
{copy.cardVisual.statsTitle}
{copy.cardVisual.stats.map((item) => ( {item.label} {item.value} ))}

{copy.cardVisual.statsNote}

{copy.stepCopy.cards.title}

{copy.stepCopy.cards.body}

{copy.stepCopy.cards.detail}
); }