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
15 changes: 9 additions & 6 deletions app/ui/layout.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<SessionProvider appConfig={appConfig}>
<AppSessionProvider appConfig={appConfig}>
<div className="bg-muted/20 min-h-svh p-8">
<div className="mx-auto max-w-3xl space-y-8">
<header className="space-y-2">
<h1 className="text-5xl font-bold tracking-tight">LiveKit UI</h1>
<p className="text-muted-foreground max-w-80 leading-tight text-pretty">
A set of UI components for building LiveKit-powered voice experiences.
A set of UI Layouts for building LiveKit-powered voice experiences.
</p>
<p className="text-muted-foreground max-w-prose text-balance">
Built with{' '}
Expand All @@ -37,6 +40,6 @@ export default async function ComponentsLayout({ children }: { children: React.R
<main className="space-y-20">{children}</main>
</div>
</div>
</SessionProvider>
</AppSessionProvider>
);
}
8 changes: 4 additions & 4 deletions components/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,23 @@

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;
}

export function App({ appConfig }: AppProps) {
return (
<SessionProvider appConfig={appConfig}>
<AppSessionProvider appConfig={appConfig}>
<main className="grid h-svh grid-cols-1 place-content-center">
<ViewController />
<ViewController appConfig={appConfig} />
</main>
<StartAudio label="Start Audio" />
<RoomAudioRenderer />
<Toaster />
</SessionProvider>
</AppSessionProvider>
);
}
8 changes: 3 additions & 5 deletions components/app/chat-transcript.tsx
Original file line number Diff line number Diff line change
@@ -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');
Expand Down Expand Up @@ -50,7 +50,7 @@ const MESSAGE_MOTION_PROPS = {

interface ChatTranscriptProps {
hidden?: boolean;
messages?: ReceivedChatMessage[];
messages?: ReceivedMessage[];
}

export function ChatTranscript({
Expand All @@ -62,10 +62,9 @@ export function ChatTranscript({
<AnimatePresence>
{!hidden && (
<MotionContainer {...CONTAINER_MOTION_PROPS} {...props}>
{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 (
<MotionChatEntry
Expand All @@ -74,7 +73,6 @@ export function ChatTranscript({
timestamp={timestamp}
message={message}
messageOrigin={messageOrigin}
hasBeenEdited={hasBeenEdited}
{...MESSAGE_MOTION_PROPS}
Comment on lines 76 to 78
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: is it worth continuing to pass through this hasBeenEdited prop when the ReceivedMessage is the subtype of ReceivedChatMessage?

/>
);
Expand Down
4 changes: 2 additions & 2 deletions components/app/preconnect-message.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -32,7 +32,7 @@ const VIEW_MOTION_PROPS = {
};

interface PreConnectMessageProps {
messages?: ReceivedChatMessage[];
messages?: ReceivedMessage[];
className?: string;
}

Expand Down
41 changes: 0 additions & 41 deletions components/app/session-provider.tsx

This file was deleted.

27 changes: 17 additions & 10 deletions components/app/session-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -10,15 +11,12 @@ import {
AgentControlBar,
type ControlBarControls,
} from '@/components/livekit/agent-control-bar/agent-control-bar';
import { useChatMessages } from '@/hooks/useChatMessages';
import { useConnectionTimeout } from '@/hooks/useConnectionTimout';
import { useDebugMode } from '@/hooks/useDebug';
import { useAppSession } from '@/hooks/useAppSession';
import { cn } from '@/lib/utils';
import { ScrollArea } from '../livekit/scroll-area/scroll-area';

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

const IN_DEVELOPMENT = process.env.NODE_ENV !== 'production';
const BOTTOM_VIEW_MOTION_PROPS = {
variants: {
visible: {
Expand Down Expand Up @@ -58,6 +56,7 @@ export function Fade({ top = false, bottom = false, className }: FadeProps) {
/>
);
}

interface SessionViewProps {
appConfig: AppConfig;
}
Expand All @@ -66,10 +65,8 @@ export const SessionView = ({
appConfig,
...props
}: React.ComponentProps<'section'> & SessionViewProps) => {
useConnectionTimeout(200_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<HTMLDivElement>(null);

Expand All @@ -90,6 +87,11 @@ export const SessionView = ({
}
}, [messages]);

const handleDisconnect = () => {
// pass false so we can manually end the session when the exit animation completes
endSession(false);
};

return (
<section className="bg-background relative z-10 h-full w-full overflow-hidden" {...props}>
{/* Chat Transcript */}
Expand All @@ -100,7 +102,7 @@ export const SessionView = ({
)}
>
<Fade top className="absolute inset-x-4 top-0 h-40" />
<ScrollArea ref={scrollAreaRef} className="px-4 pt-40 pb-[150px] md:px-6 md:pb-[180px]">
<ScrollArea ref={scrollAreaRef} className="px-4 pt-40 pb-[150px] md:px-6 md:pb-[200px]">
<ChatTranscript
hidden={!chatOpen}
messages={messages}
Expand All @@ -122,7 +124,12 @@ export const SessionView = ({
)}
<div className="bg-background relative mx-auto max-w-2xl pb-3 md:pb-12">
<Fade bottom className="absolute inset-x-0 top-0 h-4 -translate-y-full" />
<AgentControlBar controls={controls} onChatOpenChange={setChatOpen} />
<AgentControlBar
controls={controls}
isSessionActive={isSessionActive}
onDisconnect={handleDisconnect}
onChatOpenChange={setChatOpen}
/>
</div>
</MotionBottom>
</section>
Expand Down
45 changes: 28 additions & 17 deletions components/app/view-controller.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
'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 { useAgentErrors } from '@/hooks/useAgentErrors';
import { useAppSession } from '@/hooks/useAppSession';
import { useDebugMode } from '@/hooks/useDebug';

const IN_DEVELOPMENT = process.env.NODE_ENV !== 'production';

const MotionWelcomeView = motion.create(WelcomeView);
const MotionSessionView = motion.create(SessionView);
Expand All @@ -28,20 +33,26 @@ const VIEW_MOTION_PROPS = {
},
};

export function ViewController() {
const room = useRoomContext();
const isSessionActiveRef = useRef(false);
const { appConfig, isSessionActive, startSession } = useSession();
interface ViewControllerProps {
appConfig: AppConfig;
}

export function ViewController({ appConfig }: ViewControllerProps) {
const session = useSessionContext();
const { isSessionActive, startSession } = useAppSession();

// animation handler holds a reference to stale isSessionActive value
isSessionActiveRef.current = isSessionActive;
useDebugMode({ enabled: IN_DEVELOPMENT });
useAgentErrors();

// 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 (
<AnimatePresence mode="wait">
Expand All @@ -50,7 +61,7 @@ export function ViewController() {
<MotionWelcomeView
key="welcome"
{...VIEW_MOTION_PROPS}
startButtonText={appConfig.startButtonText}
startButtonText={appConfig?.startButtonText ?? ''}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: is appConfig ever able to be unset? It looks like in the props for the component appConfig should always be passed so I think this is redundant can can be just made into a regular property access?

Suggested change
startButtonText={appConfig?.startButtonText ?? ''}
startButtonText={appConfig.startButtonText}

onStartCall={startSession}
/>
)}
Expand Down
13 changes: 3 additions & 10 deletions components/livekit/agent-control-bar/agent-control-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
}
Expand All @@ -36,6 +35,7 @@ export function AgentControlBar({
controls,
saveUserChoices = true,
className,
isSessionActive = false,
onDisconnect,
onDeviceError,
onChatOpenChange,
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -164,7 +157,7 @@ export function AgentControlBar({
{visibleControls.leave && (
<Button
variant="destructive"
onClick={handleDisconnect}
onClick={onDisconnect}
disabled={!isSessionActive}
className="font-mono"
>
Expand Down
2 changes: 1 addition & 1 deletion components/livekit/alert-toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function AlertToast(props: ToastProps) {
const { title, description, id } = props;

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