feat(treatment): add voice detail entry #65

Merged
admin merged 32 commits from feat/voice-treatment-entry into master 2026-08-24 11:13:48 +03:30
Showing only changes of commit 6a4c0cb1bb - Show all commits

View File

@@ -219,6 +219,14 @@ export function useVoiceCapture({
// minutes of dictation because a timer expired would be the worst failure.
if (maxMs != null && elapsed >= maxMs) stop();
}, LEVEL_POLL_MS);
} catch {
// `new MediaRecorder(...)` and `recorder.start()` both throw on some browsers,
// and by then the stream is already live. Without this the promise rejects
// unhandled, the UI sits at 'idle' with nothing shown, and the browser's
// recording indicator stays lit until the workspace unmounts.
teardown();
setPhase('idle');
onError(clientError('VOICE_MIC_DENIED'));
} finally {
startingRef.current = false;
}
@@ -262,12 +270,23 @@ function attachLevelMeter(
source.connect(analyser);
const data = new Uint8Array(analyser.frequencyBinCount);
const tick = () => {
// Sample every frame so a transient is not missed, but publish at LEVEL_POLL_MS.
// This hook lives in TreatmentWorkspace, so an unthrottled setLevel re-renders the
// details editor, the FDI chart and the lab panel on every animation frame — about
// 7,200 whole-tree renders across a two-minute recording.
let peakSinceEmit = 0;
let lastEmit = 0;
const tick = (now: number) => {
if (contextRef.current !== context || context.state === 'closed') return;
analyser.getByteTimeDomainData(data);
let peak = 0;
for (const sample of data) peak = Math.max(peak, Math.abs(sample - 128));
setLevel(Math.min(1, peak / 128));
peakSinceEmit = Math.max(peakSinceEmit, peak);
if (now - lastEmit >= LEVEL_POLL_MS) {
lastEmit = now;
setLevel(Math.min(1, peakSinceEmit / 128));
peakSinceEmit = 0;
}
requestAnimationFrame(tick);
};
requestAnimationFrame(tick);