From 7b254b828e3e48d4f56f20570edf07854311a7b9 Mon Sep 17 00:00:00 2001 From: Thomas Yuill Date: Fri, 7 Aug 2026 13:59:31 -0400 Subject: [PATCH 1/6] Remove sandbox code from agent-starter-react Strips the LiveKit Cloud sandbox infrastructure (remote config injection, sandbox token source, sandbox env vars/docs/CI) now that sandboxes are being deprecated, leaving a plain front-end starter backed by the self-hosted /api/token route. Fixes CLT-3229 Co-Authored-By: Claude Sonnet 5 --- .env.example | 4 - .github/workflows/sync-to-production.yaml | 16 ---- README.md | 7 -- TEMPLATE.md | 17 ---- app-config.ts | 6 -- app/layout.tsx | 4 +- app/opengraph-image.tsx | 31 +------- app/page.tsx | 4 +- components/app/app.tsx | 7 +- lib/utils.ts | 96 +---------------------- 10 files changed, 5 insertions(+), 187 deletions(-) delete mode 100644 .github/workflows/sync-to-production.yaml delete mode 100644 TEMPLATE.md diff --git a/.env.example b/.env.example index 561314069..3607e675d 100644 --- a/.env.example +++ b/.env.example @@ -7,7 +7,3 @@ LIVEKIT_URL=wss://.livekit.cloud # Leave AGENT_NAME blank to enable automatic dispatch # Provide an agent name to enable explicit dispatch AGENT_NAME= - -# Internally used environment variables -NEXT_PUBLIC_APP_CONFIG_ENDPOINT= -SANDBOX_ID= diff --git a/.github/workflows/sync-to-production.yaml b/.github/workflows/sync-to-production.yaml deleted file mode 100644 index 3922a1b9c..000000000 --- a/.github/workflows/sync-to-production.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# .github/workflows/sync-to-production.yaml -name: Sync main to sandbox-production - -on: - workflow_dispatch: - -jobs: - sync: - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: livekit-examples/sandbox-deploy-action@v1 - with: - production_branch: 'sandbox-production' - token: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 08251e01d..49a9ef53e 100644 --- a/README.md +++ b/README.md @@ -141,9 +141,6 @@ export const APP_CONFIG_DEFAULTS: AppConfig = { // agent dispatch configuration agentName: undefined, - - // LiveKit Cloud Sandbox configuration - sandboxId: undefined, }; ``` @@ -161,10 +158,6 @@ Set `audioVisualizerType` in [`app-config.ts`](./app-config.ts) to switch visual Use `audioVisualizerColor` to set a shared accent color across all visualizer modes. -> [!NOTE] -> The `sandboxId` is for the LiveKit Cloud Sandbox environment. -> It is not used for local development. - #### Environment Variables You'll also need to configure your LiveKit credentials in `.env.local` (copy `.env.example` if you don't have one): diff --git a/TEMPLATE.md b/TEMPLATE.md deleted file mode 100644 index b93f4c509..000000000 --- a/TEMPLATE.md +++ /dev/null @@ -1,17 +0,0 @@ -## Overview - -A Next.js frontend for a simple AI voice assistant using LiveKit's official [JavaScript SDK](https://github.com/livekit/client-sdk-js) and [React Components](https://github.com/livekit/components-js). The application implements its own token server, and supports [voice](https://docs.livekit.io/agents/start/voice-ai/), [transcription](https://docs.livekit.io/agents/build/text/), and [virtual avatars](https://docs.livekit.io/agents/integrations/avatar/). - -## Sandbox - -When deployed in a sandbox, LiveKit will host an instance of this application for you, providing a unique, shareable URL through which you can access it. Any agents running with the same LiveKit project credentials will join, meaning that you can rapidly iterate on your agent prototypes, and share the results instantly with friends and colleagues. To begin testing your agent, deploy this app in sandbox then set up an agent on your local machine using the [Voice AI Quickstart](https://docs.livekit.io/start/voice-ai): - -## Installation - -To run this application locally, clone the repo or use the [LiveKit CLI](https://docs.livekit.io/home/cli/cli-setup/): - -```console -lk app create --template agent-starter-react -``` - -For more information on prototyping using LiveKit Sandbox, see the [documentation](https://docs.livekit.io/home/cloud/sandbox/). diff --git a/app-config.ts b/app-config.ts index 2e950af5a..01b60f518 100644 --- a/app-config.ts +++ b/app-config.ts @@ -27,9 +27,6 @@ export interface AppConfig { // agent dispatch configuration agentName?: string; - - // LiveKit Cloud Sandbox configuration - sandboxId?: string; } export const APP_CONFIG_DEFAULTS: AppConfig = { @@ -66,7 +63,4 @@ export const APP_CONFIG_DEFAULTS: AppConfig = { // agent dispatch configuration agentName: process.env.AGENT_NAME ?? undefined, - - // LiveKit Cloud Sandbox configuration - sandboxId: undefined, }; diff --git a/app/layout.tsx b/app/layout.tsx index 46b51d4e2..9f1593862 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,6 +1,5 @@ import { Public_Sans } from 'next/font/google'; import localFont from 'next/font/local'; -import { headers } from 'next/headers'; import { ThemeProvider } from '@/components/app/theme-provider'; import { ThemeToggle } from '@/components/app/theme-toggle'; import { cn } from '@/lib/shadcn/utils'; @@ -44,8 +43,7 @@ interface RootLayoutProps { } export default async function RootLayout({ children }: RootLayoutProps) { - const hdrs = await headers(); - const appConfig = await getAppConfig(hdrs); + const appConfig = await getAppConfig(); const styles = getStyles(appConfig); const { pageTitle, pageDescription, companyName, logo, logoDark } = appConfig; diff --git a/app/opengraph-image.tsx b/app/opengraph-image.tsx index 9fccff1e9..1defb3a6f 100644 --- a/app/opengraph-image.tsx +++ b/app/opengraph-image.tsx @@ -1,4 +1,3 @@ -import { headers } from 'next/headers'; import { ImageResponse } from 'next/og'; import getImageSize from 'buffer-image-size'; import mime from 'mime'; @@ -102,8 +101,7 @@ export const contentType = 'image/png'; // Image generation export default async function Image() { - const hdrs = await headers(); - const appConfig = await getAppConfig(hdrs); + const appConfig = await getAppConfig(); const pageTitle = cleanPageTitle(appConfig.pageTitle); const logoUri = appConfig.logoDark || appConfig.logo; @@ -111,11 +109,9 @@ export default async function Image() { const wordmarkUri = logoUri === APP_CONFIG_DEFAULTS.logoDark ? 'public/lk-wordmark.svg' : logoUri; // Load fonts - use file system in dev, fetch in production - let commitMonoData: ArrayBuffer | undefined; let everettLightData: ArrayBuffer | undefined; try { - commitMonoData = await loadFileData('public/commit-mono-400-regular.woff'); everettLightData = await loadFileData('public/everett-light.woff'); } catch (e) { console.error('Failed to load fonts:', e); @@ -194,21 +190,6 @@ export default async function Image() { gap: 16, }} > -
- SANDBOX -
; } diff --git a/components/app/app.tsx b/components/app/app.tsx index f604cfb5a..d3e836bc8 100644 --- a/components/app/app.tsx +++ b/components/app/app.tsx @@ -11,7 +11,6 @@ import { ViewController } from '@/components/app/view-controller'; import { Toaster } from '@/components/ui/sonner'; import { useAgentErrors } from '@/hooks/useAgentErrors'; import { useDebugMode } from '@/hooks/useDebug'; -import { getSandboxTokenSource } from '@/lib/utils'; const IN_DEVELOPMENT = process.env.NODE_ENV !== 'production'; @@ -27,11 +26,7 @@ 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/token'); - }, [appConfig]); + const tokenSource = useMemo(() => TokenSource.endpoint('/api/token'), []); const session = useSession( tokenSource, diff --git a/lib/utils.ts b/lib/utils.ts index 8a71efabf..6219b569c 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,72 +1,12 @@ import { cache } from 'react'; -import { TokenSource } from 'livekit-client'; import { APP_CONFIG_DEFAULTS } from '@/app-config'; import type { AppConfig } from '@/app-config'; -export const CONFIG_ENDPOINT = process.env.NEXT_PUBLIC_APP_CONFIG_ENDPOINT; -export const SANDBOX_ID = process.env.SANDBOX_ID; - -export interface SandboxConfig { - [key: string]: - | { type: 'string'; value: string } - | { type: 'number'; value: number } - | { type: 'boolean'; value: boolean } - | null; -} - /** * Get the app configuration - * @param headers - The headers of the request * @returns The app configuration - * - * @note React will invalidate the cache for all memoized functions for each server request. - * https://react.dev/reference/react/cache#caveats */ -export const getAppConfig = cache(async (headers: Headers): Promise => { - if (CONFIG_ENDPOINT) { - const sandboxId = SANDBOX_ID ?? headers.get('x-sandbox-id') ?? ''; - - try { - if (!sandboxId) { - throw new Error('Sandbox ID is required'); - } - - const response = await fetch(CONFIG_ENDPOINT, { - cache: 'no-store', - headers: { 'X-Sandbox-ID': sandboxId }, - }); - - if (response.ok) { - const remoteConfig: SandboxConfig = await response.json(); - - const config: AppConfig = { ...APP_CONFIG_DEFAULTS, sandboxId }; - - for (const [key, entry] of Object.entries(remoteConfig)) { - if (entry === null) continue; - // Only include app config entries that are declared in defaults and, if set, - // share the same primitive type as the default value. - if ( - (key in APP_CONFIG_DEFAULTS && - APP_CONFIG_DEFAULTS[key as keyof AppConfig] === undefined) || - (typeof config[key as keyof AppConfig] === entry.type && - typeof config[key as keyof AppConfig] === typeof entry.value) - ) { - // @ts-expect-error I'm not sure quite how to appease TypeScript, but we've thoroughly checked types above - config[key as keyof AppConfig] = entry.value as AppConfig[keyof AppConfig]; - } - } - - return config; - } else { - console.error( - `ERROR: querying config endpoint failed with status ${response.status}: ${response.statusText}` - ); - } - } catch (error) { - console.error('ERROR: getAppConfig() - lib/utils.ts', error); - } - } - +export const getAppConfig = cache(async (): Promise => { return APP_CONFIG_DEFAULTS; }); @@ -89,37 +29,3 @@ export function getStyles(appConfig: AppConfig) { .filter(Boolean) .join('\n'); } - -/** - * Get a token source for a sandboxed LiveKit session - * @param appConfig - The app configuration - * @returns A token source for a sandboxed LiveKit session - */ -export function getSandboxTokenSource(appConfig: AppConfig) { - return TokenSource.custom(async () => { - const url = new URL(process.env.NEXT_PUBLIC_CONN_DETAILS_ENDPOINT!, window.location.origin); - const sandboxId = appConfig.sandboxId ?? ''; - const roomConfig = appConfig.agentName - ? { - agents: [{ agent_name: appConfig.agentName }], - } - : undefined; - - try { - const res = await fetch(url.toString(), { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Sandbox-Id': sandboxId, - }, - body: JSON.stringify({ - room_config: roomConfig, - }), - }); - return await res.json(); - } catch (error) { - console.error('Error fetching connection details:', error); - throw new Error('Error fetching connection details!'); - } - }); -} From 0c84d08105c7ddbfdcb2804efd24716e162e9828 Mon Sep 17 00:00:00 2001 From: Thomas Yuill Date: Fri, 7 Aug 2026 14:07:02 -0400 Subject: [PATCH 2/6] Remove opengraph-image route Co-Authored-By: Claude Sonnet 5 --- app/opengraph-image.tsx | 226 ---------------------------------------- 1 file changed, 226 deletions(-) delete mode 100644 app/opengraph-image.tsx diff --git a/app/opengraph-image.tsx b/app/opengraph-image.tsx deleted file mode 100644 index 1defb3a6f..000000000 --- a/app/opengraph-image.tsx +++ /dev/null @@ -1,226 +0,0 @@ -import { ImageResponse } from 'next/og'; -import getImageSize from 'buffer-image-size'; -import mime from 'mime'; -import { existsSync } from 'node:fs'; -import { readFile } from 'node:fs/promises'; -import { join } from 'node:path'; -import { APP_CONFIG_DEFAULTS } from '@/app-config'; -import { getAppConfig } from '@/lib/utils'; - -type Dimensions = { - width: number; - height: number; -}; - -type ImageData = { - base64: string; - dimensions: Dimensions; -}; - -// Image metadata -export const alt = 'About Acme'; -export const size = { - width: 1200, - height: 628, -}; - -function isRemoteFile(uri: string) { - return uri.startsWith('http'); -} - -function doesLocalFileExist(uri: string) { - return existsSync(join(process.cwd(), uri)); -} - -// LOCAL FILES MUST BE IN PUBLIC FOLDER -async function loadFileData(filePath: string): Promise { - if (isRemoteFile(filePath)) { - const response = await fetch(filePath); - if (!response.ok) { - throw new Error(`Failed to fetch ${filePath} - ${response.status} ${response.statusText}`); - } - return await response.arrayBuffer(); - } - - // Try file system first (works in local development) - if (doesLocalFileExist(filePath)) { - const buffer = await readFile(join(process.cwd(), filePath)); - return buffer.buffer.slice( - buffer.byteOffset, - buffer.byteOffset + buffer.byteLength - ) as ArrayBuffer; - } - - // Fallback to fetching from public URL (works in production) - const publicFilePath = filePath.replace('public/', ''); - const fontUrl = `https://${process.env.VERCEL_URL}/${publicFilePath}`; - - const response = await fetch(fontUrl); - if (!response.ok) { - throw new Error(`Failed to fetch ${fontUrl} - ${response.status} ${response.statusText}`); - } - - return await response.arrayBuffer(); -} - -async function getImageData(uri: string, fallbackUri?: string): Promise { - try { - const fileData = await loadFileData(uri); - const buffer = Buffer.from(fileData); - const mimeType = mime.getType(uri); - - return { - base64: `data:${mimeType};base64,${buffer.toString('base64')}`, - dimensions: getImageSize(buffer), - }; - } catch (e) { - if (fallbackUri) { - return getImageData(fallbackUri, fallbackUri); - } - throw e; - } -} - -function scaleImageSize(size: { width: number; height: number }, desiredHeight: number) { - const scale = desiredHeight / size.height; - return { - width: size.width * scale, - height: desiredHeight, - }; -} - -function cleanPageTitle(appName: string) { - if (appName === APP_CONFIG_DEFAULTS.pageTitle) { - return 'Voice agent'; - } - - return appName; -} - -export const contentType = 'image/png'; - -// Image generation -export default async function Image() { - const appConfig = await getAppConfig(); - - const pageTitle = cleanPageTitle(appConfig.pageTitle); - const logoUri = appConfig.logoDark || appConfig.logo; - const isLogoUriLocal = logoUri.includes('lk-logo'); - const wordmarkUri = logoUri === APP_CONFIG_DEFAULTS.logoDark ? 'public/lk-wordmark.svg' : logoUri; - - // Load fonts - use file system in dev, fetch in production - let everettLightData: ArrayBuffer | undefined; - - try { - everettLightData = await loadFileData('public/everett-light.woff'); - } catch (e) { - console.error('Failed to load fonts:', e); - // Continue without custom fonts - will fall back to system fonts - } - - // bg - const { base64: bgSrcBase64 } = await getImageData('public/opengraph-image-bg.png'); - - // wordmark - const { base64: wordmarkSrcBase64, dimensions: wordmarkDimensions } = isLogoUriLocal - ? await getImageData(wordmarkUri) - : await getImageData(logoUri); - const wordmarkSize = scaleImageSize(wordmarkDimensions, isLogoUriLocal ? 32 : 64); - - // logo - const { base64: logoSrcBase64, dimensions: logoDimensions } = await getImageData( - logoUri, - 'public/lk-logo-dark.svg' - ); - const logoSize = scaleImageSize(logoDimensions, 24); - - return new ImageResponse( - ( - // ImageResponse JSX element -
- {/* wordmark */} -
- {/* eslint-disable-next-line jsx-a11y/alt-text */} - -
- {/* logo */} -
- {/* eslint-disable-next-line jsx-a11y/alt-text */} - -
- {/* title */} -
-
- {pageTitle} -
-
-
- ), - // ImageResponse options - { - // For convenience, we can re-use the exported opengraph-image - // size config to also set the ImageResponse's width and height. - ...size, - fonts: [ - ...(everettLightData - ? [ - { - name: 'Everett', - data: everettLightData, - style: 'normal' as const, - weight: 300 as const, - }, - ] - : []), - ], - } - ); -} From 6a52261e7ff270ae2134e0258d5308d963de2f5a Mon Sep 17 00:00:00 2001 From: Thomas Yuill Date: Fri, 7 Aug 2026 14:08:24 -0400 Subject: [PATCH 3/6] Remove getAppConfig indirection APP_CONFIG_DEFAULTS is now a static export with no per-request resolution, so page.tsx and layout.tsx can use it directly. Co-Authored-By: Claude Sonnet 5 --- app/layout.tsx | 10 +++++----- app/page.tsx | 8 +++----- lib/utils.ts | 10 ---------- 3 files changed, 8 insertions(+), 20 deletions(-) diff --git a/app/layout.tsx b/app/layout.tsx index 9f1593862..29f7f608d 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,9 +1,10 @@ import { Public_Sans } from 'next/font/google'; import localFont from 'next/font/local'; +import { APP_CONFIG_DEFAULTS } from '@/app-config'; import { ThemeProvider } from '@/components/app/theme-provider'; import { ThemeToggle } from '@/components/app/theme-toggle'; import { cn } from '@/lib/shadcn/utils'; -import { getAppConfig, getStyles } from '@/lib/utils'; +import { getStyles } from '@/lib/utils'; import '@/styles/globals.css'; const publicSans = Public_Sans({ @@ -42,10 +43,9 @@ interface RootLayoutProps { children: React.ReactNode; } -export default async function RootLayout({ children }: RootLayoutProps) { - const appConfig = await getAppConfig(); - const styles = getStyles(appConfig); - const { pageTitle, pageDescription, companyName, logo, logoDark } = appConfig; +export default function RootLayout({ children }: RootLayoutProps) { + const styles = getStyles(APP_CONFIG_DEFAULTS); + const { pageTitle, pageDescription, companyName, logo, logoDark } = APP_CONFIG_DEFAULTS; return ( ; +export default function Page() { + return ; } diff --git a/lib/utils.ts b/lib/utils.ts index 6219b569c..78d1185ce 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,15 +1,5 @@ -import { cache } from 'react'; -import { APP_CONFIG_DEFAULTS } from '@/app-config'; import type { AppConfig } from '@/app-config'; -/** - * Get the app configuration - * @returns The app configuration - */ -export const getAppConfig = cache(async (): Promise => { - return APP_CONFIG_DEFAULTS; -}); - /** * Get styles for the app * @param appConfig - The app configuration From a30bb08fc41b58fdc3178f1e1a9d277099327935 Mon Sep 17 00:00:00 2001 From: Thomas Yuill Date: Fri, 7 Aug 2026 14:22:36 -0400 Subject: [PATCH 4/6] Remove app-config.ts and inline literal defaults Deletes the AppConfig abstraction entirely and inlines its default values directly where they're used (layout branding/styles, agent dispatch name, view-controller feature flags). Customization now happens by editing those components directly. Co-Authored-By: Claude Sonnet 5 --- README.md | 48 ++-------------------- app-config.ts | 66 ------------------------------ app/layout.tsx | 18 ++++---- app/page.tsx | 3 +- components/app/app.tsx | 12 ++---- components/app/view-controller.tsx | 32 +++------------ lib/utils.ts | 21 ---------- 7 files changed, 21 insertions(+), 179 deletions(-) delete mode 100644 app-config.ts delete mode 100644 lib/utils.ts diff --git a/README.md b/README.md index 49a9ef53e..b678c4f35 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Also available for: - Multiple audio visualizer styles (`bar`, `grid`, `radial`, `wave`, `aura`) - Virtual avatar integration - Light/dark theme switching with system preference detection -- Customizable branding, colors, and UI text via configuration +- Customizable branding, colors, and UI text This template is built with Next.js and is free for you to use or modify as you see fit. @@ -102,53 +102,11 @@ You'll also need an agent to speak with. Try our starter agent for [Python](http ## Configuration -This starter is designed to be flexible so you can adapt it to your specific agent use case. You can easily configure it to work with different types of inputs and outputs: - -#### Example: App configuration (`app-config.ts`) - -```ts -export const APP_CONFIG_DEFAULTS: AppConfig = { - companyName: 'LiveKit', - pageTitle: 'LiveKit Voice Agent', - pageDescription: 'A voice agent built with LiveKit', - - supportsChatInput: true, - supportsVideoInput: true, - supportsScreenShare: true, - isPreConnectBufferEnabled: true, - - logo: '/lk-logo.svg', - accent: '#002cf2', - logoDark: '/lk-logo-dark.svg', - accentDark: '#1fd5f9', - startButtonText: 'Start call', - - // optional: audio visualization configuration - // audioVisualizerColor: '#002cf2', - // audioVisualizerColorDark: '#1fd5f9', - // audioVisualizerType: 'bar', - // audioVisualizerBarCount: 5, - // audioVisualizerType: 'radial', - // audioVisualizerRadialBarCount: 24, - // audioVisualizerRadialRadius: 100, - // audioVisualizerType: 'grid', - // audioVisualizerGridRowCount: 25, - // audioVisualizerGridColumnCount: 25, - // audioVisualizerType: 'wave', - // audioVisualizerWaveLineWidth: 3, - // audioVisualizerType: 'aura', - // audioVisualizerAuraColorShift: 0.3, - - // agent dispatch configuration - agentName: undefined, -}; -``` - -You can update these values in [`app-config.ts`](./app-config.ts) to customize branding, features, and UI text for your deployment. +This starter is designed to be flexible so you can adapt it to your specific agent use case. Branding, feature toggles, and UI text are set directly in the components that use them — see [`app/layout.tsx`](./app/layout.tsx) for branding (logo, colors, page title/description) and [`components/app/view-controller.tsx`](./components/app/view-controller.tsx) for feature toggles and the audio visualizer. #### Audio visualizer presets -Set `audioVisualizerType` in [`app-config.ts`](./app-config.ts) to switch visualizer styles: +Set `audioVisualizerType` in [`components/app/view-controller.tsx`](./components/app/view-controller.tsx) to switch visualizer styles: - `bar` (default): vertical bars with optional `audioVisualizerBarCount` - `grid`: dot grid with `audioVisualizerGridRowCount` and `audioVisualizerGridColumnCount` diff --git a/app-config.ts b/app-config.ts deleted file mode 100644 index 01b60f518..000000000 --- a/app-config.ts +++ /dev/null @@ -1,66 +0,0 @@ -export interface AppConfig { - pageTitle: string; - pageDescription: string; - companyName: string; - - supportsChatInput: boolean; - supportsVideoInput: boolean; - supportsScreenShare: boolean; - isPreConnectBufferEnabled: boolean; - - logo: string; - startButtonText: string; - accent?: string; - logoDark?: string; - accentDark?: string; - - audioVisualizerType?: 'bar' | 'wave' | 'grid' | 'radial' | 'aura'; - audioVisualizerColor?: `#${string}`; - audioVisualizerColorDark?: `#${string}`; - audioVisualizerColorShift?: number; - audioVisualizerBarCount?: number; - audioVisualizerGridRowCount?: number; - audioVisualizerGridColumnCount?: number; - audioVisualizerRadialBarCount?: number; - audioVisualizerRadialRadius?: number; - audioVisualizerWaveLineWidth?: number; - - // agent dispatch configuration - agentName?: string; -} - -export const APP_CONFIG_DEFAULTS: AppConfig = { - companyName: 'LiveKit', - pageTitle: 'LiveKit Voice Agent', - pageDescription: 'A voice agent built with LiveKit', - - supportsChatInput: true, - supportsVideoInput: true, - supportsScreenShare: true, - isPreConnectBufferEnabled: true, - - logo: '/lk-logo.svg', - accent: '#002cf2', - logoDark: '/lk-logo-dark.svg', - accentDark: '#1fd5f9', - startButtonText: 'Start call', - - // optional: audio visualization configuration - // audioVisualizerType: 'bar', - // audioVisualizerColor: '#002cf2', - // audioVisualizerColorDark: '#1fd5f9', - // audioVisualizerColorShift: 0.3, - // audioVisualizerBarCount: 5, - // audioVisualizerType: 'radial', - // audioVisualizerRadialBarCount: 24, - // audioVisualizerRadialRadius: 100, - // audioVisualizerType: 'grid', - // audioVisualizerGridRowCount: 25, - // audioVisualizerGridColumnCount: 25, - // audioVisualizerType: 'wave', - // audioVisualizerWaveLineWidth: 3, - // audioVisualizerType: 'aura', - - // agent dispatch configuration - agentName: process.env.AGENT_NAME ?? undefined, -}; diff --git a/app/layout.tsx b/app/layout.tsx index 29f7f608d..e09ac9fe8 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,10 +1,8 @@ import { Public_Sans } from 'next/font/google'; import localFont from 'next/font/local'; -import { APP_CONFIG_DEFAULTS } from '@/app-config'; import { ThemeProvider } from '@/components/app/theme-provider'; import { ThemeToggle } from '@/components/app/theme-toggle'; import { cn } from '@/lib/shadcn/utils'; -import { getStyles } from '@/lib/utils'; import '@/styles/globals.css'; const publicSans = Public_Sans({ @@ -44,9 +42,6 @@ interface RootLayoutProps { } export default function RootLayout({ children }: RootLayoutProps) { - const styles = getStyles(APP_CONFIG_DEFAULTS); - const { pageTitle, pageDescription, companyName, logo, logoDark } = APP_CONFIG_DEFAULTS; - return ( - {styles && } - {pageTitle} - + + LiveKit Voice Agent + {/* eslint-disable-next-line @next/next/no-img-element */} - {`${companyName} + LiveKit Logo {/* eslint-disable-next-line @next/next/no-img-element */} {`${companyName} diff --git a/app/page.tsx b/app/page.tsx index e4951b324..8bbd4da97 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,6 +1,5 @@ -import { APP_CONFIG_DEFAULTS } from '@/app-config'; import { App } from '@/components/app/app'; export default function Page() { - return ; + return ; } diff --git a/components/app/app.tsx b/components/app/app.tsx index d3e836bc8..5a68728a0 100644 --- a/components/app/app.tsx +++ b/components/app/app.tsx @@ -4,7 +4,6 @@ import { useMemo } from 'react'; import { TokenSource } from 'livekit-client'; import { useSession } from '@livekit/components-react'; import { WarningIcon } from '@phosphor-icons/react/dist/ssr'; -import type { AppConfig } from '@/app-config'; import { AgentSessionProvider } from '@/components/agents-ui/agent-session-provider'; import { StartAudioButton } from '@/components/agents-ui/start-audio-button'; import { ViewController } from '@/components/app/view-controller'; @@ -22,22 +21,19 @@ function AppSetup() { } interface AppProps { - appConfig: AppConfig; + agentName?: string; } -export function App({ appConfig }: AppProps) { +export function App({ agentName }: AppProps) { const tokenSource = useMemo(() => TokenSource.endpoint('/api/token'), []); - const session = useSession( - tokenSource, - appConfig.agentName ? { agentName: appConfig.agentName } : undefined - ); + const session = useSession(tokenSource, agentName ? { agentName } : undefined); return (
- +
@@ -43,7 +36,7 @@ export function ViewController({ appConfig }: ViewControllerProps) { )} @@ -52,23 +45,10 @@ export function ViewController({ appConfig }: ViewControllerProps) { )} diff --git a/lib/utils.ts b/lib/utils.ts deleted file mode 100644 index 78d1185ce..000000000 --- a/lib/utils.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { AppConfig } from '@/app-config'; - -/** - * Get styles for the app - * @param appConfig - The app configuration - * @returns A string of styles - */ -export function getStyles(appConfig: AppConfig) { - const { accent, accentDark } = appConfig; - - return [ - accent - ? `:root { --primary: ${accent}; --primary-hover: color-mix(in srgb, ${accent} 80%, #000); }` - : '', - accentDark - ? `.dark { --primary: ${accentDark}; --primary-hover: color-mix(in srgb, ${accentDark} 80%, #000); }` - : '', - ] - .filter(Boolean) - .join('\n'); -} From d99673453c4bde34c3185d913653bc37ff02e951 Mon Sep 17 00:00:00 2001 From: Thomas Yuill Date: Mon, 10 Aug 2026 00:48:22 -0400 Subject: [PATCH 5/6] Move brand color overrides into globals.css Folds the inline LiveKit Voice Agent diff --git a/styles/globals.css b/styles/globals.css index ac92ee4e5..bfe31c3fe 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -12,7 +12,8 @@ --card-foreground: oklch(0.145 0 0); --popover: oklch(1 0 0); --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); + --primary: #002cf2; + --primary-hover: color-mix(in srgb, #002cf2 80%, #000); --primary-foreground: oklch(0.985 0 0); --secondary: oklch(0.97 0 0); --secondary-foreground: oklch(0.205 0 0); @@ -46,7 +47,8 @@ --card-foreground: oklch(0.985 0 0); --popover: oklch(0.269 0 0); --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.922 0 0); + --primary: #1fd5f9; + --primary-hover: color-mix(in srgb, #1fd5f9 80%, #000); --primary-foreground: oklch(0.205 0 0); --secondary: oklch(0.269 0 0); --secondary-foreground: oklch(0.985 0 0); From 51fa48c4f70c78ac5de7609ef09b59bf72311d5a Mon Sep 17 00:00:00 2001 From: Thomas Yuill Date: Mon, 10 Aug 2026 10:21:19 -0400 Subject: [PATCH 6/6] Pass resolved theme to AgentSessionView_01 The audio visualizer needs to know the active color scheme; this was dropped when the appConfig-driven audioVisualizerColor logic was removed. Co-Authored-By: Claude Sonnet 5 --- components/app/view-controller.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/components/app/view-controller.tsx b/components/app/view-controller.tsx index 12b1e6754..f53d8309c 100644 --- a/components/app/view-controller.tsx +++ b/components/app/view-controller.tsx @@ -1,5 +1,6 @@ 'use client'; +import { useTheme } from 'next-themes'; import { AnimatePresence, motion } from 'motion/react'; import { useSessionContext } from '@livekit/components-react'; import { AgentSessionView_01 } from '@/components/agents-ui/blocks/agent-session-view-01'; @@ -28,6 +29,7 @@ const VIEW_MOTION_PROPS = { export function ViewController() { const { isConnected, start } = useSessionContext(); + const { resolvedTheme } = useTheme(); return ( @@ -49,6 +51,7 @@ export function ViewController() { supportsVideoInput={true} supportsScreenShare={true} isPreConnectBufferEnabled={true} + themeMode={resolvedTheme === 'dark' ? 'dark' : 'light'} className="fixed inset-0" /> )}