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