@assistant-ui/mcp-docs-server 0.1.26 → 0.1.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.docs/organized/code-examples/waterfall.md +2 -2
- package/.docs/organized/code-examples/with-a2a.md +2 -2
- package/.docs/organized/code-examples/with-ag-ui.md +3 -3
- package/.docs/organized/code-examples/with-ai-sdk-v6.md +4 -4
- package/.docs/organized/code-examples/with-artifacts.md +4 -4
- package/.docs/organized/code-examples/with-assistant-transport.md +2 -2
- package/.docs/organized/code-examples/with-chain-of-thought.md +4 -4
- package/.docs/organized/code-examples/with-cloud-standalone.md +4 -4
- package/.docs/organized/code-examples/with-cloud.md +4 -4
- package/.docs/organized/code-examples/with-custom-thread-list.md +4 -4
- package/.docs/organized/code-examples/with-elevenlabs-conversational.md +511 -0
- package/.docs/organized/code-examples/with-elevenlabs-scribe.md +6 -6
- package/.docs/organized/code-examples/with-expo.md +17 -17
- package/.docs/organized/code-examples/with-external-store.md +2 -2
- package/.docs/organized/code-examples/with-ffmpeg.md +217 -63
- package/.docs/organized/code-examples/with-generative-ui.md +841 -0
- package/.docs/organized/code-examples/with-google-adk.md +3 -3
- package/.docs/organized/code-examples/with-heat-graph.md +2 -2
- package/.docs/organized/code-examples/with-interactables.md +67 -9
- package/.docs/organized/code-examples/with-langgraph.md +3 -3
- package/.docs/organized/code-examples/with-livekit.md +591 -0
- package/.docs/organized/code-examples/with-parent-id-grouping.md +3 -3
- package/.docs/organized/code-examples/with-react-hook-form.md +5 -5
- package/.docs/organized/code-examples/with-react-ink.md +1 -1
- package/.docs/organized/code-examples/with-react-router.md +7 -7
- package/.docs/organized/code-examples/with-store.md +8 -3
- package/.docs/organized/code-examples/with-tanstack.md +4 -4
- package/.docs/organized/code-examples/with-tap-runtime.md +2 -2
- package/.docs/raw/docs/(docs)/copilots/model-context.mdx +9 -1
- package/.docs/raw/docs/(docs)/guides/interactables.mdx +99 -37
- package/.docs/raw/docs/(docs)/guides/mentions.mdx +406 -0
- package/.docs/raw/docs/(docs)/guides/slash-commands.mdx +275 -0
- package/.docs/raw/docs/(docs)/guides/tool-ui.mdx +29 -0
- package/.docs/raw/docs/(docs)/guides/voice.mdx +333 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/message-part.mdx +23 -0
- package/.docs/raw/docs/primitives/composer.mdx +27 -4
- package/.docs/raw/docs/runtimes/a2a/index.mdx +4 -0
- package/.docs/raw/docs/runtimes/ai-sdk/v6.mdx +2 -2
- package/.docs/raw/docs/runtimes/assistant-transport.mdx +6 -2
- package/.docs/raw/docs/ui/context-display.mdx +2 -2
- package/.docs/raw/docs/ui/model-selector.mdx +1 -1
- package/.docs/raw/docs/ui/voice.mdx +172 -0
- package/package.json +5 -6
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
# Example: with-livekit
|
|
2
|
+
|
|
3
|
+
## app/api/chat/route.ts
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
import { openai } from "@ai-sdk/openai";
|
|
7
|
+
import { streamText, UIMessage, convertToModelMessages, stepCountIs } from "ai";
|
|
8
|
+
|
|
9
|
+
export const maxDuration = 30;
|
|
10
|
+
|
|
11
|
+
export async function POST(req: Request) {
|
|
12
|
+
const { messages }: { messages: UIMessage[] } = await req.json();
|
|
13
|
+
|
|
14
|
+
const result = streamText({
|
|
15
|
+
model: openai("gpt-4o"),
|
|
16
|
+
messages: await convertToModelMessages(messages),
|
|
17
|
+
stopWhen: stepCountIs(10),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
return result.toUIMessageStreamResponse();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## app/api/livekit-token/route.ts
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { AccessToken } from "livekit-server-sdk";
|
|
29
|
+
import { NextResponse } from "next/server";
|
|
30
|
+
|
|
31
|
+
export async function POST() {
|
|
32
|
+
const apiKey = process.env.LIVEKIT_API_KEY;
|
|
33
|
+
const apiSecret = process.env.LIVEKIT_API_SECRET;
|
|
34
|
+
const roomName = process.env.LIVEKIT_ROOM_NAME ?? "assistant-room";
|
|
35
|
+
|
|
36
|
+
if (!apiKey || !apiSecret) {
|
|
37
|
+
return NextResponse.json(
|
|
38
|
+
{ error: "LIVEKIT_API_KEY and LIVEKIT_API_SECRET must be set" },
|
|
39
|
+
{ status: 500 },
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const participantIdentity = `user-${Date.now()}`;
|
|
44
|
+
|
|
45
|
+
const at = new AccessToken(apiKey, apiSecret, {
|
|
46
|
+
identity: participantIdentity,
|
|
47
|
+
});
|
|
48
|
+
at.addGrant({
|
|
49
|
+
roomJoin: true,
|
|
50
|
+
room: roomName,
|
|
51
|
+
canPublish: true,
|
|
52
|
+
canSubscribe: true,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const token = await at.toJwt();
|
|
56
|
+
|
|
57
|
+
return NextResponse.json({ token });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## app/globals.css
|
|
63
|
+
|
|
64
|
+
```css
|
|
65
|
+
@import "tailwindcss";
|
|
66
|
+
@import "tw-animate-css";
|
|
67
|
+
|
|
68
|
+
@source "../../../packages/ui/src";
|
|
69
|
+
|
|
70
|
+
@custom-variant dark (&:is(.dark *));
|
|
71
|
+
|
|
72
|
+
@theme inline {
|
|
73
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
74
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
75
|
+
--radius-lg: var(--radius);
|
|
76
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
77
|
+
--color-background: var(--background);
|
|
78
|
+
--color-foreground: var(--foreground);
|
|
79
|
+
--color-card: var(--card);
|
|
80
|
+
--color-card-foreground: var(--card-foreground);
|
|
81
|
+
--color-popover: var(--popover);
|
|
82
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
83
|
+
--color-primary: var(--primary);
|
|
84
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
85
|
+
--color-secondary: var(--secondary);
|
|
86
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
87
|
+
--color-muted: var(--muted);
|
|
88
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
89
|
+
--color-accent: var(--accent);
|
|
90
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
91
|
+
--color-destructive: var(--destructive);
|
|
92
|
+
--color-border: var(--border);
|
|
93
|
+
--color-input: var(--input);
|
|
94
|
+
--color-ring: var(--ring);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
:root {
|
|
98
|
+
--radius: 0.625rem;
|
|
99
|
+
--background: oklch(1 0 0);
|
|
100
|
+
--foreground: oklch(0.141 0.005 285.823);
|
|
101
|
+
--primary: oklch(0.21 0.006 285.885);
|
|
102
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
103
|
+
--secondary: oklch(0.967 0.001 286.375);
|
|
104
|
+
--secondary-foreground: oklch(0.21 0.006 285.885);
|
|
105
|
+
--muted: oklch(0.967 0.001 286.375);
|
|
106
|
+
--muted-foreground: oklch(0.552 0.016 285.938);
|
|
107
|
+
--accent: oklch(0.967 0.001 286.375);
|
|
108
|
+
--accent-foreground: oklch(0.21 0.006 285.885);
|
|
109
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
110
|
+
--border: oklch(0.92 0.004 286.32);
|
|
111
|
+
--input: oklch(0.92 0.004 286.32);
|
|
112
|
+
--ring: oklch(0.705 0.015 286.067);
|
|
113
|
+
--card: oklch(1 0 0);
|
|
114
|
+
--card-foreground: oklch(0.141 0.005 285.823);
|
|
115
|
+
--popover: oklch(1 0 0);
|
|
116
|
+
--popover-foreground: oklch(0.141 0.005 285.823);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
@layer base {
|
|
120
|
+
* {
|
|
121
|
+
@apply border-border outline-ring/50;
|
|
122
|
+
}
|
|
123
|
+
body {
|
|
124
|
+
@apply bg-background text-foreground;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## app/layout.tsx
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
import type { Metadata } from "next";
|
|
134
|
+
import "./globals.css";
|
|
135
|
+
|
|
136
|
+
export const metadata: Metadata = {
|
|
137
|
+
title: "LiveKit Voice Example",
|
|
138
|
+
description:
|
|
139
|
+
"Example using @assistant-ui/react with LiveKit for realtime voice",
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export default function RootLayout({
|
|
143
|
+
children,
|
|
144
|
+
}: Readonly<{
|
|
145
|
+
children: React.ReactNode;
|
|
146
|
+
}>) {
|
|
147
|
+
return (
|
|
148
|
+
<html lang="en">
|
|
149
|
+
<body className="h-dvh">{children}</body>
|
|
150
|
+
</html>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## app/page.tsx
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
"use client";
|
|
160
|
+
|
|
161
|
+
import { AssistantRuntimeProvider } from "@assistant-ui/react";
|
|
162
|
+
import { useChatRuntime } from "@assistant-ui/react-ai-sdk";
|
|
163
|
+
import { LiveKitVoiceAdapter } from "@/lib/livekit-voice-adapter";
|
|
164
|
+
import { VoiceThread } from "./voice-thread";
|
|
165
|
+
|
|
166
|
+
export default function Home() {
|
|
167
|
+
const runtime = useChatRuntime({
|
|
168
|
+
adapters: {
|
|
169
|
+
voice: new LiveKitVoiceAdapter({
|
|
170
|
+
url: process.env.NEXT_PUBLIC_LIVEKIT_URL ?? "ws://localhost:7880",
|
|
171
|
+
token: async () => {
|
|
172
|
+
const res = await fetch("/api/livekit-token", { method: "POST" });
|
|
173
|
+
const { token } = await res.json();
|
|
174
|
+
return token;
|
|
175
|
+
},
|
|
176
|
+
}),
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<AssistantRuntimeProvider runtime={runtime}>
|
|
182
|
+
<VoiceThread />
|
|
183
|
+
</AssistantRuntimeProvider>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## app/voice-thread.tsx
|
|
190
|
+
|
|
191
|
+
```tsx
|
|
192
|
+
"use client";
|
|
193
|
+
|
|
194
|
+
import { MarkdownText } from "@/components/assistant-ui/markdown-text";
|
|
195
|
+
import {
|
|
196
|
+
VoiceOrb,
|
|
197
|
+
VoiceConnectButton,
|
|
198
|
+
VoiceMuteButton,
|
|
199
|
+
VoiceDisconnectButton,
|
|
200
|
+
deriveVoiceOrbState,
|
|
201
|
+
} from "@/components/assistant-ui/voice";
|
|
202
|
+
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
|
203
|
+
import {
|
|
204
|
+
AuiIf,
|
|
205
|
+
MessagePrimitive,
|
|
206
|
+
ThreadPrimitive,
|
|
207
|
+
useAuiState,
|
|
208
|
+
useVoiceState,
|
|
209
|
+
useVoiceVolume,
|
|
210
|
+
} from "@assistant-ui/react";
|
|
211
|
+
import { ArrowDownIcon } from "lucide-react";
|
|
212
|
+
import type { FC } from "react";
|
|
213
|
+
|
|
214
|
+
export const VoiceThread: FC = () => {
|
|
215
|
+
return (
|
|
216
|
+
<ThreadPrimitive.Root
|
|
217
|
+
className="aui-root aui-thread-root flex h-full flex-col bg-background"
|
|
218
|
+
style={{ ["--thread-max-width" as string]: "44rem" }}
|
|
219
|
+
>
|
|
220
|
+
<ThreadPrimitive.Viewport className="aui-thread-viewport relative flex flex-1 flex-col overflow-y-scroll scroll-smooth px-4 pt-4">
|
|
221
|
+
<AuiIf condition={(s) => s.thread.isEmpty}>
|
|
222
|
+
<VoiceWelcome />
|
|
223
|
+
</AuiIf>
|
|
224
|
+
|
|
225
|
+
<ThreadPrimitive.Messages>
|
|
226
|
+
{() => <ThreadMessage />}
|
|
227
|
+
</ThreadPrimitive.Messages>
|
|
228
|
+
|
|
229
|
+
<ThreadPrimitive.ViewportFooter className="sticky bottom-0 mx-auto mt-auto flex w-full max-w-(--thread-max-width) flex-col items-center bg-background pt-4 pb-6">
|
|
230
|
+
<ThreadScrollToBottom />
|
|
231
|
+
<VoiceControlCenter />
|
|
232
|
+
</ThreadPrimitive.ViewportFooter>
|
|
233
|
+
</ThreadPrimitive.Viewport>
|
|
234
|
+
</ThreadPrimitive.Root>
|
|
235
|
+
);
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const VoiceWelcome: FC = () => {
|
|
239
|
+
return (
|
|
240
|
+
<div className="aui-thread-welcome-root mx-auto my-auto flex w-full max-w-(--thread-max-width) grow flex-col items-center justify-center gap-2">
|
|
241
|
+
<p className="fade-in slide-in-from-bottom-1 animate-in fill-mode-both font-semibold text-2xl duration-200">
|
|
242
|
+
Voice Conversation
|
|
243
|
+
</p>
|
|
244
|
+
<p className="fade-in slide-in-from-bottom-1 animate-in fill-mode-both text-muted-foreground delay-75 duration-200">
|
|
245
|
+
Tap connect to start speaking
|
|
246
|
+
</p>
|
|
247
|
+
</div>
|
|
248
|
+
);
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const ThreadScrollToBottom: FC = () => {
|
|
252
|
+
return (
|
|
253
|
+
<ThreadPrimitive.ScrollToBottom asChild>
|
|
254
|
+
<TooltipIconButton
|
|
255
|
+
tooltip="Scroll to bottom"
|
|
256
|
+
variant="outline"
|
|
257
|
+
className="absolute -top-12 z-10 self-center rounded-full p-4 disabled:invisible dark:border-border dark:bg-background dark:hover:bg-accent"
|
|
258
|
+
>
|
|
259
|
+
<ArrowDownIcon />
|
|
260
|
+
</TooltipIconButton>
|
|
261
|
+
</ThreadPrimitive.ScrollToBottom>
|
|
262
|
+
);
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const VoiceControlCenter: FC = () => {
|
|
266
|
+
return (
|
|
267
|
+
<div className="aui-voice-control-center flex flex-col items-center gap-4">
|
|
268
|
+
<VoiceOrb variant="blue" className="size-20" />
|
|
269
|
+
<VoiceWaveform />
|
|
270
|
+
|
|
271
|
+
<div className="flex items-center gap-3">
|
|
272
|
+
<AuiIf
|
|
273
|
+
condition={(s) =>
|
|
274
|
+
s.thread.voice == null || s.thread.voice.status.type === "ended"
|
|
275
|
+
}
|
|
276
|
+
>
|
|
277
|
+
<VoiceConnectButton />
|
|
278
|
+
</AuiIf>
|
|
279
|
+
|
|
280
|
+
<AuiIf condition={(s) => s.thread.voice?.status.type === "starting"}>
|
|
281
|
+
<span className="text-muted-foreground text-sm">Connecting...</span>
|
|
282
|
+
</AuiIf>
|
|
283
|
+
|
|
284
|
+
<AuiIf condition={(s) => s.thread.voice?.status.type === "running"}>
|
|
285
|
+
<VoiceMuteButton />
|
|
286
|
+
<VoiceDisconnectButton />
|
|
287
|
+
</AuiIf>
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
);
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const BAR_WEIGHTS = [0.4, 0.65, 0.85, 1.0, 0.9, 0.75, 0.5];
|
|
294
|
+
|
|
295
|
+
const VoiceWaveform: FC = () => {
|
|
296
|
+
const voiceState = useVoiceState();
|
|
297
|
+
const state = deriveVoiceOrbState(voiceState);
|
|
298
|
+
const volume = useVoiceVolume();
|
|
299
|
+
const isActive = state === "listening" || state === "speaking";
|
|
300
|
+
|
|
301
|
+
return (
|
|
302
|
+
<div className="aui-voice-waveform flex h-8 items-center justify-center gap-[3px]">
|
|
303
|
+
{BAR_WEIGHTS.map((weight, i) => {
|
|
304
|
+
const scale = isActive ? 0.1 + volume * 0.9 * weight : 0.1;
|
|
305
|
+
return (
|
|
306
|
+
<span
|
|
307
|
+
key={i}
|
|
308
|
+
className="aui-voice-waveform-bar h-full w-[3px] origin-center rounded-full bg-foreground/50 transition-transform duration-100"
|
|
309
|
+
style={{ transform: `scaleY(${scale})` }}
|
|
310
|
+
/>
|
|
311
|
+
);
|
|
312
|
+
})}
|
|
313
|
+
</div>
|
|
314
|
+
);
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const ThreadMessage: FC = () => {
|
|
318
|
+
const role = useAuiState((s) => s.message.role);
|
|
319
|
+
if (role === "user") return <UserMessage />;
|
|
320
|
+
return <AssistantMessage />;
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const AssistantMessage: FC = () => {
|
|
324
|
+
return (
|
|
325
|
+
<MessagePrimitive.Root
|
|
326
|
+
className="aui-assistant-message-root fade-in slide-in-from-bottom-1 relative mx-auto w-full max-w-(--thread-max-width) animate-in py-3 duration-150"
|
|
327
|
+
data-role="assistant"
|
|
328
|
+
>
|
|
329
|
+
<div className="aui-assistant-message-content wrap-break-word px-2 text-foreground leading-relaxed">
|
|
330
|
+
<MessagePrimitive.Parts>
|
|
331
|
+
{({ part }) => {
|
|
332
|
+
if (part.type === "text") return <MarkdownText />;
|
|
333
|
+
return null;
|
|
334
|
+
}}
|
|
335
|
+
</MessagePrimitive.Parts>
|
|
336
|
+
</div>
|
|
337
|
+
</MessagePrimitive.Root>
|
|
338
|
+
);
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const UserMessage: FC = () => {
|
|
342
|
+
return (
|
|
343
|
+
<MessagePrimitive.Root
|
|
344
|
+
className="aui-user-message-root fade-in slide-in-from-bottom-1 mx-auto flex w-full max-w-(--thread-max-width) animate-in justify-end px-2 py-3 duration-150"
|
|
345
|
+
data-role="user"
|
|
346
|
+
>
|
|
347
|
+
<div className="aui-user-message-content wrap-break-word rounded-2xl bg-muted px-4 py-2.5 text-foreground">
|
|
348
|
+
<MessagePrimitive.Parts />
|
|
349
|
+
</div>
|
|
350
|
+
</MessagePrimitive.Root>
|
|
351
|
+
);
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
## components.json
|
|
357
|
+
|
|
358
|
+
```json
|
|
359
|
+
{
|
|
360
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
361
|
+
"style": "new-york",
|
|
362
|
+
"rsc": true,
|
|
363
|
+
"tsx": true,
|
|
364
|
+
"tailwind": {
|
|
365
|
+
"config": "",
|
|
366
|
+
"css": "app/globals.css",
|
|
367
|
+
"baseColor": "zinc",
|
|
368
|
+
"cssVariables": true,
|
|
369
|
+
"prefix": ""
|
|
370
|
+
},
|
|
371
|
+
"aliases": {
|
|
372
|
+
"components": "@/components",
|
|
373
|
+
"utils": "@/lib/utils",
|
|
374
|
+
"ui": "@/components/ui",
|
|
375
|
+
"lib": "@/lib",
|
|
376
|
+
"hooks": "@/hooks"
|
|
377
|
+
},
|
|
378
|
+
"iconLibrary": "lucide",
|
|
379
|
+
"registries": {
|
|
380
|
+
"@assistant-ui": "https://r.assistant-ui.com/{name}.json"
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
## lib/livekit-voice-adapter.ts
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
import type { RealtimeVoiceAdapter } from "@assistant-ui/react";
|
|
390
|
+
import { createVoiceSession } from "@assistant-ui/react";
|
|
391
|
+
import { Room, RoomEvent, type RoomOptions } from "livekit-client";
|
|
392
|
+
|
|
393
|
+
export type LiveKitVoiceAdapterOptions = {
|
|
394
|
+
url: string;
|
|
395
|
+
token: string | (() => Promise<string>);
|
|
396
|
+
roomOptions?: RoomOptions;
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
export class LiveKitVoiceAdapter implements RealtimeVoiceAdapter {
|
|
400
|
+
private _url: string;
|
|
401
|
+
private _token: string | (() => Promise<string>);
|
|
402
|
+
private _roomOptions: RoomOptions | undefined;
|
|
403
|
+
|
|
404
|
+
constructor(options: LiveKitVoiceAdapterOptions) {
|
|
405
|
+
this._url = options.url;
|
|
406
|
+
this._token = options.token;
|
|
407
|
+
this._roomOptions = options.roomOptions;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
connect(options: {
|
|
411
|
+
abortSignal?: AbortSignal;
|
|
412
|
+
}): RealtimeVoiceAdapter.Session {
|
|
413
|
+
const room = new Room(this._roomOptions);
|
|
414
|
+
|
|
415
|
+
return createVoiceSession(options, async (session) => {
|
|
416
|
+
let volumeInterval: ReturnType<typeof setInterval> | null = null;
|
|
417
|
+
|
|
418
|
+
room.on(RoomEvent.Connected, () => {
|
|
419
|
+
session.setStatus({ type: "running" });
|
|
420
|
+
if (volumeInterval) clearInterval(volumeInterval);
|
|
421
|
+
volumeInterval = setInterval(() => {
|
|
422
|
+
if (session.isDisposed()) return;
|
|
423
|
+
const localLevel = room.localParticipant.audioLevel ?? 0;
|
|
424
|
+
let remoteLevel = 0;
|
|
425
|
+
for (const p of room.remoteParticipants.values()) {
|
|
426
|
+
remoteLevel = Math.max(remoteLevel, p.audioLevel ?? 0);
|
|
427
|
+
}
|
|
428
|
+
session.emitVolume(Math.max(localLevel, remoteLevel));
|
|
429
|
+
}, 100);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
room.on(RoomEvent.Disconnected, () => {
|
|
433
|
+
if (volumeInterval) clearInterval(volumeInterval);
|
|
434
|
+
session.end("finished");
|
|
435
|
+
});
|
|
436
|
+
room.on(RoomEvent.MediaDevicesError, (error) => {
|
|
437
|
+
if (volumeInterval) clearInterval(volumeInterval);
|
|
438
|
+
session.end("error", error);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
room.on(RoomEvent.ActiveSpeakersChanged, (speakers) => {
|
|
442
|
+
if (session.isDisposed()) return;
|
|
443
|
+
const remoteIsSpeaking = speakers.some(
|
|
444
|
+
(s) => s !== room.localParticipant,
|
|
445
|
+
);
|
|
446
|
+
session.emitMode(remoteIsSpeaking ? "speaking" : "listening");
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
room.on(
|
|
450
|
+
RoomEvent.TranscriptionReceived,
|
|
451
|
+
(segments, participant, _publication) => {
|
|
452
|
+
if (session.isDisposed()) return;
|
|
453
|
+
const role =
|
|
454
|
+
participant === room.localParticipant ? "user" : "assistant";
|
|
455
|
+
for (const segment of segments) {
|
|
456
|
+
session.emitTranscript({
|
|
457
|
+
role,
|
|
458
|
+
text: segment.text,
|
|
459
|
+
isFinal: segment.final,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
const token =
|
|
466
|
+
typeof this._token === "function" ? await this._token() : this._token;
|
|
467
|
+
if (session.isDisposed())
|
|
468
|
+
return { disconnect: () => {}, mute: () => {}, unmute: () => {} };
|
|
469
|
+
|
|
470
|
+
await room.connect(this._url, token);
|
|
471
|
+
if (session.isDisposed())
|
|
472
|
+
return { disconnect: () => {}, mute: () => {}, unmute: () => {} };
|
|
473
|
+
|
|
474
|
+
await room.localParticipant.setMicrophoneEnabled(true);
|
|
475
|
+
|
|
476
|
+
return {
|
|
477
|
+
disconnect: () => {
|
|
478
|
+
if (volumeInterval) clearInterval(volumeInterval);
|
|
479
|
+
room.disconnect();
|
|
480
|
+
},
|
|
481
|
+
mute: () => {
|
|
482
|
+
room.localParticipant.setMicrophoneEnabled(false).catch(() => {});
|
|
483
|
+
},
|
|
484
|
+
unmute: () => {
|
|
485
|
+
room.localParticipant.setMicrophoneEnabled(true).catch(() => {});
|
|
486
|
+
},
|
|
487
|
+
};
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
## next.config.js
|
|
495
|
+
|
|
496
|
+
```javascript
|
|
497
|
+
/** @type {import('next').NextConfig} */
|
|
498
|
+
const nextConfig = {
|
|
499
|
+
transpilePackages: ["@assistant-ui/react", "@assistant-ui/react-ai-sdk"],
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
export default nextConfig;
|
|
503
|
+
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
## package.json
|
|
507
|
+
|
|
508
|
+
```json
|
|
509
|
+
{
|
|
510
|
+
"name": "with-livekit",
|
|
511
|
+
"private": true,
|
|
512
|
+
"version": "0.0.0",
|
|
513
|
+
"type": "module",
|
|
514
|
+
"dependencies": {
|
|
515
|
+
"@ai-sdk/openai": "^3.0.51",
|
|
516
|
+
"@ai-sdk/react": "^3.0.150",
|
|
517
|
+
"@assistant-ui/react": "workspace:^",
|
|
518
|
+
"@assistant-ui/react-ai-sdk": "workspace:*",
|
|
519
|
+
"@assistant-ui/react-markdown": "workspace:^",
|
|
520
|
+
"@assistant-ui/ui": "workspace:*",
|
|
521
|
+
"@tailwindcss/postcss": "^4.2.2",
|
|
522
|
+
"ai": "^6.0.148",
|
|
523
|
+
"class-variance-authority": "^0.7.1",
|
|
524
|
+
"clsx": "^2.1.1",
|
|
525
|
+
"livekit-client": "^2.18.1",
|
|
526
|
+
"livekit-server-sdk": "^2.15.0",
|
|
527
|
+
"lucide-react": "^1.7.0",
|
|
528
|
+
"motion": "^12.38.0",
|
|
529
|
+
"next": "^16.2.2",
|
|
530
|
+
"postcss": "^8.5.8",
|
|
531
|
+
"react": "^19.2.4",
|
|
532
|
+
"react-dom": "^19.2.4",
|
|
533
|
+
"tailwind-merge": "^3.5.0",
|
|
534
|
+
"tailwindcss": "^4.2.2",
|
|
535
|
+
"zod": "^4.3.6"
|
|
536
|
+
},
|
|
537
|
+
"devDependencies": {
|
|
538
|
+
"@assistant-ui/x-buildutils": "workspace:*",
|
|
539
|
+
"@types/node": "^25.5.2",
|
|
540
|
+
"@types/react": "^19.2.14",
|
|
541
|
+
"@types/react-dom": "^19.2.3",
|
|
542
|
+
"tw-animate-css": "^1.4.0",
|
|
543
|
+
"typescript": "5.9.3"
|
|
544
|
+
},
|
|
545
|
+
"scripts": {
|
|
546
|
+
"dev": "next dev",
|
|
547
|
+
"build": "next build",
|
|
548
|
+
"start": "next start"
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
## tsconfig.json
|
|
555
|
+
|
|
556
|
+
```json
|
|
557
|
+
{
|
|
558
|
+
"extends": "@assistant-ui/x-buildutils/ts/base",
|
|
559
|
+
"compilerOptions": {
|
|
560
|
+
"target": "ES6",
|
|
561
|
+
"module": "ESNext",
|
|
562
|
+
"incremental": true,
|
|
563
|
+
"plugins": [
|
|
564
|
+
{
|
|
565
|
+
"name": "next"
|
|
566
|
+
}
|
|
567
|
+
],
|
|
568
|
+
"allowJs": true,
|
|
569
|
+
"strictNullChecks": true,
|
|
570
|
+
"jsx": "preserve",
|
|
571
|
+
"paths": {
|
|
572
|
+
"@/*": ["./*"],
|
|
573
|
+
"@/components/assistant-ui/*": [
|
|
574
|
+
"../../packages/ui/src/components/assistant-ui/*"
|
|
575
|
+
],
|
|
576
|
+
"@/components/ui/*": ["../../packages/ui/src/components/ui/*"],
|
|
577
|
+
"@/lib/utils": ["../../packages/ui/src/lib/utils"],
|
|
578
|
+
"@assistant-ui/ui/*": ["../../packages/ui/src/*"],
|
|
579
|
+
"@assistant-ui/*": ["../../packages/*/src"],
|
|
580
|
+
"@assistant-ui/react/*": ["../../packages/react/src/*"],
|
|
581
|
+
"@assistant-ui/tap/*": ["../../packages/tap/src/*"],
|
|
582
|
+
"assistant-stream": ["../../packages/assistant-stream/src"],
|
|
583
|
+
"assistant-stream/*": ["../../packages/assistant-stream/src/*"]
|
|
584
|
+
}
|
|
585
|
+
},
|
|
586
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
587
|
+
"exclude": ["node_modules"]
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
```
|
|
591
|
+
|
|
@@ -474,14 +474,14 @@ export default nextConfig;
|
|
|
474
474
|
"start": "next start"
|
|
475
475
|
},
|
|
476
476
|
"dependencies": {
|
|
477
|
-
"@ai-sdk/openai": "^3.0.
|
|
477
|
+
"@ai-sdk/openai": "^3.0.51",
|
|
478
478
|
"@assistant-ui/react": "workspace:*",
|
|
479
479
|
"@assistant-ui/react-markdown": "workspace:*",
|
|
480
480
|
"@assistant-ui/ui": "workspace:*",
|
|
481
481
|
"class-variance-authority": "^0.7.1",
|
|
482
482
|
"clsx": "^2.1.1",
|
|
483
483
|
"lucide-react": "^1.7.0",
|
|
484
|
-
"next": "^16.2.
|
|
484
|
+
"next": "^16.2.2",
|
|
485
485
|
"react": "^19.2.4",
|
|
486
486
|
"react-dom": "^19.2.4",
|
|
487
487
|
"tailwind-merge": "^3.5.0"
|
|
@@ -489,7 +489,7 @@ export default nextConfig;
|
|
|
489
489
|
"devDependencies": {
|
|
490
490
|
"@assistant-ui/x-buildutils": "workspace:*",
|
|
491
491
|
"@tailwindcss/postcss": "^4.2.2",
|
|
492
|
-
"@types/node": "^25.5.
|
|
492
|
+
"@types/node": "^25.5.2",
|
|
493
493
|
"@types/react": "^19.2.14",
|
|
494
494
|
"@types/react-dom": "^19.2.3",
|
|
495
495
|
"postcss": "^8.5.8",
|
|
@@ -537,26 +537,26 @@ export default nextConfig;
|
|
|
537
537
|
"start": "next start"
|
|
538
538
|
},
|
|
539
539
|
"dependencies": {
|
|
540
|
-
"@ai-sdk/openai": "^3.0.
|
|
540
|
+
"@ai-sdk/openai": "^3.0.51",
|
|
541
541
|
"@assistant-ui/react": "workspace:*",
|
|
542
542
|
"@assistant-ui/react-ai-sdk": "workspace:*",
|
|
543
543
|
"@assistant-ui/react-hook-form": "workspace:*",
|
|
544
544
|
"@assistant-ui/react-markdown": "workspace:*",
|
|
545
545
|
"@assistant-ui/ui": "workspace:*",
|
|
546
|
-
"ai": "^6.0.
|
|
546
|
+
"ai": "^6.0.148",
|
|
547
547
|
"class-variance-authority": "^0.7.1",
|
|
548
548
|
"clsx": "^2.1.1",
|
|
549
549
|
"lucide-react": "^1.7.0",
|
|
550
|
-
"next": "^16.2.
|
|
550
|
+
"next": "^16.2.2",
|
|
551
551
|
"react": "^19.2.4",
|
|
552
552
|
"react-dom": "^19.2.4",
|
|
553
|
-
"react-hook-form": "^7.72.
|
|
553
|
+
"react-hook-form": "^7.72.1",
|
|
554
554
|
"tailwind-merge": "^3.5.0"
|
|
555
555
|
},
|
|
556
556
|
"devDependencies": {
|
|
557
557
|
"@assistant-ui/x-buildutils": "workspace:*",
|
|
558
558
|
"@tailwindcss/postcss": "^4.2.2",
|
|
559
|
-
"@types/node": "^25.5.
|
|
559
|
+
"@types/node": "^25.5.2",
|
|
560
560
|
"@types/react": "^19.2.14",
|
|
561
561
|
"@types/react-dom": "^19.2.3",
|
|
562
562
|
"postcss": "^8.5.8",
|
|
@@ -772,30 +772,30 @@ export default function Home() {
|
|
|
772
772
|
"dependencies": {
|
|
773
773
|
"@assistant-ui/react": "workspace:*",
|
|
774
774
|
"@assistant-ui/react-markdown": "workspace:*",
|
|
775
|
-
"@react-router/node": "^7.
|
|
776
|
-
"@react-router/serve": "^7.
|
|
775
|
+
"@react-router/node": "^7.14.0",
|
|
776
|
+
"@react-router/serve": "^7.14.0",
|
|
777
777
|
"class-variance-authority": "^0.7.1",
|
|
778
778
|
"clsx": "^2.1.1",
|
|
779
779
|
"eventsource-parser": "^3.0.6",
|
|
780
|
-
"isbot": "^5.1.
|
|
780
|
+
"isbot": "^5.1.37",
|
|
781
781
|
"lucide-react": "^1.7.0",
|
|
782
782
|
"openai": "^6.33.0",
|
|
783
783
|
"react": "^19.2.4",
|
|
784
784
|
"react-dom": "^19.2.4",
|
|
785
|
-
"react-router": "^7.
|
|
785
|
+
"react-router": "^7.14.0",
|
|
786
786
|
"remark-gfm": "^4.0.1",
|
|
787
787
|
"tailwind-merge": "^3.5.0"
|
|
788
788
|
},
|
|
789
789
|
"devDependencies": {
|
|
790
|
-
"@react-router/dev": "^7.
|
|
790
|
+
"@react-router/dev": "^7.14.0",
|
|
791
791
|
"@tailwindcss/vite": "^4.2.2",
|
|
792
|
-
"@types/node": "^25.5.
|
|
792
|
+
"@types/node": "^25.5.2",
|
|
793
793
|
"@types/react": "^19.2.14",
|
|
794
794
|
"@types/react-dom": "^19.2.3",
|
|
795
795
|
"tailwindcss": "^4.2.2",
|
|
796
796
|
"tw-animate-css": "^1.4.0",
|
|
797
797
|
"typescript": "5.9.3",
|
|
798
|
-
"vite": "^8.0.
|
|
798
|
+
"vite": "^8.0.5",
|
|
799
799
|
"vite-tsconfig-paths": "^6.1.1"
|
|
800
800
|
}
|
|
801
801
|
}
|