import { useEffect, useMemo, useState } from 'react'; import type { DemoLocale } from './types'; type InvoiceScene = { id: 'source' | 'ocr' | 'fields'; durationMs: number; }; type InvoiceFeatureDemoProps = { locale: DemoLocale; isBusy: boolean; }; const COPY = { en: { title: 'Invoice Demo', busy: 'Running', hidden: 'Hidden', play: 'Play', pause: 'Pause', replay: 'Replay', hide: 'Hide', show: 'Show', videoName: 'invoice_demo.webm', step: 'Step', lanes: { source: 'Source', ocr: 'OCR', fields: 'Fields', }, captions: { source: 'Upload and validate invoice source', ocr: 'Extract raw text and parse blocks', fields: 'Return structured invoice fields', }, words: { source: ['File', 'Role', 'Note'], ocr: ['Read', 'Parse', 'Check'], fields: ['Total', 'Due', 'Status'], streamWords: ['Reading', 'Parsing', 'Checking', 'Packing'], sourceImage: 'https://upload.wikimedia.org/wikipedia/commons/9/90/Standard_Fapiao.jpg', }, }, 'zh-TW': { title: '發票導覽', busy: '執行中', hidden: '已隱藏', play: '播放', pause: '暫停', replay: '重播', hide: '隱藏', show: '顯示', videoName: '發票導覽影片.webm', step: '步驟', lanes: { source: '來源', ocr: '辨識', fields: '欄位', }, captions: { source: '上傳並驗證發票來源', ocr: '擷取原文並解析區塊', fields: '輸出結構化發票欄位', }, words: { source: ['檔案', '角色', '備註'], ocr: ['讀取', '解析', '驗證'], fields: ['總額', '到期', '狀態'], streamWords: ['讀取中', '解析中', '驗證中', '封裝中'], sourceImage: 'https://upload.wikimedia.org/wikipedia/commons/9/90/Standard_Fapiao.jpg', }, }, } as const; const SCENES: InvoiceScene[] = [ { id: 'source', durationMs: 1650 }, { id: 'ocr', durationMs: 1750 }, { id: 'fields', durationMs: 1700 }, ]; export default function InvoiceFeatureDemo({ locale, isBusy }: InvoiceFeatureDemoProps) { 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 streamOffset = Math.floor(elapsedMs / 420); const activeStreamWords = copy.words.streamWords.map( (_word, index) => copy.words.streamWords[(streamOffset + index) % copy.words.streamWords.length] ); 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.source}
{copy.words.source.map((item) => ( {item} ))}
{locale

{locale === 'zh-TW' ? 'invoice_sample.jpg' : 'invoice_sample.jpg'}

2 {copy.lanes.ocr}
{copy.words.ocr.map((item) => ( {item} ))}
{activeStreamWords.slice(0, 3).map((word, index) => (
{word}
))}
3 {copy.lanes.fields}
{copy.words.fields.map((item) => (
{item} {item === copy.words.fields[0] ? 'NT$ 6,665' : item === copy.words.fields[1] ? '2026-03-31' : locale === 'zh-TW' ? '草稿' : 'Draft'}
))}

{locale === 'zh-TW' ? '已產生 JSON 與可讀預覽。' : 'JSON payload and readable preview generated.'}

); }