diff --git a/app/layout.tsx b/app/layout.tsx index 171d4453e..252e2bcbd 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,7 +1,8 @@ import { Public_Sans } from 'next/font/google'; import localFont from 'next/font/local'; import { headers } from 'next/headers'; -import { ApplyThemeScript, ThemeToggle } from '@/components/app/theme-toggle'; +import { ThemeProvider } from '@/components/app/theme-provider'; +import { ThemeToggle } from '@/components/app/theme-toggle'; import { cn, getAppConfig, getStyles } from '@/lib/utils'; import '@/styles/globals.css'; @@ -61,13 +62,19 @@ export default async function RootLayout({ children }: RootLayoutProps) { {styles && } {pageTitle} - - {children} -
- -
+ + {children} +
+ +
+
); diff --git a/app/ui/layout.tsx b/app/ui/layout.tsx deleted file mode 100644 index 5cfb89816..000000000 --- a/app/ui/layout.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { headers } from 'next/headers'; -import { ConnectionProvider } from '@/hooks/useConnection'; -import { getAppConfig } from '@/lib/utils'; - -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 Layouts for building LiveKit-powered voice experiences. -

-

- Built with{' '} - - Shadcn - - ,{' '} - - Motion - - , and{' '} - - LiveKit - - . -

-

Open Source.

-
- -
{children}
-
-
-
- ); -} diff --git a/app/ui/page.tsx b/app/ui/page.tsx deleted file mode 100644 index 83e1a7ba4..000000000 --- a/app/ui/page.tsx +++ /dev/null @@ -1,268 +0,0 @@ -import { type VariantProps } from 'class-variance-authority'; -import { Track } from 'livekit-client'; -import { MicrophoneIcon } from '@phosphor-icons/react/dist/ssr'; -import { AgentControlBar } from '@/components/livekit/agent-control-bar/agent-control-bar'; -import { TrackDeviceSelect } from '@/components/livekit/agent-control-bar/track-device-select'; -import { TrackSelector } from '@/components/livekit/agent-control-bar/track-selector'; -import { TrackToggle } from '@/components/livekit/agent-control-bar/track-toggle'; -import { Alert, AlertDescription, AlertTitle, alertVariants } from '@/components/livekit/alert'; -import { AlertToast } from '@/components/livekit/alert-toast'; -import { Button, buttonVariants } from '@/components/livekit/button'; -import { ChatEntry } from '@/components/livekit/chat-entry'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/livekit/select'; -import { ShimmerText } from '@/components/livekit/shimmer-text'; -import { Toggle, toggleVariants } from '@/components/livekit/toggle'; -import { cn } from '@/lib/utils'; - -type toggleVariantsType = VariantProps['variant']; -type toggleVariantsSizeType = VariantProps['size']; -type buttonVariantsType = VariantProps['variant']; -type buttonVariantsSizeType = VariantProps['size']; -type alertVariantsType = VariantProps['variant']; - -interface ContainerProps { - componentName?: string; - children: React.ReactNode; - className?: string; -} - -function Container({ componentName, children, className }: ContainerProps) { - return ( -
-

- {componentName} -

