Skip to content

Commit 264aa32

Browse files
feat: migrate to Agents SDK (useSession hook) (#298)
1 parent c93f1ea commit 264aa32

16 files changed

+243
-318
lines changed

app/ui/layout.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
1-
import * as React from 'react';
21
import { headers } from 'next/headers';
3-
import { SessionProvider } from '@/components/app/session-provider';
2+
import { ConnectionProvider } from '@/hooks/useConnection';
43
import { getAppConfig } from '@/lib/utils';
54

6-
export default async function ComponentsLayout({ children }: { children: React.ReactNode }) {
5+
interface LayoutProps {
6+
children: React.ReactNode;
7+
}
8+
9+
export default async function Layout({ children }: LayoutProps) {
710
const hdrs = await headers();
811
const appConfig = await getAppConfig(hdrs);
912

1013
return (
11-
<SessionProvider appConfig={appConfig}>
14+
<ConnectionProvider appConfig={appConfig}>
1215
<div className="bg-muted/20 min-h-svh p-8">
1316
<div className="mx-auto max-w-3xl space-y-8">
1417
<header className="space-y-2">
1518
<h1 className="text-5xl font-bold tracking-tight">LiveKit UI</h1>
1619
<p className="text-muted-foreground max-w-80 leading-tight text-pretty">
17-
A set of UI components for building LiveKit-powered voice experiences.
20+
A set of UI Layouts for building LiveKit-powered voice experiences.
1821
</p>
1922
<p className="text-muted-foreground max-w-prose text-balance">
2023
Built with{' '}
@@ -37,6 +40,6 @@ export default async function ComponentsLayout({ children }: { children: React.R
3740
<main className="space-y-20">{children}</main>
3841
</div>
3942
</div>
40-
</SessionProvider>
43+
</ConnectionProvider>
4144
);
4245
}

components/app/app.tsx

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,35 @@
22

33
import { RoomAudioRenderer, StartAudio } from '@livekit/components-react';
44
import type { AppConfig } from '@/app-config';
5-
import { SessionProvider } from '@/components/app/session-provider';
65
import { ViewController } from '@/components/app/view-controller';
76
import { Toaster } from '@/components/livekit/toaster';
7+
import { useAgentErrors } from '@/hooks/useAgentErrors';
8+
import { ConnectionProvider } from '@/hooks/useConnection';
9+
import { useDebugMode } from '@/hooks/useDebug';
10+
11+
const IN_DEVELOPMENT = process.env.NODE_ENV !== 'production';
12+
13+
function AppSetup() {
14+
useDebugMode({ enabled: IN_DEVELOPMENT });
15+
useAgentErrors();
16+
17+
return null;
18+
}
819

920
interface AppProps {
1021
appConfig: AppConfig;
1122
}
1223

1324
export function App({ appConfig }: AppProps) {
1425
return (
15-
<SessionProvider appConfig={appConfig}>
26+
<ConnectionProvider appConfig={appConfig}>
27+
<AppSetup />
1628
<main className="grid h-svh grid-cols-1 place-content-center">
17-
<ViewController />
29+
<ViewController appConfig={appConfig} />
1830
</main>
1931
<StartAudio label="Start Audio" />
2032
<RoomAudioRenderer />
2133
<Toaster />
22-
</SessionProvider>
34+
</ConnectionProvider>
2335
);
2436
}

components/app/chat-transcript.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client';
22

33
import { AnimatePresence, type HTMLMotionProps, motion } from 'motion/react';
4-
import { type ReceivedChatMessage } from '@livekit/components-react';
4+
import { type ReceivedMessage } from '@livekit/components-react';
55
import { ChatEntry } from '@/components/livekit/chat-entry';
66

77
const MotionContainer = motion.create('div');
@@ -50,7 +50,7 @@ const MESSAGE_MOTION_PROPS = {
5050

5151
interface ChatTranscriptProps {
5252
hidden?: boolean;
53-
messages?: ReceivedChatMessage[];
53+
messages?: ReceivedMessage[];
5454
}
5555

5656
export function ChatTranscript({
@@ -62,10 +62,12 @@ export function ChatTranscript({
6262
<AnimatePresence>
6363
{!hidden && (
6464
<MotionContainer {...CONTAINER_MOTION_PROPS} {...props}>
65-
{messages.map(({ id, timestamp, from, message, editTimestamp }: ReceivedChatMessage) => {
65+
{messages.map((receivedMessage) => {
66+
const { id, timestamp, from, message } = receivedMessage;
6667
const locale = navigator?.language ?? 'en-US';
6768
const messageOrigin = from?.isLocal ? 'local' : 'remote';
68-
const hasBeenEdited = !!editTimestamp;
69+
const hasBeenEdited =
70+
receivedMessage.type === 'chatMessage' && !!receivedMessage.editTimestamp;
6971

7072
return (
7173
<MotionChatEntry

components/app/preconnect-message.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client';
22

33
import { AnimatePresence, motion } from 'motion/react';
4-
import { type ReceivedChatMessage } from '@livekit/components-react';
4+
import { type ReceivedMessage } from '@livekit/components-react';
55
import { ShimmerText } from '@/components/livekit/shimmer-text';
66
import { cn } from '@/lib/utils';
77

@@ -32,7 +32,7 @@ const VIEW_MOTION_PROPS = {
3232
};
3333

3434
interface PreConnectMessageProps {
35-
messages?: ReceivedChatMessage[];
35+
messages?: ReceivedMessage[];
3636
className?: string;
3737
}
3838

components/app/session-provider.tsx

Lines changed: 0 additions & 41 deletions
This file was deleted.

components/app/session-view.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import React, { useEffect, useRef, useState } from 'react';
44
import { motion } from 'motion/react';
5+
import { useSessionContext, useSessionMessages } from '@livekit/components-react';
56
import type { AppConfig } from '@/app-config';
67
import { ChatTranscript } from '@/components/app/chat-transcript';
78
import { PreConnectMessage } from '@/components/app/preconnect-message';
@@ -10,15 +11,12 @@ import {
1011
AgentControlBar,
1112
type ControlBarControls,
1213
} from '@/components/livekit/agent-control-bar/agent-control-bar';
13-
import { useChatMessages } from '@/hooks/useChatMessages';
14-
import { useConnectionTimeout } from '@/hooks/useConnectionTimout';
15-
import { useDebugMode } from '@/hooks/useDebug';
14+
import { useConnection } from '@/hooks/useConnection';
1615
import { cn } from '@/lib/utils';
1716
import { ScrollArea } from '../livekit/scroll-area/scroll-area';
1817

1918
const MotionBottom = motion.create('div');
2019

21-
const IN_DEVELOPMENT = process.env.NODE_ENV !== 'production';
2220
const BOTTOM_VIEW_MOTION_PROPS = {
2321
variants: {
2422
visible: {
@@ -58,6 +56,7 @@ export function Fade({ top = false, bottom = false, className }: FadeProps) {
5856
/>
5957
);
6058
}
59+
6160
interface SessionViewProps {
6261
appConfig: AppConfig;
6362
}
@@ -66,11 +65,10 @@ export const SessionView = ({
6665
appConfig,
6766
...props
6867
}: React.ComponentProps<'section'> & SessionViewProps) => {
69-
useConnectionTimeout(200_000);
70-
useDebugMode({ enabled: IN_DEVELOPMENT });
71-
72-
const messages = useChatMessages();
68+
const session = useSessionContext();
69+
const { messages } = useSessionMessages(session);
7370
const [chatOpen, setChatOpen] = useState(false);
71+
const { isConnectionActive, startDisconnectTransition } = useConnection();
7472
const scrollAreaRef = useRef<HTMLDivElement>(null);
7573

7674
const controls: ControlBarControls = {
@@ -100,7 +98,7 @@ export const SessionView = ({
10098
)}
10199
>
102100
<Fade top className="absolute inset-x-4 top-0 h-40" />
103-
<ScrollArea ref={scrollAreaRef} className="px-4 pt-40 pb-[150px] md:px-6 md:pb-[180px]">
101+
<ScrollArea ref={scrollAreaRef} className="px-4 pt-40 pb-[150px] md:px-6 md:pb-[200px]">
104102
<ChatTranscript
105103
hidden={!chatOpen}
106104
messages={messages}
@@ -122,7 +120,12 @@ export const SessionView = ({
122120
)}
123121
<div className="bg-background relative mx-auto max-w-2xl pb-3 md:pb-12">
124122
<Fade bottom className="absolute inset-x-0 top-0 h-4 -translate-y-full" />
125-
<AgentControlBar controls={controls} onChatOpenChange={setChatOpen} />
123+
<AgentControlBar
124+
controls={controls}
125+
isConnectionActive={isConnectionActive}
126+
onDisconnect={startDisconnectTransition}
127+
onChatOpenChange={setChatOpen}
128+
/>
126129
</div>
127130
</MotionBottom>
128131
</section>

components/app/view-controller.tsx

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
'use client';
22

3-
import { useRef } from 'react';
4-
import { AnimatePresence, motion } from 'motion/react';
5-
import { useRoomContext } from '@livekit/components-react';
6-
import { useSession } from '@/components/app/session-provider';
3+
import { useCallback } from 'react';
4+
import { AnimatePresence, type AnimationDefinition, motion } from 'motion/react';
5+
import type { AppConfig } from '@/app-config';
76
import { SessionView } from '@/components/app/session-view';
87
import { WelcomeView } from '@/components/app/welcome-view';
8+
import { useConnection } from '@/hooks/useConnection';
99

1010
const MotionWelcomeView = motion.create(WelcomeView);
1111
const MotionSessionView = motion.create(SessionView);
@@ -28,34 +28,36 @@ const VIEW_MOTION_PROPS = {
2828
},
2929
};
3030

31-
export function ViewController() {
32-
const room = useRoomContext();
33-
const isSessionActiveRef = useRef(false);
34-
const { appConfig, isSessionActive, startSession } = useSession();
31+
interface ViewControllerProps {
32+
appConfig: AppConfig;
33+
}
3534

36-
// animation handler holds a reference to stale isSessionActive value
37-
isSessionActiveRef.current = isSessionActive;
35+
export function ViewController({ appConfig }: ViewControllerProps) {
36+
const { isConnectionActive, connect, onDisconnectTransitionComplete } = useConnection();
3837

39-
// disconnect room after animation completes
40-
const handleAnimationComplete = () => {
41-
if (!isSessionActiveRef.current && room.state !== 'disconnected') {
42-
room.disconnect();
43-
}
44-
};
38+
const handleAnimationComplete = useCallback(
39+
(definition: AnimationDefinition) => {
40+
// manually end the session when the exit animation completes
41+
if (definition === 'hidden') {
42+
onDisconnectTransitionComplete();
43+
}
44+
},
45+
[onDisconnectTransitionComplete]
46+
);
4547

4648
return (
4749
<AnimatePresence mode="wait">
48-
{/* Welcome screen */}
49-
{!isSessionActive && (
50+
{/* Welcome view */}
51+
{!isConnectionActive && (
5052
<MotionWelcomeView
5153
key="welcome"
5254
{...VIEW_MOTION_PROPS}
5355
startButtonText={appConfig.startButtonText}
54-
onStartCall={startSession}
56+
onStartCall={connect}
5557
/>
5658
)}
5759
{/* Session view */}
58-
{isSessionActive && (
60+
{isConnectionActive && (
5961
<MotionSessionView
6062
key="session-view"
6163
{...VIEW_MOTION_PROPS}

components/livekit/agent-control-bar/agent-control-bar.tsx

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { type HTMLAttributes, useCallback, useState } from 'react';
44
import { Track } from 'livekit-client';
55
import { useChat, useRemoteParticipants } from '@livekit/components-react';
66
import { ChatTextIcon, PhoneDisconnectIcon } from '@phosphor-icons/react/dist/ssr';
7-
import { useSession } from '@/components/app/session-provider';
87
import { TrackToggle } from '@/components/livekit/agent-control-bar/track-toggle';
98
import { Button } from '@/components/livekit/button';
109
import { Toggle } from '@/components/livekit/toggle';
@@ -23,8 +22,8 @@ export interface ControlBarControls {
2322
}
2423

2524
export interface AgentControlBarProps extends UseInputControlsProps {
25+
isConnectionActive?: boolean;
2626
controls?: ControlBarControls;
27-
onDisconnect?: () => void;
2827
onChatOpenChange?: (open: boolean) => void;
2928
onDeviceError?: (error: { source: Track.Source; error: Error }) => void;
3029
}
@@ -36,6 +35,7 @@ export function AgentControlBar({
3635
controls,
3736
saveUserChoices = true,
3837
className,
38+
isConnectionActive = false,
3939
onDisconnect,
4040
onDeviceError,
4141
onChatOpenChange,
@@ -45,8 +45,6 @@ export function AgentControlBar({
4545
const participants = useRemoteParticipants();
4646
const [chatOpen, setChatOpen] = useState(false);
4747
const publishPermissions = usePublishPermissions();
48-
const { isSessionActive, endSession } = useSession();
49-
5048
const {
5149
micTrackRef,
5250
cameraToggle,
@@ -70,11 +68,6 @@ export function AgentControlBar({
7068
[onChatOpenChange, setChatOpen]
7169
);
7270

73-
const handleDisconnect = useCallback(async () => {
74-
endSession();
75-
onDisconnect?.();
76-
}, [endSession, onDisconnect]);
77-
7871
const visibleControls = {
7972
leave: controls?.leave ?? true,
8073
microphone: controls?.microphone ?? publishPermissions.microphone,
@@ -164,8 +157,8 @@ export function AgentControlBar({
164157
{visibleControls.leave && (
165158
<Button
166159
variant="destructive"
167-
onClick={handleDisconnect}
168-
disabled={!isSessionActive}
160+
onClick={onDisconnect}
161+
disabled={!isConnectionActive}
169162
className="font-mono"
170163
>
171164
<PhoneDisconnectIcon weight="bold" />

components/livekit/alert-toast.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export function AlertToast(props: ToastProps) {
2222
const { title, description, id } = props;
2323

2424
return (
25-
<Alert onClick={() => sonnerToast.dismiss(id)} className="bg-accent">
25+
<Alert onClick={() => sonnerToast.dismiss(id)} className="bg-accent w-full md:w-[364px]">
2626
<WarningIcon weight="bold" />
2727
<AlertTitle>{title}</AlertTitle>
2828
{description && <AlertDescription>{description}</AlertDescription>}

0 commit comments

Comments
 (0)