Skip to content
Draft
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
2 changes: 2 additions & 0 deletions app-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface AppConfig {
accent?: string;
logoDark?: string;
accentDark?: string;
audioVisualizer?: 'bar' | 'radial' | 'grid' | 'aura' | 'wave';

// for LiveKit Cloud Sandbox
sandboxId?: string;
Expand All @@ -34,6 +35,7 @@ export const APP_CONFIG_DEFAULTS: AppConfig = {
logoDark: '/lk-logo-dark.svg',
accentDark: '#1fd5f9',
startButtonText: 'Start call',
audioVisualizer: 'aura',

// for LiveKit Cloud Sandbox
sandboxId: undefined,
Expand Down
19 changes: 13 additions & 6 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -61,13 +62,19 @@ export default async function RootLayout({ children }: RootLayoutProps) {
{styles && <style>{styles}</style>}
<title>{pageTitle}</title>
<meta name="description" content={pageDescription} />
<ApplyThemeScript />
</head>
<body className="overflow-x-hidden">
{children}
<div className="group fixed bottom-0 left-1/2 z-50 mb-2 -translate-x-1/2">
<ThemeToggle className="translate-y-20 transition-transform delay-150 duration-300 group-hover:translate-y-0" />
</div>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
<div className="group fixed bottom-0 left-1/2 z-50 mb-2 -translate-x-1/2">
<ThemeToggle className="translate-y-20 transition-transform delay-150 duration-300 group-hover:translate-y-0" />
</div>
</ThemeProvider>
</body>
</html>
);
Expand Down
102 changes: 102 additions & 0 deletions app/ui/(landing-page)/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
'use client';

import Link from 'next/link';
import { useVoiceAssistant } from '@livekit/components-react';
import { AgentControlBar } from '@/components/livekit/agent-control-bar/agent-control-bar';
import { AudioBarVisualizer } from '@/components/livekit/audio-visualizer/audio-bar-visualizer/audio-bar-visualizer';
import { Button } from '@/components/livekit/button';
import { ChatEntry } from '@/components/livekit/chat-entry';
import { useMicrophone } from '@/hooks/useMicrophone';

export default function Page() {
const { state, audioTrack } = useVoiceAssistant();

useMicrophone();

return (
<>
<header className="grid h-96 place-content-center space-y-6 text-center">
<h1 className="flex items-baseline justify-center gap-2 text-5xl">
<svg
height="48"
viewBox="0 0 123 28"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="text-foreground"
>
<path
d="M4.7 0H0v27.6h17v-4H4.7V0ZM24.8 12.5h-4.5v15h4.5v-15ZM38.2 27 32.4 8H28l6 19.6h8.6l6-19.6H44l-5.8 19ZM59.8 7.6c-5.9 0-9.6 4.2-9.6 10.2 0 6 3.6 10.2 9.6 10.2 4.6 0 8-2 9.2-6.2h-4.6c-.7 1.9-2 3-4.5 3-2.8 0-4.7-2-5-5.7h14.4l.1-1.4c0-6.1-3.8-10.1-9.6-10.1Zm-5 8.4c.5-3.6 2.4-5.2 5-5.2 2.9 0 4.7 2 5 5.2h-10ZM96 0h-5.9L78.7 12.6V0H74v27.6h4.7v-14l12.6 14h6L84.1 13 96.1 0ZM104 8h-4.6v15h4.5V8ZM20.3 8h-4.6v4.5h4.6V8ZM108.5 23h-4.6v4.6h4.6V23ZM122 23h-4.5v4.6h4.6V23ZM122 12.5V8h-4.5V0H113v8h-4.6v4.5h4.6V23h4.5V12.5h4.6Z"
fill="currentColor"
/>
</svg>
<span className="font-extralight tracking-tighter">UI</span>
</h1>
<p className="text-lg text-pretty">
A set of Open Source UI components for
<br />
building beautiful voice experiences.
</p>
<div className="flex justify-center gap-4">
<Button variant="primary" asChild>
<Link href="/ui/components">View components</Link>
</Button>
<Button variant="ghost" asChild>
<Link href="https://docs.livekit.io/agents/start/frontend/">Read our docs</Link>
</Button>
</div>
</header>

<main className="mx-auto max-w-5xl space-y-8">
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div className="border-border bg-background h-96 rounded-3xl border p-8">
<div className="flex h-full flex-col gap-4">
<div className="grid flex-1 grow place-content-center">
<AudioBarVisualizer state={state} audioTrack={audioTrack!} />
</div>
<AgentControlBar
className="w-full"
controls={{
leave: true,
chat: true,
camera: true,
microphone: true,
screenShare: true,
}}
/>
</div>
</div>
<div className="border-border bg-background h-96 rounded-3xl border p-8">
<div className="flex h-full flex-col gap-4">
<div className="flex-1 grow">
<ChatEntry
locale="en-US"
name="User"
message="Hello, how are you?"
messageOrigin="local"
timestamp={1761096559966}
/>
<ChatEntry
locale="en-US"
name="Agent"
message="I am good, how about you?"
messageOrigin="remote"
timestamp={1761096569216}
/>
</div>
<AgentControlBar
className="w-full"
controls={{
leave: true,
chat: true,
camera: true,
microphone: true,
screenShare: true,
}}
/>
</div>
</div>
</div>
</main>
</>
);
}
13 changes: 13 additions & 0 deletions app/ui/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
THIS IS NOT PART OF THE MAIN APPLICATION CODE.

This folder contains code for testing and previewing LiveKit's UI component library in isolation.

## Getting started

To run the development server, run the following command:

```bash
npm run dev
```

Then, navigate to `http://localhost:3000/ui` to see the components.
21 changes: 21 additions & 0 deletions app/ui/components/[...slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use client';

import dynamic from 'next/dynamic';
import { redirect, useParams } from 'next/navigation';

export default function Page() {
const { slug = [] } = useParams();
const [componentName] = slug;
const ComponentDemo = dynamic(() => import(`@/components/demos/${componentName}`));

if (!ComponentDemo) {
return redirect('/ui');
}

return (
<>
<h1 className="text-foreground mb-8 text-5xl font-bold">{componentName}</h1>
<ComponentDemo />
</>
);
}
26 changes: 26 additions & 0 deletions app/ui/components/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { SideNav } from '@/components/docs/side-nav';
import { getComponentNames } from '@/lib/components';

interface LayoutProps {
children: React.ReactNode;
}

export default function Layout({ children }: LayoutProps) {
const componentNames = getComponentNames();

return (
<div className="grid grid-cols-1 gap-8 md:grid-cols-[100px_1fr_100px]">
<aside className="sticky top-0 hidden py-10 md:block">
<div className="flex flex-col gap-2">
<SideNav componentNames={componentNames} />
</div>
</aside>

<div className="space-y-8 py-8">
<main className="mx-auto max-w-3xl space-y-8">{children}</main>
</div>

<aside className="sticky top-0 hidden md:block"></aside>
</div>
);
}
31 changes: 31 additions & 0 deletions app/ui/components/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Link from 'next/link';
import { getComponentNames } from '@/lib/components';

export default function Page() {
const componentNames = getComponentNames();

return (
<>
<h2 id="components" className="mb-8 text-4xl font-bold tracking-tighter">
Components
</h2>
<p className="text-muted-foreground text-balance">
Build beautiful voice experiences with our components.
</p>

<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{componentNames
.sort((a, b) => a.localeCompare(b))
.map((componentName) => (
<Link
href={`/ui/components/${componentName}`}

Check failure

Code scanning / CodeQL

Stored cross-site scripting High

Stored cross-site scripting vulnerability due to
stored value
.

Copilot Autofix

AI 9 days ago

The best fix is to ensure that only valid, expected component names are used, and that they are sanitized before inclusion in URLs and as visible text. Specifically:

  • Only allow component names that match safe slug/identifier patterns (e.g., only letters, numbers, hyphens, and underscores).
  • Filter out unsafe component names when listing them.
  • Sanitize each component name before creating the URL and rendering the link text.

A robust approach is to update getComponentNames() in lib/components.tsx so it only returns component names that match a safe pattern (e.g., /^[a-zA-Z0-9_-]+$/).
Make the change directly in lib/components.tsx.
No changes are needed in app/ui/components/page.tsx, as the source of truth will now be validated and only return safe values.

Suggested changeset 1
lib/components.tsx
Outside changed files

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/lib/components.tsx b/lib/components.tsx
--- a/lib/components.tsx
+++ b/lib/components.tsx
@@ -6,5 +6,7 @@
   const componentNames = fs.readdirSync(componentsDir);
   return componentNames
     .filter((file) => file.endsWith('.tsx'))
-    .map((file) => file.replace('.tsx', ''));
+    .map((file) => file.replace('.tsx', ''))
+    // Only allow component names containing [A-Za-z0-9_-]
+    .filter((name) => /^[a-zA-Z0-9_-]+$/.test(name));
 }
EOF
@@ -6,5 +6,7 @@
const componentNames = fs.readdirSync(componentsDir);
return componentNames
.filter((file) => file.endsWith('.tsx'))
.map((file) => file.replace('.tsx', ''));
.map((file) => file.replace('.tsx', ''))
// Only allow component names containing [A-Za-z0-9_-]
.filter((name) => /^[a-zA-Z0-9_-]+$/.test(name));
}
Copilot is powered by AI and may make mistakes. Always verify output.
key={componentName}

Check failure

Code scanning / CodeQL

Stored cross-site scripting High

Stored cross-site scripting vulnerability due to
stored value
.

Copilot Autofix

AI 9 days ago

The best way to fix the problem is to ensure that all untrusted inputs rendered in the URL context are safely encoded. Specifically, when constructing the href for <Link>, componentName must be passed through encodeURIComponent() to prevent injection of slashes or special characters that could break the URL structure or enable attacks. Also, as a defense-in-depth, if rendering as HTML or if file names might contain suspicious Unicode, one could further escape text using a utility like escape-html, but in this case, React’s default escaping suffices for element children.

Files to change:

  • app/ui/components/page.tsx:
    Change {/ui/components/${componentName}} to {/ui/components/${encodeURIComponent(componentName)}} in the href prop.
    Import encodeURIComponent is not required since it is a global function.

No code changes necessary in lib/components.tsx because filtering occurs after file reading, but does not ensure file name “safety” -- the only thing needed is output encoding as above.


Suggested changeset 1
app/ui/components/page.tsx

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/app/ui/components/page.tsx b/app/ui/components/page.tsx
--- a/app/ui/components/page.tsx
+++ b/app/ui/components/page.tsx
@@ -18,7 +18,7 @@
           .sort((a, b) => a.localeCompare(b))
           .map((componentName) => (
             <Link
-              href={`/ui/components/${componentName}`}
+              href={`/ui/components/${encodeURIComponent(componentName)}`}
               key={componentName}
               className="font-semibold underline-offset-4 hover:underline focus:underline"
             >
EOF
@@ -18,7 +18,7 @@
.sort((a, b) => a.localeCompare(b))
.map((componentName) => (
<Link
href={`/ui/components/${componentName}`}
href={`/ui/components/${encodeURIComponent(componentName)}`}
key={componentName}
className="font-semibold underline-offset-4 hover:underline focus:underline"
>
Copilot is powered by AI and may make mistakes. Always verify output.
className="font-semibold underline-offset-4 hover:underline focus:underline"
>
{componentName}
</Link>
))}
</div>
</>
);
}
83 changes: 54 additions & 29 deletions app/ui/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,69 @@
import { headers } from 'next/headers';
import Link from 'next/link';
import { ConnectionProvider } from '@/hooks/useConnection';
import { getAppConfig } from '@/lib/utils';

interface LayoutProps {
children: React.ReactNode;
}

export default async function Layout({ children }: LayoutProps) {
export default async function Layout({ children }: { children: React.ReactNode }) {
const hdrs = await headers();
const appConfig = await getAppConfig(hdrs);

return (
<ConnectionProvider 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 Layouts for building LiveKit-powered voice experiences.
</p>
<p className="text-muted-foreground max-w-prose text-balance">
Built with{' '}
<a href="https://shadcn.com" className="underline underline-offset-2">
Shadcn
</a>
,{' '}
<a href="https://motion.dev" className="underline underline-offset-2">
Motion
</a>
, and{' '}
<a href="https://livekit.io" className="underline underline-offset-2">
LiveKit
</a>
.
</p>
<p className="text-foreground max-w-prose text-balance">Open Source.</p>
<div className="px-8">
<div className="min-h-svh">
<header className="flex items-baseline gap-8 py-4">
<Link
href="/ui"
className="hover:text-primary focus:text-primary flex cursor-pointer items-baseline gap-1 leading-4"
>
<svg
height="20"
viewBox="0 0 123 28"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="text-foreground"
>
<path
d="M4.7 0H0v27.6h17v-4H4.7V0ZM24.8 12.5h-4.5v15h4.5v-15ZM38.2 27 32.4 8H28l6 19.6h8.6l6-19.6H44l-5.8 19ZM59.8 7.6c-5.9 0-9.6 4.2-9.6 10.2 0 6 3.6 10.2 9.6 10.2 4.6 0 8-2 9.2-6.2h-4.6c-.7 1.9-2 3-4.5 3-2.8 0-4.7-2-5-5.7h14.4l.1-1.4c0-6.1-3.8-10.1-9.6-10.1Zm-5 8.4c.5-3.6 2.4-5.2 5-5.2 2.9 0 4.7 2 5 5.2h-10ZM96 0h-5.9L78.7 12.6V0H74v27.6h4.7v-14l12.6 14h6L84.1 13 96.1 0ZM104 8h-4.6v15h4.5V8ZM20.3 8h-4.6v4.5h4.6V8ZM108.5 23h-4.6v4.6h4.6V23ZM122 23h-4.5v4.6h4.6V23ZM122 12.5V8h-4.5V0H113v8h-4.6v4.5h4.6V23h4.5V12.5h4.6Z"
fill="currentColor"
/>
</svg>
<span className="text-[20px] tracking-tighter">UI</span>
</Link>
<Link
href="https://docs.livekit.io/agents/start/frontend/"
className="text-sm font-semibold underline-offset-4 hover:underline focus:underline"
>
Docs
</Link>
<Link
href="/ui/components"
className="text-sm font-semibold underline-offset-4 hover:underline focus:underline"
>
Components
</Link>
</header>

<main className="space-y-20">{children}</main>
{children}
</div>

<footer className="text-muted-foreground p-8 text-center text-sm">
<p className="text-muted-foreground text-balance">
Built with{' '}
<a href="https://shadcn.com" className="underline underline-offset-2">
Shadcn
</a>
,{' '}
<a href="https://motion.dev" className="underline underline-offset-2">
Motion
</a>
, and{' '}
<a href="https://livekit.io" className="underline underline-offset-2">
LiveKit
</a>
.
</p>
</footer>
</div>
</ConnectionProvider>
);
Expand Down
Loading
Loading