-
- {children} -
-
- ); -} - -function StoryTitle({ children }: { children: React.ReactNode }) { - return

{children}

; -} - -export default function Base() { - return ( - <> -

Primitives

- - {/* Button */} - - - - - - - - - - - - - {['default', 'primary', 'secondary', 'outline', 'ghost', 'link', 'destructive'].map( - (variant) => ( - - - {['sm', 'default', 'lg', 'icon'].map((size) => ( - - ))} - - ) - )} - -
SmallDefaultLargeIcon
{variant} - -
-
- - {/* Toggle */} - - - - - - - - - - - - - {['default', 'primary', 'secondary', 'outline'].map((variant) => ( - - - {['sm', 'default', 'lg', 'icon'].map((size) => ( - - ))} - - ))} - -
SmallDefaultLargeIcon
{variant} - - {size === 'icon' ? : 'Toggle'} - -
-
- - {/* Alert */} - - {['default', 'destructive'].map((variant) => ( -
- {variant} - - Alert {variant} title - This is a {variant} alert description. - -
- ))} -
- - {/* Select */} - -
-
- Size default - -
-
- Size sm - -
-
-
- -

Components

- - {/* Agent control bar */} - -
- -
-
- - {/* Track device select */} - -
-
- Size default - -
-
- Size sm - -
-
-
- - {/* Track toggle */} - -
-
- Track.Source.Microphone - -
-
- Track.Source.Camera - -
-
-
- - {/* Track selector */} - -
-
- Track.Source.Camera - -
-
- Track.Source.Microphone - -
-
-
- - {/* Chat entry */} - -
- - -
-
- - {/* Shimmer text */} - -
- This is shimmer text -
-
- - {/* Alert toast */} - - Alert toast -
- -
-
- - ); -} diff --git a/components/app/app.tsx b/components/app/app.tsx index 792525857..6dbec1d26 100644 --- a/components/app/app.tsx +++ b/components/app/app.tsx @@ -1,12 +1,19 @@ 'use client'; -import { RoomAudioRenderer, StartAudio } from '@livekit/components-react'; +import { useMemo } from 'react'; +import { TokenSource } from 'livekit-client'; +import { + RoomAudioRenderer, + SessionProvider, + StartAudio, + useSession, +} from '@livekit/components-react'; import type { AppConfig } from '@/app-config'; import { ViewController } from '@/components/app/view-controller'; import { Toaster } from '@/components/livekit/toaster'; import { useAgentErrors } from '@/hooks/useAgentErrors'; -import { ConnectionProvider } from '@/hooks/useConnection'; import { useDebugMode } from '@/hooks/useDebug'; +import { getSandboxTokenSource } from '@/lib/utils'; const IN_DEVELOPMENT = process.env.NODE_ENV !== 'production'; @@ -22,8 +29,19 @@ interface AppProps { } export function App({ appConfig }: AppProps) { + const tokenSource = useMemo(() => { + return typeof process.env.NEXT_PUBLIC_CONN_DETAILS_ENDPOINT === 'string' + ? getSandboxTokenSource(appConfig) + : TokenSource.endpoint('/api/connection-details'); + }, [appConfig]); + + const session = useSession( + tokenSource, + appConfig.agentName ? { agentName: appConfig.agentName } : undefined + ); + return ( - +
@@ -31,6 +49,6 @@ export function App({ appConfig }: AppProps) { - + ); } diff --git a/components/app/session-view.tsx b/components/app/session-view.tsx index 380295648..5d7366405 100644 --- a/components/app/session-view.tsx +++ b/components/app/session-view.tsx @@ -11,7 +11,6 @@ import { AgentControlBar, type ControlBarControls, } from '@/components/livekit/agent-control-bar/agent-control-bar'; -import { useConnection } from '@/hooks/useConnection'; import { cn } from '@/lib/utils'; import { ScrollArea } from '../livekit/scroll-area/scroll-area'; @@ -68,7 +67,6 @@ export const SessionView = ({ const session = useSessionContext(); const { messages } = useSessionMessages(session); const [chatOpen, setChatOpen] = useState(false); - const { isConnectionActive, startDisconnectTransition } = useConnection(); const scrollAreaRef = useRef(null); const controls: ControlBarControls = { @@ -122,8 +120,8 @@ export const SessionView = ({ diff --git a/components/app/theme-provider.tsx b/components/app/theme-provider.tsx new file mode 100644 index 000000000..9bf53d860 --- /dev/null +++ b/components/app/theme-provider.tsx @@ -0,0 +1,11 @@ +'use client'; + +import * as React from 'react'; +import { ThemeProvider as NextThemesProvider } from 'next-themes'; + +export function ThemeProvider({ + children, + ...props +}: React.ComponentProps) { + return {children}; +} diff --git a/components/app/theme-toggle.tsx b/components/app/theme-toggle.tsx index ffefc0da1..43d1cd59a 100644 --- a/components/app/theme-toggle.tsx +++ b/components/app/theme-toggle.tsx @@ -1,67 +1,15 @@ 'use client'; -import { useEffect, useState } from 'react'; +import { useTheme } from 'next-themes'; import { MonitorIcon, MoonIcon, SunIcon } from '@phosphor-icons/react'; -import { THEME_MEDIA_QUERY, THEME_STORAGE_KEY, cn } from '@/lib/utils'; - -const THEME_SCRIPT = ` - const doc = document.documentElement; - const theme = localStorage.getItem("${THEME_STORAGE_KEY}") ?? "system"; - - if (theme === "system") { - if (window.matchMedia("${THEME_MEDIA_QUERY}").matches) { - doc.classList.add("dark"); - } else { - doc.classList.add("light"); - } - } else { - doc.classList.add(theme); - } -` - .trim() - .replace(/\n/g, '') - .replace(/\s+/g, ' '); - -export type ThemeMode = 'dark' | 'light' | 'system'; - -function applyTheme(theme: ThemeMode) { - const doc = document.documentElement; - - doc.classList.remove('dark', 'light'); - localStorage.setItem(THEME_STORAGE_KEY, theme); - - if (theme === 'system') { - if (window.matchMedia(THEME_MEDIA_QUERY).matches) { - doc.classList.add('dark'); - } else { - doc.classList.add('light'); - } - } else { - doc.classList.add(theme); - } -} +import { cn } from '@/lib/utils'; interface ThemeToggleProps { className?: string; } -export function ApplyThemeScript() { - return ; -} - export function ThemeToggle({ className }: ThemeToggleProps) { - const [theme, setTheme] = useState(undefined); - - useEffect(() => { - const storedTheme = (localStorage.getItem(THEME_STORAGE_KEY) as ThemeMode) ?? 'system'; - - setTheme(storedTheme); - }, []); - - function handleThemeChange(theme: ThemeMode) { - applyTheme(theme); - setTheme(theme); - } + const { theme, setTheme } = useTheme(); return (
Color scheme toggle -
); diff --git a/components/app/view-controller.tsx b/components/app/view-controller.tsx index cfe987013..d64e00afa 100644 --- a/components/app/view-controller.tsx +++ b/components/app/view-controller.tsx @@ -1,11 +1,10 @@ 'use client'; -import { useCallback } from 'react'; -import { AnimatePresence, type AnimationDefinition, motion } from 'motion/react'; +import { AnimatePresence, motion } from 'motion/react'; +import { useSessionContext } from '@livekit/components-react'; import type { AppConfig } from '@/app-config'; import { SessionView } from '@/components/app/session-view'; import { WelcomeView } from '@/components/app/welcome-view'; -import { useConnection } from '@/hooks/useConnection'; const MotionWelcomeView = motion.create(WelcomeView); const MotionSessionView = motion.create(SessionView); @@ -33,37 +32,22 @@ interface ViewControllerProps { } export function ViewController({ appConfig }: ViewControllerProps) { - const { isConnectionActive, connect, onDisconnectTransitionComplete } = useConnection(); - - const handleAnimationComplete = useCallback( - (definition: AnimationDefinition) => { - // manually end the session when the exit animation completes - if (definition === 'hidden') { - onDisconnectTransitionComplete(); - } - }, - [onDisconnectTransitionComplete] - ); + const { isConnected, start } = useSessionContext(); return ( {/* Welcome view */} - {!isConnectionActive && ( + {!isConnected && ( )} {/* Session view */} - {isConnectionActive && ( - + {isConnected && ( + )} ); diff --git a/components/livekit/agent-control-bar/agent-control-bar.tsx b/components/livekit/agent-control-bar/agent-control-bar.tsx index 52b99b59a..ccba63c76 100644 --- a/components/livekit/agent-control-bar/agent-control-bar.tsx +++ b/components/livekit/agent-control-bar/agent-control-bar.tsx @@ -22,8 +22,8 @@ export interface ControlBarControls { } export interface AgentControlBarProps extends UseInputControlsProps { - isConnectionActive?: boolean; controls?: ControlBarControls; + isConnected?: boolean; onChatOpenChange?: (open: boolean) => void; onDeviceError?: (error: { source: Track.Source; error: Error }) => void; } @@ -35,7 +35,7 @@ export function AgentControlBar({ controls, saveUserChoices = true, className, - isConnectionActive = false, + isConnected = false, onDisconnect, onDeviceError, onChatOpenChange, @@ -158,7 +158,7 @@ export function AgentControlBar({