diff --git a/.env.example b/.env.example index 4b7e546..59b2361 100644 --- a/.env.example +++ b/.env.example @@ -3,7 +3,6 @@ LIVEKIT_API_KEY= LIVEKIT_API_SECRET= LIVEKIT_URL=wss://.livekit.cloud - # Internally used environment variables -NEXT_PUBLIC_APP_CONFIG_ENDPOINT= -SANDBOX_ID= +NEXT_PUBLIC_CONN_DETAILS_ENDPOINT=http://localhost:3000/api/connection-details +# NEXT_PUBLIC_APP_CONFIG_ENDPOINT= diff --git a/.github/assets/frontend-screenshot.png b/.github/assets/frontend-screenshot.png deleted file mode 100644 index 13446af..0000000 Binary files a/.github/assets/frontend-screenshot.png and /dev/null differ diff --git a/.github/assets/readme-hero-dark.webp b/.github/assets/readme-hero-dark.webp new file mode 100644 index 0000000..5f1af31 Binary files /dev/null and b/.github/assets/readme-hero-dark.webp differ diff --git a/.github/assets/readme-hero-light.webp b/.github/assets/readme-hero-light.webp new file mode 100644 index 0000000..90dff5c Binary files /dev/null and b/.github/assets/readme-hero-light.webp differ diff --git a/.github/assets/screenshot-dark.webp b/.github/assets/screenshot-dark.webp new file mode 100644 index 0000000..7139d06 Binary files /dev/null and b/.github/assets/screenshot-dark.webp differ diff --git a/.github/assets/screenshot-light.webp b/.github/assets/screenshot-light.webp new file mode 100644 index 0000000..f0c61ae Binary files /dev/null and b/.github/assets/screenshot-light.webp differ diff --git a/README.md b/README.md index 01ae5c9..89faa5e 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,61 @@ -Voice Assistant App Icon - # Web Embed Agent Starter This is a starter template for [LiveKit Agents](https://docs.livekit.io/agents) that provides an example of how you might approach building web embed using the [LiveKit JavaScript SDK](https://github.com/livekit/client-sdk-js). It supports [voice](https://docs.livekit.io/agents/start/voice-ai) and [transcriptions](https://docs.livekit.io/agents/build/text/). This template is built with Next.js and is free for you to use or modify as you see fit. -![App screenshot](/.github/assets/frontend-screenshot.png) + + + + App screenshot + + +### Features: + +- Real-time voice interaction with LiveKit Agents +- Camera video streaming support +- Screen sharing capabilities +- Audio visualization and level monitoring +- Virtual avatar integration +- Light/dark theme switching with system preference detection +- Customizable branding, colors, and UI text via configuration + +This template is built with Next.js and is free for you to use or modify as you see fit. + +### Project structure + +``` +agent-starter-react/ +├── app/ +│ ├── (app)/ +│ ├── (iframe)/ +│ ├── api/ +│ ├── test/ +│ ├── favicon.ico +├── components/ +│ ├── embed-iframe/ +│ ├── embed-popup/ +│ ├── livekit/ +│ ├── ui/ +│ ├── popup-page.tsx +│ ├── root-layout.tsx +│ └── theme-toggle.tsx +│ └── welcome.tsx +│ └── ... +├── hooks/ +├── lib/ +├── public/ +├── styles/ +└── package.json +``` ## Getting started > [!TIP] > If you'd like to try this application without modification, you can deploy an instance in just a few clicks with [LiveKit Cloud Sandbox](https://cloud.livekit.io/projects/p_/sandbox/templates/agent-starter-embed). +[![Open on LiveKit](https://img.shields.io/badge/Open%20on%20LiveKit%20Cloud-002CF2?style=for-the-badge&logo=external-link)](https://cloud.livekit.io/projects/p_/sandbox/templates/agent-starter-embed) + Run the following command to automatically clone this template. ```bash @@ -34,15 +77,54 @@ You'll also need an agent to speak with. Try our starter agent for [Python](http > [!NOTE] > If you need to modify the LiveKit project credentials used, you can edit `.env.local` (copy from `.env.example` if you don't have one) to suit your needs. +## 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 = { + supportsChatInput: true, + supportsVideoInput: true, + supportsScreenShare: true, + isPreConnectBufferEnabled: true, +}; +``` + +You can update these values in [`app-config.ts`](./app-config.ts) to customize branding, features, and UI text for your deployment. + +#### Environment Variables + +You'll also need to configure your LiveKit credentials in `.env.local` (copy `.env.example` if you don't have one): + +```env +LIVEKIT_API_KEY=your_livekit_api_key +LIVEKIT_API_SECRET=your_livekit_api_secret +LIVEKIT_URL=https://your-livekit-server-url + +NEXT_PUBLIC_CONN_DETAILS_ENDPOINT=http://localhost:3000/api/connection-details +``` + +These are required for the voice agent functionality to work with your LiveKit project. + ## Local Development http://localhost:3000 will respond to code changes in real time through [NextJS Fast Refresh](https://nextjs.org/docs/architecture/fast-refresh) to support a rapid iteration feedback loop. ## Production deployment of embed-popup.js script -Any code changes you see locally will not be reflected in `embed-popup.js` until you run `pnpm build-embed-popup-script`. +Once your environment is set up and you've made any configuration changes, you can copy the embed code generated on the welcome page of your LiveKit Sandbox and paste it into your website. + +> [!IMPORTANT] +> You MUST use the embed code generated on the welcome page of your LiveKit Sandbox to ensure LiveKit connection tokens are generated correctly. + +## Debugging the build of embed-popup.js script + +You can test and debug your latest build of `embed-popup.js` locally at http://localhost:3000/test/popup. -You can test your latest build of `embed-popup.js` at http://localhost:3000/popup. +> [!IMPORTANT] +> Code changes you make locally will not be reflected in the bundled `embed-popup.js` script until you run `pnpm build-embed-popup-script`. ## Contributing diff --git a/app-config.ts b/app-config.ts index 6c75673..345e00c 100644 --- a/app-config.ts +++ b/app-config.ts @@ -1,18 +1,8 @@ import type { AppConfig } from './lib/types'; export const APP_CONFIG_DEFAULTS: AppConfig = { - companyName: 'LiveKit', - pageTitle: 'LiveKit Embed', - pageDescription: 'A web embed connected to an 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', }; diff --git a/app/(app)/popup/layout.tsx b/app/(app)/popup/layout.tsx deleted file mode 100644 index 312f7f0..0000000 --- a/app/(app)/popup/layout.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { ApplyThemeScript } from '@/components/theme-toggle'; - -export default function Layout({ children }: { children: React.ReactNode }) { - return ( - <> - - {children} - - ); -} diff --git a/app/(app)/popup/page.tsx b/app/(app)/popup/page.tsx deleted file mode 100644 index d25addd..0000000 --- a/app/(app)/popup/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import PopupPageDynamic from '@/components/popup-page-dynamic'; - -export default function Page() { - return ; -} diff --git a/app/api/connection-details/route.ts b/app/api/connection-details/route.ts index 04f3160..02d6542 100644 --- a/app/api/connection-details/route.ts +++ b/app/api/connection-details/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from 'next/server'; import { AccessToken, type AccessTokenOptions, type VideoGrant } from 'livekit-server-sdk'; +import { RoomConfiguration } from '@livekit/protocol'; // NOTE: you are expected to define the following environment variables in `.env.local`: const API_KEY = process.env.LIVEKIT_API_KEY; @@ -16,7 +17,7 @@ export type ConnectionDetails = { participantToken: string; }; -export async function GET() { +export async function POST(req: Request) { try { if (LIVEKIT_URL === undefined) { throw new Error('LIVEKIT_URL is not defined'); @@ -28,13 +29,19 @@ export async function GET() { throw new Error('LIVEKIT_API_SECRET is not defined'); } + // Parse agent configuration from request body + const body = await req.json(); + const agentName: string = body?.room_config?.agents?.[0]?.agent_name; + // Generate participant token const participantName = 'user'; const participantIdentity = `voice_assistant_user_${Math.floor(Math.random() * 10_000)}`; const roomName = `voice_assistant_room_${Math.floor(Math.random() * 10_000)}`; + const participantToken = await createParticipantToken( { identity: participantIdentity, name: participantName }, - roomName + roomName, + agentName ); // Return connection details @@ -56,7 +63,11 @@ export async function GET() { } } -function createParticipantToken(userInfo: AccessTokenOptions, roomName: string) { +function createParticipantToken( + userInfo: AccessTokenOptions, + roomName: string, + agentName?: string +): Promise { const at = new AccessToken(API_KEY, API_SECRET, { ...userInfo, ttl: '15m', @@ -69,5 +80,12 @@ function createParticipantToken(userInfo: AccessTokenOptions, roomName: string) canSubscribe: true, }; at.addGrant(grant); + + if (agentName) { + at.roomConfig = new RoomConfiguration({ + agents: [{ agentName }], + }); + } + return at.toJwt(); } diff --git a/app/test/layout.tsx b/app/test/layout.tsx new file mode 100644 index 0000000..8f80eae --- /dev/null +++ b/app/test/layout.tsx @@ -0,0 +1,11 @@ +interface RootLayoutProps { + children: React.ReactNode; +} + +export default async function Layout({ children }: RootLayoutProps) { + return ( + + {children} + + ); +} diff --git a/app/test/popup/page.tsx b/app/test/popup/page.tsx new file mode 100644 index 0000000..e948fb5 --- /dev/null +++ b/app/test/popup/page.tsx @@ -0,0 +1,58 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import Script from 'next/script'; +import { getSandboxId } from '@/lib/env'; +import './styles.css'; + +const CODE_SNIPPET = ` +function toggleTheme() { + var embedWrapper = document.querySelector('#lk-embed-wrapper'); + + if (embedWrapper) { + embedWrapper.classList.toggle('dark'); + } +} +`.trim(); + +export default function Page() { + const [sandboxId, setSandboxId] = useState(''); + + useEffect(() => { + setSandboxId(getSandboxId(window.location.origin)); + }, []); + + function handleToggleTheme() { + const doc = document.documentElement; + const popupWrapper = document.querySelector('#lk-embed-wrapper'); + + doc.classList.toggle('page-dark'); + + if (popupWrapper) { + popupWrapper.classList.toggle('dark'); + } + } + + return ( +
+

This page has a minimal stylesheet inorder to test the embed-popup.js bundled styles

+

+ Ensure you have run pnpm build-embed-popup-script after your latest code + changes. +

+

+ In order to toggle the theme on the popup,
+ apply the class `dark` to the root element (#lk-embed-wrapper) +

+ +
+        {CODE_SNIPPET}
+      
+ +

+ +

+ {sandboxId &&