@ensembleapp/client-sdk 0.0.55 → 0.0.57

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.
@@ -0,0 +1,417 @@
1
+ import {
2
+ createLogger
3
+ } from "./chunk-LFKJR7WX.js";
4
+ import "./chunk-YLJ4XMA6.js";
5
+
6
+ // lib/chat/components/RealtimeVoicePanel.tsx
7
+ import React2, { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
8
+ import { Room, RoomEvent, Track } from "livekit-client";
9
+ import { useTranscriptions } from "@livekit/components-react";
10
+
11
+ // lib/chat/components/VoiceChatView.tsx
12
+ import { useEffect, useRef, useState } from "react";
13
+ import { Maximize2, Minimize2, X } from "lucide-react";
14
+ import { jsx, jsxs } from "react/jsx-runtime";
15
+ var logger = createLogger("VoiceChatView");
16
+ function VoiceChatView({ media, captions, content, contentKey = "content", children, visible }) {
17
+ const viewRef = useRef(null);
18
+ const [fullscreen, setFullscreen] = useState(false);
19
+ const [fullscreenSupported, setFullscreenSupported] = useState(false);
20
+ useEffect(() => {
21
+ setFullscreenSupported(Boolean(document.fullscreenEnabled && viewRef.current?.requestFullscreen));
22
+ const update = () => setFullscreen(document.fullscreenElement === viewRef.current);
23
+ document.addEventListener("fullscreenchange", update);
24
+ return () => document.removeEventListener("fullscreenchange", update);
25
+ }, []);
26
+ useEffect(() => {
27
+ if (!visible && document.fullscreenElement === viewRef.current) {
28
+ void document.exitFullscreen().catch((err) => logger.warn({ err }, "Unable to exit fullscreen"));
29
+ }
30
+ }, [visible]);
31
+ const toggleFullscreen = async () => {
32
+ try {
33
+ if (document.fullscreenElement === viewRef.current) await document.exitFullscreen();
34
+ else await viewRef.current?.requestFullscreen();
35
+ } catch (err) {
36
+ logger.warn({ err }, "Unable to change voice fullscreen mode");
37
+ }
38
+ };
39
+ const [dismissedContent, setDismissedContent] = useState();
40
+ const showContent = Boolean(content) && dismissedContent !== contentKey;
41
+ return /* @__PURE__ */ jsxs("div", { ref: viewRef, className: `chat-widget__voice-view${showContent ? " chat-widget__voice-view--content" : ""}`, hidden: !visible, children: [
42
+ /* @__PURE__ */ jsxs("div", { className: "chat-widget__voice-scene", children: [
43
+ media,
44
+ fullscreenSupported && /* @__PURE__ */ jsx(
45
+ "button",
46
+ {
47
+ type: "button",
48
+ className: "chat-widget__voice-fullscreen",
49
+ "aria-label": fullscreen ? "Exit fullscreen" : "Enter fullscreen",
50
+ title: fullscreen ? "Exit fullscreen" : "Enter fullscreen",
51
+ "aria-pressed": fullscreen,
52
+ onClick: () => void toggleFullscreen(),
53
+ children: fullscreen ? /* @__PURE__ */ jsx(Minimize2, { size: 18 }) : /* @__PURE__ */ jsx(Maximize2, { size: 18 })
54
+ }
55
+ ),
56
+ /* @__PURE__ */ jsxs("div", { className: "chat-widget__voice-overlay", children: [
57
+ captions,
58
+ children
59
+ ] })
60
+ ] }),
61
+ /* @__PURE__ */ jsx("div", { className: "chat-widget__voice-content-wrap", "aria-hidden": !showContent, inert: !showContent, children: /* @__PURE__ */ jsxs("div", { className: "chat-widget__voice-content", children: [
62
+ content && /* @__PURE__ */ jsx(
63
+ "button",
64
+ {
65
+ type: "button",
66
+ className: "chat-widget__dismiss-content",
67
+ "aria-label": "Dismiss content",
68
+ title: "Dismiss content",
69
+ onClick: () => setDismissedContent(contentKey),
70
+ children: /* @__PURE__ */ jsx(X, { size: 18 })
71
+ }
72
+ ),
73
+ content
74
+ ] }) })
75
+ ] });
76
+ }
77
+
78
+ // lib/chat/components/speech-segments.ts
79
+ function takeSpeechSegments(text, done) {
80
+ const segments = [];
81
+ while (text.length) {
82
+ const sentence = /[.!?]\s|\n/.exec(text);
83
+ let end = sentence ? sentence.index + sentence[0].length : 0;
84
+ if (!end && text.length >= 200) end = text.lastIndexOf(" ", 200) + 1;
85
+ if (!end && done) end = text.length;
86
+ if (!end) break;
87
+ const chunk = text.slice(0, end).replace(/\[([^\]]+)\]\([^)]*\)/g, "$1").replace(/[*#`]/g, "").trim();
88
+ if (chunk) segments.push(chunk + " ");
89
+ text = text.slice(end);
90
+ }
91
+ return { segments, remainder: text };
92
+ }
93
+
94
+ // lib/chat/components/speech-text-cursor.ts
95
+ var SpeechTextCursor = class {
96
+ constructor() {
97
+ this.part = 0;
98
+ this.offset = 0;
99
+ }
100
+ read(parts, done) {
101
+ const chunks = [];
102
+ while (this.part < parts.length) {
103
+ const part = parts[this.part];
104
+ if (part.type !== "text") {
105
+ this.part++;
106
+ continue;
107
+ }
108
+ const text = part.text.slice(this.offset);
109
+ if (text) chunks.push({ text });
110
+ this.offset = part.text.length;
111
+ if (part.state !== "done" && !done) break;
112
+ chunks.push({ flush: true });
113
+ this.part++;
114
+ this.offset = 0;
115
+ }
116
+ return chunks;
117
+ }
118
+ };
119
+
120
+ // lib/chat/components/caption-words.ts
121
+ function captionWords(preview, spoken) {
122
+ const heard = spoken.trim().match(/\S+/g) ?? [];
123
+ const candidates = (Array.isArray(preview) ? preview : [preview]).map((text) => text.trim().match(/\S+/g) ?? []);
124
+ const ahead = candidates.find((words) => heard.every((word, index) => word === words[index])) ?? [];
125
+ const matches = heard.length > 0 && heard.every((word, index) => word === ahead[index]);
126
+ return { words: matches ? ahead : heard, currentWord: heard.length - 1 };
127
+ }
128
+
129
+ // lib/chat/components/RealtimeVoicePanel.tsx
130
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
131
+ function voiceSessionUrl(baseUrl) {
132
+ const base = baseUrl.replace(/\/+$/, "");
133
+ return `${base.endsWith("/api") ? base : `${base}/api`}/voice/session`;
134
+ }
135
+ function LiveCaptions({ room, preview }) {
136
+ const transcriptions = useTranscriptions({ room });
137
+ const latest = transcriptions.at(-1);
138
+ const text = latest?.text ?? "";
139
+ const isAssistant = latest?.participantInfo.identity === "ensemble-voice-worker";
140
+ const { words, currentWord } = captionWords(isAssistant ? preview : "", text);
141
+ const captionRef = useRef2(null);
142
+ const currentRef = useRef2(null);
143
+ useEffect2(() => {
144
+ const element = captionRef.current;
145
+ const word = currentRef.current;
146
+ if (element && word) {
147
+ const pageHeight = element.clientHeight;
148
+ if (pageHeight) element.scrollTop = Math.floor(word.offsetTop / pageHeight) * pageHeight;
149
+ }
150
+ }, [text, preview]);
151
+ return text ? /* @__PURE__ */ jsx2("div", { className: "chat-widget__subtitles", ref: captionRef, children: words.map((word, index) => /* @__PURE__ */ jsxs2(React2.Fragment, { children: [
152
+ index > 0 && " ",
153
+ /* @__PURE__ */ jsx2(
154
+ "span",
155
+ {
156
+ ref: index === currentWord ? currentRef : void 0,
157
+ className: isAssistant && index === currentWord ? "chat-widget__subtitle-current" : void 0,
158
+ children: word
159
+ }
160
+ )
161
+ ] }, index)) }) : null;
162
+ }
163
+ function RealtimeVoicePanel(props) {
164
+ const [state, setState] = useState2("idle");
165
+ const [muted, setMuted] = useState2(false);
166
+ const [captionRoom, setCaptionRoom] = useState2();
167
+ useEffect2(() => {
168
+ props.onCallStateChange(state !== "idle");
169
+ }, [state, props.onCallStateChange]);
170
+ const [error, setError] = useState2("");
171
+ const [needsAudioGesture, setNeedsAudioGesture] = useState2(false);
172
+ const roomRef = useRef2(void 0);
173
+ const sessionRef = useRef2(void 0);
174
+ const mediaRef = useRef2(null);
175
+ const propsRef = useRef2(props);
176
+ propsRef.current = props;
177
+ const sendQueue = useRef2(Promise.resolve());
178
+ const spoken = useRef2(void 0);
179
+ const ignored = useRef2(/* @__PURE__ */ new Set());
180
+ const generation = useRef2(0);
181
+ const lastConnectRequest = useRef2(props.connectRequest);
182
+ const send = (event) => {
183
+ const room = roomRef.current;
184
+ sendQueue.current = sendQueue.current.then(async () => {
185
+ if (room && room === roomRef.current)
186
+ await room.localParticipant.sendText(JSON.stringify(event), {
187
+ topic: "ensemble.speech",
188
+ destinationIdentities: ["ensemble-voice-worker"]
189
+ });
190
+ }).catch(() => {
191
+ setError("Voice connection interrupted");
192
+ });
193
+ };
194
+ const close = async () => {
195
+ generation.current++;
196
+ const room = roomRef.current;
197
+ roomRef.current = void 0;
198
+ setCaptionRoom(void 0);
199
+ const session = sessionRef.current;
200
+ sessionRef.current = void 0;
201
+ spoken.current = void 0;
202
+ await room?.disconnect();
203
+ mediaRef.current?.replaceChildren();
204
+ setState("idle");
205
+ if (session) {
206
+ const { api } = session;
207
+ await fetch(`${voiceSessionUrl(api.baseUrl)}/${encodeURIComponent(session.room)}`, {
208
+ method: "DELETE",
209
+ headers: { ...api.headers, Authorization: `Bearer ${api.token}` }
210
+ }).catch(() => void 0);
211
+ }
212
+ };
213
+ const start = async () => {
214
+ const attempt = ++generation.current;
215
+ setState("connecting");
216
+ setError("");
217
+ setMuted(false);
218
+ ignored.current = new Set(propsRef.current.messages.map((message) => message.id));
219
+ try {
220
+ const { api, agentId, threadId, version, avatar } = propsRef.current;
221
+ const response = await fetch(voiceSessionUrl(api.baseUrl), {
222
+ method: "POST",
223
+ headers: {
224
+ ...api.headers,
225
+ Authorization: `Bearer ${api.token}`,
226
+ "Content-Type": "application/json"
227
+ },
228
+ body: JSON.stringify({ agentId, threadId, version, avatar })
229
+ });
230
+ if (!response.headers.get("content-type")?.includes("application/json")) {
231
+ throw new Error(
232
+ `Voice endpoint returned an unexpected response (HTTP ${response.status}). Check the server URL and that the updated API server is running.`
233
+ );
234
+ }
235
+ const body = await response.json();
236
+ if (!response.ok) {
237
+ throw new Error(body.error || "Unable to start call");
238
+ }
239
+ const session = body;
240
+ if (attempt !== generation.current) {
241
+ await fetch(`${voiceSessionUrl(api.baseUrl)}/${encodeURIComponent(session.room)}`, {
242
+ method: "DELETE",
243
+ headers: { ...api.headers, Authorization: `Bearer ${api.token}` }
244
+ });
245
+ return;
246
+ }
247
+ sessionRef.current = { room: session.room, api };
248
+ const room = new Room();
249
+ roomRef.current = room;
250
+ setCaptionRoom(room);
251
+ room.on(
252
+ RoomEvent.AudioPlaybackStatusChanged,
253
+ () => setNeedsAudioGesture(!room.canPlaybackAudio)
254
+ );
255
+ room.on(RoomEvent.TrackSubscribed, (track) => {
256
+ const element = track.attach();
257
+ element.autoplay = true;
258
+ if (track.kind === Track.Kind.Video) {
259
+ element.style.width = "100%";
260
+ element.setAttribute("playsinline", "");
261
+ }
262
+ mediaRef.current?.appendChild(element);
263
+ });
264
+ room.on(
265
+ RoomEvent.TrackUnsubscribed,
266
+ (track) => track.detach().forEach((element) => element.remove())
267
+ );
268
+ room.on(RoomEvent.Disconnected, () => {
269
+ if (roomRef.current === room) void close();
270
+ });
271
+ room.registerTextStreamHandler("ensemble.error", async (reader, participant) => {
272
+ if (participant.identity !== "ensemble-voice-worker") return;
273
+ const text = await reader.readAll();
274
+ if (roomRef.current === room) setError(text);
275
+ });
276
+ room.registerTextStreamHandler("ensemble.transcript", async (reader, participant) => {
277
+ if (participant.identity !== "ensemble-voice-worker") return;
278
+ const text = await reader.readAll();
279
+ if (roomRef.current === room) propsRef.current.onTranscript(text);
280
+ });
281
+ room.registerTextStreamHandler("ensemble.closed", (_reader, participant) => {
282
+ if (participant.identity === "ensemble-voice-worker" && roomRef.current === room) {
283
+ propsRef.current.onInterrupt();
284
+ void close();
285
+ }
286
+ });
287
+ room.registerTextStreamHandler("ensemble.interrupt", async (_reader, participant) => {
288
+ if (participant.identity !== "ensemble-voice-worker" || roomRef.current !== room) return;
289
+ const active = spoken.current;
290
+ if (active) ignored.current.add(active.id);
291
+ spoken.current = void 0;
292
+ if (active) send({ id: active.id, cancel: true });
293
+ propsRef.current.onInterrupt();
294
+ });
295
+ const ready = new Promise((resolve, reject) => {
296
+ const timeout = setTimeout(() => reject(new Error("Voice worker did not connect")), 3e4);
297
+ room.registerTextStreamHandler("ensemble.ready", (_reader, participant) => {
298
+ if (participant.identity === "ensemble-voice-worker") {
299
+ clearTimeout(timeout);
300
+ resolve();
301
+ }
302
+ });
303
+ });
304
+ void ready.catch(() => void 0);
305
+ await room.connect(session.url, session.token);
306
+ if (attempt !== generation.current) {
307
+ await room.disconnect();
308
+ return;
309
+ }
310
+ await room.localParticipant.sendText(JSON.stringify({ id: "ping", ping: true }), {
311
+ topic: "ensemble.speech",
312
+ destinationIdentities: ["ensemble-voice-worker"]
313
+ });
314
+ await ready;
315
+ if (attempt !== generation.current) return;
316
+ await room.localParticipant.setMicrophoneEnabled(true);
317
+ setState("ready");
318
+ } catch (err) {
319
+ if (attempt === generation.current) {
320
+ setError(err instanceof Error ? err.message : "Unable to start call");
321
+ await close();
322
+ }
323
+ }
324
+ };
325
+ useEffect2(() => {
326
+ if (props.connectRequest !== lastConnectRequest.current) {
327
+ lastConnectRequest.current = props.connectRequest;
328
+ void start();
329
+ }
330
+ }, [props.connectRequest]);
331
+ useEffect2(() => {
332
+ return () => {
333
+ void close();
334
+ };
335
+ }, [props.threadId, props.agentId]);
336
+ useEffect2(() => {
337
+ if (state !== "ready") return;
338
+ const last = props.messages.at(-1);
339
+ if ((props.status === "streaming" || props.status === "ready") && last?.role === "assistant" && !ignored.current.has(last.id)) {
340
+ if (spoken.current?.id !== last.id) spoken.current = { id: last.id, cursor: new SpeechTextCursor() };
341
+ const active = spoken.current;
342
+ for (const chunk of active.cursor.read(last.parts, props.status === "ready")) {
343
+ send({ id: active.id, ...chunk });
344
+ }
345
+ if (props.status === "ready") {
346
+ send({ id: active.id, done: true });
347
+ }
348
+ }
349
+ if (props.status === "error" && spoken.current) {
350
+ send({ id: spoken.current.id, done: true, cancel: true });
351
+ ignored.current.add(spoken.current.id);
352
+ spoken.current = void 0;
353
+ }
354
+ }, [props.messages, props.status, state]);
355
+ const lastMessage = props.messages.at(-1);
356
+ const preview = lastMessage?.role === "assistant" ? lastMessage.parts.filter((part) => part.type === "text").map((part) => takeSpeechSegments(part.text, true).segments.join("")) : [];
357
+ return /* @__PURE__ */ jsx2(
358
+ VoiceChatView,
359
+ {
360
+ visible: state !== "idle" || Boolean(error),
361
+ media: /* @__PURE__ */ jsx2("div", { className: "chat-widget__avatar-stage", ref: mediaRef }),
362
+ captions: captionRoom ? /* @__PURE__ */ jsx2(LiveCaptions, { room: captionRoom, preview }) : null,
363
+ content: props.content,
364
+ contentKey: props.contentKey,
365
+ children: /* @__PURE__ */ jsxs2("div", { className: "chat-widget__call-footer", children: [
366
+ /* @__PURE__ */ jsxs2("div", { className: "chat-widget__call-controls", children: [
367
+ state === "idle" ? /* @__PURE__ */ jsx2("span", { role: "status", children: "Call disconnected" }) : /* @__PURE__ */ jsxs2(Fragment, { children: [
368
+ state === "connecting" && /* @__PURE__ */ jsx2("span", { role: "status", children: "Connecting\u2026" }),
369
+ /* @__PURE__ */ jsx2(
370
+ "button",
371
+ {
372
+ type: "button",
373
+ disabled: state !== "ready",
374
+ onClick: async () => {
375
+ try {
376
+ await roomRef.current?.localParticipant.setMicrophoneEnabled(muted);
377
+ setMuted(!muted);
378
+ } catch {
379
+ setError("Unable to change microphone");
380
+ }
381
+ },
382
+ children: muted ? "Unmute" : "Mute"
383
+ }
384
+ ),
385
+ " ",
386
+ /* @__PURE__ */ jsx2(
387
+ "button",
388
+ {
389
+ type: "button",
390
+ onClick: () => {
391
+ propsRef.current.onInterrupt();
392
+ void close();
393
+ },
394
+ children: "End call"
395
+ }
396
+ )
397
+ ] }),
398
+ needsAudioGesture && state === "ready" && /* @__PURE__ */ jsx2(
399
+ "button",
400
+ {
401
+ type: "button",
402
+ onClick: () => {
403
+ void roomRef.current?.startAudio().catch(() => setError("Unable to enable audio"));
404
+ },
405
+ children: "Enable audio"
406
+ }
407
+ )
408
+ ] }),
409
+ error && /* @__PURE__ */ jsx2("div", { role: "alert", children: error })
410
+ ] })
411
+ }
412
+ );
413
+ }
414
+ export {
415
+ RealtimeVoicePanel
416
+ };
417
+ //# sourceMappingURL=RealtimeVoicePanel-IZD3FICH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../lib/chat/components/RealtimeVoicePanel.tsx","../lib/chat/components/VoiceChatView.tsx","../lib/chat/components/speech-segments.ts","../lib/chat/components/speech-text-cursor.ts","../lib/chat/components/caption-words.ts"],"sourcesContent":["import React, { useEffect, useRef, useState } from 'react';\nimport { Room, RoomEvent, RemoteTrack, Track } from 'livekit-client';\nimport { useTranscriptions } from '@livekit/components-react';\nimport type { UIMessage } from 'ai';\nimport type { ApiConfig, AgentVersion } from '../../hooks/useChat';\nimport { VoiceChatView } from './VoiceChatView';\nimport { takeSpeechSegments } from './speech-segments';\nimport { SpeechTextCursor } from './speech-text-cursor';\nimport { captionWords } from './caption-words';\n\nexport interface RealtimeVoiceOptions {\n enabled?: boolean;\n avatar?: boolean;\n}\ninterface Props {\n content?: React.ReactNode;\n contentKey?: string;\n connectRequest: number;\n onCallStateChange: (active: boolean) => void;\n api: ApiConfig;\n threadId: string;\n agentId: string;\n version?: AgentVersion;\n avatar?: boolean;\n messages: UIMessage[];\n status: string;\n onTranscript: (text: string) => void;\n onInterrupt: () => void;\n}\nfunction voiceSessionUrl(baseUrl: string): string {\n const base = baseUrl.replace(/\\/+$/, '');\n return `${base.endsWith('/api') ? base : `${base}/api`}/voice/session`;\n}\n/** LiveKit advances the cursor; the UI only colors it and pages the two-line preview. */\nfunction LiveCaptions({ room, preview }: { room: Room; preview: string[] }) {\n const transcriptions = useTranscriptions({ room });\n const latest = transcriptions.at(-1);\n const text = latest?.text ?? '';\n const isAssistant = latest?.participantInfo.identity === 'ensemble-voice-worker';\n const { words, currentWord } = captionWords(isAssistant ? preview : '', text);\n const captionRef = useRef<HTMLDivElement>(null);\n const currentRef = useRef<HTMLSpanElement>(null);\n useEffect(() => {\n const element = captionRef.current;\n const word = currentRef.current;\n if (element && word) {\n const pageHeight = element.clientHeight;\n if (pageHeight) element.scrollTop = Math.floor(word.offsetTop / pageHeight) * pageHeight;\n }\n }, [text, preview]);\n return text ? (\n <div className=\"chat-widget__subtitles\" ref={captionRef}>\n {words.map((word, index) => <React.Fragment key={index}>\n {index > 0 && ' '}\n <span ref={index === currentWord ? currentRef : undefined}\n className={isAssistant && index === currentWord ? 'chat-widget__subtitle-current' : undefined}>\n {word}\n </span>\n </React.Fragment>)}\n </div>\n ) : null;\n}\n\n/** One-to-one voice bridge. The existing chat request remains the only inference request. */\nexport function RealtimeVoicePanel(props: Props) {\n const [state, setState] = useState<'idle' | 'connecting' | 'ready'>('idle');\n const [muted, setMuted] = useState(false);\n const [captionRoom, setCaptionRoom] = useState<Room>();\n useEffect(() => { props.onCallStateChange(state !== 'idle'); }, [state, props.onCallStateChange]);\n const [error, setError] = useState('');\n const [needsAudioGesture, setNeedsAudioGesture] = useState(false);\n const roomRef = useRef<Room | undefined>(undefined);\n const sessionRef = useRef<{ room: string; api: ApiConfig } | undefined>(undefined);\n const mediaRef = useRef<HTMLDivElement>(null);\n const propsRef = useRef(props);\n propsRef.current = props;\n const sendQueue = useRef(Promise.resolve());\n const spoken = useRef<{ id: string; cursor: SpeechTextCursor } | undefined>(undefined);\n const ignored = useRef(new Set<string>());\n const generation = useRef(0);\n const lastConnectRequest = useRef(props.connectRequest);\n\n const send = (event: Record<string, unknown>) => {\n const room = roomRef.current;\n sendQueue.current = sendQueue.current\n .then(async () => {\n if (room && room === roomRef.current)\n await room.localParticipant.sendText(JSON.stringify(event), {\n topic: 'ensemble.speech',\n destinationIdentities: ['ensemble-voice-worker'],\n });\n })\n .catch(() => {\n setError('Voice connection interrupted');\n });\n };\n const close = async () => {\n generation.current++;\n const room = roomRef.current;\n roomRef.current = undefined;\n setCaptionRoom(undefined);\n const session = sessionRef.current;\n sessionRef.current = undefined;\n spoken.current = undefined;\n await room?.disconnect();\n mediaRef.current?.replaceChildren();\n setState('idle');\n if (session) {\n const { api } = session;\n await fetch(`${voiceSessionUrl(api.baseUrl)}/${encodeURIComponent(session.room)}`, {\n method: 'DELETE',\n headers: { ...api.headers, Authorization: `Bearer ${api.token}` },\n }).catch(() => undefined);\n }\n };\n const start = async () => {\n const attempt = ++generation.current;\n setState('connecting');\n setError('');\n setMuted(false);\n ignored.current = new Set(propsRef.current.messages.map((message) => message.id));\n try {\n const { api, agentId, threadId, version, avatar } = propsRef.current;\n const response = await fetch(voiceSessionUrl(api.baseUrl), {\n method: 'POST',\n headers: {\n ...api.headers,\n Authorization: `Bearer ${api.token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ agentId, threadId, version, avatar }),\n });\n if (!response.headers.get('content-type')?.includes('application/json')) {\n throw new Error(\n `Voice endpoint returned an unexpected response (HTTP ${response.status}). Check the server URL and that the updated API server is running.`,\n );\n }\n const body = await response.json();\n if (!response.ok) {\n throw new Error(body.error || 'Unable to start call');\n }\n const session = body as { url: string; token: string; room: string };\n // A cancelled/unmounted start must not leave a newly created billable room behind.\n if (attempt !== generation.current) {\n await fetch(`${voiceSessionUrl(api.baseUrl)}/${encodeURIComponent(session.room)}`, {\n method: 'DELETE',\n headers: { ...api.headers, Authorization: `Bearer ${api.token}` },\n });\n return;\n }\n sessionRef.current = { room: session.room, api };\n const room = new Room();\n roomRef.current = room;\n setCaptionRoom(room);\n room.on(RoomEvent.AudioPlaybackStatusChanged, () =>\n setNeedsAudioGesture(!room.canPlaybackAudio),\n );\n room.on(RoomEvent.TrackSubscribed, (track: RemoteTrack) => {\n const element = track.attach();\n element.autoplay = true;\n if (track.kind === Track.Kind.Video) {\n element.style.width = '100%';\n element.setAttribute('playsinline', '');\n }\n mediaRef.current?.appendChild(element);\n });\n room.on(RoomEvent.TrackUnsubscribed, (track) =>\n track.detach().forEach((element) => element.remove()),\n );\n room.on(RoomEvent.Disconnected, () => {\n if (roomRef.current === room) void close();\n });\n room.registerTextStreamHandler('ensemble.error', async (reader, participant) => {\n if (participant.identity !== 'ensemble-voice-worker') return;\n const text = await reader.readAll();\n if (roomRef.current === room) setError(text);\n });\n room.registerTextStreamHandler('ensemble.transcript', async (reader, participant) => {\n if (participant.identity !== 'ensemble-voice-worker') return;\n const text = await reader.readAll();\n if (roomRef.current === room) propsRef.current.onTranscript(text);\n });\n room.registerTextStreamHandler('ensemble.closed', (_reader, participant) => {\n if (participant.identity === 'ensemble-voice-worker' && roomRef.current === room) {\n propsRef.current.onInterrupt();\n void close();\n }\n });\n room.registerTextStreamHandler('ensemble.interrupt', async (_reader, participant) => {\n if (participant.identity !== 'ensemble-voice-worker' || roomRef.current !== room) return;\n const active = spoken.current;\n if (active) ignored.current.add(active.id);\n spoken.current = undefined;\n if (active) send({ id: active.id, cancel: true });\n propsRef.current.onInterrupt();\n });\n const ready = new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(() => reject(new Error('Voice worker did not connect')), 30_000);\n room.registerTextStreamHandler('ensemble.ready', (_reader, participant) => {\n if (participant.identity === 'ensemble-voice-worker') {\n clearTimeout(timeout);\n resolve();\n }\n });\n });\n void ready.catch(() => undefined);\n await room.connect(session.url, session.token);\n if (attempt !== generation.current) {\n await room.disconnect();\n return;\n }\n // Ping covers the case where the worker announced readiness before the browser joined.\n await room.localParticipant.sendText(JSON.stringify({ id: 'ping', ping: true }), {\n topic: 'ensemble.speech',\n destinationIdentities: ['ensemble-voice-worker'],\n });\n await ready;\n if (attempt !== generation.current) return;\n await room.localParticipant.setMicrophoneEnabled(true);\n setState('ready');\n } catch (err) {\n if (attempt === generation.current) {\n setError(err instanceof Error ? err.message : 'Unable to start call');\n await close();\n }\n }\n };\n useEffect(() => {\n if (props.connectRequest !== lastConnectRequest.current) {\n lastConnectRequest.current = props.connectRequest;\n void start();\n }\n }, [props.connectRequest]);\n useEffect(() => {\n return () => {\n void close();\n };\n }, [props.threadId, props.agentId]);\n useEffect(() => {\n if (state !== 'ready') return;\n const last = props.messages.at(-1);\n if ((props.status === 'streaming' || props.status === 'ready') &&\n last?.role === 'assistant' && !ignored.current.has(last.id)) {\n if (spoken.current?.id !== last.id) spoken.current = { id: last.id, cursor: new SpeechTextCursor() };\n const active = spoken.current;\n for (const chunk of active.cursor.read(last.parts, props.status === 'ready')) {\n send({ id: active.id, ...chunk });\n }\n if (props.status === 'ready') {\n send({ id: active.id, done: true });\n // AI SDK can continue this same message after a tool request. Keep the\n // cursor so appended text is spoken without replaying completed parts.\n }\n }\n if (props.status === 'error' && spoken.current) {\n send({ id: spoken.current.id, done: true, cancel: true });\n ignored.current.add(spoken.current.id);\n spoken.current = undefined;\n }\n }, [props.messages, props.status, state]);\n const lastMessage = props.messages.at(-1);\n const preview = lastMessage?.role === 'assistant'\n ? lastMessage.parts.filter((part) => part.type === 'text')\n .map((part) => takeSpeechSegments(part.text, true).segments.join(''))\n : [];\n return (\n <VoiceChatView\n visible={state !== 'idle' || Boolean(error)}\n media={<div className=\"chat-widget__avatar-stage\" ref={mediaRef} />}\n captions={captionRoom ? <LiveCaptions room={captionRoom} preview={preview} /> : null}\n content={props.content}\n contentKey={props.contentKey}\n >\n <div className=\"chat-widget__call-footer\">\n <div className=\"chat-widget__call-controls\">\n {state === 'idle' ? (\n <span role=\"status\">Call disconnected</span>\n ) : (\n <>\n {state === 'connecting' && <span role=\"status\">Connecting…</span>}\n <button\n type=\"button\"\n disabled={state !== 'ready'}\n onClick={async () => {\n try {\n await roomRef.current?.localParticipant.setMicrophoneEnabled(muted);\n setMuted(!muted);\n } catch {\n setError('Unable to change microphone');\n }\n }}\n >\n {muted ? 'Unmute' : 'Mute'}\n </button>{' '}\n <button\n type=\"button\"\n onClick={() => {\n propsRef.current.onInterrupt();\n void close();\n }}\n >\n End call\n </button>\n </>\n )}\n {needsAudioGesture && state === 'ready' && (\n <button\n type=\"button\"\n onClick={() => {\n void roomRef.current?.startAudio().catch(() => setError('Unable to enable audio'));\n }}\n >\n Enable audio\n </button>\n )}\n </div>\n {error && <div role=\"alert\">{error}</div>}\n </div>\n </VoiceChatView>\n );\n}\n","import React, { useEffect, useRef, useState } from 'react';\nimport { Maximize2, Minimize2, X } from 'lucide-react';\nimport { createLogger } from '@repo/shared-common';\n\nconst logger = createLogger('VoiceChatView');\n\n/** A separate presentation for connected calls; normal chat retains its own renderer. */\nexport function VoiceChatView({ media, captions, content, contentKey = 'content', children, visible }: {\n media: React.ReactNode;\n captions?: React.ReactNode;\n content?: React.ReactNode;\n contentKey?: string;\n children: React.ReactNode;\n visible: boolean;\n}) {\n const viewRef = useRef<HTMLDivElement>(null);\n const [fullscreen, setFullscreen] = useState(false);\n const [fullscreenSupported, setFullscreenSupported] = useState(false);\n useEffect(() => {\n setFullscreenSupported(Boolean(document.fullscreenEnabled && viewRef.current?.requestFullscreen));\n const update = () => setFullscreen(document.fullscreenElement === viewRef.current);\n document.addEventListener('fullscreenchange', update);\n return () => document.removeEventListener('fullscreenchange', update);\n }, []);\n useEffect(() => {\n if (!visible && document.fullscreenElement === viewRef.current) {\n void document.exitFullscreen().catch((err) => logger.warn({ err }, 'Unable to exit fullscreen'));\n }\n }, [visible]);\n const toggleFullscreen = async () => {\n try {\n if (document.fullscreenElement === viewRef.current) await document.exitFullscreen();\n else await viewRef.current?.requestFullscreen();\n } catch (err) {\n logger.warn({ err }, 'Unable to change voice fullscreen mode');\n }\n };\n const [dismissedContent, setDismissedContent] = useState<string>();\n const showContent = Boolean(content) && dismissedContent !== contentKey;\n return (\n <div ref={viewRef} className={`chat-widget__voice-view${showContent ? ' chat-widget__voice-view--content' : ''}`} hidden={!visible}>\n <div className=\"chat-widget__voice-scene\">\n {media}\n {fullscreenSupported && <button type=\"button\" className=\"chat-widget__voice-fullscreen\"\n aria-label={fullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}\n title={fullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}\n aria-pressed={fullscreen} onClick={() => void toggleFullscreen()}>\n {fullscreen ? <Minimize2 size={18} /> : <Maximize2 size={18} />}\n </button>}\n <div className=\"chat-widget__voice-overlay\">\n {captions}\n {children}\n </div>\n </div>\n <div className=\"chat-widget__voice-content-wrap\" aria-hidden={!showContent} inert={!showContent}>\n <div className=\"chat-widget__voice-content\">\n {content && <button type=\"button\" className=\"chat-widget__dismiss-content\"\n aria-label=\"Dismiss content\" title=\"Dismiss content\" onClick={() => setDismissedContent(contentKey)}>\n <X size={18} />\n </button>}\n {content}\n </div>\n </div>\n </div>\n );\n}\n","/** Small natural chunks for TTS; citations and markup remain in the chat UI. */\nexport function takeSpeechSegments(text: string, done: boolean) {\n const segments: string[] = [];\n while (text.length) {\n const sentence = /[.!?]\\s|\\n/.exec(text);\n let end = sentence ? sentence.index + sentence[0].length : 0;\n if (!end && text.length >= 200) end = text.lastIndexOf(' ', 200) + 1;\n if (!end && done) end = text.length;\n if (!end) break;\n const chunk = text\n .slice(0, end)\n .replace(/\\[([^\\]]+)\\]\\([^)]*\\)/g, '$1')\n .replace(/[*#`]/g, '')\n .trim();\n if (chunk) segments.push(chunk + ' ');\n text = text.slice(end);\n }\n return { segments, remainder: text };\n}\n","import type { UIMessage } from 'ai';\n\nexport type SpeechTextChunk = { text: string } | { flush: true };\n\n/** Forward text deltas only; AI SDK text-end boundaries close each speech input. */\nexport class SpeechTextCursor {\n private part = 0;\n private offset = 0;\n\n read(parts: UIMessage['parts'], done: boolean): SpeechTextChunk[] {\n const chunks: SpeechTextChunk[] = [];\n while (this.part < parts.length) {\n const part = parts[this.part]!;\n if (part.type !== 'text') {\n this.part++;\n continue;\n }\n const text = part.text.slice(this.offset);\n if (text) chunks.push({ text });\n this.offset = part.text.length;\n if (part.state !== 'done' && !done) break;\n chunks.push({ flush: true });\n this.part++;\n this.offset = 0;\n }\n return chunks;\n }\n}\n","/** Use LiveKit's synchronized text as the cursor; never estimate speech timing. */\nexport function captionWords(preview: string | string[], spoken: string) {\n const heard = spoken.trim().match(/\\S+/g) ?? [];\n const candidates = (Array.isArray(preview) ? preview : [preview]).map((text) => text.trim().match(/\\S+/g) ?? []);\n const ahead = candidates.find((words) => heard.every((word, index) => word === words[index])) ?? [];\n // If TTS rewrites the text, show the authoritative transcript rather than highlight a wrong word.\n const matches = heard.length > 0 && heard.every((word, index) => word === ahead[index]);\n return { words: matches ? ahead : heard, currentWord: heard.length - 1 };\n}\n"],"mappings":"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","names":["React","useEffect","useRef","useState","jsx","jsxs","useRef","useEffect","React","useState"]}