diff --git a/src/app/(app)/ToggleRealtime.tsx b/src/app/(app)/ToggleRealtime.tsx index e4fdbd7..fc32ad7 100644 --- a/src/app/(app)/ToggleRealtime.tsx +++ b/src/app/(app)/ToggleRealtime.tsx @@ -67,8 +67,16 @@ const saveButtonClasses = [ type ConnectionState = 'idle' | 'requesting' | 'ready' | 'error' export default function ToggleRealtime() { - const { start, stop, remoteStream, transcripts, updateInstructions, updateTurnDelaySeconds } = - useRealtimeVoiceSession() + const { + start, + stop, + remoteStream, + transcripts, + updateInstructions, + updateTurnDelaySeconds, + updateSpeechEnabled, + sendText + } = useRealtimeVoiceSession() const [connectionState, setConnectionState] = useState('idle') const [errorMessage, setErrorMessage] = useState(null) const [languageOrder, setLanguageOrder] = useState(languagePhrases) @@ -78,6 +86,8 @@ export default function ToggleRealtime() { const [saveConfirmation, setSaveConfirmation] = useState('') const [turnDelaySeconds, setTurnDelaySeconds] = useState(1.2) const [turnDelayDraft, setTurnDelayDraft] = useState('1.2') + const [speechEnabled, setSpeechEnabled] = useState(true) + const [textDraft, setTextDraft] = useState('') const audioContextRef = useRef(null) const sourceRef = useRef(null) const startedRef = useRef(false) @@ -118,6 +128,13 @@ export default function ToggleRealtime() { setTurnDelayDraft(String(normalized)) }, [normalizeTurnDelaySeconds]) + useEffect(() => { + if (typeof window === 'undefined') return + const stored = window.localStorage.getItem('lilac.speechEnabled') + if (stored === null) return + setSpeechEnabled(stored !== 'false') + }, []) + useEffect(() => { if (!saveConfirmation) return const timer = window.setTimeout(() => setSaveConfirmation(''), 1800) @@ -138,6 +155,12 @@ export default function ToggleRealtime() { updateTurnDelaySeconds(turnDelaySeconds) }, [turnDelaySeconds, updateTurnDelaySeconds]) + useEffect(() => { + if (typeof window === 'undefined') return + window.localStorage.setItem('lilac.speechEnabled', speechEnabled ? 'true' : 'false') + updateSpeechEnabled(speechEnabled) + }, [speechEnabled, updateSpeechEnabled]) + useEffect(() => { if (typeof navigator === 'undefined') return const navLanguages = navigator.languages ?? [navigator.language] @@ -206,6 +229,18 @@ export default function ToggleRealtime() { tracks: remoteStream?.getTracks().length }) + if (!speechEnabled) { + try { + sourceRef.current?.disconnect() + } catch {} + sourceRef.current = null + try { + void audioContextRef.current?.close() + } catch {} + audioContextRef.current = null + return + } + if (!remoteStream.getAudioTracks().length) { const onAddTrack = () => { remoteStream.removeEventListener('addtrack', onAddTrack as EventListener) @@ -240,7 +275,7 @@ export default function ToggleRealtime() { } catch {} audioContextRef.current = null } - }, [remoteStream, ensureAudioContext]) + }, [remoteStream, ensureAudioContext, speechEnabled]) const beginSession = useCallback(async () => { if (startedRef.current) return @@ -250,7 +285,10 @@ export default function ToggleRealtime() { try { ensureAudioContext() - await start({ instructions: instructionsText, voice: 'verse' }) + await start({ + instructions: instructionsText, + ...(speechEnabled ? { voice: 'verse' } : {}) + }) if (cancelInitRef.current) { startedRef.current = false return @@ -265,7 +303,7 @@ export default function ToggleRealtime() { error instanceof Error ? error.message : 'Something went wrong while starting Lilac.' setErrorMessage(message) } - }, [ensureAudioContext, start, instructionsText]) + }, [ensureAudioContext, start, instructionsText, speechEnabled]) useEffect(() => { cancelInitRef.current = false @@ -319,6 +357,7 @@ export default function ToggleRealtime() { const phrase = languageOrder[activeIndex] ?? languageOrder[0] const footerText = tab === 'session' ? statusText : '' + const canSendText = textDraft.trim().length > 0 useEffect(() => { const el = transcriptListRef.current @@ -346,9 +385,37 @@ export default function ToggleRealtime() { const content = tab === 'session' ? (
+
+
+ + Speech output + + + {speechEnabled ? 'Spoken replies enabled' : 'Text-only replies'} + +
+ +
{transcripts.length ? (
@@ -399,6 +466,33 @@ export default function ToggleRealtime() {
)}
+
+
+ Text input +
+
+