Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 15 additions & 10 deletions src/app/(app)/ToggleRealtime.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
'use client'

import { AnimatePresence, motion } from 'framer-motion'
import { type CSSProperties, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import {
type CSSProperties,
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState
} from 'react'

import { useRealtimeVoiceSession } from '@/realtime/provider'

Expand Down Expand Up @@ -357,7 +365,6 @@ export default function ToggleRealtime() {

const [activeIndex, setActiveIndex] = useState(0)
const transcriptListRef = useRef<HTMLDivElement | null>(null)
const transcriptBottomRef = useRef<HTMLDivElement | null>(null)
const stickToBottomRef = useRef(true)

useEffect(() => {
Expand All @@ -373,6 +380,7 @@ export default function ToggleRealtime() {
const phrase = languageOrder[activeIndex] ?? languageOrder[0]
const footerText = tab === 'session' ? statusText : ''
const canSendText = textDraft.trim().length > 0
const transcriptCount = transcripts.length

useEffect(() => {
const el = transcriptListRef.current
Expand All @@ -390,12 +398,11 @@ export default function ToggleRealtime() {
}
}, [])

useEffect(() => {
if (!stickToBottomRef.current) return
// Trigger on streaming updates (deltas) while the user is pinned to the bottom.
void transcripts
transcriptBottomRef.current?.scrollIntoView({ behavior: 'auto' })
}, [transcripts])
useLayoutEffect(() => {
const el = transcriptListRef.current
if (!el || !stickToBottomRef.current || transcriptCount === 0) return
el.scrollTop = el.scrollHeight
}, [transcriptCount])

const content =
tab === 'session' ? (
Expand Down Expand Up @@ -430,7 +437,6 @@ export default function ToggleRealtime() {
</div>
)
})}
<div ref={transcriptBottomRef} />
</div>
) : (
<div className="flex h-full flex-col items-center justify-center px-6 text-center">
Expand All @@ -449,7 +455,6 @@ export default function ToggleRealtime() {
<p className="mt-6 text-[var(--lilac-ink-muted)] text-sm">
Your spoken conversation will appear here as a live transcript.
</p>
<div ref={transcriptBottomRef} />
</div>
)}
</div>
Expand Down
9 changes: 7 additions & 2 deletions src/realtime/provider.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { createContext, useCallback, useContext, useMemo, useRef, useState } from 'react'
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'

import { createRealtimeSession } from '@/app/actions/realtime'

Expand Down Expand Up @@ -134,6 +134,7 @@ export function RealtimeProvider({ children }: { children: React.ReactNode }) {
const peerRef = useRef<RTCPeerConnection | null>(null)
const localRef = useRef<MediaStream | null>(null)
const turnDelaySecondsRef = useRef<number>(getInitialTurnDelaySeconds())
const latestTimelineItemIdRef = useRef<string | null>(null)
// Cancels in-flight `start()` calls and prevents multiple concurrent sessions.
const startGenerationRef = useRef(0)
// Stable transcript item id we choose for a given response_id (so we don't "split" a message mid-stream).
Expand Down Expand Up @@ -183,6 +184,10 @@ export function RealtimeProvider({ children }: { children: React.ReactNode }) {
setTranscripts(prev => orderTranscriptsByPreviousItemId(prev, previousItemIdByIdRef.current))
}, [])

useEffect(() => {
latestTimelineItemIdRef.current = transcripts.at(-1)?.id ?? null
}, [transcripts])

const upsertTranscript = useCallback(
(update: {
id: string
Expand Down Expand Up @@ -854,7 +859,7 @@ export function RealtimeProvider({ children }: { children: React.ReactNode }) {
const trimmed = text.trim()
if (!trimmed || !dataChannel) return false
const id = crypto.randomUUID()
const previousItemId = latestCommittedInputItemIdRef.current
const previousItemId = latestTimelineItemIdRef.current
previousItemIdByIdRef.current.set(id, previousItemId ?? null)
latestCommittedInputItemIdRef.current = id
upsertTranscript({
Expand Down