Files
triqura-ecd/lib/swift/use-swift-voice.ts
colinislit 7f91e048f3 feat(swift): E1 Command Center voltooid
E1.S1 Command Center layout:
- 4-zone layout (context, canvas, recent, input)
- Keyboard shortcuts (⌘K focus, Escape close)
- CanvasArea met empty state + voorbeelden

E1.S2 Context Bar:
- Shift indicator met icons per dienst
- Patient chip met avatar + clear button
- Terug naar EPD link

E1.S3 Command Input:
- Dynamic placeholder op basis van context
- Send button (verschijnt bij input)
- Focus state met ring

E1.S4 Voice Input:
- useSwiftVoice hook (wraps Deepgram)
- Real-time waveform visualisatie
- Streaming transcript naar input

E1.S5 Recent Strip:
- Intent-based chips met icons + kleuren
- Relative time (zojuist, 5m, 2u)
- Click-to-repeat functionaliteit
- Quick hints bij lege state

Bouwplan bijgewerkt: E0+E1 done (21/68 SP, 31%)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-23 22:44:39 +01:00

116 lines
3.0 KiB
TypeScript

'use client';
/**
* Swift Voice Hook
*
* Wraps useDeepgramStreaming for Swift-specific voice input behavior.
* Streams transcript directly to the command input.
*/
import { useCallback, useEffect, useRef } from 'react';
import { useSwiftStore } from '@/stores/swift-store';
import {
useDeepgramStreaming,
type TranscriptResult,
} from '@/hooks/use-deepgram-streaming';
export interface UseSwiftVoiceReturn {
isRecording: boolean;
isConnecting: boolean;
isConnected: boolean;
error: string | null;
startRecording: () => Promise<void>;
stopRecording: () => void;
analyserNode: AnalyserNode | null;
isBrowserSupported: boolean;
}
export function useSwiftVoice(): UseSwiftVoiceReturn {
const { setInputValue, setVoiceActive, inputValue } = useSwiftStore();
// Track the base text (what was in input before recording started)
const baseTextRef = useRef('');
// Track interim transcript for replacement
const lastInterimRef = useRef('');
const handleTranscript = useCallback(
(result: TranscriptResult) => {
const { transcript, isFinal } = result;
if (isFinal) {
// Final transcript: append to base text and update base
const newText = baseTextRef.current
? `${baseTextRef.current} ${transcript}`
: transcript;
baseTextRef.current = newText;
lastInterimRef.current = '';
setInputValue(newText);
} else {
// Interim transcript: show as preview (replace previous interim)
const previewText = baseTextRef.current
? `${baseTextRef.current} ${transcript}`
: transcript;
setInputValue(previewText);
lastInterimRef.current = transcript;
}
},
[setInputValue]
);
const handleError = useCallback(
(error: Error) => {
console.error('[SwiftVoice] Error:', error.message);
},
[]
);
const {
status,
isRecording,
startRecording: startDeepgram,
stopRecording: stopDeepgram,
analyserNode,
error,
isBrowserSupported,
} = useDeepgramStreaming({
onTranscript: handleTranscript,
onError: handleError,
language: 'nl',
model: 'nova-2',
endpointingMs: 2000, // Shorter for command-style input
});
const startRecording = useCallback(async () => {
// Store current input as base text
baseTextRef.current = inputValue;
lastInterimRef.current = '';
setVoiceActive(true);
await startDeepgram();
}, [inputValue, setVoiceActive, startDeepgram]);
const stopRecording = useCallback(() => {
stopDeepgram();
setVoiceActive(false);
// Keep whatever text is in the input
lastInterimRef.current = '';
}, [stopDeepgram, setVoiceActive]);
// Sync voice active state with recording state
useEffect(() => {
if (!isRecording) {
setVoiceActive(false);
}
}, [isRecording, setVoiceActive]);
return {
isRecording,
isConnecting: status === 'connecting' || status === 'reconnecting',
isConnected: status === 'connected',
error,
startRecording,
stopRecording,
analyserNode,
isBrowserSupported,
};
}