Skip to content

Commit ecbb208

Browse files
authored
Add avatar support + remove krisp (#107)
1 parent 257bb2f commit ecbb208

File tree

6 files changed

+119
-75
lines changed

6 files changed

+119
-75
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
# Web Voice Assistant
44

5-
This is a starter template for [LiveKit Agents](https://docs.livekit.io/agents/overview/) that provides a simple voice interface using the [LiveKit JavaScript SDK](https://github.com/livekit/client-sdk-js).
5+
This is a starter template for [LiveKit Agents](https://docs.livekit.io/agents) that provides a simple voice interface using the [LiveKit JavaScript SDK](https://github.com/livekit/client-sdk-js). It supports [voice](https://docs.livekit.io/agents/start/voice-ai), [transcriptions](https://docs.livekit.io/agents/build/text/), and [virtual avatars](https://docs.livekit.io/agents/integrations/avatar).
66

77
This template is built with Next.js and is free for you to use or modify as you see fit.
88

@@ -28,7 +28,7 @@ pnpm dev
2828

2929
And open http://localhost:3000 in your browser.
3030

31-
You'll also need an agent to speak with. Try our sample voice assistant agent for [Python](https://github.com/livekit-examples/voice-pipeline-agent-python), [Node.js](https://github.com/livekit-examples/voice-pipeline-agent-node), or [create your own from scratch](https://docs.livekit.io/agents/quickstart/).
31+
You'll also need an agent to speak with. Try our [Voice AI Quickstart](https://docs.livekit.io/start/voice-ai) for the easiest way to get started.
3232

3333
> [!NOTE]
3434
> 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.

app/layout.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import "@livekit/components-styles";
2+
import { Metadata } from "next";
23
import { Public_Sans } from "next/font/google";
34
import "./globals.css";
45

@@ -7,6 +8,10 @@ const publicSans400 = Public_Sans({
78
subsets: ["latin"],
89
});
910

11+
export const metadata: Metadata = {
12+
title: "Voice Assistant",
13+
};
14+
1015
export default function RootLayout({
1116
children,
1217
}: Readonly<{

app/page.tsx

Lines changed: 81 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ import {
88
DisconnectButton,
99
RoomAudioRenderer,
1010
RoomContext,
11+
VideoTrack,
1112
VoiceAssistantControlBar,
1213
useVoiceAssistant,
1314
} from "@livekit/components-react";
14-
import { useKrispNoiseFilter } from "@livekit/components-react/krisp";
1515
import { AnimatePresence, motion } from "framer-motion";
1616
import { Room, RoomEvent } from "livekit-client";
1717
import { useCallback, useEffect, useState } from "react";
@@ -52,7 +52,7 @@ export default function Page() {
5252
return (
5353
<main data-lk-theme="default" className="h-full grid content-center bg-[var(--lk-bg)]">
5454
<RoomContext.Provider value={room}>
55-
<div className="lk-room-container max-h-[90vh]">
55+
<div className="lk-room-container max-w-[1024px] w-[90vw] mx-auto max-h-[90vh]">
5656
<SimpleVoiceAssistant onConnectButtonClicked={onConnectButtonClicked} />
5757
</div>
5858
</RoomContext.Provider>
@@ -62,8 +62,82 @@ export default function Page() {
6262

6363
function SimpleVoiceAssistant(props: { onConnectButtonClicked: () => void }) {
6464
const { state: agentState } = useVoiceAssistant();
65+
6566
return (
6667
<>
68+
<AnimatePresence mode="wait">
69+
{agentState === "disconnected" ? (
70+
<motion.div
71+
key="disconnected"
72+
initial={{ opacity: 0, scale: 0.95 }}
73+
animate={{ opacity: 1, scale: 1 }}
74+
exit={{ opacity: 0, scale: 0.95 }}
75+
transition={{ duration: 0.3, ease: [0.09, 1.04, 0.245, 1.055] }}
76+
className="grid items-center justify-center h-full"
77+
>
78+
<motion.button
79+
initial={{ opacity: 0 }}
80+
animate={{ opacity: 1 }}
81+
transition={{ duration: 0.3, delay: 0.1 }}
82+
className="uppercase px-4 py-2 bg-white text-black rounded-md"
83+
onClick={() => props.onConnectButtonClicked()}
84+
>
85+
Start a conversation
86+
</motion.button>
87+
</motion.div>
88+
) : (
89+
<motion.div
90+
key="connected"
91+
initial={{ opacity: 0, y: 20 }}
92+
animate={{ opacity: 1, y: 0 }}
93+
exit={{ opacity: 0, y: -20 }}
94+
transition={{ duration: 0.3, ease: [0.09, 1.04, 0.245, 1.055] }}
95+
className="flex flex-col items-center gap-4 h-full"
96+
>
97+
<AgentVisualizer />
98+
<div className="flex-1 w-full">
99+
<TranscriptionView />
100+
</div>
101+
<div className="w-full">
102+
<ControlBar onConnectButtonClicked={props.onConnectButtonClicked} />
103+
</div>
104+
<RoomAudioRenderer />
105+
<NoAgentNotification state={agentState} />
106+
</motion.div>
107+
)}
108+
</AnimatePresence>
109+
</>
110+
);
111+
}
112+
113+
function AgentVisualizer() {
114+
const { state: agentState, videoTrack, audioTrack } = useVoiceAssistant();
115+
116+
if (videoTrack) {
117+
return (
118+
<div className="h-[512px] w-[512px] rounded-lg overflow-hidden">
119+
<VideoTrack trackRef={videoTrack} />
120+
</div>
121+
);
122+
}
123+
return (
124+
<div className="h-[300px] w-full">
125+
<BarVisualizer
126+
state={agentState}
127+
barCount={5}
128+
trackRef={audioTrack}
129+
className="agent-visualizer"
130+
options={{ minHeight: 24 }}
131+
/>
132+
</div>
133+
);
134+
}
135+
136+
function ControlBar(props: { onConnectButtonClicked: () => void }) {
137+
const { state: agentState } = useVoiceAssistant();
138+
139+
return (
140+
<div className="relative h-[60px]">
67141
<AnimatePresence>
68142
{agentState === "disconnected" && (
69143
<motion.button
@@ -77,56 +151,20 @@ function SimpleVoiceAssistant(props: { onConnectButtonClicked: () => void }) {
77151
Start a conversation
78152
</motion.button>
79153
)}
80-
<div className="w-3/4 lg:w-1/2 mx-auto h-full">
81-
<TranscriptionView />
82-
</div>
83154
</AnimatePresence>
84-
85-
<RoomAudioRenderer />
86-
<NoAgentNotification state={agentState} />
87-
<div className="fixed bottom-0 w-full px-4 py-2">
88-
<ControlBar />
89-
</div>
90-
</>
91-
);
92-
}
93-
94-
function ControlBar() {
95-
/**
96-
* Use Krisp background noise reduction when available.
97-
* Note: This is only available on Scale plan, see {@link https://livekit.io/pricing | LiveKit Pricing} for more details.
98-
*/
99-
const krisp = useKrispNoiseFilter();
100-
useEffect(() => {
101-
krisp.setNoiseFilterEnabled(true);
102-
}, []);
103-
104-
const { state: agentState, audioTrack } = useVoiceAssistant();
105-
106-
return (
107-
<div className="relative h-[100px]">
108155
<AnimatePresence>
109156
{agentState !== "disconnected" && agentState !== "connecting" && (
110157
<motion.div
111158
initial={{ opacity: 0, top: "10px" }}
112159
animate={{ opacity: 1, top: 0 }}
113160
exit={{ opacity: 0, top: "-10px" }}
114161
transition={{ duration: 0.4, ease: [0.09, 1.04, 0.245, 1.055] }}
115-
className="flex absolute w-full h-full justify-between px-8 sm:px-4"
162+
className="flex h-8 absolute left-1/2 -translate-x-1/2 justify-center"
116163
>
117-
<BarVisualizer
118-
state={agentState}
119-
barCount={5}
120-
trackRef={audioTrack}
121-
className="agent-visualizer w-24 gap-2"
122-
options={{ minHeight: 12 }}
123-
/>
124-
<div className="flex items-center">
125-
<VoiceAssistantControlBar controls={{ leave: false }} />
126-
<DisconnectButton>
127-
<CloseIcon />
128-
</DisconnectButton>
129-
</div>
164+
<VoiceAssistantControlBar controls={{ leave: false }} />
165+
<DisconnectButton>
166+
<CloseIcon />
167+
</DisconnectButton>
130168
</motion.div>
131169
)}
132170
</AnimatePresence>

components/TranscriptionView.tsx

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,37 @@ import * as React from "react";
33

44
export default function TranscriptionView() {
55
const combinedTranscriptions = useCombinedTranscriptions();
6+
const containerRef = React.useRef<HTMLDivElement>(null);
67

78
// scroll to bottom when new transcription is added
89
React.useEffect(() => {
9-
const transcription = combinedTranscriptions[combinedTranscriptions.length - 1];
10-
if (transcription) {
11-
const transcriptionElement = document.getElementById(transcription.id);
12-
if (transcriptionElement) {
13-
transcriptionElement.scrollIntoView({ behavior: "smooth" });
14-
}
10+
if (containerRef.current) {
11+
containerRef.current.scrollTop = containerRef.current.scrollHeight;
1512
}
1613
}, [combinedTranscriptions]);
1714

1815
return (
19-
<div className="h-full flex flex-col gap-2 overflow-y-auto">
20-
{combinedTranscriptions.map((segment) => (
21-
<div
22-
id={segment.id}
23-
key={segment.id}
24-
className={
25-
segment.role === "assistant"
26-
? "p-2 self-start fit-content"
27-
: "bg-gray-800 rounded-md p-2 self-end fit-content"
28-
}
29-
>
30-
{segment.text}
31-
</div>
32-
))}
16+
<div className="relative h-[200px] w-[512px] max-w-[90vw] mx-auto">
17+
{/* Fade-out gradient mask */}
18+
<div className="absolute top-0 left-0 right-0 h-8 bg-gradient-to-b from-[var(--lk-bg)] to-transparent z-10 pointer-events-none" />
19+
<div className="absolute bottom-0 left-0 right-0 h-8 bg-gradient-to-t from-[var(--lk-bg)] to-transparent z-10 pointer-events-none" />
20+
21+
{/* Scrollable content */}
22+
<div ref={containerRef} className="h-full flex flex-col gap-2 overflow-y-auto px-4 py-8">
23+
{combinedTranscriptions.map((segment) => (
24+
<div
25+
id={segment.id}
26+
key={segment.id}
27+
className={
28+
segment.role === "assistant"
29+
? "p-2 self-start fit-content"
30+
: "bg-gray-800 rounded-md p-2 self-end fit-content"
31+
}
32+
>
33+
{segment.text}
34+
</div>
35+
))}
36+
</div>
3337
</div>
3438
);
3539
}

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,8 @@
1111
"format:write": "prettier --write ."
1212
},
1313
"dependencies": {
14-
"@livekit/components-react": "^2.7.0",
14+
"@livekit/components-react": "^2.9.3",
1515
"@livekit/components-styles": "^1.1.4",
16-
"@livekit/krisp-noise-filter": "^0.2.14",
1716
"framer-motion": "^11.18.0",
1817
"livekit-client": "^2.8.0",
1918
"livekit-server-sdk": "^2.9.7",

pnpm-lock.yaml

Lines changed: 6 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)