@keyframelabs/elements 0.4.0 → 0.5.1
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 +3 -3
- package/dist/KflEmbedElement.d.ts +23 -1
- package/dist/agents/audio-utils.d.ts +5 -0
- package/dist/agents/index.d.ts +9 -5
- package/dist/agents/kfl.d.ts +26 -0
- package/dist/agents/openai-realtime.d.ts +0 -19
- package/dist/index.d.ts +2 -2
- package/dist/index.js +446 -171
- package/dist/kfl-embed.js +1890 -1620
- package/dist/types.d.ts +1 -4
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
import { createClient as
|
|
2
|
-
const
|
|
3
|
-
function
|
|
1
|
+
import { createClient as T } from "@keyframelabs/sdk";
|
|
2
|
+
const h = 24e3;
|
|
3
|
+
function _(s) {
|
|
4
4
|
const e = atob(s), t = new Uint8Array(e.length);
|
|
5
5
|
for (let i = 0; i < e.length; i++)
|
|
6
6
|
t[i] = e.charCodeAt(i);
|
|
7
7
|
return t;
|
|
8
8
|
}
|
|
9
|
-
function
|
|
9
|
+
function b(s) {
|
|
10
10
|
let e = "";
|
|
11
11
|
for (let t = 0; t < s.length; t++)
|
|
12
12
|
e += String.fromCharCode(s[t]);
|
|
13
13
|
return btoa(e);
|
|
14
14
|
}
|
|
15
|
-
function
|
|
15
|
+
function u(s, e, t) {
|
|
16
16
|
if (e === t)
|
|
17
17
|
return s;
|
|
18
|
-
const i = new Int16Array(s.buffer, s.byteOffset, s.length / 2), n = e / t, o = Math.floor(i.length / n),
|
|
19
|
-
for (let
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
i[
|
|
18
|
+
const i = new Int16Array(s.buffer, s.byteOffset, s.length / 2), n = e / t, o = Math.floor(i.length / n), c = new Int16Array(o);
|
|
19
|
+
for (let d = 0; d < o; d++) {
|
|
20
|
+
const w = d * n, p = Math.floor(w), O = Math.min(p + 1, i.length - 1), y = w - p;
|
|
21
|
+
c[d] = Math.round(
|
|
22
|
+
i[p] * (1 - y) + i[O] * y
|
|
23
23
|
);
|
|
24
24
|
}
|
|
25
|
-
return new Uint8Array(
|
|
25
|
+
return new Uint8Array(c.buffer);
|
|
26
26
|
}
|
|
27
|
-
function
|
|
27
|
+
function L() {
|
|
28
28
|
const s = /* @__PURE__ */ new Map();
|
|
29
29
|
return {
|
|
30
30
|
on(e, t) {
|
|
@@ -41,7 +41,7 @@ function x() {
|
|
|
41
41
|
}
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
|
-
function
|
|
44
|
+
function A(s) {
|
|
45
45
|
const e = new Int16Array(s.length);
|
|
46
46
|
for (let t = 0; t < s.length; t++) {
|
|
47
47
|
const i = Math.max(-1, Math.min(1, s[t]));
|
|
@@ -49,12 +49,49 @@ function E(s) {
|
|
|
49
49
|
}
|
|
50
50
|
return new Uint8Array(e.buffer);
|
|
51
51
|
}
|
|
52
|
-
const
|
|
53
|
-
class
|
|
52
|
+
const I = "pcm-capture", P = `
|
|
53
|
+
class PcmCaptureProcessor extends AudioWorkletProcessor {
|
|
54
|
+
constructor() {
|
|
55
|
+
super();
|
|
56
|
+
this._buf = new Float32Array(1024);
|
|
57
|
+
this._pos = 0;
|
|
58
|
+
}
|
|
59
|
+
process(inputs) {
|
|
60
|
+
const ch = inputs[0]?.[0];
|
|
61
|
+
if (!ch) return true;
|
|
62
|
+
let i = 0;
|
|
63
|
+
while (i < ch.length) {
|
|
64
|
+
const n = Math.min(this._buf.length - this._pos, ch.length - i);
|
|
65
|
+
this._buf.set(ch.subarray(i, i + n), this._pos);
|
|
66
|
+
this._pos += n;
|
|
67
|
+
i += n;
|
|
68
|
+
if (this._pos >= this._buf.length) {
|
|
69
|
+
this.port.postMessage(this._buf);
|
|
70
|
+
this._buf = new Float32Array(1024);
|
|
71
|
+
this._pos = 0;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
registerProcessor('${I}', PcmCaptureProcessor);
|
|
78
|
+
`;
|
|
79
|
+
let g = null;
|
|
80
|
+
async function x(s) {
|
|
81
|
+
return g || (g = URL.createObjectURL(
|
|
82
|
+
new Blob([P], { type: "application/javascript" })
|
|
83
|
+
)), await s.audioWorklet.addModule(g), new AudioWorkletNode(s, I, {
|
|
84
|
+
numberOfInputs: 1,
|
|
85
|
+
numberOfOutputs: 1,
|
|
86
|
+
channelCount: 1
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
const M = 16e3;
|
|
90
|
+
class v {
|
|
54
91
|
ws = null;
|
|
55
92
|
_state = "idle";
|
|
56
|
-
events =
|
|
57
|
-
inputSampleRate =
|
|
93
|
+
events = L();
|
|
94
|
+
inputSampleRate = M;
|
|
58
95
|
/** Current agent state */
|
|
59
96
|
get state() {
|
|
60
97
|
return this._state;
|
|
@@ -113,8 +150,8 @@ class S {
|
|
|
113
150
|
this.events.emit("closed", { code: e, reason: t });
|
|
114
151
|
}
|
|
115
152
|
}
|
|
116
|
-
const
|
|
117
|
-
class
|
|
153
|
+
const N = ["neutral", "angry", "sad", "happy"], B = "wss://api.elevenlabs.io/v1/convai/conversation";
|
|
154
|
+
class F extends v {
|
|
118
155
|
agentName = "ElevenLabs";
|
|
119
156
|
outputSampleRate = 24e3;
|
|
120
157
|
// Default, updated from metadata
|
|
@@ -139,7 +176,7 @@ class M extends S {
|
|
|
139
176
|
throw new Error("ElevenLabs agent ID or signed URL is required");
|
|
140
177
|
e.inputSampleRate && (this.sourceInputSampleRate = e.inputSampleRate);
|
|
141
178
|
let t;
|
|
142
|
-
return e.signedUrl ? t = e.signedUrl : (t = `${
|
|
179
|
+
return e.signedUrl ? t = e.signedUrl : (t = `${B}?agent_id=${e.agentId}`, e.apiKey && (t += `&xi-api-key=${e.apiKey}`)), new Promise((i, n) => {
|
|
143
180
|
this.ws = new WebSocket(t), this.ws.onopen = () => {
|
|
144
181
|
this.setState("listening"), i();
|
|
145
182
|
}, this.ws.onerror = () => {
|
|
@@ -205,9 +242,9 @@ class M extends S {
|
|
|
205
242
|
if (!t?.audio_base_64 || (t.event_id ?? 0) <= this.lastInterruptId)
|
|
206
243
|
return;
|
|
207
244
|
this._state !== "speaking" && (this.events.emit("turnStart", void 0), this.setState("speaking"));
|
|
208
|
-
let n =
|
|
209
|
-
this.outputSampleRate !==
|
|
210
|
-
const o = n.length / 2 /
|
|
245
|
+
let n = _(t.audio_base_64);
|
|
246
|
+
this.outputSampleRate !== h && (n = u(n, this.outputSampleRate, h)), this.events.emit("audio", n);
|
|
247
|
+
const o = n.length / 2 / h * 1e3;
|
|
211
248
|
this.turnStartTime === 0 && (this.turnStartTime = Date.now()), this.accumulatedDurationMs += o, console.debug(
|
|
212
249
|
`[ElevenLabs] audio chunk: ${n.length} bytes, +${o.toFixed(0)}ms, totalDuration=${this.accumulatedDurationMs.toFixed(0)}ms, agentResponse=${this.agentResponseReceived}`
|
|
213
250
|
), this.scheduleVirtualBufferCheck();
|
|
@@ -253,7 +290,7 @@ class M extends S {
|
|
|
253
290
|
if (t) {
|
|
254
291
|
if (t.tool_name === "set_emotion") {
|
|
255
292
|
const i = t.parameters?.emotion?.toLowerCase();
|
|
256
|
-
i &&
|
|
293
|
+
i && N.includes(i) && this.events.emit("emotion", i);
|
|
257
294
|
}
|
|
258
295
|
this.ws && this.ws.readyState === WebSocket.OPEN && this.ws.send(JSON.stringify({
|
|
259
296
|
type: "client_tool_result",
|
|
@@ -273,8 +310,8 @@ class M extends S {
|
|
|
273
310
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !this.initialized)
|
|
274
311
|
return;
|
|
275
312
|
let t = e;
|
|
276
|
-
this.sourceInputSampleRate !== this.expectedInputSampleRate && (t =
|
|
277
|
-
user_audio_chunk:
|
|
313
|
+
this.sourceInputSampleRate !== this.expectedInputSampleRate && (t = u(e, this.sourceInputSampleRate, this.expectedInputSampleRate)), this.ws.send(JSON.stringify({
|
|
314
|
+
user_audio_chunk: b(t)
|
|
278
315
|
}));
|
|
279
316
|
}
|
|
280
317
|
/**
|
|
@@ -307,7 +344,7 @@ class M extends S {
|
|
|
307
344
|
this.initialized = !1, this.lastInterruptId = 0, this.resetTurnState(), super.close();
|
|
308
345
|
}
|
|
309
346
|
}
|
|
310
|
-
const
|
|
347
|
+
const j = ["neutral", "angry", "sad", "happy"], U = "wss://api.openai.com/v1/realtime", D = "gpt-realtime", m = 24e3, z = {
|
|
311
348
|
type: "function",
|
|
312
349
|
name: "set_emotion",
|
|
313
350
|
description: "Set the emotional expression of the avatar. Call this on every turn to reflect the tone of your response.",
|
|
@@ -323,7 +360,7 @@ const R = ["neutral", "angry", "sad", "happy"], O = "wss://api.openai.com/v1/rea
|
|
|
323
360
|
required: ["emotion"]
|
|
324
361
|
}
|
|
325
362
|
};
|
|
326
|
-
class
|
|
363
|
+
class $ extends v {
|
|
327
364
|
agentName = "OpenAIRealtime";
|
|
328
365
|
connectResolve = null;
|
|
329
366
|
connectReject = null;
|
|
@@ -341,20 +378,20 @@ class L extends S {
|
|
|
341
378
|
if (!e.apiKey)
|
|
342
379
|
throw new Error("OpenAI Realtime token is required");
|
|
343
380
|
e.inputSampleRate && (this.sourceInputSampleRate = e.inputSampleRate);
|
|
344
|
-
const t = e.model ??
|
|
345
|
-
return this.initialSessionUpdate = this.buildSessionUpdate(
|
|
381
|
+
const t = e.model ?? D;
|
|
382
|
+
return this.initialSessionUpdate = this.buildSessionUpdate(t), new Promise((i, n) => {
|
|
346
383
|
this.connectResolve = i, this.connectReject = n, this.connectTimeout = setTimeout(() => {
|
|
347
384
|
this.rejectPendingConnect(new Error("Timed out waiting for OpenAI Realtime session setup")), this.close();
|
|
348
385
|
}, 1e4), this.ws = new WebSocket(
|
|
349
|
-
`${
|
|
386
|
+
`${U}?model=${encodeURIComponent(t)}`,
|
|
350
387
|
["realtime", `openai-insecure-api-key.${e.apiKey}`]
|
|
351
388
|
), this.ws.onopen = () => {
|
|
352
389
|
}, this.ws.onerror = () => {
|
|
353
390
|
this.rejectPendingConnect(new Error("Failed to connect to OpenAI Realtime"));
|
|
354
391
|
}, this.ws.onclose = (o) => {
|
|
355
392
|
if (this.clearConnectTimeout(), this.connectReject) {
|
|
356
|
-
const
|
|
357
|
-
this.rejectPendingConnect(new Error(`OpenAI Realtime closed before initialization (${o.code}${
|
|
393
|
+
const c = o.reason ? `: ${o.reason}` : "";
|
|
394
|
+
this.rejectPendingConnect(new Error(`OpenAI Realtime closed before initialization (${o.code}${c})`));
|
|
358
395
|
}
|
|
359
396
|
this.resetTurnState(), this.initialSessionUpdate = null, this.ws = null, this.setState("idle"), this.emitClosed(o.code, o.reason);
|
|
360
397
|
}, this.ws.onmessage = (o) => {
|
|
@@ -385,7 +422,7 @@ class L extends S {
|
|
|
385
422
|
}
|
|
386
423
|
this.currentResponseHasAudio = !0, this.events.emit("turnStart", void 0), this.setState("speaking");
|
|
387
424
|
}
|
|
388
|
-
this.events.emit("audio",
|
|
425
|
+
this.events.emit("audio", _(t.delta));
|
|
389
426
|
break;
|
|
390
427
|
case "response.output_audio_transcript.delta":
|
|
391
428
|
if (!t.delta)
|
|
@@ -424,40 +461,36 @@ class L extends S {
|
|
|
424
461
|
return;
|
|
425
462
|
}
|
|
426
463
|
let t = e;
|
|
427
|
-
this.sourceInputSampleRate !==
|
|
464
|
+
this.sourceInputSampleRate !== m && (t = u(e, this.sourceInputSampleRate, m)), this.ws.send(JSON.stringify({
|
|
428
465
|
type: "input_audio_buffer.append",
|
|
429
|
-
audio:
|
|
466
|
+
audio: b(t)
|
|
430
467
|
}));
|
|
431
468
|
}
|
|
432
469
|
close() {
|
|
433
470
|
this.rejectPendingConnect(new Error("Connection closed")), this.clearConnectTimeout(), this.resetTurnState(), this.initialSessionUpdate = null, this.handledFunctionCallIds.clear(), super.close();
|
|
434
471
|
}
|
|
435
|
-
buildSessionUpdate(e
|
|
436
|
-
const i = e.turnDetection ?? { type: "semantic_vad", eagerness: "high" };
|
|
472
|
+
buildSessionUpdate(e) {
|
|
437
473
|
return {
|
|
438
474
|
type: "session.update",
|
|
439
475
|
session: {
|
|
440
476
|
type: "realtime",
|
|
441
|
-
model:
|
|
477
|
+
model: e,
|
|
442
478
|
output_modalities: ["audio"],
|
|
443
|
-
instructions: e.systemPrompt,
|
|
444
479
|
audio: {
|
|
445
480
|
input: {
|
|
446
481
|
format: {
|
|
447
482
|
type: "audio/pcm",
|
|
448
|
-
rate:
|
|
449
|
-
}
|
|
450
|
-
turn_detection: i
|
|
483
|
+
rate: m
|
|
484
|
+
}
|
|
451
485
|
},
|
|
452
486
|
output: {
|
|
453
487
|
format: {
|
|
454
488
|
type: "audio/pcm",
|
|
455
|
-
rate:
|
|
456
|
-
}
|
|
457
|
-
...e.voice ? { voice: e.voice } : {}
|
|
489
|
+
rate: h
|
|
490
|
+
}
|
|
458
491
|
}
|
|
459
492
|
},
|
|
460
|
-
tools: [
|
|
493
|
+
tools: [z],
|
|
461
494
|
tool_choice: "auto"
|
|
462
495
|
}
|
|
463
496
|
};
|
|
@@ -470,7 +503,7 @@ class L extends S {
|
|
|
470
503
|
this.currentResponseHasAudio && this.finishAudioTurn();
|
|
471
504
|
return;
|
|
472
505
|
}
|
|
473
|
-
const t = e.output.filter(
|
|
506
|
+
const t = e.output.filter(H);
|
|
474
507
|
if (t.length > 0) {
|
|
475
508
|
this.handleFunctionCalls(t);
|
|
476
509
|
return;
|
|
@@ -503,7 +536,7 @@ class L extends S {
|
|
|
503
536
|
return { error: `Unsupported function: ${e.name}` };
|
|
504
537
|
try {
|
|
505
538
|
const i = (e.arguments ? JSON.parse(e.arguments) : {}).emotion?.toLowerCase();
|
|
506
|
-
return i &&
|
|
539
|
+
return i && j.includes(i) ? (this.events.emit("emotion", i), { result: "ok" }) : { error: "Invalid emotion" };
|
|
507
540
|
} catch {
|
|
508
541
|
return { error: "Invalid function arguments" };
|
|
509
542
|
}
|
|
@@ -533,27 +566,150 @@ class L extends S {
|
|
|
533
566
|
this.connectTimeout !== null && (clearTimeout(this.connectTimeout), this.connectTimeout = null);
|
|
534
567
|
}
|
|
535
568
|
}
|
|
536
|
-
function
|
|
569
|
+
function H(s) {
|
|
537
570
|
return s.type === "function_call";
|
|
538
571
|
}
|
|
539
|
-
const
|
|
572
|
+
const k = 16e3, W = 1e4, V = 1e4, K = ["neutral", "angry", "sad", "happy"];
|
|
573
|
+
class J extends v {
|
|
574
|
+
agentName = "KflVoiceAgent";
|
|
575
|
+
connectResolve = null;
|
|
576
|
+
connectReject = null;
|
|
577
|
+
connectTimeout = null;
|
|
578
|
+
pingInterval = null;
|
|
579
|
+
sourceInputSampleRate = M;
|
|
580
|
+
turnEnded = !1;
|
|
581
|
+
async connect(e) {
|
|
582
|
+
if (this.ws)
|
|
583
|
+
throw new Error("Already connected");
|
|
584
|
+
if (!e.wsUrl)
|
|
585
|
+
throw new Error("KFL voice agent wsUrl is required");
|
|
586
|
+
return e.inputSampleRate && (this.sourceInputSampleRate = e.inputSampleRate), new Promise((t, i) => {
|
|
587
|
+
this.connectResolve = t, this.connectReject = i, this.connectTimeout = setTimeout(() => {
|
|
588
|
+
this.rejectPendingConnect(new Error("Timed out waiting for kfl-voice-agent session.ready")), this.close();
|
|
589
|
+
}, V), this.ws = new WebSocket(e.wsUrl), this.ws.onopen = () => {
|
|
590
|
+
this.startPings(e.pingIntervalMs);
|
|
591
|
+
}, this.ws.onerror = () => {
|
|
592
|
+
this.rejectPendingConnect(new Error("Failed to connect to kfl-voice-agent"));
|
|
593
|
+
}, this.ws.onclose = (n) => {
|
|
594
|
+
if (this.stopPings(), this.connectReject) {
|
|
595
|
+
const o = n.reason ? `: ${n.reason}` : "";
|
|
596
|
+
this.rejectPendingConnect(
|
|
597
|
+
new Error(`kfl-voice-agent closed before initialization (${n.code}${o})`)
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
this.ws = null, this.setState("idle"), this.emitClosed(n.code, n.reason);
|
|
601
|
+
}, this.ws.onmessage = (n) => {
|
|
602
|
+
this.handleMessage(n.data);
|
|
603
|
+
};
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
sendAudio(e) {
|
|
607
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
608
|
+
console.warn("[KflVoiceAgent] Cannot send audio: not connected");
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
let t = e;
|
|
612
|
+
this.sourceInputSampleRate !== k && (t = u(e, this.sourceInputSampleRate, k)), this.ws.send(JSON.stringify({
|
|
613
|
+
type: "input_audio_buffer.append",
|
|
614
|
+
audio: b(t)
|
|
615
|
+
}));
|
|
616
|
+
}
|
|
617
|
+
close() {
|
|
618
|
+
this.rejectPendingConnect(new Error("Connection closed")), this.clearConnectTimeout(), this.stopPings(), super.close();
|
|
619
|
+
}
|
|
620
|
+
handleParsedMessage(e) {
|
|
621
|
+
const t = e, i = t.type;
|
|
622
|
+
if (i)
|
|
623
|
+
switch (i) {
|
|
624
|
+
case "session.ready":
|
|
625
|
+
this.setState("listening"), this.resolvePendingConnect();
|
|
626
|
+
break;
|
|
627
|
+
case "session.pong":
|
|
628
|
+
break;
|
|
629
|
+
case "turn_start":
|
|
630
|
+
this.turnEnded = !1, this.events.emit("turnStart", void 0), this.setState("speaking");
|
|
631
|
+
break;
|
|
632
|
+
case "output_audio.chunk":
|
|
633
|
+
t.audio && this.events.emit("audio", _(t.audio));
|
|
634
|
+
break;
|
|
635
|
+
case "output_audio.done":
|
|
636
|
+
this.turnEnded || (this.turnEnded = !0, this.events.emit("turnEnd", void 0));
|
|
637
|
+
break;
|
|
638
|
+
case "playback_done":
|
|
639
|
+
this.turnEnded || (this.turnEnded = !0, this.events.emit("turnEnd", void 0)), this.setState("listening");
|
|
640
|
+
break;
|
|
641
|
+
case "interrupted":
|
|
642
|
+
this.turnEnded = !0, this.events.emit("interrupted", void 0), this.setState("listening");
|
|
643
|
+
break;
|
|
644
|
+
case "user_transcript":
|
|
645
|
+
t.text && this.events.emit("transcript", {
|
|
646
|
+
role: "user",
|
|
647
|
+
text: t.text,
|
|
648
|
+
isFinal: !0
|
|
649
|
+
});
|
|
650
|
+
break;
|
|
651
|
+
case "assistant_done":
|
|
652
|
+
t.text && this.events.emit("transcript", {
|
|
653
|
+
role: "assistant",
|
|
654
|
+
text: t.text,
|
|
655
|
+
isFinal: !0
|
|
656
|
+
});
|
|
657
|
+
break;
|
|
658
|
+
case "emotion":
|
|
659
|
+
t.emotion && K.includes(t.emotion) && this.events.emit("emotion", t.emotion);
|
|
660
|
+
break;
|
|
661
|
+
case "error": {
|
|
662
|
+
const n = t.message ?? "Unknown kfl-voice-agent error";
|
|
663
|
+
this.rejectPendingConnect(new Error(n)), console.error("[KflVoiceAgent] Server error:", t);
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
startPings(e) {
|
|
669
|
+
this.stopPings();
|
|
670
|
+
const t = e ?? W;
|
|
671
|
+
this.pingInterval = setInterval(() => {
|
|
672
|
+
!this.ws || this.ws.readyState !== WebSocket.OPEN || this.ws.send(JSON.stringify({ type: "session.ping" }));
|
|
673
|
+
}, t);
|
|
674
|
+
}
|
|
675
|
+
stopPings() {
|
|
676
|
+
this.pingInterval && (clearInterval(this.pingInterval), this.pingInterval = null);
|
|
677
|
+
}
|
|
678
|
+
clearConnectTimeout() {
|
|
679
|
+
this.connectTimeout && (clearTimeout(this.connectTimeout), this.connectTimeout = null);
|
|
680
|
+
}
|
|
681
|
+
resolvePendingConnect() {
|
|
682
|
+
this.clearConnectTimeout();
|
|
683
|
+
const e = this.connectResolve;
|
|
684
|
+
this.connectResolve = null, this.connectReject = null, e?.();
|
|
685
|
+
}
|
|
686
|
+
rejectPendingConnect(e) {
|
|
687
|
+
this.clearConnectTimeout();
|
|
688
|
+
const t = this.connectReject;
|
|
689
|
+
this.connectResolve = null, this.connectReject = null, t?.(e);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
const q = [
|
|
540
693
|
{ id: "elevenlabs", name: "ElevenLabs", description: "ElevenLabs Conversational AI" },
|
|
541
|
-
{ id: "openai", name: "OpenAI Realtime", description: "OpenAI Realtime API" }
|
|
694
|
+
{ id: "openai", name: "OpenAI Realtime", description: "OpenAI Realtime API" },
|
|
695
|
+
{ id: "kfl", name: "KFL Voice Agent", description: "kfl-voice-agent WebSocket API" }
|
|
542
696
|
];
|
|
543
|
-
function
|
|
697
|
+
function R(s) {
|
|
544
698
|
switch (s) {
|
|
545
699
|
case "elevenlabs":
|
|
546
|
-
return new
|
|
700
|
+
return new F();
|
|
547
701
|
case "openai":
|
|
548
|
-
return new
|
|
702
|
+
return new $();
|
|
703
|
+
case "kfl":
|
|
704
|
+
return new J();
|
|
549
705
|
default:
|
|
550
706
|
throw new Error(`Unknown agent type: ${s}`);
|
|
551
707
|
}
|
|
552
708
|
}
|
|
553
|
-
function
|
|
554
|
-
return
|
|
709
|
+
function _e(s) {
|
|
710
|
+
return q.find((e) => e.id === s);
|
|
555
711
|
}
|
|
556
|
-
class
|
|
712
|
+
class G extends Error {
|
|
557
713
|
status;
|
|
558
714
|
payload;
|
|
559
715
|
url;
|
|
@@ -561,8 +717,8 @@ class F extends Error {
|
|
|
561
717
|
super(e.message), this.name = "ApiError", this.status = e.status, this.payload = e.payload, this.url = e.url;
|
|
562
718
|
}
|
|
563
719
|
}
|
|
564
|
-
const
|
|
565
|
-
class
|
|
720
|
+
const r = /* @__PURE__ */ new Set();
|
|
721
|
+
class Y {
|
|
566
722
|
apiBaseUrl;
|
|
567
723
|
publishableKey;
|
|
568
724
|
callbacks;
|
|
@@ -606,31 +762,31 @@ class P {
|
|
|
606
762
|
}
|
|
607
763
|
/** Connect to the embed session */
|
|
608
764
|
async connect() {
|
|
609
|
-
if (
|
|
765
|
+
if (r.has(this.publishableKey)) {
|
|
610
766
|
console.log("[PersonaEmbed] Connection already in progress, skipping");
|
|
611
767
|
return;
|
|
612
768
|
}
|
|
613
|
-
|
|
769
|
+
r.add(this.publishableKey), this.mounted = !0, this.abortController = new AbortController(), this.setStatus("connecting");
|
|
614
770
|
try {
|
|
615
771
|
const e = await this.fetchSession(this.abortController.signal);
|
|
616
772
|
if (!this.mounted) {
|
|
617
|
-
|
|
773
|
+
r.delete(this.publishableKey);
|
|
618
774
|
return;
|
|
619
775
|
}
|
|
620
776
|
if (await this.initSession(e), await this.initMicrophone(), await this.connectAgent(e.voice_agent_details), !this.mounted) {
|
|
621
|
-
this.cleanup(),
|
|
777
|
+
this.cleanup(), r.delete(this.publishableKey);
|
|
622
778
|
return;
|
|
623
779
|
}
|
|
624
780
|
this.setStatus("connected");
|
|
625
781
|
} catch (e) {
|
|
626
|
-
if (
|
|
782
|
+
if (r.delete(this.publishableKey), e instanceof Error && e.name === "AbortError")
|
|
627
783
|
return;
|
|
628
784
|
console.error("[PersonaEmbed]", e), this.mounted && (this.setStatus("error"), this.callbacks.onError?.(e));
|
|
629
785
|
}
|
|
630
786
|
}
|
|
631
787
|
/** Disconnect and cleanup */
|
|
632
788
|
disconnect() {
|
|
633
|
-
this.mounted = !1, this.abortController?.abort(), this.abortController = null,
|
|
789
|
+
this.mounted = !1, this.abortController?.abort(), this.abortController = null, r.delete(this.publishableKey), this.cleanup(), this.setStatus("disconnected");
|
|
634
790
|
}
|
|
635
791
|
/** Toggle microphone mute */
|
|
636
792
|
toggleMute() {
|
|
@@ -655,7 +811,7 @@ class P {
|
|
|
655
811
|
i = await t.json();
|
|
656
812
|
} catch {
|
|
657
813
|
}
|
|
658
|
-
throw new
|
|
814
|
+
throw new G({
|
|
659
815
|
message: i?.message ?? "create_session failed",
|
|
660
816
|
status: t.status,
|
|
661
817
|
payload: i,
|
|
@@ -669,7 +825,7 @@ class P {
|
|
|
669
825
|
return t.json();
|
|
670
826
|
}
|
|
671
827
|
async initSession(e) {
|
|
672
|
-
this.session =
|
|
828
|
+
this.session = T({
|
|
673
829
|
serverUrl: e.session_details.server_url,
|
|
674
830
|
participantToken: e.session_details.participant_token,
|
|
675
831
|
agentIdentity: e.session_details.agent_identity,
|
|
@@ -681,7 +837,7 @@ class P {
|
|
|
681
837
|
});
|
|
682
838
|
},
|
|
683
839
|
onStateChange: (t) => {
|
|
684
|
-
this.mounted && t === "disconnected" && (this.
|
|
840
|
+
this.mounted && t === "disconnected" && (this.disconnect(), this.callbacks.onDisconnect?.());
|
|
685
841
|
},
|
|
686
842
|
onAgentStateChange: (t) => {
|
|
687
843
|
this.mounted && this.setAgentState(t);
|
|
@@ -692,10 +848,10 @@ class P {
|
|
|
692
848
|
onClose: () => {
|
|
693
849
|
this.mounted && this.callbacks.onDisconnect?.();
|
|
694
850
|
}
|
|
695
|
-
}), this.agent =
|
|
851
|
+
}), this.agent = R(e.voice_agent_details.type), this.agent.on("audio", (t) => this.session?.sendAudio(t)), this.agent.on("turnEnd", () => this.session?.endAudioTurn()), this.agent.on("interrupted", () => {
|
|
696
852
|
this.session?.endAudioTurn(), this.session?.interrupt();
|
|
697
853
|
}), this.agent.on("closed", () => {
|
|
698
|
-
this.mounted && this.callbacks.onDisconnect?.();
|
|
854
|
+
this.mounted && (this.disconnect(), this.callbacks.onDisconnect?.());
|
|
699
855
|
}), this.agent.on("emotion", (t) => this.session?.setEmotion(t)), await this.session.connect();
|
|
700
856
|
}
|
|
701
857
|
async initMicrophone() {
|
|
@@ -703,9 +859,9 @@ class P {
|
|
|
703
859
|
audio: { sampleRate: 16e3, echoCancellation: !0, noiseSuppression: !0 }
|
|
704
860
|
}), this.audioContext = new AudioContext({ sampleRate: 16e3 });
|
|
705
861
|
const e = this.audioContext.createMediaStreamSource(this.stream);
|
|
706
|
-
this.processor = this.audioContext
|
|
862
|
+
this.processor = await x(this.audioContext), this.processor.port.onmessage = (t) => {
|
|
707
863
|
if (!this._isMuted) {
|
|
708
|
-
const i =
|
|
864
|
+
const i = A(t.data);
|
|
709
865
|
this.agent?.sendAudio(i);
|
|
710
866
|
}
|
|
711
867
|
}, e.connect(this.processor), this.processor.connect(this.audioContext.destination);
|
|
@@ -713,24 +869,32 @@ class P {
|
|
|
713
869
|
async connectAgent(e) {
|
|
714
870
|
if (!this.agent) return;
|
|
715
871
|
const t = { inputSampleRate: 16e3 };
|
|
716
|
-
e.type === "elevenlabs"
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
872
|
+
if (e.type === "elevenlabs")
|
|
873
|
+
await this.agent.connect({
|
|
874
|
+
...t,
|
|
875
|
+
agentId: e.agent_id,
|
|
876
|
+
signedUrl: e.signed_url
|
|
877
|
+
});
|
|
878
|
+
else if (e.type === "openai")
|
|
879
|
+
await this.agent.connect({
|
|
880
|
+
...t,
|
|
881
|
+
apiKey: e.token
|
|
882
|
+
});
|
|
883
|
+
else if (e.type === "kfl") {
|
|
884
|
+
if (!e.signed_url)
|
|
885
|
+
throw new Error("KFL voice agent requires signed_url");
|
|
886
|
+
await this.agent.connect({
|
|
887
|
+
...t,
|
|
888
|
+
wsUrl: e.signed_url
|
|
889
|
+
});
|
|
890
|
+
}
|
|
727
891
|
}
|
|
728
892
|
cleanup() {
|
|
729
893
|
this.stream?.getTracks().forEach((e) => e.stop()), this.processor?.disconnect(), this.audioContext?.close(), this.agent?.close(), this.session?.close(), this.stream = null, this.processor = null, this.audioContext = null, this.agent = null, this.session = null;
|
|
730
894
|
}
|
|
731
895
|
}
|
|
732
|
-
const
|
|
733
|
-
class
|
|
896
|
+
const l = /* @__PURE__ */ new Set();
|
|
897
|
+
class be {
|
|
734
898
|
voiceAgentDetails;
|
|
735
899
|
sessionDetails;
|
|
736
900
|
callbacks;
|
|
@@ -774,24 +938,24 @@ class Q {
|
|
|
774
938
|
}
|
|
775
939
|
/** Connect to the session */
|
|
776
940
|
async connect() {
|
|
777
|
-
if (
|
|
941
|
+
if (l.has(this.connectionId)) {
|
|
778
942
|
console.log("[PersonaView] Connection already in progress, skipping");
|
|
779
943
|
return;
|
|
780
944
|
}
|
|
781
|
-
|
|
945
|
+
l.add(this.connectionId), this.mounted = !0, this.setStatus("connecting");
|
|
782
946
|
try {
|
|
783
947
|
if (await this.initSession(), await this.initMicrophone(), await this.connectAgent(), !this.mounted) {
|
|
784
|
-
this.cleanup(),
|
|
948
|
+
this.cleanup(), l.delete(this.connectionId);
|
|
785
949
|
return;
|
|
786
950
|
}
|
|
787
951
|
this.setStatus("connected");
|
|
788
952
|
} catch (e) {
|
|
789
|
-
|
|
953
|
+
l.delete(this.connectionId), console.error("[PersonaView]", e), this.mounted && (this.setStatus("error"), this.callbacks.onError?.(e));
|
|
790
954
|
}
|
|
791
955
|
}
|
|
792
956
|
/** Disconnect and cleanup */
|
|
793
957
|
disconnect() {
|
|
794
|
-
this.mounted = !1,
|
|
958
|
+
this.mounted = !1, l.delete(this.connectionId), this.cleanup(), this.setStatus("disconnected");
|
|
795
959
|
}
|
|
796
960
|
/** Toggle microphone mute */
|
|
797
961
|
toggleMute() {
|
|
@@ -804,7 +968,7 @@ class Q {
|
|
|
804
968
|
this._agentState !== e && (this._agentState = e, this.callbacks.onAgentStateChange?.(e));
|
|
805
969
|
}
|
|
806
970
|
async initSession() {
|
|
807
|
-
this.session =
|
|
971
|
+
this.session = T({
|
|
808
972
|
serverUrl: this.sessionDetails.server_url,
|
|
809
973
|
participantToken: this.sessionDetails.participant_token,
|
|
810
974
|
agentIdentity: this.sessionDetails.agent_identity,
|
|
@@ -816,7 +980,7 @@ class Q {
|
|
|
816
980
|
});
|
|
817
981
|
},
|
|
818
982
|
onStateChange: (e) => {
|
|
819
|
-
this.mounted && e === "disconnected" && (this.
|
|
983
|
+
this.mounted && e === "disconnected" && (this.disconnect(), this.callbacks.onDisconnect?.());
|
|
820
984
|
},
|
|
821
985
|
onAgentStateChange: (e) => {
|
|
822
986
|
this.mounted && this.setAgentState(e);
|
|
@@ -827,10 +991,10 @@ class Q {
|
|
|
827
991
|
onClose: () => {
|
|
828
992
|
this.mounted && this.callbacks.onDisconnect?.();
|
|
829
993
|
}
|
|
830
|
-
}), this.agent =
|
|
994
|
+
}), this.agent = R(this.voiceAgentDetails.type), this.agent.on("audio", (e) => this.session?.sendAudio(e)), this.agent.on("turnEnd", () => this.session?.endAudioTurn()), this.agent.on("interrupted", () => {
|
|
831
995
|
this.session?.endAudioTurn(), this.session?.interrupt();
|
|
832
996
|
}), this.agent.on("closed", () => {
|
|
833
|
-
this.mounted && this.callbacks.onDisconnect?.();
|
|
997
|
+
this.mounted && (this.disconnect(), this.callbacks.onDisconnect?.());
|
|
834
998
|
}), this.agent.on("emotion", (e) => this.session?.setEmotion(e)), await this.session.connect();
|
|
835
999
|
}
|
|
836
1000
|
async initMicrophone() {
|
|
@@ -838,9 +1002,9 @@ class Q {
|
|
|
838
1002
|
audio: { sampleRate: 16e3, echoCancellation: !0, noiseSuppression: !0 }
|
|
839
1003
|
}), this.audioContext = new AudioContext({ sampleRate: 16e3 });
|
|
840
1004
|
const e = this.audioContext.createMediaStreamSource(this.stream);
|
|
841
|
-
this.processor = this.audioContext
|
|
1005
|
+
this.processor = await x(this.audioContext), this.processor.port.onmessage = (t) => {
|
|
842
1006
|
if (!this._isMuted) {
|
|
843
|
-
const i =
|
|
1007
|
+
const i = A(t.data);
|
|
844
1008
|
this.agent?.sendAudio(i);
|
|
845
1009
|
}
|
|
846
1010
|
}, e.connect(this.processor), this.processor.connect(this.audioContext.destination);
|
|
@@ -848,23 +1012,31 @@ class Q {
|
|
|
848
1012
|
async connectAgent() {
|
|
849
1013
|
if (!this.agent) return;
|
|
850
1014
|
const e = this.voiceAgentDetails, t = { inputSampleRate: 16e3 };
|
|
851
|
-
e.type === "elevenlabs"
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
1015
|
+
if (e.type === "elevenlabs")
|
|
1016
|
+
await this.agent.connect({
|
|
1017
|
+
...t,
|
|
1018
|
+
agentId: e.agent_id,
|
|
1019
|
+
signedUrl: e.signed_url
|
|
1020
|
+
});
|
|
1021
|
+
else if (e.type === "openai")
|
|
1022
|
+
await this.agent.connect({
|
|
1023
|
+
...t,
|
|
1024
|
+
apiKey: e.token
|
|
1025
|
+
});
|
|
1026
|
+
else if (e.type === "kfl") {
|
|
1027
|
+
if (!e.signed_url)
|
|
1028
|
+
throw new Error("KFL voice agent requires signed_url");
|
|
1029
|
+
await this.agent.connect({
|
|
1030
|
+
...t,
|
|
1031
|
+
wsUrl: e.signed_url
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
862
1034
|
}
|
|
863
1035
|
cleanup() {
|
|
864
1036
|
this.stream?.getTracks().forEach((e) => e.stop()), this.processor?.disconnect(), this.audioContext?.close(), this.agent?.close(), this.session?.close(), this.stream = null, this.processor = null, this.audioContext = null, this.agent = null, this.session = null;
|
|
865
1037
|
}
|
|
866
1038
|
}
|
|
867
|
-
const f = ["minimized", "active", "hidden"],
|
|
1039
|
+
const f = ["minimized", "active", "hidden"], Z = ["bottom-right", "bottom-left", "top-right", "top-left"], X = [
|
|
868
1040
|
"publishable-key",
|
|
869
1041
|
"api-base-url",
|
|
870
1042
|
"initial-state",
|
|
@@ -881,14 +1053,15 @@ const f = ["minimized", "active", "hidden"], U = ["bottom-right", "bottom-left",
|
|
|
881
1053
|
"button-color",
|
|
882
1054
|
"button-color-opacity",
|
|
883
1055
|
"video-fit",
|
|
884
|
-
"preview-image"
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
1056
|
+
"preview-image",
|
|
1057
|
+
"call-to-action"
|
|
1058
|
+
], E = "https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap";
|
|
1059
|
+
function Q() {
|
|
1060
|
+
if (document.querySelector(`link[href="${E}"]`)) return;
|
|
888
1061
|
const s = document.createElement("link");
|
|
889
|
-
s.rel = "stylesheet", s.href =
|
|
1062
|
+
s.rel = "stylesheet", s.href = E, document.head.appendChild(s);
|
|
890
1063
|
}
|
|
891
|
-
const
|
|
1064
|
+
const ee = `
|
|
892
1065
|
:host {
|
|
893
1066
|
display: block;
|
|
894
1067
|
font-family: 'Noto Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
@@ -1143,16 +1316,33 @@ const H = `
|
|
|
1143
1316
|
opacity: 1;
|
|
1144
1317
|
transform: translateY(0);
|
|
1145
1318
|
}
|
|
1146
|
-
`,
|
|
1147
|
-
class
|
|
1319
|
+
`, te = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/></svg>', ie = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.68 13.31a16 16 0 0 0 3.41 2.6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7 2 2 0 0 1 1.72 2v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.42 19.42 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91"/><line x1="23" y1="1" x2="1" y2="23"/></svg>', ne = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 14 10 14 10 20"/><polyline points="20 10 14 10 14 4"/><line x1="14" y1="10" x2="21" y2="3"/><line x1="3" y1="21" x2="10" y2="14"/></svg>', se = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>', oe = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>', re = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>', S = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" x2="12" y1="19" y2="22"/></svg>', ae = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="2" x2="22" y1="2" y2="22"/><path d="M18.89 13.23A7.12 7.12 0 0 0 19 12v-2"/><path d="M5 10v2a7 7 0 0 0 12 5"/><path d="M15 9.34V5a3 3 0 0 0-5.68-1.33"/><path d="M9 9v3a3 3 0 0 0 5.12 2.12"/><line x1="12" x2="12" y1="19" y2="22"/></svg>', le = "Join call", ce = 252, de = 377, he = 144, ue = 216, pe = "#676565", ge = 0.7, a = "16px", C = 40, me = `
|
|
1320
|
+
<div class="kfl-widget">
|
|
1321
|
+
<div class="kfl-widget-inner">
|
|
1322
|
+
<img class="kfl-preview-img" alt="Persona preview" draggable="false" />
|
|
1323
|
+
<div class="kfl-embed-container"></div>
|
|
1324
|
+
<div class="kfl-spinner"><div class="kfl-spinner-ring"></div></div>
|
|
1325
|
+
<div class="kfl-error-toast"></div>
|
|
1326
|
+
<button type="button" class="kfl-join-btn"></button>
|
|
1327
|
+
<div class="kfl-toolbar">
|
|
1328
|
+
<button type="button" class="kfl-mic-btn"></button>
|
|
1329
|
+
<button type="button" class="kfl-endcall-btn"></button>
|
|
1330
|
+
</div>
|
|
1331
|
+
<button type="button" class="kfl-toggle-btn"></button>
|
|
1332
|
+
</div>
|
|
1333
|
+
<button type="button" class="kfl-reveal-btn"></button>
|
|
1334
|
+
</div>
|
|
1335
|
+
`;
|
|
1336
|
+
class ve extends HTMLElement {
|
|
1148
1337
|
static get observedAttributes() {
|
|
1149
|
-
return [...
|
|
1338
|
+
return [...X];
|
|
1150
1339
|
}
|
|
1151
1340
|
shadow;
|
|
1152
1341
|
embed = null;
|
|
1153
1342
|
_widgetState = "minimized";
|
|
1154
1343
|
_connected = !1;
|
|
1155
1344
|
_connecting = !1;
|
|
1345
|
+
_initialized = !1;
|
|
1156
1346
|
widgetEl;
|
|
1157
1347
|
innerEl;
|
|
1158
1348
|
containerEl;
|
|
@@ -1166,25 +1356,43 @@ class ee extends HTMLElement {
|
|
|
1166
1356
|
revealBtn;
|
|
1167
1357
|
toolbarEl;
|
|
1168
1358
|
micBtn;
|
|
1359
|
+
_onJoinClick = (e) => {
|
|
1360
|
+
e.stopPropagation(), this._handleJoinCall();
|
|
1361
|
+
};
|
|
1362
|
+
_onEndCallClick = () => this._handleEndCall();
|
|
1363
|
+
_onMicClick = () => this._handleMicToggle();
|
|
1364
|
+
_onToggleClick = (e) => {
|
|
1365
|
+
e.stopPropagation(), this._handleToggle();
|
|
1366
|
+
};
|
|
1367
|
+
_onRevealClick = () => this._handleReveal();
|
|
1368
|
+
_onInnerClick = (e) => this._handleInnerClick(e);
|
|
1169
1369
|
constructor() {
|
|
1170
1370
|
super(), this.shadow = this.attachShadow({ mode: "open" });
|
|
1171
1371
|
}
|
|
1172
1372
|
connectedCallback() {
|
|
1173
|
-
|
|
1174
|
-
const e = document.createElement("style");
|
|
1175
|
-
e.textContent = H, this.shadow.appendChild(e), this.widgetEl = document.createElement("div"), this.widgetEl.className = "kfl-widget", this.innerEl = document.createElement("div"), this.innerEl.className = "kfl-widget-inner", this.containerEl = document.createElement("div"), this.containerEl.className = "kfl-embed-container", this.previewImg = document.createElement("img"), this.previewImg.className = "kfl-preview-img", this.previewImg.alt = "Persona preview", this.previewImg.draggable = !1;
|
|
1176
|
-
const t = this.getAttribute("preview-image");
|
|
1177
|
-
t ? (this.previewImg.src = t, this.previewImg.style.display = "block") : this.previewImg.style.display = "none", this.spinnerEl = document.createElement("div"), this.spinnerEl.className = "kfl-spinner", this.spinnerEl.innerHTML = '<div class="kfl-spinner-ring"></div>', this.errorToast = document.createElement("div"), this.errorToast.className = "kfl-error-toast", this.joinBtn = document.createElement("button"), this.joinBtn.className = "kfl-join-btn", this.joinBtn.innerHTML = `${V} Join call`, this.joinBtn.addEventListener("click", () => this._handleJoinCall()), this.endCallBtn = document.createElement("button"), this.endCallBtn.className = "kfl-endcall-btn", this.endCallBtn.innerHTML = K, this.endCallBtn.addEventListener("click", () => this._handleEndCall()), this.micBtn = document.createElement("button"), this.micBtn.className = "kfl-mic-btn", this.micBtn.innerHTML = _, this.micBtn.addEventListener("click", () => this._handleMicToggle()), this.toolbarEl = document.createElement("div"), this.toolbarEl.className = "kfl-toolbar", this.toolbarEl.appendChild(this.micBtn), this.toolbarEl.appendChild(this.endCallBtn), this.toggleBtn = document.createElement("button"), this.toggleBtn.className = "kfl-toggle-btn", this.toggleBtn.addEventListener("click", () => this._handleToggle()), this.revealBtn = document.createElement("button"), this.revealBtn.className = "kfl-reveal-btn", this.revealBtn.addEventListener("click", () => this._handleReveal()), this.innerEl.appendChild(this.previewImg), this.innerEl.appendChild(this.containerEl), this.innerEl.appendChild(this.spinnerEl), this.innerEl.appendChild(this.errorToast), this.innerEl.appendChild(this.joinBtn), this.innerEl.appendChild(this.toolbarEl), this.innerEl.appendChild(this.toggleBtn), this.widgetEl.appendChild(this.innerEl), this.widgetEl.appendChild(this.revealBtn), this.shadow.appendChild(this.widgetEl);
|
|
1178
|
-
const i = this.getAttribute("initial-state");
|
|
1179
|
-
this._widgetState = i && f.includes(i) ? i : "minimized", this._applyState();
|
|
1373
|
+
Q(), this._initialized || (this._initDom(), this._initialized = !0), this._widgetState = this._resolveInitialState(), this._applyState();
|
|
1180
1374
|
}
|
|
1181
1375
|
disconnectedCallback() {
|
|
1182
|
-
this.errorTimer && clearTimeout(this.errorTimer), this.
|
|
1376
|
+
this.errorTimer && clearTimeout(this.errorTimer), this._disconnectSession(), this._setWidgetState("minimized", { emit: !1 });
|
|
1183
1377
|
}
|
|
1184
1378
|
attributeChangedCallback(e, t, i) {
|
|
1185
|
-
|
|
1379
|
+
if (this._initialized) {
|
|
1380
|
+
if (e === "call-to-action") {
|
|
1381
|
+
this._renderJoinLabel();
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
if (e === "preview-image") {
|
|
1385
|
+
this._applyPreviewImage(i);
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
if (e === "controlled-widget-state") {
|
|
1389
|
+
i && f.includes(i) ? this._setWidgetState(i, { emit: !1 }) : this._applyState();
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
this._applyState();
|
|
1393
|
+
}
|
|
1186
1394
|
}
|
|
1187
|
-
// --- Public API
|
|
1395
|
+
// --- Public API ---
|
|
1188
1396
|
async mute() {
|
|
1189
1397
|
this.embed && (this.embed.audioElement.muted = !0);
|
|
1190
1398
|
}
|
|
@@ -1198,7 +1406,7 @@ class ee extends HTMLElement {
|
|
|
1198
1406
|
return this._connected;
|
|
1199
1407
|
}
|
|
1200
1408
|
async micOn() {
|
|
1201
|
-
this._connected || (this.
|
|
1409
|
+
this._connected || (this._connecting = !0, this._setWidgetState("active", { emit: !1 }), await this._connect()), this.embed && this.embed.isMuted && this.embed.toggleMute(), this._updateMicIcon();
|
|
1202
1410
|
}
|
|
1203
1411
|
async micOff() {
|
|
1204
1412
|
this.embed && !this.embed.isMuted && this.embed.toggleMute(), this._updateMicIcon();
|
|
@@ -1213,6 +1421,51 @@ class ee extends HTMLElement {
|
|
|
1213
1421
|
console.warn("[kfl-embed] setEmotion requires direct session access (not yet exposed by PersonaEmbed)");
|
|
1214
1422
|
}
|
|
1215
1423
|
// --- Internals ---
|
|
1424
|
+
_initDom() {
|
|
1425
|
+
const e = document.createElement("style");
|
|
1426
|
+
e.textContent = ee, this.shadow.appendChild(e);
|
|
1427
|
+
const t = document.createElement("template");
|
|
1428
|
+
t.innerHTML = me;
|
|
1429
|
+
const i = t.content.cloneNode(!0);
|
|
1430
|
+
this.widgetEl = this._requireElement(i, ".kfl-widget"), this.innerEl = this._requireElement(i, ".kfl-widget-inner"), this.containerEl = this._requireElement(i, ".kfl-embed-container"), this.previewImg = this._requireElement(i, ".kfl-preview-img"), this.spinnerEl = this._requireElement(i, ".kfl-spinner"), this.errorToast = this._requireElement(i, ".kfl-error-toast"), this.joinBtn = this._requireElement(i, ".kfl-join-btn"), this.toolbarEl = this._requireElement(i, ".kfl-toolbar"), this.micBtn = this._requireElement(i, ".kfl-mic-btn"), this.endCallBtn = this._requireElement(i, ".kfl-endcall-btn"), this.toggleBtn = this._requireElement(i, ".kfl-toggle-btn"), this.revealBtn = this._requireElement(i, ".kfl-reveal-btn"), this.endCallBtn.innerHTML = ie, this.micBtn.innerHTML = S, this._renderJoinLabel(), this._applyPreviewImage(this.getAttribute("preview-image")), this.joinBtn.addEventListener("click", this._onJoinClick), this.endCallBtn.addEventListener("click", this._onEndCallClick), this.micBtn.addEventListener("click", this._onMicClick), this.toggleBtn.addEventListener("click", this._onToggleClick), this.revealBtn.addEventListener("click", this._onRevealClick), this.innerEl.addEventListener("click", this._onInnerClick), this.shadow.appendChild(i);
|
|
1431
|
+
}
|
|
1432
|
+
_requireElement(e, t) {
|
|
1433
|
+
const i = e.querySelector(t);
|
|
1434
|
+
if (!i) throw new Error(`[kfl-embed] Missing template node: ${t}`);
|
|
1435
|
+
return i;
|
|
1436
|
+
}
|
|
1437
|
+
_resolveInitialState() {
|
|
1438
|
+
const e = this.getAttribute("controlled-widget-state");
|
|
1439
|
+
if (e && f.includes(e))
|
|
1440
|
+
return e;
|
|
1441
|
+
const t = this.getAttribute("initial-state");
|
|
1442
|
+
return t && f.includes(t) ? t : "minimized";
|
|
1443
|
+
}
|
|
1444
|
+
_renderJoinLabel() {
|
|
1445
|
+
this.joinBtn.innerHTML = `${te} ${this.getAttribute("call-to-action") || le}`;
|
|
1446
|
+
}
|
|
1447
|
+
_applyPreviewImage(e) {
|
|
1448
|
+
if (e) {
|
|
1449
|
+
this.previewImg.src = e, this.previewImg.style.display = this._connected ? "none" : "block";
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
this.previewImg.removeAttribute("src"), this.previewImg.style.display = "none";
|
|
1453
|
+
}
|
|
1454
|
+
_readRenderConfig() {
|
|
1455
|
+
const e = this._widgetState === "minimized", t = this._widgetState === "hidden";
|
|
1456
|
+
return {
|
|
1457
|
+
hideUI: this.hasAttribute("hide-ui"),
|
|
1458
|
+
isInline: this.hasAttribute("inline"),
|
|
1459
|
+
showMin: this._shouldShowMinimizeButton(),
|
|
1460
|
+
isMinimized: e,
|
|
1461
|
+
isHidden: t,
|
|
1462
|
+
isActive: this._widgetState === "active",
|
|
1463
|
+
canExpandFromMinimized: e && this._hasLiveSession(),
|
|
1464
|
+
corner: this._getCorner(),
|
|
1465
|
+
buttonColor: this.getAttribute("button-color") || pe,
|
|
1466
|
+
buttonOpacity: this._getAttrNum("button-color-opacity", ge)
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1216
1469
|
_getAttrNum(e, t) {
|
|
1217
1470
|
const i = this.getAttribute(e);
|
|
1218
1471
|
if (!i) return t;
|
|
@@ -1221,47 +1474,62 @@ class ee extends HTMLElement {
|
|
|
1221
1474
|
}
|
|
1222
1475
|
_getCorner() {
|
|
1223
1476
|
const e = this.getAttribute("corner");
|
|
1224
|
-
return e &&
|
|
1477
|
+
return e && Z.includes(e) ? e : "bottom-right";
|
|
1225
1478
|
}
|
|
1226
|
-
_applyLayout() {
|
|
1479
|
+
_applyLayout(e) {
|
|
1227
1480
|
if (!this.widgetEl) return;
|
|
1228
|
-
const
|
|
1229
|
-
|
|
1230
|
-
this.widgetEl.style.position = "relative", this.widgetEl.style.inset = "";
|
|
1231
|
-
else {
|
|
1232
|
-
this.widgetEl.style.position = "fixed", this.widgetEl.style.top = "", this.widgetEl.style.bottom = "", this.widgetEl.style.left = "", this.widgetEl.style.right = "";
|
|
1233
|
-
const a = "16px";
|
|
1234
|
-
t.includes("bottom") && (this.widgetEl.style.bottom = a), t.includes("top") && (this.widgetEl.style.top = a), t.includes("right") && (this.widgetEl.style.right = n ? "0" : a), t.includes("left") && (this.widgetEl.style.left = n ? "0" : a);
|
|
1235
|
-
}
|
|
1481
|
+
const t = e.isHidden ? C : e.isActive ? this._getAttrNum("active-width", ce) : this._getAttrNum("minimized-width", he), i = e.isHidden ? C : e.isActive ? this._getAttrNum("active-height", de) : this._getAttrNum("minimized-height", ue);
|
|
1482
|
+
this.widgetEl.style.width = `${t}px`, this.widgetEl.style.height = `${i}px`, e.isInline ? (this.widgetEl.style.position = "relative", this.widgetEl.style.inset = "") : (this.widgetEl.style.position = "fixed", this.widgetEl.style.top = "", this.widgetEl.style.bottom = "", this.widgetEl.style.left = "", this.widgetEl.style.right = "", e.corner.includes("bottom") && (this.widgetEl.style.bottom = a), e.corner.includes("top") && (this.widgetEl.style.top = a), e.corner.includes("right") && (this.widgetEl.style.right = e.isHidden ? "0" : a), e.corner.includes("left") && (this.widgetEl.style.left = e.isHidden ? "0" : a));
|
|
1236
1483
|
}
|
|
1237
1484
|
_applyState() {
|
|
1238
1485
|
if (!this.widgetEl) return;
|
|
1239
|
-
const e = this.
|
|
1240
|
-
this.widgetEl.classList.toggle("kfl-widget--hidden",
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1486
|
+
const e = this._readRenderConfig();
|
|
1487
|
+
this.widgetEl.classList.toggle("kfl-widget--hidden", e.isHidden), this._renderJoinButton(e), this._renderToolbar(e), this._renderJoinButtonColor(e), this._renderToggleButton(e), this._renderRevealButton(e), this.innerEl.style.cursor = e.canExpandFromMinimized ? "pointer" : "", this.spinnerEl.classList.toggle("kfl-spinner--visible", this._connecting && !this._connected), this._updateMicIcon(), this._applyLayout(e);
|
|
1488
|
+
}
|
|
1489
|
+
_renderJoinButton(e) {
|
|
1490
|
+
this.joinBtn.style.display = this._connected || this._connecting || e.isHidden ? "none" : "flex";
|
|
1491
|
+
}
|
|
1492
|
+
_renderToolbar(e) {
|
|
1493
|
+
this.toolbarEl.style.display = this._connected && !e.hideUI && !e.isMinimized && !e.isHidden ? "flex" : "none";
|
|
1494
|
+
}
|
|
1495
|
+
_renderToggleButton(e) {
|
|
1496
|
+
!e.hideUI && e.isMinimized && e.showMin && !e.isInline ? (this.toggleBtn.style.display = "flex", this.toggleBtn.innerHTML = se) : !e.hideUI && !e.isMinimized && !e.isHidden && this._connected ? (this.toggleBtn.style.display = "flex", this.toggleBtn.innerHTML = ne) : this.toggleBtn.style.display = "none";
|
|
1497
|
+
}
|
|
1498
|
+
_renderRevealButton(e) {
|
|
1499
|
+
const t = e.corner.includes("right");
|
|
1500
|
+
this.revealBtn.className = `kfl-reveal-btn ${t ? "kfl-reveal-btn--right" : "kfl-reveal-btn--left"}`, this.revealBtn.innerHTML = t ? oe : re, e.isHidden && (this.revealBtn.style.top = "", this.revealBtn.style.bottom = "", e.corner.includes("bottom") ? this.revealBtn.style.bottom = a : this.revealBtn.style.top = a);
|
|
1501
|
+
}
|
|
1502
|
+
_renderJoinButtonColor(e) {
|
|
1503
|
+
const t = Math.max(0, Math.min(1, e.buttonOpacity)), i = Math.round(t * 255).toString(16).padStart(2, "0");
|
|
1504
|
+
this.joinBtn.style.backgroundColor = `${e.buttonColor}${i}`;
|
|
1245
1505
|
}
|
|
1246
1506
|
_shouldShowMinimizeButton() {
|
|
1247
1507
|
const e = this.getAttribute("controlled-show-minimize-button");
|
|
1248
1508
|
return e !== null ? e !== "false" : this.getAttribute("show-minimize-button") !== "false";
|
|
1249
1509
|
}
|
|
1250
1510
|
_handleJoinCall() {
|
|
1251
|
-
this.
|
|
1511
|
+
this._connecting = !0, this._setWidgetState("active", { emit: !1 }), this._connect();
|
|
1252
1512
|
}
|
|
1253
1513
|
_handleToggle() {
|
|
1254
1514
|
if (this._widgetState === "minimized") {
|
|
1255
1515
|
if (this.hasAttribute("inline")) return;
|
|
1256
|
-
this.
|
|
1257
|
-
} else this._widgetState === "active" &&
|
|
1258
|
-
|
|
1516
|
+
this._hasLiveSession() && this._disconnectSession(), this._setWidgetState("hidden");
|
|
1517
|
+
} else this._widgetState === "active" && this._setWidgetState("minimized");
|
|
1518
|
+
}
|
|
1519
|
+
_handleInnerClick(e) {
|
|
1520
|
+
if (this._widgetState !== "minimized" || !this._hasLiveSession()) return;
|
|
1521
|
+
const t = e.composedPath();
|
|
1522
|
+
t.includes(this.joinBtn) || t.includes(this.toggleBtn) || this._setWidgetState("active");
|
|
1259
1523
|
}
|
|
1260
1524
|
_handleEndCall() {
|
|
1261
|
-
this._widgetState === "active" && (this.
|
|
1525
|
+
this._widgetState === "active" && (this._disconnectSession(), this._setWidgetState("minimized"));
|
|
1262
1526
|
}
|
|
1263
1527
|
_handleReveal() {
|
|
1264
|
-
this.
|
|
1528
|
+
this._setWidgetState("minimized");
|
|
1529
|
+
}
|
|
1530
|
+
_setWidgetState(e, t) {
|
|
1531
|
+
const i = t?.emit ?? !0, n = this._widgetState !== e;
|
|
1532
|
+
this._widgetState = e, n && i && this.dispatchEvent(new CustomEvent("widgetstatechange", { detail: { state: e } })), this._applyState();
|
|
1265
1533
|
}
|
|
1266
1534
|
_handleMicToggle() {
|
|
1267
1535
|
this.embed && (this.embed.toggleMute(), this._updateMicIcon());
|
|
@@ -1269,15 +1537,21 @@ class ee extends HTMLElement {
|
|
|
1269
1537
|
_updateMicIcon() {
|
|
1270
1538
|
if (!this.micBtn) return;
|
|
1271
1539
|
const e = this.embed ? this.embed.isMuted : !1;
|
|
1272
|
-
this.micBtn.innerHTML = e ?
|
|
1540
|
+
this.micBtn.innerHTML = e ? ae : S;
|
|
1273
1541
|
}
|
|
1274
|
-
|
|
1542
|
+
_hasLiveSession() {
|
|
1543
|
+
return this._connected || this._connecting || !!this.embed;
|
|
1544
|
+
}
|
|
1545
|
+
_disconnectSession() {
|
|
1275
1546
|
try {
|
|
1276
1547
|
this.embed?.disconnect();
|
|
1277
1548
|
} catch (e) {
|
|
1278
1549
|
console.warn("[kfl-embed] Disconnect error:", e);
|
|
1279
1550
|
}
|
|
1280
|
-
this._connected = !1, this._connecting = !1, this.embed = null, this.containerEl.innerHTML = "", this.containerEl.removeAttribute("style"), this.
|
|
1551
|
+
this._connected = !1, this._connecting = !1, this.embed = null, this.containerEl.innerHTML = "", this.containerEl.removeAttribute("style"), this._applyPreviewImage(this.getAttribute("preview-image"));
|
|
1552
|
+
}
|
|
1553
|
+
_resetToMinimized() {
|
|
1554
|
+
this._disconnectSession(), this._setWidgetState("minimized", { emit: !1 });
|
|
1281
1555
|
}
|
|
1282
1556
|
_showError(e) {
|
|
1283
1557
|
this.errorTimer && clearTimeout(this.errorTimer), this.errorToast.textContent = e, this.errorToast.classList.add("kfl-error-toast--visible"), this.errorTimer = setTimeout(() => {
|
|
@@ -1294,7 +1568,7 @@ class ee extends HTMLElement {
|
|
|
1294
1568
|
const t = this.getAttribute("api-base-url") || void 0, i = this.getAttribute("video-fit") || "cover";
|
|
1295
1569
|
this.previewImg.style.display = "none";
|
|
1296
1570
|
try {
|
|
1297
|
-
this.embed = new
|
|
1571
|
+
this.embed = new Y({
|
|
1298
1572
|
container: this.containerEl,
|
|
1299
1573
|
publishableKey: e,
|
|
1300
1574
|
apiBaseUrl: t,
|
|
@@ -1318,20 +1592,21 @@ class ee extends HTMLElement {
|
|
|
1318
1592
|
}
|
|
1319
1593
|
}
|
|
1320
1594
|
export {
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1595
|
+
q as AGENT_REGISTRY,
|
|
1596
|
+
v as BaseAgent,
|
|
1597
|
+
F as ElevenLabsAgent,
|
|
1598
|
+
G as KeyframeApiError,
|
|
1599
|
+
J as KflAgent,
|
|
1600
|
+
ve as KflEmbedElement,
|
|
1601
|
+
$ as OpenAIRealtimeAgent,
|
|
1602
|
+
Y as PersonaEmbed,
|
|
1603
|
+
be as PersonaView,
|
|
1604
|
+
h as SAMPLE_RATE,
|
|
1605
|
+
_ as base64ToBytes,
|
|
1606
|
+
b as bytesToBase64,
|
|
1607
|
+
R as createAgent,
|
|
1608
|
+
L as createEventEmitter,
|
|
1609
|
+
A as floatTo16BitPCM,
|
|
1610
|
+
_e as getAgentInfo,
|
|
1611
|
+
u as resamplePcm
|
|
1337
1612
|
};
|