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/README.md +166 -0
- package/dist/index.css +1092 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.mts +98 -0
- package/dist/index.d.ts +98 -0
- package/dist/index.js +1705 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1709 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +48 -0
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
|