'use client'; import { useRef, useState } from 'react'; const SENTENCE = { text: '我想买书', language: 'zh-CN' }; export default function Home() { const [state, setState] = useState('idle'); const [result, setResult] = useState(null); const recorder = useRef(null); async function start() { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); const chunks = []; const rec = new MediaRecorder(stream); rec.ondataavailable = e => chunks.push(e.data); rec.onstop = async () => { stream.getTracks().forEach(t => t.stop()); setState('scoring'); const form = new FormData(); form.append('audio', new Blob(chunks, { type: rec.mimeType }), 'recording.webm'); form.append('text', SENTENCE.text); form.append('language', SENTENCE.language); const response = await fetch('/api/assess', { method: 'POST', body: form, headers: { 'Idempotency-Key': crypto.randomUUID() } }); setResult(await response.json()); setState('idle'); }; recorder.current = rec; rec.start(); setState('recording'); } return (

Say: {SENTENCE.text}

{state === 'recording' ? : } {result?.error &&

{result.error.message}

} {result && !result.error && !result.assessable &&

Could not score that recording ({result.reason}). Try again closer to the microphone.

} {result?.assessable && ( {result.words.map((w, i) => { const tone = w.units.find(u => u.dimension === 'tone'); return ; })}
SyllableScoreTone expected → heard
{w.text} {w.pinyin}{w.score}{tone ? `${tone.expected} → ${tone.heard ?? '?'}` : '—'}
)} {result?.test &&

Test key: fixed example result.

}
); }