From 70d9de0a41f4055de1515c29c313c445dd59cf3c Mon Sep 17 00:00:00 2001 From: Thomas Yuill Date: Fri, 7 Nov 2025 11:38:28 -0500 Subject: [PATCH 1/2] feat: migrate to useSession hook --- app/ui/layout.tsx | 15 ++- components/app/app.tsx | 8 +- components/app/chat-transcript.tsx | 8 +- components/app/preconnect-message.tsx | 4 +- components/app/session-provider.tsx | 41 ------- components/app/session-view.tsx | 23 +++- components/app/view-controller.tsx | 38 +++--- .../agent-control-bar/agent-control-bar.tsx | 13 +-- hooks/useAppSession.tsx | 103 +++++++++++++++++ hooks/useChatMessages.ts | 39 ------- hooks/useConnectionTimout.tsx | 9 +- hooks/useRoom.ts | 108 ------------------ package.json | 2 +- pnpm-lock.yaml | 24 ++-- 14 files changed, 182 insertions(+), 253 deletions(-) delete mode 100644 components/app/session-provider.tsx create mode 100644 hooks/useAppSession.tsx delete mode 100644 hooks/useChatMessages.ts delete mode 100644 hooks/useRoom.ts diff --git a/app/ui/layout.tsx b/app/ui/layout.tsx index c7202785..67b0967a 100644 --- a/app/ui/layout.tsx +++ b/app/ui/layout.tsx @@ -1,20 +1,23 @@ -import * as React from 'react'; import { headers } from 'next/headers'; -import { SessionProvider } from '@/components/app/session-provider'; +import { AppSessionProvider } from '@/hooks/useAppSession'; import { getAppConfig } from '@/lib/utils'; -export default async function ComponentsLayout({ children }: { children: React.ReactNode }) { +interface LayoutProps { + children: React.ReactNode; +} + +export default async function Layout({ children }: LayoutProps) { const hdrs = await headers(); const appConfig = await getAppConfig(hdrs); return ( - +

LiveKit UI

- A set of UI components for building LiveKit-powered voice experiences. + A set of UI Layouts for building LiveKit-powered voice experiences.

Built with{' '} @@ -37,6 +40,6 @@ export default async function ComponentsLayout({ children }: { children: React.R

{children}
-
+ ); } diff --git a/components/app/app.tsx b/components/app/app.tsx index b390e062..9514664b 100644 --- a/components/app/app.tsx +++ b/components/app/app.tsx @@ -2,9 +2,9 @@ import { RoomAudioRenderer, StartAudio } from '@livekit/components-react'; import type { AppConfig } from '@/app-config'; -import { SessionProvider } from '@/components/app/session-provider'; import { ViewController } from '@/components/app/view-controller'; import { Toaster } from '@/components/livekit/toaster'; +import { AppSessionProvider } from '@/hooks/useAppSession'; interface AppProps { appConfig: AppConfig; @@ -12,13 +12,13 @@ interface AppProps { export function App({ appConfig }: AppProps) { return ( - +
- +
-
+ ); } diff --git a/components/app/chat-transcript.tsx b/components/app/chat-transcript.tsx index b67d0a67..8a897182 100644 --- a/components/app/chat-transcript.tsx +++ b/components/app/chat-transcript.tsx @@ -1,7 +1,7 @@ 'use client'; import { AnimatePresence, type HTMLMotionProps, motion } from 'motion/react'; -import { type ReceivedChatMessage } from '@livekit/components-react'; +import { type ReceivedMessage } from '@livekit/components-react'; import { ChatEntry } from '@/components/livekit/chat-entry'; const MotionContainer = motion.create('div'); @@ -50,7 +50,7 @@ const MESSAGE_MOTION_PROPS = { interface ChatTranscriptProps { hidden?: boolean; - messages?: ReceivedChatMessage[]; + messages?: ReceivedMessage[]; } export function ChatTranscript({ @@ -62,10 +62,9 @@ export function ChatTranscript({ {!hidden && ( - {messages.map(({ id, timestamp, from, message, editTimestamp }: ReceivedChatMessage) => { + {messages.map(({ id, timestamp, from, message }: ReceivedMessage) => { const locale = navigator?.language ?? 'en-US'; const messageOrigin = from?.isLocal ? 'local' : 'remote'; - const hasBeenEdited = !!editTimestamp; return ( ); diff --git a/components/app/preconnect-message.tsx b/components/app/preconnect-message.tsx index 719c3813..f28044ea 100644 --- a/components/app/preconnect-message.tsx +++ b/components/app/preconnect-message.tsx @@ -1,7 +1,7 @@ 'use client'; import { AnimatePresence, motion } from 'motion/react'; -import { type ReceivedChatMessage } from '@livekit/components-react'; +import { type ReceivedMessage } from '@livekit/components-react'; import { ShimmerText } from '@/components/livekit/shimmer-text'; import { cn } from '@/lib/utils'; @@ -32,7 +32,7 @@ const VIEW_MOTION_PROPS = { }; interface PreConnectMessageProps { - messages?: ReceivedChatMessage[]; + messages?: ReceivedMessage[]; className?: string; } diff --git a/components/app/session-provider.tsx b/components/app/session-provider.tsx deleted file mode 100644 index 1906f4ca..00000000 --- a/components/app/session-provider.tsx +++ /dev/null @@ -1,41 +0,0 @@ -'use client'; - -import { createContext, useContext, useMemo } from 'react'; -import { RoomContext } from '@livekit/components-react'; -import { APP_CONFIG_DEFAULTS, type AppConfig } from '@/app-config'; -import { useRoom } from '@/hooks/useRoom'; - -const SessionContext = createContext<{ - appConfig: AppConfig; - isSessionActive: boolean; - startSession: () => void; - endSession: () => void; -}>({ - appConfig: APP_CONFIG_DEFAULTS, - isSessionActive: false, - startSession: () => {}, - endSession: () => {}, -}); - -interface SessionProviderProps { - appConfig: AppConfig; - children: React.ReactNode; -} - -export const SessionProvider = ({ appConfig, children }: SessionProviderProps) => { - const { room, isSessionActive, startSession, endSession } = useRoom(appConfig); - const contextValue = useMemo( - () => ({ appConfig, isSessionActive, startSession, endSession }), - [appConfig, isSessionActive, startSession, endSession] - ); - - return ( - - {children} - - ); -}; - -export function useSession() { - return useContext(SessionContext); -} diff --git a/components/app/session-view.tsx b/components/app/session-view.tsx index 460baea1..94a14ece 100644 --- a/components/app/session-view.tsx +++ b/components/app/session-view.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from 'react'; import { motion } from 'motion/react'; +import { useSessionMessages } from '@livekit/components-react'; import type { AppConfig } from '@/app-config'; import { ChatTranscript } from '@/components/app/chat-transcript'; import { PreConnectMessage } from '@/components/app/preconnect-message'; @@ -10,7 +11,7 @@ import { AgentControlBar, type ControlBarControls, } from '@/components/livekit/agent-control-bar/agent-control-bar'; -import { useChatMessages } from '@/hooks/useChatMessages'; +import { useAppSession } from '@/hooks/useAppSession'; import { useConnectionTimeout } from '@/hooks/useConnectionTimout'; import { useDebugMode } from '@/hooks/useDebug'; import { cn } from '@/lib/utils'; @@ -58,6 +59,7 @@ export function Fade({ top = false, bottom = false, className }: FadeProps) { /> ); } + interface SessionViewProps { appConfig: AppConfig; } @@ -66,10 +68,11 @@ export const SessionView = ({ appConfig, ...props }: React.ComponentProps<'section'> & SessionViewProps) => { - useConnectionTimeout(200_000); + useConnectionTimeout(20_000); useDebugMode({ enabled: IN_DEVELOPMENT }); - const messages = useChatMessages(); + const { session, isSessionActive, endSession } = useAppSession(); + const { messages } = useSessionMessages(session); const [chatOpen, setChatOpen] = useState(false); const scrollAreaRef = useRef(null); @@ -90,6 +93,11 @@ export const SessionView = ({ } }, [messages]); + const handleDisconnect = () => { + // pass false so we can manually end the session when the exit animation completes + endSession(false); + }; + return (
{/* Chat Transcript */} @@ -100,7 +108,7 @@ export const SessionView = ({ )} > - +
diff --git a/components/app/view-controller.tsx b/components/app/view-controller.tsx index 4519c44f..1b5e6d79 100644 --- a/components/app/view-controller.tsx +++ b/components/app/view-controller.tsx @@ -1,11 +1,12 @@ 'use client'; -import { useRef } from 'react'; -import { AnimatePresence, motion } from 'motion/react'; -import { useRoomContext } from '@livekit/components-react'; -import { useSession } from '@/components/app/session-provider'; +import { useCallback } from 'react'; +import { AnimatePresence, type AnimationDefinition, motion } from 'motion/react'; +import { useSessionContext } from '@livekit/components-react'; +import { AppConfig } from '@/app-config'; import { SessionView } from '@/components/app/session-view'; import { WelcomeView } from '@/components/app/welcome-view'; +import { useAppSession } from '@/hooks/useAppSession'; const MotionWelcomeView = motion.create(WelcomeView); const MotionSessionView = motion.create(SessionView); @@ -28,20 +29,23 @@ const VIEW_MOTION_PROPS = { }, }; -export function ViewController() { - const room = useRoomContext(); - const isSessionActiveRef = useRef(false); - const { appConfig, isSessionActive, startSession } = useSession(); +interface ViewControllerProps { + appConfig: AppConfig; +} - // animation handler holds a reference to stale isSessionActive value - isSessionActiveRef.current = isSessionActive; +export function ViewController({ appConfig }: ViewControllerProps) { + const session = useSessionContext(); + const { isSessionActive, startSession } = useAppSession(); - // disconnect room after animation completes - const handleAnimationComplete = () => { - if (!isSessionActiveRef.current && room.state !== 'disconnected') { - room.disconnect(); - } - }; + const handleAnimationComplete = useCallback( + (definition: AnimationDefinition) => { + // manually end the session when the exit animation completes + if (definition === 'hidden') { + session.end(); + } + }, + [session] + ); return ( @@ -50,7 +54,7 @@ export function ViewController() { )} diff --git a/components/livekit/agent-control-bar/agent-control-bar.tsx b/components/livekit/agent-control-bar/agent-control-bar.tsx index 1b53b1c4..22dc7d4e 100644 --- a/components/livekit/agent-control-bar/agent-control-bar.tsx +++ b/components/livekit/agent-control-bar/agent-control-bar.tsx @@ -4,7 +4,6 @@ import { type HTMLAttributes, useCallback, useState } from 'react'; import { Track } from 'livekit-client'; import { useChat, useRemoteParticipants } from '@livekit/components-react'; import { ChatTextIcon, PhoneDisconnectIcon } from '@phosphor-icons/react/dist/ssr'; -import { useSession } from '@/components/app/session-provider'; import { TrackToggle } from '@/components/livekit/agent-control-bar/track-toggle'; import { Button } from '@/components/livekit/button'; import { Toggle } from '@/components/livekit/toggle'; @@ -23,8 +22,8 @@ export interface ControlBarControls { } export interface AgentControlBarProps extends UseInputControlsProps { + isSessionActive?: boolean; controls?: ControlBarControls; - onDisconnect?: () => void; onChatOpenChange?: (open: boolean) => void; onDeviceError?: (error: { source: Track.Source; error: Error }) => void; } @@ -36,6 +35,7 @@ export function AgentControlBar({ controls, saveUserChoices = true, className, + isSessionActive = false, onDisconnect, onDeviceError, onChatOpenChange, @@ -45,8 +45,6 @@ export function AgentControlBar({ const participants = useRemoteParticipants(); const [chatOpen, setChatOpen] = useState(false); const publishPermissions = usePublishPermissions(); - const { isSessionActive, endSession } = useSession(); - const { micTrackRef, cameraToggle, @@ -70,11 +68,6 @@ export function AgentControlBar({ [onChatOpenChange, setChatOpen] ); - const handleDisconnect = useCallback(async () => { - endSession(); - onDisconnect?.(); - }, [endSession, onDisconnect]); - const visibleControls = { leave: controls?.leave ?? true, microphone: controls?.microphone ?? publishPermissions.microphone, @@ -164,7 +157,7 @@ export function AgentControlBar({ {visibleControls.leave && (