From dbbb0b716039690070988a2a4ff68a86bebd127d Mon Sep 17 00:00:00 2001 From: DexterStorey Date: Mon, 2 Mar 2026 16:48:21 -0500 Subject: [PATCH 01/24] feat: rebuild realtime app into chat/translate/transcribe modes --- biome.json | 5 +- src/app/(app)/ChatMode.tsx | 182 +++++ src/app/(app)/ModeShell.tsx | 125 ++++ src/app/(app)/ToggleRealtime.tsx | 546 -------------- src/app/(app)/TranscribeMode.tsx | 118 +++ src/app/(app)/TranslateMode.tsx | 155 ++++ src/app/(app)/page.tsx | 4 +- src/app/actions/realtime.ts | 342 +++++++-- src/app/layout.tsx | 12 +- src/app/styles.css | 6 + src/realtime/chatRealtimeClient.ts | 399 +++++++++++ src/realtime/modeRuntimeStore.tsx | 684 ++++++++++++++++++ src/realtime/provider.tsx | 834 ---------------------- src/realtime/schemas.ts | 191 +++++ src/realtime/sessionTypes.ts | 78 ++ src/realtime/transcriptionSocketClient.ts | 329 +++++++++ 16 files changed, 2580 insertions(+), 1430 deletions(-) create mode 100644 src/app/(app)/ChatMode.tsx create mode 100644 src/app/(app)/ModeShell.tsx delete mode 100644 src/app/(app)/ToggleRealtime.tsx create mode 100644 src/app/(app)/TranscribeMode.tsx create mode 100644 src/app/(app)/TranslateMode.tsx create mode 100644 src/realtime/chatRealtimeClient.ts create mode 100644 src/realtime/modeRuntimeStore.tsx delete mode 100644 src/realtime/provider.tsx create mode 100644 src/realtime/schemas.ts create mode 100644 src/realtime/sessionTypes.ts create mode 100644 src/realtime/transcriptionSocketClient.ts diff --git a/biome.json b/biome.json index e8d99dd..b628b7b 100644 --- a/biome.json +++ b/biome.json @@ -1,3 +1,6 @@ { - "extends": ["@rubriclab/config/biome"] + "extends": ["@rubriclab/config/biome"], + "files": { + "includes": ["**", "!next-env.d.ts"] + } } diff --git a/src/app/(app)/ChatMode.tsx b/src/app/(app)/ChatMode.tsx new file mode 100644 index 0000000..37f70ab --- /dev/null +++ b/src/app/(app)/ChatMode.tsx @@ -0,0 +1,182 @@ +'use client' + +import { useEffect, useRef, useState } from 'react' + +import { useLilacModeRuntime } from '@/realtime/modeRuntimeStore' + +export default function ChatMode() { + const { + chatInstructions, + chatTranscripts, + chatTurnDelaySeconds, + remoteAudioStream, + setChatInstructions, + setChatTurnDelaySeconds + } = useLilacModeRuntime() + const [draftInstructions, setDraftInstructions] = useState(chatInstructions) + const [saveMessage, setSaveMessage] = useState('') + const transcriptListRef = useRef(null) + const transcriptBottomRef = useRef(null) + const stayPinnedToBottomRef = useRef(true) + const playbackAudioElementRef = useRef(null) + + useEffect(() => { + setDraftInstructions(chatInstructions) + }, [chatInstructions]) + + useEffect(() => { + if (!saveMessage) return + const timeoutId = window.setTimeout(() => setSaveMessage(''), 1600) + return () => { + window.clearTimeout(timeoutId) + } + }, [saveMessage]) + + useEffect(() => { + const transcriptListElement = transcriptListRef.current + if (!transcriptListElement) return + const activeTranscriptListElement = transcriptListElement + + function onScroll(): void { + const distanceFromBottom = + activeTranscriptListElement.scrollHeight - + activeTranscriptListElement.scrollTop - + activeTranscriptListElement.clientHeight + stayPinnedToBottomRef.current = distanceFromBottom < 120 + } + + activeTranscriptListElement.addEventListener('scroll', onScroll) + onScroll() + return () => { + activeTranscriptListElement.removeEventListener('scroll', onScroll) + } + }, []) + + useEffect(() => { + if (!stayPinnedToBottomRef.current) return + void chatTranscripts + transcriptBottomRef.current?.scrollIntoView({ behavior: 'auto' }) + }, [chatTranscripts]) + + useEffect(() => { + if (!playbackAudioElementRef.current) { + playbackAudioElementRef.current = new Audio() + playbackAudioElementRef.current.autoplay = true + } + const playbackAudioElement = playbackAudioElementRef.current + if (!playbackAudioElement) return + playbackAudioElement.srcObject = remoteAudioStream + if (remoteAudioStream) { + void playbackAudioElement.play().catch(() => {}) + } + return () => { + playbackAudioElement.pause() + playbackAudioElement.srcObject = null + } + }, [remoteAudioStream]) + + return ( +
+
+ {chatTranscripts.length ? ( +
+ {chatTranscripts.map(message => { + const isUser = message.role === 'user' + const bubbleBaseClasses = + 'max-w-[94%] whitespace-pre-wrap rounded-3xl px-4 py-3 text-sm leading-relaxed shadow-sm' + const bubbleClasses = isUser + ? `${bubbleBaseClasses} self-end bg-[var(--lilac-ink)] text-[var(--lilac-surface)]` + : `${bubbleBaseClasses} self-start border border-white/30 bg-white/80 text-[var(--lilac-ink)] dark:bg-white/12` + const label = isUser ? 'You' : 'Lilac' + return ( +
+
+ {label} + {message.status === 'streaming' ? : null} +
+
{message.text.trim() || '…'}
+
+ ) + })} +
+
+ ) : ( +
+ Speak to start chatting with Lilac. +
+ )} +
+ +
+
+
+ Instructions +
+