vibrium-speak-sdk 1.0.0

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/dist/index.js ADDED
@@ -0,0 +1,1705 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ FloatingMic: () => FloatingMic,
24
+ LiveKitProvider: () => LiveKitProvider,
25
+ VoicePanel: () => VoicePanel,
26
+ default: () => index_default
27
+ });
28
+ module.exports = __toCommonJS(index_exports);
29
+
30
+ // src/components/FloatingMic.tsx
31
+ var import_react4 = require("react");
32
+ var import_lucide_react2 = require("lucide-react");
33
+
34
+ // src/components/VoicePannel.tsx
35
+ var import_react = require("react");
36
+ var import_lucide_react = require("lucide-react");
37
+ var import_jsx_runtime = require("react/jsx-runtime");
38
+ function renderInline(text) {
39
+ const nodes = [];
40
+ let lastIndex = 0;
41
+ const boldRe = /\*\*(.+?)\*\*/g;
42
+ let m;
43
+ while ((m = boldRe.exec(text)) !== null) {
44
+ const idx = m.index;
45
+ if (idx > lastIndex) {
46
+ nodes.push(
47
+ text.slice(lastIndex, idx)
48
+ );
49
+ }
50
+ nodes.push(
51
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: m[1] }, idx)
52
+ );
53
+ lastIndex = idx + m[0].length;
54
+ }
55
+ if (lastIndex < text.length) {
56
+ nodes.push(
57
+ text.slice(lastIndex)
58
+ );
59
+ }
60
+ return nodes;
61
+ }
62
+ function formatMessage(text) {
63
+ try {
64
+ const lines = text.split("\n");
65
+ const out = [];
66
+ let inList = [];
67
+ const flushList = (keyBase) => {
68
+ if (inList.length) {
69
+ out.push(
70
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
71
+ "ul",
72
+ {
73
+ className: "list-disc pl-5 ml-2 space-y-1",
74
+ children: inList.map(
75
+ (li, idx) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
76
+ "li",
77
+ {
78
+ className: "text-sm",
79
+ children: li
80
+ },
81
+ keyBase + "-li-" + idx
82
+ )
83
+ )
84
+ },
85
+ keyBase + "-ul"
86
+ )
87
+ );
88
+ }
89
+ inList = [];
90
+ };
91
+ lines.forEach(
92
+ (rawLine, idx) => {
93
+ const line = rawLine.replace(/\r/g, "").trimEnd();
94
+ const hMatch = line.match(
95
+ /^\s*(#{1,3})\s*(.*)$/
96
+ );
97
+ if (hMatch) {
98
+ flushList("h" + idx);
99
+ const level = hMatch[1].length;
100
+ const content = hMatch[2];
101
+ const cls = "font-semibold";
102
+ if (level === 1) {
103
+ out.push(
104
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
105
+ "h1",
106
+ {
107
+ className: "text-xl " + cls,
108
+ children: renderInline(
109
+ content
110
+ )
111
+ },
112
+ "h1-" + idx
113
+ )
114
+ );
115
+ } else if (level === 2) {
116
+ out.push(
117
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
118
+ "h2",
119
+ {
120
+ className: "text-lg " + cls,
121
+ children: renderInline(
122
+ content
123
+ )
124
+ },
125
+ "h2-" + idx
126
+ )
127
+ );
128
+ } else {
129
+ out.push(
130
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
131
+ "h3",
132
+ {
133
+ className: "text-base " + cls,
134
+ children: renderInline(
135
+ content
136
+ )
137
+ },
138
+ "h3-" + idx
139
+ )
140
+ );
141
+ }
142
+ return;
143
+ }
144
+ const bMatch = line.match(
145
+ /^\s*([-\*\.]|\u2022)\s+(.*)$/
146
+ );
147
+ if (bMatch) {
148
+ inList.push(
149
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: renderInline(
150
+ bMatch[2]
151
+ ) })
152
+ );
153
+ return;
154
+ }
155
+ if (line.trim() === "") {
156
+ flushList(
157
+ "empty-" + idx
158
+ );
159
+ out.push(
160
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
161
+ "div",
162
+ {
163
+ className: "my-2"
164
+ },
165
+ "br-" + idx
166
+ )
167
+ );
168
+ return;
169
+ }
170
+ flushList("p-" + idx);
171
+ out.push(
172
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
173
+ "div",
174
+ {
175
+ className: "text-sm leading-snug",
176
+ children: renderInline(line)
177
+ },
178
+ "p-" + idx
179
+ )
180
+ );
181
+ }
182
+ );
183
+ if (inList.length) {
184
+ out.push(
185
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
186
+ "ul",
187
+ {
188
+ className: "list-disc pl-5 ml-2 space-y-1",
189
+ children: inList.map(
190
+ (li, idx) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
191
+ "li",
192
+ {
193
+ className: "text-sm",
194
+ children: li
195
+ },
196
+ "last-li-" + idx
197
+ )
198
+ )
199
+ },
200
+ "last-ul"
201
+ )
202
+ );
203
+ }
204
+ return out;
205
+ } catch {
206
+ return text;
207
+ }
208
+ }
209
+ var MessageBubble = (0, import_react.memo)(
210
+ ({
211
+ message
212
+ }) => {
213
+ const isUser = message.type === "user_voice" || message.type === "user_text";
214
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
215
+ "div",
216
+ {
217
+ className: `p-3 rounded-[16px] max-w-[85%] text-xs leading-relaxed ${isUser ? "bg-[#1a73e8] text-white self-end rounded-tr-sm shadow-md shadow-blue-500/10 border border-blue-500/10" : "bg-white text-gray-800 self-start rounded-tl-sm shadow-md shadow-slate-100 border border-slate-100"}`,
218
+ children: formatMessage(
219
+ String(
220
+ message.text || ""
221
+ )
222
+ )
223
+ }
224
+ );
225
+ }
226
+ );
227
+ MessageBubble.displayName = "MessageBubble";
228
+ function VoicePanel({
229
+ enabled,
230
+ connectionState,
231
+ agentState = "idle",
232
+ messages,
233
+ participants = [],
234
+ onDisable,
235
+ textEnabled,
236
+ micMuted = false,
237
+ speakerMuted = false,
238
+ onMicMutedChange,
239
+ onSpeakerMutedChange,
240
+ onSendText
241
+ }) {
242
+ const [input, setInput] = (0, import_react.useState)("");
243
+ const [minimized, setMinimized] = (0, import_react.useState)(false);
244
+ const containerRef = (0, import_react.useRef)(
245
+ null
246
+ );
247
+ const orderedMessages = (0, import_react.useMemo)(() => {
248
+ return [...messages].sort(
249
+ (a, b) => a.timestamp - b.timestamp
250
+ );
251
+ }, [messages]);
252
+ (0, import_react.useEffect)(() => {
253
+ const el = containerRef.current;
254
+ if (!el) return;
255
+ requestAnimationFrame(() => {
256
+ el.scrollTo({
257
+ top: el.scrollHeight,
258
+ behavior: "smooth"
259
+ });
260
+ });
261
+ }, [orderedMessages, minimized]);
262
+ const sendMessage = () => {
263
+ const text = input.trim();
264
+ if (!text || !onSendText) {
265
+ return;
266
+ }
267
+ console.log("[VoicePanel] sendMessage", text);
268
+ onSendText(text);
269
+ setInput("");
270
+ };
271
+ const handleMicClick = () => {
272
+ onMicMutedChange?.(
273
+ !micMuted
274
+ );
275
+ };
276
+ const handleSpeakerClick = () => {
277
+ onSpeakerMutedChange?.(
278
+ !speakerMuted
279
+ );
280
+ };
281
+ const handleKey = (e) => {
282
+ if (e.key === "Enter") {
283
+ e.preventDefault();
284
+ sendMessage();
285
+ }
286
+ };
287
+ const statusText = (() => {
288
+ if (micMuted && speakerMuted) {
289
+ return "Mic & Speaker muted";
290
+ }
291
+ if (micMuted) {
292
+ return "Mic muted";
293
+ }
294
+ if (speakerMuted) {
295
+ return "Speaker muted";
296
+ }
297
+ if (connectionState === "reconnecting") {
298
+ return "Reconnecting...";
299
+ }
300
+ if (connectionState === "connecting") {
301
+ return "Connecting...";
302
+ }
303
+ if (connectionState === "error") {
304
+ return "Connection error";
305
+ }
306
+ if (connectionState === "connected") {
307
+ switch (agentState) {
308
+ case "listening":
309
+ return "Listening...";
310
+ case "thinking":
311
+ return "Thinking...";
312
+ case "speaking":
313
+ return "Speaking...";
314
+ default:
315
+ return "Ready to help";
316
+ }
317
+ }
318
+ return "Disconnected";
319
+ })();
320
+ if (enabled && minimized) {
321
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
322
+ "div",
323
+ {
324
+ onClick: () => setMinimized(
325
+ false
326
+ ),
327
+ className: "fixed right-6 bottom-6 z-[1100] cursor-pointer",
328
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
329
+ "div",
330
+ {
331
+ className: `w-14 h-14 rounded-full flex items-center justify-center shadow-lg text-white ${connectionState === "connected" ? "bg-green-500 animate-pulse" : connectionState === "connecting" ? "bg-blue-500" : connectionState === "error" ? "bg-red-500" : "bg-blue-600"}`,
332
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Mic, { className: "h-4 w-4" })
333
+ }
334
+ )
335
+ }
336
+ );
337
+ }
338
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "fixed bottom-24 right-6 w-80 bg-white shadow-xl rounded-[20px] flex flex-col z-[1100] border border-gray-100/80 overflow-hidden font-sans", children: [
339
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center justify-between p-4 bg-[#1a73e8] text-white", children: [
340
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-3", children: [
341
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "w-9 h-9 rounded-full bg-white/15 flex items-center justify-center relative shrink-0", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "relative flex items-center justify-center w-2.5 h-2.5", children: [
342
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
343
+ "span",
344
+ {
345
+ className: `w-2.5 h-2.5 rounded-full transition-colors duration-300 ${connectionState === "connected" ? "bg-[#fbbc05]" : connectionState === "connecting" || connectionState === "reconnecting" ? "bg-yellow-400 animate-pulse" : connectionState === "error" ? "bg-red-500" : "bg-gray-400"}`
346
+ }
347
+ ),
348
+ connectionState === "connected" && (agentState === "speaking" || agentState === "listening" || agentState === "thinking") && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "absolute inline-flex h-4 w-4 rounded-full bg-[#fbbc05]/50 animate-ping" })
349
+ ] }) }),
350
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex flex-col", children: [
351
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-base font-semibold leading-tight", children: "Voice Assistant" }),
352
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-[11px] text-white/80 mt-0.5 font-normal", children: statusText })
353
+ ] })
354
+ ] }),
355
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex gap-1.5", children: [
356
+ enabled && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
357
+ "button",
358
+ {
359
+ onClick: () => setMinimized(true),
360
+ className: "bg-white/10 w-8 h-8 rounded-lg flex items-center justify-center hover:bg-white/20 transition-all duration-200 active:scale-95 cursor-pointer",
361
+ title: "Minimize",
362
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.ChevronRight, { className: "rotate-90 h-4.5 w-4.5" })
363
+ }
364
+ ),
365
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
366
+ "button",
367
+ {
368
+ onClick: onDisable,
369
+ className: "bg-white/10 w-8 h-8 rounded-lg flex items-center justify-center hover:bg-white/20 transition-all duration-200 active:scale-95 cursor-pointer",
370
+ title: "Close",
371
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.X, { className: "h-4.5 w-4.5" })
372
+ }
373
+ )
374
+ ] })
375
+ ] }),
376
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "grid grid-cols-2 gap-2.5 p-3.5 bg-white border-b border-gray-100", children: [
377
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
378
+ "div",
379
+ {
380
+ onClick: handleSpeakerClick,
381
+ className: `flex items-center gap-2.5 p-3 rounded-xl cursor-pointer transition-all duration-200 active:scale-[0.98] ${speakerMuted ? "bg-[#f1f3f4] hover:bg-[#e8eaed]" : "bg-[#e8f0fe] hover:bg-[#d2e3fc]"}`,
382
+ children: [
383
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
384
+ "div",
385
+ {
386
+ className: `w-10 h-10 rounded-lg flex items-center justify-center transition-colors shrink-0 ${speakerMuted ? "bg-[#e8eaed] text-gray-500" : "bg-[#1a73e8] text-white"}`,
387
+ children: speakerMuted ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.VolumeX, { className: "h-4.5 w-4.5" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Volume2, { className: "h-4.5 w-4.5" })
388
+ }
389
+ ),
390
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "min-w-0", children: [
391
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-xs font-semibold text-gray-800 truncate", children: "Speaker" }),
392
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-[10px] text-gray-500 mt-0.5 truncate", children: speakerMuted ? "Disabled" : "Active" })
393
+ ] })
394
+ ]
395
+ }
396
+ ),
397
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
398
+ "div",
399
+ {
400
+ onClick: handleMicClick,
401
+ className: `flex items-center gap-2.5 p-3 rounded-xl cursor-pointer transition-all duration-200 active:scale-[0.98] ${micMuted ? "bg-[#f1f3f4] hover:bg-[#e8eaed]" : "bg-[#e8f0fe] hover:bg-[#d2e3fc]"}`,
402
+ children: [
403
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
404
+ "div",
405
+ {
406
+ className: `w-10 h-10 rounded-lg flex items-center justify-center transition-colors shrink-0 ${micMuted ? "bg-[#e8eaed] text-gray-500" : "bg-[#1a73e8] text-white"}`,
407
+ children: micMuted ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.MicOff, { className: "h-4.5 w-4.5" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Mic, { className: "h-4.5 w-4.5" })
408
+ }
409
+ ),
410
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "min-w-0", children: [
411
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-xs font-semibold text-gray-800 truncate", children: "Mic" }),
412
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-[10px] text-gray-500 mt-0.5 truncate", children: micMuted ? "Disabled" : "Active" })
413
+ ] })
414
+ ]
415
+ }
416
+ )
417
+ ] }),
418
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
419
+ "div",
420
+ {
421
+ ref: containerRef,
422
+ className: "flex-1 p-3.5 overflow-y-auto max-h-[300px] min-h-[240px] bg-[#f8f9fa] flex flex-col gap-3 scroll-smooth",
423
+ children: [
424
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex justify-start", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-white border border-gray-200/80 shadow-sm text-[10px] font-semibold text-slate-700", children: [
425
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "\u{1F3A4}" }),
426
+ " Voice enabled"
427
+ ] }) }),
428
+ participants && participants.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "p-2.5 rounded-lg bg-blue-50/50 border border-blue-100/50 mb-2", children: [
429
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-[10px] font-semibold text-blue-900 mb-0.5", children: "Participants:" }),
430
+ participants.map((p, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { className: "text-[10px] text-blue-800", children: [
431
+ "\u2022 ",
432
+ p
433
+ ] }, i))
434
+ ] }),
435
+ orderedMessages.map((m) => {
436
+ if (m.text === "__SESSION_END__") {
437
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
438
+ "div",
439
+ {
440
+ className: "p-3.5 rounded-xl max-w-[85%] text-xs text-amber-800 bg-amber-50 border border-amber-100 self-center text-center shadow-sm",
441
+ children: [
442
+ "Session ended.",
443
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("br", {}),
444
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
445
+ "button",
446
+ {
447
+ style: {
448
+ color: "#1a73e8",
449
+ textDecoration: "underline",
450
+ cursor: "pointer",
451
+ background: "none",
452
+ border: "none",
453
+ padding: 0,
454
+ fontWeight: 600
455
+ },
456
+ className: "hover:text-blue-700 transition-colors mt-1",
457
+ onClick: () => window.location.reload(),
458
+ children: "Click here"
459
+ }
460
+ ),
461
+ " ",
462
+ "to start session again"
463
+ ]
464
+ },
465
+ m.id
466
+ );
467
+ }
468
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
469
+ MessageBubble,
470
+ {
471
+ message: m
472
+ },
473
+ m.id
474
+ );
475
+ })
476
+ ]
477
+ }
478
+ ),
479
+ textEnabled && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "p-3 bg-white flex gap-2.5 items-center border-t border-gray-100", children: [
480
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
481
+ "input",
482
+ {
483
+ value: input,
484
+ onChange: (e) => setInput(e.target.value),
485
+ onKeyDown: handleKey,
486
+ placeholder: "Type your message...",
487
+ className: "flex-1 px-3.5 py-2.5 text-xs bg-[#f1f3f4] border border-transparent rounded-full focus:outline-none focus:bg-white focus:border-gray-200 focus:ring-2 focus:ring-blue-100 text-gray-900 placeholder-gray-500 transition-all duration-200"
488
+ }
489
+ ),
490
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
491
+ "button",
492
+ {
493
+ onClick: sendMessage,
494
+ className: "w-10 h-10 rounded-xl bg-[#1a73e8] text-white flex items-center justify-center hover:bg-[#1557b0] transition-colors shadow-md shadow-blue-500/20 active:scale-95 shrink-0 cursor-pointer",
495
+ title: "Send message",
496
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.SendHorizontal, { className: "h-4.5 w-4.5" })
497
+ }
498
+ )
499
+ ] })
500
+ ] });
501
+ }
502
+
503
+ // src/providers/LiveKitProvider.tsx
504
+ var import_react3 = require("react");
505
+ var import_livekit_client2 = require("livekit-client");
506
+
507
+ // src/services/auth.service.ts
508
+ async function getBearerToken({
509
+ customerId,
510
+ apiKey
511
+ }) {
512
+ const response = await fetch(
513
+ `https://api.vibrium.ai/v1/customers/${customerId}/auth/token`,
514
+ {
515
+ method: "POST",
516
+ headers: {
517
+ "Content-Type": "application/json",
518
+ Authorization: `Basic ${apiKey}`
519
+ }
520
+ }
521
+ );
522
+ if (!response.ok) {
523
+ const text = await response.text();
524
+ throw new Error(
525
+ `Auth failed: ${text}`
526
+ );
527
+ }
528
+ const json = await response.json();
529
+ const token = json?.token || json?.data?.token || json?.access_token;
530
+ if (!token) {
531
+ throw new Error(
532
+ "Missing bearer token"
533
+ );
534
+ }
535
+ return token;
536
+ }
537
+
538
+ // src/services/token.service.ts
539
+ async function fetchSession(config) {
540
+ console.log("customerId", config.customerId);
541
+ console.log("apiKey", config.apiKey);
542
+ const bearerToken = await getBearerToken({
543
+ customerId: config.customerId,
544
+ apiKey: config.apiKey
545
+ });
546
+ const response = await fetch(
547
+ config.lambdaUrl,
548
+ {
549
+ method: "POST",
550
+ headers: {
551
+ "Content-Type": "application/json",
552
+ Authorization: `Bearer ${bearerToken}`
553
+ },
554
+ body: JSON.stringify({
555
+ customer_id: config.customerId,
556
+ bot_id: config.botId,
557
+ mode: config.mode ?? "hybrid",
558
+ ...config.customParameters && {
559
+ user: config.customParameters.user
560
+ },
561
+ ...config.customParameters && {
562
+ context: config.customParameters.context
563
+ }
564
+ })
565
+ }
566
+ );
567
+ if (!response.ok) {
568
+ const text = await response.text();
569
+ throw new Error(
570
+ `Lambda failed: ${text}`
571
+ );
572
+ }
573
+ const json = await response.json();
574
+ return {
575
+ token: json.token || json.data?.token,
576
+ url: json.url || json.data?.url,
577
+ roomName: json.roomName || json.data?.room_name,
578
+ participantIdentity: json.participantIdentity || json.data?.participant_identity
579
+ };
580
+ }
581
+
582
+ // src/services/livekit.service.ts
583
+ var import_livekit_client = require("livekit-client");
584
+
585
+ // src/utils/audio.ts
586
+ function attachAudioTrack(track, participantId, container) {
587
+ const element = track.attach();
588
+ element.id = `audio-${participantId}`;
589
+ element.autoplay = false;
590
+ element.playsInline = true;
591
+ container.appendChild(element);
592
+ return element;
593
+ }
594
+ function detachAudioTrack(track) {
595
+ track.detach().forEach((el) => el.remove());
596
+ }
597
+
598
+ // src/services/action-registry.ts
599
+ var ActionRegistry = class {
600
+ handlers = /* @__PURE__ */ new Map();
601
+ /**
602
+ * Register handler
603
+ *
604
+ * Example:
605
+ *
606
+ * registry.register(
607
+ * "evt_search_product",
608
+ * handler
609
+ * );
610
+ */
611
+ register(eventId, handler) {
612
+ this.handlers.set(
613
+ eventId,
614
+ handler
615
+ );
616
+ }
617
+ /**
618
+ * Remove handler
619
+ */
620
+ unregister(eventId) {
621
+ return this.handlers.delete(
622
+ eventId
623
+ );
624
+ }
625
+ /**
626
+ * Resolve handler
627
+ *
628
+ * Resolution order:
629
+ *
630
+ * 1. Exact match
631
+ * 2. Wildcard (*)
632
+ */
633
+ resolve(eventId) {
634
+ return this.handlers.get(eventId) ?? this.handlers.get("*");
635
+ }
636
+ /**
637
+ * Check existence
638
+ */
639
+ has(eventId) {
640
+ return this.handlers.has(
641
+ eventId
642
+ );
643
+ }
644
+ /**
645
+ * Remove everything
646
+ */
647
+ clear() {
648
+ this.handlers.clear();
649
+ }
650
+ /**
651
+ * Registered event ids
652
+ */
653
+ getRegisteredEvents() {
654
+ return Array.from(
655
+ this.handlers.keys()
656
+ );
657
+ }
658
+ /**
659
+ * Count handlers
660
+ */
661
+ size() {
662
+ return this.handlers.size;
663
+ }
664
+ };
665
+
666
+ // src/services/livekit.service.ts
667
+ var LiveKitService = class {
668
+ room = null;
669
+ audioContainer;
670
+ speakerMuted = true;
671
+ user;
672
+ context;
673
+ mode;
674
+ VOICE_ACTION_TOPIC = "vibrium.voice.action";
675
+ VOICE_ACTION_RESULT_TOPIC = "vibrium.voice.action.result";
676
+ VOICE_ACTION_TIMEOUT_MS = 3e4;
677
+ actionRegistry = new ActionRegistry();
678
+ actionListeners = /* @__PURE__ */ new Set();
679
+ /**
680
+ * --------------------------------------------------------------------------
681
+ * Event handlers
682
+ * --------------------------------------------------------------------------
683
+ */
684
+ onConnected;
685
+ onDisconnected;
686
+ onReconnecting;
687
+ onReconnected;
688
+ onMessage;
689
+ onAgentState;
690
+ onError;
691
+ onMicState;
692
+ onQuality;
693
+ onVoiceAction;
694
+ onVoiceActionResult;
695
+ registerEventHandler(eventId, handler) {
696
+ this.actionRegistry.register(
697
+ eventId,
698
+ handler
699
+ );
700
+ }
701
+ pendingActions = /* @__PURE__ */ new Map();
702
+ constructor(audioContainer, user, context, mode) {
703
+ this.audioContainer = audioContainer;
704
+ this.user = user;
705
+ this.context = context;
706
+ this.mode = mode;
707
+ }
708
+ addVoiceActionListener(listener) {
709
+ this.actionListeners.add(listener);
710
+ return () => {
711
+ this.actionListeners.delete(listener);
712
+ };
713
+ }
714
+ validateVoiceAction(event) {
715
+ if (!event || typeof event !== "object") {
716
+ throw new Error(
717
+ "Invalid event payload"
718
+ );
719
+ }
720
+ if (typeof event.request_id !== "string") {
721
+ throw new Error(
722
+ "Missing request_id"
723
+ );
724
+ }
725
+ if (typeof event.event_id !== "string") {
726
+ throw new Error(
727
+ "Missing event_id"
728
+ );
729
+ }
730
+ }
731
+ async handleVoiceAction(event) {
732
+ this.validateVoiceAction(event);
733
+ this.actionListeners.forEach(
734
+ (listener) => {
735
+ try {
736
+ listener(event);
737
+ } catch (e) {
738
+ console.error(e);
739
+ }
740
+ }
741
+ );
742
+ this.pendingActions.set(
743
+ event.request_id,
744
+ {
745
+ eventId: event.event_id,
746
+ startedAt: Date.now()
747
+ }
748
+ );
749
+ const handler = this.actionRegistry.resolve(event.event_id);
750
+ if (!handler) {
751
+ await this.publishActionResult({
752
+ request_id: event.request_id,
753
+ event_id: event.event_id,
754
+ status: "failure",
755
+ error: {
756
+ code: "handler_not_found",
757
+ message: "No registered handler"
758
+ }
759
+ });
760
+ return;
761
+ }
762
+ try {
763
+ const response = await Promise.race([
764
+ handler(event),
765
+ new Promise(
766
+ (_, reject) => setTimeout(
767
+ () => reject(new Error("Action timeout")),
768
+ this.VOICE_ACTION_TIMEOUT_MS
769
+ )
770
+ )
771
+ ]);
772
+ await this.publishActionResult({
773
+ request_id: event.request_id,
774
+ event_id: event.event_id,
775
+ ...response
776
+ });
777
+ this.pendingActions.delete(
778
+ event.request_id
779
+ );
780
+ } catch (error) {
781
+ await this.publishActionResult({
782
+ request_id: event.request_id,
783
+ event_id: event.event_id,
784
+ status: "failure",
785
+ error: {
786
+ code: "handler_error",
787
+ message: error?.message || "Handler failed"
788
+ }
789
+ });
790
+ this.pendingActions.delete(
791
+ event.request_id
792
+ );
793
+ }
794
+ }
795
+ /**
796
+ * --------------------------------------------------------------------------
797
+ * Connect
798
+ * --------------------------------------------------------------------------
799
+ */
800
+ async connect(url, token) {
801
+ const room = new import_livekit_client.Room({
802
+ adaptiveStream: true,
803
+ dynacast: true
804
+ });
805
+ this.room = room;
806
+ const registerTextStreamHandler = room.registerTextStreamHandler;
807
+ if (typeof registerTextStreamHandler === "function") {
808
+ registerTextStreamHandler.call(
809
+ room,
810
+ "lk.chat",
811
+ async (reader, participantInfo) => {
812
+ const text = await reader.readAll();
813
+ console.log(
814
+ "[TEXT_STREAM]",
815
+ participantInfo?.identity,
816
+ text
817
+ );
818
+ this.onMessage?.({
819
+ id: crypto.randomUUID(),
820
+ type: "assistant_text",
821
+ text,
822
+ final: true,
823
+ timestamp: Date.now()
824
+ });
825
+ }
826
+ );
827
+ }
828
+ room.on(import_livekit_client.RoomEvent.Connected, async () => {
829
+ this.onConnected?.();
830
+ try {
831
+ await room.startAudio();
832
+ if (this.user || this.context) {
833
+ await room.localParticipant.publishData(
834
+ new TextEncoder().encode(
835
+ JSON.stringify({
836
+ type: "session_init",
837
+ user: this.user,
838
+ context: this.context
839
+ })
840
+ ),
841
+ {
842
+ reliable: true,
843
+ topic: "session"
844
+ }
845
+ );
846
+ }
847
+ if (this.mode !== "chat") {
848
+ await room.localParticipant.setMicrophoneEnabled(
849
+ true
850
+ );
851
+ }
852
+ this.onMicState?.(true);
853
+ } catch (err) {
854
+ this.onError?.(
855
+ err?.message || "Failed to start audio"
856
+ );
857
+ }
858
+ });
859
+ room.on(import_livekit_client.RoomEvent.Disconnected, () => {
860
+ this.onDisconnected?.();
861
+ });
862
+ room.on(import_livekit_client.RoomEvent.Reconnecting, () => {
863
+ this.onReconnecting?.();
864
+ });
865
+ room.on(import_livekit_client.RoomEvent.Reconnected, () => {
866
+ this.onReconnected?.();
867
+ });
868
+ room.on(
869
+ import_livekit_client.RoomEvent.TrackSubscribed,
870
+ (track, _, participant) => {
871
+ console.log("Track subscribed:", participant.identity);
872
+ if (track.kind === import_livekit_client.Track.Kind.Audio) {
873
+ const audioElement = attachAudioTrack(
874
+ track,
875
+ participant.identity,
876
+ this.audioContainer
877
+ );
878
+ audioElement.muted = this.speakerMuted;
879
+ audioElement.volume = this.speakerMuted ? 0 : 1;
880
+ if (!this.speakerMuted) {
881
+ audioElement.play().catch(console.error);
882
+ }
883
+ }
884
+ }
885
+ );
886
+ room.on(
887
+ import_livekit_client.RoomEvent.TrackUnsubscribed,
888
+ (track) => {
889
+ if (track.kind === import_livekit_client.Track.Kind.Audio) {
890
+ detachAudioTrack(track);
891
+ }
892
+ }
893
+ );
894
+ room.on(
895
+ import_livekit_client.RoomEvent.ConnectionQualityChanged,
896
+ (quality, participant) => {
897
+ if (participant === room.localParticipant) {
898
+ this.onQuality?.(quality);
899
+ }
900
+ }
901
+ );
902
+ room.on(
903
+ import_livekit_client.RoomEvent.ParticipantConnected,
904
+ (participant) => {
905
+ if (participant.kind === import_livekit_client.ParticipantKind.AGENT) {
906
+ this.onAgentState?.(
907
+ "listening"
908
+ );
909
+ }
910
+ }
911
+ );
912
+ room.on(
913
+ import_livekit_client.RoomEvent.TranscriptionReceived,
914
+ (segments, participant) => {
915
+ for (const segment of segments) {
916
+ const isAgent = participant?.kind === import_livekit_client.ParticipantKind.AGENT;
917
+ this.onMessage?.({
918
+ id: segment.id,
919
+ type: isAgent ? "assistant_voice" : "user_voice",
920
+ text: segment.text,
921
+ final: segment.final,
922
+ timestamp: Date.now()
923
+ });
924
+ }
925
+ }
926
+ );
927
+ room.on(
928
+ import_livekit_client.RoomEvent.DataReceived,
929
+ (payload, participant, kind, topic) => {
930
+ try {
931
+ const decoded = JSON.parse(new TextDecoder().decode(payload));
932
+ console.log(
933
+ "[DATA_RECEIVED]",
934
+ {
935
+ topic,
936
+ participant: participant?.identity,
937
+ raw: decoded
938
+ }
939
+ );
940
+ if (topic === this.VOICE_ACTION_TOPIC) {
941
+ const event = decoded;
942
+ this.onVoiceAction?.(event);
943
+ this.handleVoiceAction(event);
944
+ return;
945
+ }
946
+ if (topic === this.VOICE_ACTION_RESULT_TOPIC) {
947
+ this.onVoiceActionResult?.(
948
+ decoded
949
+ );
950
+ return;
951
+ }
952
+ if (decoded.type === "agent_state") {
953
+ this.onAgentState?.(
954
+ decoded.state
955
+ );
956
+ }
957
+ if (decoded.type === "assistant_text") {
958
+ this.onMessage?.({
959
+ id: decoded.id,
960
+ type: "assistant_text",
961
+ text: decoded.text,
962
+ final: true,
963
+ timestamp: Date.now()
964
+ });
965
+ }
966
+ } catch (err) {
967
+ console.error(err);
968
+ }
969
+ }
970
+ );
971
+ await room.connect(url, token);
972
+ }
973
+ /**
974
+ * --------------------------------------------------------------------------
975
+ * Disconnect
976
+ * --------------------------------------------------------------------------
977
+ */
978
+ async disconnect() {
979
+ if (!this.room) return;
980
+ try {
981
+ await this.room.localParticipant.setMicrophoneEnabled(
982
+ false
983
+ );
984
+ } catch {
985
+ }
986
+ await this.room.disconnect();
987
+ this.room = null;
988
+ }
989
+ /**
990
+ * --------------------------------------------------------------------------
991
+ * Toggle microphone
992
+ * --------------------------------------------------------------------------
993
+ */
994
+ async toggleMic(enabled) {
995
+ if (!this.room) return;
996
+ await this.room.localParticipant.setMicrophoneEnabled(
997
+ enabled
998
+ );
999
+ this.onMicState?.(enabled);
1000
+ }
1001
+ setSpeakerMuted(muted) {
1002
+ this.speakerMuted = muted;
1003
+ const audioElements = this.audioContainer.querySelectorAll("audio");
1004
+ audioElements.forEach((audio) => {
1005
+ const el = audio;
1006
+ el.muted = muted;
1007
+ el.volume = muted ? 0 : 1;
1008
+ if (!muted) {
1009
+ el.play().catch(console.error);
1010
+ }
1011
+ });
1012
+ }
1013
+ /**
1014
+ * --------------------------------------------------------------------------
1015
+ * Send text message
1016
+ * --------------------------------------------------------------------------
1017
+ */
1018
+ async sendText(text) {
1019
+ if (!this.room) return;
1020
+ console.log("[sendText] called:", text);
1021
+ const lp = this.room.localParticipant;
1022
+ try {
1023
+ if (typeof lp.sendText === "function") {
1024
+ await lp.sendText(text, {
1025
+ topic: "lk.chat"
1026
+ });
1027
+ console.log(
1028
+ "[sendText] sent via LiveKit TextStream"
1029
+ );
1030
+ } else {
1031
+ await lp.publishData(
1032
+ new TextEncoder().encode(
1033
+ JSON.stringify({
1034
+ message: text
1035
+ })
1036
+ ),
1037
+ {
1038
+ reliable: true,
1039
+ topic: "lk.chat"
1040
+ }
1041
+ );
1042
+ console.log(
1043
+ "[sendText] sent via publishData fallback"
1044
+ );
1045
+ }
1046
+ this.onMessage?.({
1047
+ id: crypto.randomUUID(),
1048
+ type: "user_text",
1049
+ text,
1050
+ final: true,
1051
+ timestamp: Date.now()
1052
+ });
1053
+ } catch (err) {
1054
+ console.error(
1055
+ "[sendText] failed",
1056
+ err
1057
+ );
1058
+ }
1059
+ }
1060
+ async publishActionResult(result) {
1061
+ if (!this.room) return;
1062
+ console.log(
1063
+ "[publishActionResult] Sending result:",
1064
+ JSON.stringify(result, null, 2)
1065
+ );
1066
+ await this.room.localParticipant.publishData(
1067
+ new TextEncoder().encode(
1068
+ JSON.stringify(result)
1069
+ ),
1070
+ {
1071
+ reliable: true,
1072
+ topic: this.VOICE_ACTION_RESULT_TOPIC
1073
+ }
1074
+ );
1075
+ }
1076
+ };
1077
+
1078
+ // src/hooks/useTranscriptSync.ts
1079
+ var import_react2 = require("react");
1080
+ function useTranscriptSync(options) {
1081
+ const {
1082
+ maxMessages = 500,
1083
+ enableGrouping = true
1084
+ } = options || {};
1085
+ const [messages, setMessages] = (0, import_react2.useState)([]);
1086
+ const messageMapRef = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
1087
+ const commitMessages = (0, import_react2.useCallback)(() => {
1088
+ const merged = Array.from(
1089
+ messageMapRef.current.values()
1090
+ ).sort((a, b) => a.timestamp - b.timestamp).slice(-maxMessages);
1091
+ setMessages(merged);
1092
+ }, [maxMessages]);
1093
+ const mergeAdjacentPartials = (0, import_react2.useCallback)(
1094
+ (existing, incoming) => {
1095
+ if (!enableGrouping) return incoming;
1096
+ if (existing.type === incoming.type && !existing.final && !incoming.final) {
1097
+ return {
1098
+ ...existing,
1099
+ text: incoming.text,
1100
+ timestamp: incoming.timestamp
1101
+ };
1102
+ }
1103
+ return incoming;
1104
+ },
1105
+ [enableGrouping]
1106
+ );
1107
+ const addTranscriptSegment = (0, import_react2.useCallback)(
1108
+ (segment, type) => {
1109
+ const timestamp = segment.timestamp || Date.now();
1110
+ const incomingMessage = {
1111
+ id: segment.id,
1112
+ type,
1113
+ text: segment.text,
1114
+ final: segment.final,
1115
+ timestamp
1116
+ };
1117
+ const existing = messageMapRef.current.get(segment.id);
1118
+ if (existing) {
1119
+ const merged = mergeAdjacentPartials(
1120
+ existing,
1121
+ incomingMessage
1122
+ );
1123
+ if (existing.final) {
1124
+ return;
1125
+ }
1126
+ messageMapRef.current.set(segment.id, {
1127
+ ...merged,
1128
+ final: segment.final
1129
+ });
1130
+ commitMessages();
1131
+ return;
1132
+ }
1133
+ messageMapRef.current.set(
1134
+ segment.id,
1135
+ incomingMessage
1136
+ );
1137
+ commitMessages();
1138
+ },
1139
+ [commitMessages, mergeAdjacentPartials]
1140
+ );
1141
+ const addTextMessage = (0, import_react2.useCallback)(
1142
+ (text, type) => {
1143
+ const id = crypto.randomUUID();
1144
+ const message = {
1145
+ id,
1146
+ type,
1147
+ text,
1148
+ final: true,
1149
+ timestamp: Date.now()
1150
+ };
1151
+ messageMapRef.current.set(id, message);
1152
+ commitMessages();
1153
+ },
1154
+ [commitMessages]
1155
+ );
1156
+ const updateMessage = (0, import_react2.useCallback)(
1157
+ (id, text, final = true) => {
1158
+ const existing = messageMapRef.current.get(id);
1159
+ if (!existing) return;
1160
+ messageMapRef.current.set(id, {
1161
+ ...existing,
1162
+ text,
1163
+ final,
1164
+ timestamp: Date.now()
1165
+ });
1166
+ commitMessages();
1167
+ },
1168
+ [commitMessages]
1169
+ );
1170
+ const removeMessage = (0, import_react2.useCallback)(
1171
+ (id) => {
1172
+ messageMapRef.current.delete(id);
1173
+ commitMessages();
1174
+ },
1175
+ [commitMessages]
1176
+ );
1177
+ const clearMessages = (0, import_react2.useCallback)(() => {
1178
+ messageMapRef.current.clear();
1179
+ setMessages([]);
1180
+ }, []);
1181
+ const getLatestMessage = (0, import_react2.useCallback)(() => {
1182
+ if (!messages.length) return null;
1183
+ return messages[messages.length - 1];
1184
+ }, [messages]);
1185
+ const hasPendingMessages = (0, import_react2.useMemo)(() => {
1186
+ return messages.some((m) => !m.final);
1187
+ }, [messages]);
1188
+ return {
1189
+ messages,
1190
+ addTranscriptSegment,
1191
+ addTextMessage,
1192
+ updateMessage,
1193
+ removeMessage,
1194
+ clearMessages,
1195
+ getLatestMessage,
1196
+ hasPendingMessages
1197
+ };
1198
+ }
1199
+
1200
+ // src/providers/LiveKitProvider.tsx
1201
+ var import_jsx_runtime2 = require("react/jsx-runtime");
1202
+ var VoiceAssistantContext = (0, import_react3.createContext)(
1203
+ null
1204
+ );
1205
+ function LiveKitProvider({
1206
+ children,
1207
+ config
1208
+ }) {
1209
+ const audioContainerRef = (0, import_react3.useRef)(null);
1210
+ const serviceRef = (0, import_react3.useRef)(null);
1211
+ const [connectionState, setConnectionState] = (0, import_react3.useState)(
1212
+ import_livekit_client2.ConnectionState.Disconnected
1213
+ );
1214
+ const [connected, setConnected] = (0, import_react3.useState)(false);
1215
+ const [connecting, setConnecting] = (0, import_react3.useState)(false);
1216
+ const [reconnecting, setReconnecting] = (0, import_react3.useState)(false);
1217
+ const [micEnabled, setMicEnabled] = (0, import_react3.useState)(false);
1218
+ const [speakerEnabled, setSpeakerEnabled] = (0, import_react3.useState)(false);
1219
+ const [agentState, setAgentState] = (0, import_react3.useState)("idle");
1220
+ const [quality, setQuality] = (0, import_react3.useState)(
1221
+ null
1222
+ );
1223
+ const [error, setError] = (0, import_react3.useState)(null);
1224
+ const {
1225
+ messages,
1226
+ addTranscriptSegment,
1227
+ addTextMessage,
1228
+ clearMessages
1229
+ } = useTranscriptSync({
1230
+ maxMessages: 1e3,
1231
+ enableGrouping: true
1232
+ });
1233
+ (0, import_react3.useEffect)(() => {
1234
+ if (!audioContainerRef.current) return;
1235
+ const service = new LiveKitService(
1236
+ audioContainerRef.current,
1237
+ config?.customParameters?.user,
1238
+ config?.customParameters?.context,
1239
+ config.mode
1240
+ );
1241
+ serviceRef.current = service;
1242
+ const registerConfiguredHandlers = () => {
1243
+ const handlers = config.actionHandlers;
1244
+ if (!handlers) return;
1245
+ Object.entries(handlers).forEach(
1246
+ ([eventId, handler]) => {
1247
+ service.registerEventHandler(
1248
+ eventId,
1249
+ handler
1250
+ );
1251
+ }
1252
+ );
1253
+ };
1254
+ registerConfiguredHandlers();
1255
+ service.onConnected = () => {
1256
+ setConnected(true);
1257
+ setConnecting(false);
1258
+ setReconnecting(false);
1259
+ setConnectionState(
1260
+ import_livekit_client2.ConnectionState.Connected
1261
+ );
1262
+ setSpeakerEnabled(false);
1263
+ setError(null);
1264
+ };
1265
+ service.onDisconnected = () => {
1266
+ setConnected(false);
1267
+ setConnecting(false);
1268
+ setReconnecting(false);
1269
+ setMicEnabled(false);
1270
+ setAgentState("idle");
1271
+ setConnectionState(
1272
+ import_livekit_client2.ConnectionState.Disconnected
1273
+ );
1274
+ };
1275
+ service.onReconnecting = () => {
1276
+ setReconnecting(true);
1277
+ setConnectionState(
1278
+ import_livekit_client2.ConnectionState.Reconnecting
1279
+ );
1280
+ };
1281
+ service.onReconnected = () => {
1282
+ setReconnecting(false);
1283
+ setConnectionState(
1284
+ import_livekit_client2.ConnectionState.Connected
1285
+ );
1286
+ };
1287
+ service.onMicState = (enabled) => {
1288
+ setMicEnabled(enabled);
1289
+ };
1290
+ service.onAgentState = (state) => {
1291
+ setAgentState(state);
1292
+ };
1293
+ service.onQuality = (q) => {
1294
+ setQuality(q);
1295
+ };
1296
+ service.onError = (err) => {
1297
+ console.error("LiveKit Error:", err);
1298
+ setError(err);
1299
+ setConnecting(false);
1300
+ setConnected(false);
1301
+ };
1302
+ service.onMessage = (message) => {
1303
+ if (message.type === "user_voice" || message.type === "assistant_voice") {
1304
+ addTranscriptSegment(
1305
+ {
1306
+ id: message.id,
1307
+ text: message.text,
1308
+ final: message.final ?? false,
1309
+ timestamp: message.timestamp
1310
+ },
1311
+ message.type
1312
+ );
1313
+ return;
1314
+ }
1315
+ addTextMessage(
1316
+ message.text,
1317
+ message.type
1318
+ );
1319
+ };
1320
+ return () => {
1321
+ void service.disconnect();
1322
+ serviceRef.current = null;
1323
+ };
1324
+ }, [
1325
+ addTranscriptSegment,
1326
+ addTextMessage,
1327
+ config.actionHandlers
1328
+ ]);
1329
+ (0, import_react3.useEffect)(() => {
1330
+ const container = audioContainerRef.current;
1331
+ if (!container) return;
1332
+ const audioElements = container.querySelectorAll("audio");
1333
+ audioElements.forEach((audio) => {
1334
+ const element = audio;
1335
+ element.muted = !speakerEnabled;
1336
+ element.volume = speakerEnabled ? 1 : 0;
1337
+ });
1338
+ }, [speakerEnabled]);
1339
+ const connect = (0, import_react3.useCallback)(async () => {
1340
+ if (!serviceRef.current) return;
1341
+ if (connected || connecting || reconnecting) {
1342
+ return;
1343
+ }
1344
+ try {
1345
+ setConnecting(true);
1346
+ setError(null);
1347
+ const session = await fetchSession(config);
1348
+ await serviceRef.current.connect(
1349
+ session.url,
1350
+ session.token
1351
+ );
1352
+ } catch (err) {
1353
+ console.error(err);
1354
+ let userMessage = err?.message || "Failed to connect";
1355
+ if (/permission|microphone|notallowed/i.test(
1356
+ userMessage
1357
+ )) {
1358
+ userMessage = "Microphone access denied.";
1359
+ }
1360
+ if (/jwt|token|expired/i.test(userMessage)) {
1361
+ userMessage = "Session expired. Please refresh.";
1362
+ }
1363
+ if (/network/i.test(userMessage)) {
1364
+ userMessage = "Network connection failed.";
1365
+ }
1366
+ setError(userMessage);
1367
+ setConnecting(false);
1368
+ setConnected(false);
1369
+ }
1370
+ }, [
1371
+ connected,
1372
+ connecting,
1373
+ reconnecting,
1374
+ config
1375
+ ]);
1376
+ const disconnect = (0, import_react3.useCallback)(async () => {
1377
+ try {
1378
+ await serviceRef.current?.disconnect();
1379
+ setConnected(false);
1380
+ setConnecting(false);
1381
+ setReconnecting(false);
1382
+ setMicEnabled(false);
1383
+ setAgentState("idle");
1384
+ setConnectionState(
1385
+ import_livekit_client2.ConnectionState.Disconnected
1386
+ );
1387
+ } catch (err) {
1388
+ console.error(err);
1389
+ }
1390
+ }, []);
1391
+ const toggleMic = (0, import_react3.useCallback)(async () => {
1392
+ try {
1393
+ const next = !micEnabled;
1394
+ await serviceRef.current?.toggleMic(
1395
+ next
1396
+ );
1397
+ console.log("LIVEKIT MIC ENABLED:", next);
1398
+ setMicEnabled(next);
1399
+ } catch (err) {
1400
+ console.error(err);
1401
+ setError(
1402
+ err?.message || "Failed to toggle microphone"
1403
+ );
1404
+ }
1405
+ }, [micEnabled]);
1406
+ const setSpeakerMuted = (0, import_react3.useCallback)(
1407
+ (muted) => {
1408
+ console.log("LIVEKIT SPEAKER MUTED:", muted);
1409
+ serviceRef.current?.setSpeakerMuted(
1410
+ muted
1411
+ );
1412
+ setSpeakerEnabled(!muted);
1413
+ },
1414
+ []
1415
+ );
1416
+ const sendText = (0, import_react3.useCallback)(
1417
+ async (text) => {
1418
+ if (!text.trim()) return;
1419
+ try {
1420
+ await serviceRef.current?.sendText(
1421
+ text
1422
+ );
1423
+ } catch (err) {
1424
+ console.error(err);
1425
+ setError(
1426
+ err?.message || "Failed to send message"
1427
+ );
1428
+ }
1429
+ },
1430
+ []
1431
+ );
1432
+ const resetConversation = (0, import_react3.useCallback)(() => {
1433
+ clearMessages();
1434
+ }, [clearMessages]);
1435
+ const registerEventHandler = (0, import_react3.useCallback)(
1436
+ (eventId, handler) => {
1437
+ serviceRef.current?.registerEventHandler(
1438
+ eventId,
1439
+ handler
1440
+ );
1441
+ },
1442
+ []
1443
+ );
1444
+ const onVoiceAction = (0, import_react3.useCallback)(
1445
+ (callback) => {
1446
+ return serviceRef.current?.addVoiceActionListener(
1447
+ callback
1448
+ ) || (() => {
1449
+ });
1450
+ },
1451
+ []
1452
+ );
1453
+ const onVoiceActionResult = (0, import_react3.useCallback)(
1454
+ (callback) => {
1455
+ if (!serviceRef.current) {
1456
+ return () => {
1457
+ };
1458
+ }
1459
+ serviceRef.current.onVoiceActionResult = callback;
1460
+ return () => {
1461
+ if (serviceRef.current?.onVoiceActionResult === callback) {
1462
+ serviceRef.current.onVoiceActionResult = void 0;
1463
+ }
1464
+ };
1465
+ },
1466
+ []
1467
+ );
1468
+ const value = (0, import_react3.useMemo)(
1469
+ () => ({
1470
+ connected,
1471
+ connecting,
1472
+ reconnecting,
1473
+ micEnabled,
1474
+ speakerEnabled,
1475
+ messages,
1476
+ agentState,
1477
+ quality,
1478
+ error,
1479
+ connect,
1480
+ disconnect,
1481
+ toggleMic,
1482
+ setSpeakerMuted,
1483
+ sendText,
1484
+ registerEventHandler,
1485
+ onVoiceAction,
1486
+ onVoiceActionResult,
1487
+ connectionState,
1488
+ clearConversation: resetConversation
1489
+ }),
1490
+ [
1491
+ connected,
1492
+ connecting,
1493
+ reconnecting,
1494
+ micEnabled,
1495
+ speakerEnabled,
1496
+ messages,
1497
+ agentState,
1498
+ quality,
1499
+ error,
1500
+ connect,
1501
+ disconnect,
1502
+ toggleMic,
1503
+ setSpeakerMuted,
1504
+ sendText,
1505
+ registerEventHandler,
1506
+ onVoiceAction,
1507
+ onVoiceActionResult,
1508
+ connectionState,
1509
+ resetConversation
1510
+ ]
1511
+ );
1512
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1513
+ VoiceAssistantContext.Provider,
1514
+ {
1515
+ value,
1516
+ children: [
1517
+ children,
1518
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1519
+ "div",
1520
+ {
1521
+ ref: audioContainerRef,
1522
+ style: {
1523
+ position: "absolute",
1524
+ width: 1,
1525
+ height: 1,
1526
+ overflow: "hidden",
1527
+ opacity: 0,
1528
+ pointerEvents: "none"
1529
+ }
1530
+ }
1531
+ )
1532
+ ]
1533
+ }
1534
+ );
1535
+ }
1536
+ function useVoiceAssistant() {
1537
+ const context = (0, import_react3.useContext)(
1538
+ VoiceAssistantContext
1539
+ );
1540
+ if (!context) {
1541
+ throw new Error(
1542
+ "useVoiceAssistant must be used inside LiveKitProvider"
1543
+ );
1544
+ }
1545
+ return context;
1546
+ }
1547
+
1548
+ // src/components/FloatingMic.tsx
1549
+ var import_jsx_runtime3 = require("react/jsx-runtime");
1550
+ function FloatingMicInner({
1551
+ textEnabled = false
1552
+ }) {
1553
+ const {
1554
+ connected,
1555
+ connecting,
1556
+ reconnecting,
1557
+ micEnabled,
1558
+ messages,
1559
+ agentState,
1560
+ connect,
1561
+ disconnect,
1562
+ toggleMic,
1563
+ setSpeakerMuted: setLiveKitSpeakerMuted,
1564
+ sendText,
1565
+ connectionState
1566
+ } = useVoiceAssistant();
1567
+ const [showVoicePanel, setShowVoicePanel] = (0, import_react4.useState)(false);
1568
+ const [micMuted, setMicMuted] = (0, import_react4.useState)(false);
1569
+ const [speakerMuted, setSpeakerMuted] = (0, import_react4.useState)(true);
1570
+ (0, import_react4.useEffect)(() => {
1571
+ const desired = !micMuted;
1572
+ if (desired !== micEnabled) {
1573
+ toggleMic();
1574
+ }
1575
+ }, [micMuted]);
1576
+ (0, import_react4.useEffect)(() => {
1577
+ setLiveKitSpeakerMuted(speakerMuted);
1578
+ }, [
1579
+ speakerMuted,
1580
+ setLiveKitSpeakerMuted
1581
+ ]);
1582
+ const enableVoice = async () => {
1583
+ if (connecting || connected) {
1584
+ return;
1585
+ }
1586
+ try {
1587
+ await connect();
1588
+ setShowVoicePanel(
1589
+ true
1590
+ );
1591
+ } catch (err) {
1592
+ console.error(err);
1593
+ }
1594
+ };
1595
+ const disableVoice = async () => {
1596
+ await disconnect();
1597
+ setShowVoicePanel(
1598
+ false
1599
+ );
1600
+ setMicMuted(false);
1601
+ setSpeakerMuted(true);
1602
+ };
1603
+ const toggleVoicePanel = () => {
1604
+ if (!connected) {
1605
+ enableVoice();
1606
+ return;
1607
+ }
1608
+ setShowVoicePanel(
1609
+ (prev) => !prev
1610
+ );
1611
+ };
1612
+ const buttonState = (0, import_react4.useMemo)(() => {
1613
+ if (reconnecting) {
1614
+ return "reconnecting";
1615
+ }
1616
+ if (connecting) {
1617
+ return "connecting";
1618
+ }
1619
+ if (connected) {
1620
+ return "connected";
1621
+ }
1622
+ return "idle";
1623
+ }, [
1624
+ connected,
1625
+ connecting,
1626
+ reconnecting
1627
+ ]);
1628
+ const tooltip = (() => {
1629
+ switch (buttonState) {
1630
+ case "connecting":
1631
+ return "Connecting...";
1632
+ case "reconnecting":
1633
+ return "Reconnecting...";
1634
+ case "connected":
1635
+ return "Voice connected";
1636
+ default:
1637
+ return "Click to enable voice";
1638
+ }
1639
+ })();
1640
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
1641
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1642
+ "button",
1643
+ {
1644
+ onClick: toggleVoicePanel,
1645
+ disabled: connecting,
1646
+ className: "fixed bottom-6 right-6 z-[1000] w-14 h-14 rounded-full bg-gradient-to-br from-blue-500 to-blue-700 text-white flex items-center justify-center shadow-lg transition-all duration-300 hover:scale-110 disabled:opacity-50 disabled:cursor-not-allowed",
1647
+ title: tooltip,
1648
+ children: [
1649
+ connected && agentState === "listening" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "absolute -inset-2 rounded-full bg-green-500/25 animate-ping pointer-events-none" }),
1650
+ connected && agentState === "speaking" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "absolute -inset-2 rounded-full bg-blue-500/25 animate-ping pointer-events-none" }),
1651
+ connected && agentState === "thinking" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "absolute -inset-1 rounded-full bg-yellow-500/20 animate-pulse pointer-events-none" }),
1652
+ showVoicePanel && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "absolute inset-0 rounded-full ring-4 ring-blue-500/35 transition-all duration-300 pointer-events-none" }),
1653
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.Mic, { className: `w-5 h-5 transition-transform duration-300 ${connected ? "scale-110" : ""}` })
1654
+ ]
1655
+ }
1656
+ ),
1657
+ showVoicePanel && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1658
+ VoicePanel,
1659
+ {
1660
+ enabled: connected,
1661
+ connectionState,
1662
+ agentState,
1663
+ messages,
1664
+ participants: [],
1665
+ onDisable: disableVoice,
1666
+ textEnabled,
1667
+ micMuted,
1668
+ speakerMuted,
1669
+ onMicMutedChange: setMicMuted,
1670
+ onSpeakerMutedChange: setSpeakerMuted,
1671
+ onSendText: sendText
1672
+ }
1673
+ )
1674
+ ] });
1675
+ }
1676
+ function FloatingMic({
1677
+ config,
1678
+ textEnabled = false
1679
+ }) {
1680
+ console.log("FloatingMic Config", config);
1681
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1682
+ LiveKitProvider,
1683
+ {
1684
+ config: {
1685
+ ...config
1686
+ },
1687
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1688
+ FloatingMicInner,
1689
+ {
1690
+ textEnabled
1691
+ }
1692
+ )
1693
+ }
1694
+ );
1695
+ }
1696
+
1697
+ // src/index.ts
1698
+ var index_default = FloatingMic;
1699
+ // Annotate the CommonJS export names for ESM import in node:
1700
+ 0 && (module.exports = {
1701
+ FloatingMic,
1702
+ LiveKitProvider,
1703
+ VoicePanel
1704
+ });
1705
+ //# sourceMappingURL=index.js.map