@keyframelabs/elements 0.3.0 → 0.5.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 +92 -4
- package/dist/KflEmbedElement.d.ts +77 -0
- package/dist/agents/audio-utils.d.ts +5 -0
- package/dist/agents/index.d.ts +7 -3
- package/dist/agents/kfl.d.ts +26 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +901 -162
- package/dist/kfl-embed-register.d.ts +1 -0
- package/dist/kfl-embed.js +18967 -0
- package/package.json +8 -5
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
|
-
for (let
|
|
6
|
-
t[
|
|
5
|
+
for (let i = 0; i < e.length; i++)
|
|
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
|
|
19
|
-
for (let
|
|
20
|
-
const
|
|
21
|
-
c[
|
|
22
|
-
|
|
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
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) {
|
|
@@ -34,27 +34,64 @@ function C() {
|
|
|
34
34
|
s.get(e)?.delete(t);
|
|
35
35
|
},
|
|
36
36
|
emit(e, t) {
|
|
37
|
-
s.get(e)?.forEach((
|
|
37
|
+
s.get(e)?.forEach((i) => i(t));
|
|
38
38
|
},
|
|
39
39
|
removeAllListeners() {
|
|
40
40
|
s.clear();
|
|
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
|
-
const
|
|
48
|
-
e[t] =
|
|
47
|
+
const i = Math.max(-1, Math.min(1, s[t]));
|
|
48
|
+
e[t] = i < 0 ? i * 32768 : i * 32767;
|
|
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 y {
|
|
|
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,15 +176,15 @@ class R extends y {
|
|
|
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
|
-
this.setState("listening"),
|
|
181
|
+
this.setState("listening"), i();
|
|
145
182
|
}, this.ws.onerror = () => {
|
|
146
|
-
|
|
147
|
-
}, this.ws.onclose = (
|
|
148
|
-
this.ws = null, this.setState("idle"), this.emitClosed(
|
|
149
|
-
}, this.ws.onmessage = (
|
|
150
|
-
this.handleMessage(
|
|
183
|
+
n(new Error("Failed to connect to ElevenLabs"));
|
|
184
|
+
}, this.ws.onclose = (o) => {
|
|
185
|
+
this.ws = null, this.setState("idle"), this.emitClosed(o.code, o.reason);
|
|
186
|
+
}, this.ws.onmessage = (o) => {
|
|
187
|
+
this.handleMessage(o.data);
|
|
151
188
|
};
|
|
152
189
|
});
|
|
153
190
|
}
|
|
@@ -184,12 +221,12 @@ class R extends y {
|
|
|
184
221
|
const t = e.conversation_initiation_metadata_event;
|
|
185
222
|
if (t) {
|
|
186
223
|
if (t.agent_output_audio_format) {
|
|
187
|
-
const
|
|
188
|
-
|
|
224
|
+
const i = t.agent_output_audio_format.match(/pcm_(\d+)/);
|
|
225
|
+
i && (this.outputSampleRate = parseInt(i[1], 10));
|
|
189
226
|
}
|
|
190
227
|
if (t.user_input_audio_format) {
|
|
191
|
-
const
|
|
192
|
-
|
|
228
|
+
const i = t.user_input_audio_format.match(/pcm_(\d+)/);
|
|
229
|
+
i && (this.expectedInputSampleRate = parseInt(i[1], 10));
|
|
193
230
|
}
|
|
194
231
|
this.initialized = !0;
|
|
195
232
|
}
|
|
@@ -205,11 +242,11 @@ class R extends y {
|
|
|
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
|
|
209
|
-
this.outputSampleRate !==
|
|
210
|
-
const
|
|
211
|
-
this.turnStartTime === 0 && (this.turnStartTime = Date.now()), this.accumulatedDurationMs +=
|
|
212
|
-
`[ElevenLabs] audio chunk: ${
|
|
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;
|
|
248
|
+
this.turnStartTime === 0 && (this.turnStartTime = Date.now()), this.accumulatedDurationMs += o, console.debug(
|
|
249
|
+
`[ElevenLabs] audio chunk: ${n.length} bytes, +${o.toFixed(0)}ms, totalDuration=${this.accumulatedDurationMs.toFixed(0)}ms, agentResponse=${this.agentResponseReceived}`
|
|
213
250
|
), this.scheduleVirtualBufferCheck();
|
|
214
251
|
}
|
|
215
252
|
handleUserTranscript(e) {
|
|
@@ -252,8 +289,8 @@ class R extends y {
|
|
|
252
289
|
const t = e.client_tool_call;
|
|
253
290
|
if (t) {
|
|
254
291
|
if (t.tool_name === "set_emotion") {
|
|
255
|
-
const
|
|
256
|
-
|
|
292
|
+
const i = t.parameters?.emotion?.toLowerCase();
|
|
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 R extends y {
|
|
|
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 R extends y {
|
|
|
307
344
|
this.initialized = !1, this.lastInterruptId = 0, this.resetTurnState(), super.close();
|
|
308
345
|
}
|
|
309
346
|
}
|
|
310
|
-
const
|
|
347
|
+
const D = ["neutral", "angry", "sad", "happy"], j = "wss://api.openai.com/v1/realtime", U = "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 T = ["neutral", "angry", "sad", "happy"], k = "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,24 +378,24 @@ class P extends y {
|
|
|
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(e, t), new Promise((
|
|
346
|
-
this.connectResolve =
|
|
381
|
+
const t = e.model ?? U;
|
|
382
|
+
return this.initialSessionUpdate = this.buildSessionUpdate(e, t), new Promise((i, n) => {
|
|
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
|
+
`${j}?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
|
-
}, this.ws.onclose = (
|
|
391
|
+
}, this.ws.onclose = (o) => {
|
|
355
392
|
if (this.clearConnectTimeout(), this.connectReject) {
|
|
356
|
-
const c =
|
|
357
|
-
this.rejectPendingConnect(new Error(`OpenAI Realtime closed before initialization (${
|
|
393
|
+
const c = o.reason ? `: ${o.reason}` : "";
|
|
394
|
+
this.rejectPendingConnect(new Error(`OpenAI Realtime closed before initialization (${o.code}${c})`));
|
|
358
395
|
}
|
|
359
|
-
this.resetTurnState(), this.initialSessionUpdate = null, this.ws = null, this.setState("idle"), this.emitClosed(
|
|
360
|
-
}, this.ws.onmessage = (
|
|
361
|
-
this.handleMessage(
|
|
396
|
+
this.resetTurnState(), this.initialSessionUpdate = null, this.ws = null, this.setState("idle"), this.emitClosed(o.code, o.reason);
|
|
397
|
+
}, this.ws.onmessage = (o) => {
|
|
398
|
+
this.handleMessage(o.data);
|
|
362
399
|
};
|
|
363
400
|
});
|
|
364
401
|
}
|
|
@@ -377,15 +414,15 @@ class P extends y {
|
|
|
377
414
|
return;
|
|
378
415
|
if (!this.currentResponseHasAudio) {
|
|
379
416
|
if (this.pendingFunctionCallStartedAtMs !== null) {
|
|
380
|
-
const
|
|
417
|
+
const i = performance.now() - this.pendingFunctionCallStartedAtMs;
|
|
381
418
|
console.debug("[OpenAIRealtime] Function call latency", {
|
|
382
419
|
calls: this.pendingFunctionCallNames,
|
|
383
|
-
latencyMs: Math.round(
|
|
420
|
+
latencyMs: Math.round(i)
|
|
384
421
|
}), this.pendingFunctionCallStartedAtMs = null, this.pendingFunctionCallNames = [];
|
|
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)
|
|
@@ -412,8 +449,8 @@ class P extends y {
|
|
|
412
449
|
this.handleResponseDone(t.response);
|
|
413
450
|
break;
|
|
414
451
|
case "error": {
|
|
415
|
-
const
|
|
416
|
-
this.rejectPendingConnect(new Error(
|
|
452
|
+
const i = t.error?.message ?? t.message ?? "Unknown OpenAI Realtime error";
|
|
453
|
+
this.rejectPendingConnect(new Error(i)), console.error("[OpenAIRealtime] Server error:", t);
|
|
417
454
|
break;
|
|
418
455
|
}
|
|
419
456
|
}
|
|
@@ -424,16 +461,16 @@ class P extends y {
|
|
|
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
472
|
buildSessionUpdate(e, t) {
|
|
436
|
-
const
|
|
473
|
+
const i = e.turnDetection ?? { type: "semantic_vad", eagerness: "high" };
|
|
437
474
|
return {
|
|
438
475
|
type: "session.update",
|
|
439
476
|
session: {
|
|
@@ -445,19 +482,19 @@ class P extends y {
|
|
|
445
482
|
input: {
|
|
446
483
|
format: {
|
|
447
484
|
type: "audio/pcm",
|
|
448
|
-
rate:
|
|
485
|
+
rate: m
|
|
449
486
|
},
|
|
450
|
-
turn_detection:
|
|
487
|
+
turn_detection: i
|
|
451
488
|
},
|
|
452
489
|
output: {
|
|
453
490
|
format: {
|
|
454
491
|
type: "audio/pcm",
|
|
455
|
-
rate:
|
|
492
|
+
rate: h
|
|
456
493
|
},
|
|
457
494
|
...e.voice ? { voice: e.voice } : {}
|
|
458
495
|
}
|
|
459
496
|
},
|
|
460
|
-
tools: [
|
|
497
|
+
tools: [z],
|
|
461
498
|
tool_choice: "auto"
|
|
462
499
|
}
|
|
463
500
|
};
|
|
@@ -470,7 +507,7 @@ class P extends y {
|
|
|
470
507
|
this.currentResponseHasAudio && this.finishAudioTurn();
|
|
471
508
|
return;
|
|
472
509
|
}
|
|
473
|
-
const t = e.output.filter(
|
|
510
|
+
const t = e.output.filter(H);
|
|
474
511
|
if (t.length > 0) {
|
|
475
512
|
this.handleFunctionCalls(t);
|
|
476
513
|
return;
|
|
@@ -479,31 +516,31 @@ class P extends y {
|
|
|
479
516
|
}
|
|
480
517
|
handleFunctionCalls(e) {
|
|
481
518
|
let t = !1;
|
|
482
|
-
const
|
|
483
|
-
for (const
|
|
484
|
-
if (!
|
|
519
|
+
const i = [];
|
|
520
|
+
for (const n of e) {
|
|
521
|
+
if (!n.call_id || this.handledFunctionCallIds.has(n.call_id))
|
|
485
522
|
continue;
|
|
486
|
-
this.handledFunctionCallIds.add(
|
|
487
|
-
const
|
|
523
|
+
this.handledFunctionCallIds.add(n.call_id), i.push(n.name ?? "unknown");
|
|
524
|
+
const o = this.handleFunctionCall(n);
|
|
488
525
|
this.sendEvent({
|
|
489
526
|
type: "conversation.item.create",
|
|
490
527
|
item: {
|
|
491
528
|
type: "function_call_output",
|
|
492
|
-
call_id:
|
|
493
|
-
output: JSON.stringify(
|
|
529
|
+
call_id: n.call_id,
|
|
530
|
+
output: JSON.stringify(o)
|
|
494
531
|
}
|
|
495
532
|
}), t = !0;
|
|
496
533
|
}
|
|
497
|
-
t && (this.pendingFunctionCallStartedAtMs = performance.now(), this.pendingFunctionCallNames =
|
|
498
|
-
calls:
|
|
534
|
+
t && (this.pendingFunctionCallStartedAtMs = performance.now(), this.pendingFunctionCallNames = i, console.debug("[OpenAIRealtime] Function call received", {
|
|
535
|
+
calls: i
|
|
499
536
|
}), this.sendEvent({ type: "response.create" }));
|
|
500
537
|
}
|
|
501
538
|
handleFunctionCall(e) {
|
|
502
539
|
if (e.name !== "set_emotion")
|
|
503
540
|
return { error: `Unsupported function: ${e.name}` };
|
|
504
541
|
try {
|
|
505
|
-
const
|
|
506
|
-
return
|
|
542
|
+
const i = (e.arguments ? JSON.parse(e.arguments) : {}).emotion?.toLowerCase();
|
|
543
|
+
return i && D.includes(i) ? (this.events.emit("emotion", i), { result: "ok" }) : { error: "Invalid emotion" };
|
|
507
544
|
} catch {
|
|
508
545
|
return { error: "Invalid function arguments" };
|
|
509
546
|
}
|
|
@@ -533,27 +570,150 @@ class P extends y {
|
|
|
533
570
|
this.connectTimeout !== null && (clearTimeout(this.connectTimeout), this.connectTimeout = null);
|
|
534
571
|
}
|
|
535
572
|
}
|
|
536
|
-
function
|
|
573
|
+
function H(s) {
|
|
537
574
|
return s.type === "function_call";
|
|
538
575
|
}
|
|
539
|
-
const
|
|
576
|
+
const k = 16e3, W = 1e4, V = 1e4, K = ["neutral", "angry", "sad", "happy"];
|
|
577
|
+
class J extends v {
|
|
578
|
+
agentName = "KflVoiceAgent";
|
|
579
|
+
connectResolve = null;
|
|
580
|
+
connectReject = null;
|
|
581
|
+
connectTimeout = null;
|
|
582
|
+
pingInterval = null;
|
|
583
|
+
sourceInputSampleRate = M;
|
|
584
|
+
turnEnded = !1;
|
|
585
|
+
async connect(e) {
|
|
586
|
+
if (this.ws)
|
|
587
|
+
throw new Error("Already connected");
|
|
588
|
+
if (!e.wsUrl)
|
|
589
|
+
throw new Error("KFL voice agent wsUrl is required");
|
|
590
|
+
return e.inputSampleRate && (this.sourceInputSampleRate = e.inputSampleRate), new Promise((t, i) => {
|
|
591
|
+
this.connectResolve = t, this.connectReject = i, this.connectTimeout = setTimeout(() => {
|
|
592
|
+
this.rejectPendingConnect(new Error("Timed out waiting for kfl-voice-agent session.ready")), this.close();
|
|
593
|
+
}, V), this.ws = new WebSocket(e.wsUrl), this.ws.onopen = () => {
|
|
594
|
+
this.startPings(e.pingIntervalMs);
|
|
595
|
+
}, this.ws.onerror = () => {
|
|
596
|
+
this.rejectPendingConnect(new Error("Failed to connect to kfl-voice-agent"));
|
|
597
|
+
}, this.ws.onclose = (n) => {
|
|
598
|
+
if (this.stopPings(), this.connectReject) {
|
|
599
|
+
const o = n.reason ? `: ${n.reason}` : "";
|
|
600
|
+
this.rejectPendingConnect(
|
|
601
|
+
new Error(`kfl-voice-agent closed before initialization (${n.code}${o})`)
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
this.ws = null, this.setState("idle"), this.emitClosed(n.code, n.reason);
|
|
605
|
+
}, this.ws.onmessage = (n) => {
|
|
606
|
+
this.handleMessage(n.data);
|
|
607
|
+
};
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
sendAudio(e) {
|
|
611
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
612
|
+
console.warn("[KflVoiceAgent] Cannot send audio: not connected");
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
let t = e;
|
|
616
|
+
this.sourceInputSampleRate !== k && (t = u(e, this.sourceInputSampleRate, k)), this.ws.send(JSON.stringify({
|
|
617
|
+
type: "input_audio_buffer.append",
|
|
618
|
+
audio: b(t)
|
|
619
|
+
}));
|
|
620
|
+
}
|
|
621
|
+
close() {
|
|
622
|
+
this.rejectPendingConnect(new Error("Connection closed")), this.clearConnectTimeout(), this.stopPings(), super.close();
|
|
623
|
+
}
|
|
624
|
+
handleParsedMessage(e) {
|
|
625
|
+
const t = e, i = t.type;
|
|
626
|
+
if (i)
|
|
627
|
+
switch (i) {
|
|
628
|
+
case "session.ready":
|
|
629
|
+
this.setState("listening"), this.resolvePendingConnect();
|
|
630
|
+
break;
|
|
631
|
+
case "session.pong":
|
|
632
|
+
break;
|
|
633
|
+
case "turn_start":
|
|
634
|
+
this.turnEnded = !1, this.events.emit("turnStart", void 0), this.setState("speaking");
|
|
635
|
+
break;
|
|
636
|
+
case "output_audio.chunk":
|
|
637
|
+
t.audio && this.events.emit("audio", _(t.audio));
|
|
638
|
+
break;
|
|
639
|
+
case "output_audio.done":
|
|
640
|
+
this.turnEnded || (this.turnEnded = !0, this.events.emit("turnEnd", void 0));
|
|
641
|
+
break;
|
|
642
|
+
case "playback_done":
|
|
643
|
+
this.turnEnded || (this.turnEnded = !0, this.events.emit("turnEnd", void 0)), this.setState("listening");
|
|
644
|
+
break;
|
|
645
|
+
case "interrupted":
|
|
646
|
+
this.turnEnded = !0, this.events.emit("interrupted", void 0), this.setState("listening");
|
|
647
|
+
break;
|
|
648
|
+
case "user_transcript":
|
|
649
|
+
t.text && this.events.emit("transcript", {
|
|
650
|
+
role: "user",
|
|
651
|
+
text: t.text,
|
|
652
|
+
isFinal: !0
|
|
653
|
+
});
|
|
654
|
+
break;
|
|
655
|
+
case "assistant_done":
|
|
656
|
+
t.text && this.events.emit("transcript", {
|
|
657
|
+
role: "assistant",
|
|
658
|
+
text: t.text,
|
|
659
|
+
isFinal: !0
|
|
660
|
+
});
|
|
661
|
+
break;
|
|
662
|
+
case "emotion":
|
|
663
|
+
t.emotion && K.includes(t.emotion) && this.events.emit("emotion", t.emotion);
|
|
664
|
+
break;
|
|
665
|
+
case "error": {
|
|
666
|
+
const n = t.message ?? "Unknown kfl-voice-agent error";
|
|
667
|
+
this.rejectPendingConnect(new Error(n)), console.error("[KflVoiceAgent] Server error:", t);
|
|
668
|
+
break;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
startPings(e) {
|
|
673
|
+
this.stopPings();
|
|
674
|
+
const t = e ?? W;
|
|
675
|
+
this.pingInterval = setInterval(() => {
|
|
676
|
+
!this.ws || this.ws.readyState !== WebSocket.OPEN || this.ws.send(JSON.stringify({ type: "session.ping" }));
|
|
677
|
+
}, t);
|
|
678
|
+
}
|
|
679
|
+
stopPings() {
|
|
680
|
+
this.pingInterval && (clearInterval(this.pingInterval), this.pingInterval = null);
|
|
681
|
+
}
|
|
682
|
+
clearConnectTimeout() {
|
|
683
|
+
this.connectTimeout && (clearTimeout(this.connectTimeout), this.connectTimeout = null);
|
|
684
|
+
}
|
|
685
|
+
resolvePendingConnect() {
|
|
686
|
+
this.clearConnectTimeout();
|
|
687
|
+
const e = this.connectResolve;
|
|
688
|
+
this.connectResolve = null, this.connectReject = null, e?.();
|
|
689
|
+
}
|
|
690
|
+
rejectPendingConnect(e) {
|
|
691
|
+
this.clearConnectTimeout();
|
|
692
|
+
const t = this.connectReject;
|
|
693
|
+
this.connectResolve = null, this.connectReject = null, t?.(e);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
const q = [
|
|
540
697
|
{ id: "elevenlabs", name: "ElevenLabs", description: "ElevenLabs Conversational AI" },
|
|
541
|
-
{ id: "openai", name: "OpenAI Realtime", description: "OpenAI Realtime API" }
|
|
698
|
+
{ id: "openai", name: "OpenAI Realtime", description: "OpenAI Realtime API" },
|
|
699
|
+
{ id: "kfl", name: "KFL Voice Agent", description: "kfl-voice-agent WebSocket API" }
|
|
542
700
|
];
|
|
543
|
-
function
|
|
701
|
+
function R(s) {
|
|
544
702
|
switch (s) {
|
|
545
703
|
case "elevenlabs":
|
|
546
|
-
return new
|
|
704
|
+
return new F();
|
|
547
705
|
case "openai":
|
|
548
|
-
return new
|
|
706
|
+
return new $();
|
|
707
|
+
case "kfl":
|
|
708
|
+
return new J();
|
|
549
709
|
default:
|
|
550
710
|
throw new Error(`Unknown agent type: ${s}`);
|
|
551
711
|
}
|
|
552
712
|
}
|
|
553
|
-
function
|
|
554
|
-
return
|
|
713
|
+
function _e(s) {
|
|
714
|
+
return q.find((e) => e.id === s);
|
|
555
715
|
}
|
|
556
|
-
class
|
|
716
|
+
class G extends Error {
|
|
557
717
|
status;
|
|
558
718
|
payload;
|
|
559
719
|
url;
|
|
@@ -561,8 +721,8 @@ class F extends Error {
|
|
|
561
721
|
super(e.message), this.name = "ApiError", this.status = e.status, this.payload = e.payload, this.url = e.url;
|
|
562
722
|
}
|
|
563
723
|
}
|
|
564
|
-
const
|
|
565
|
-
class
|
|
724
|
+
const r = /* @__PURE__ */ new Set();
|
|
725
|
+
class Y {
|
|
566
726
|
apiBaseUrl;
|
|
567
727
|
publishableKey;
|
|
568
728
|
callbacks;
|
|
@@ -606,31 +766,31 @@ class L {
|
|
|
606
766
|
}
|
|
607
767
|
/** Connect to the embed session */
|
|
608
768
|
async connect() {
|
|
609
|
-
if (
|
|
769
|
+
if (r.has(this.publishableKey)) {
|
|
610
770
|
console.log("[PersonaEmbed] Connection already in progress, skipping");
|
|
611
771
|
return;
|
|
612
772
|
}
|
|
613
|
-
|
|
773
|
+
r.add(this.publishableKey), this.mounted = !0, this.abortController = new AbortController(), this.setStatus("connecting");
|
|
614
774
|
try {
|
|
615
775
|
const e = await this.fetchSession(this.abortController.signal);
|
|
616
776
|
if (!this.mounted) {
|
|
617
|
-
|
|
777
|
+
r.delete(this.publishableKey);
|
|
618
778
|
return;
|
|
619
779
|
}
|
|
620
780
|
if (await this.initSession(e), await this.initMicrophone(), await this.connectAgent(e.voice_agent_details), !this.mounted) {
|
|
621
|
-
this.cleanup(),
|
|
781
|
+
this.cleanup(), r.delete(this.publishableKey);
|
|
622
782
|
return;
|
|
623
783
|
}
|
|
624
784
|
this.setStatus("connected");
|
|
625
785
|
} catch (e) {
|
|
626
|
-
if (
|
|
786
|
+
if (r.delete(this.publishableKey), e instanceof Error && e.name === "AbortError")
|
|
627
787
|
return;
|
|
628
788
|
console.error("[PersonaEmbed]", e), this.mounted && (this.setStatus("error"), this.callbacks.onError?.(e));
|
|
629
789
|
}
|
|
630
790
|
}
|
|
631
791
|
/** Disconnect and cleanup */
|
|
632
792
|
disconnect() {
|
|
633
|
-
this.mounted = !1, this.abortController?.abort(), this.abortController = null,
|
|
793
|
+
this.mounted = !1, this.abortController?.abort(), this.abortController = null, r.delete(this.publishableKey), this.cleanup(), this.setStatus("disconnected");
|
|
634
794
|
}
|
|
635
795
|
/** Toggle microphone mute */
|
|
636
796
|
toggleMute() {
|
|
@@ -650,38 +810,38 @@ class L {
|
|
|
650
810
|
signal: e
|
|
651
811
|
});
|
|
652
812
|
if (!t.ok) {
|
|
653
|
-
let
|
|
813
|
+
let i;
|
|
654
814
|
try {
|
|
655
|
-
|
|
815
|
+
i = await t.json();
|
|
656
816
|
} catch {
|
|
657
817
|
}
|
|
658
|
-
throw new
|
|
659
|
-
message:
|
|
818
|
+
throw new G({
|
|
819
|
+
message: i?.message ?? "create_session failed",
|
|
660
820
|
status: t.status,
|
|
661
|
-
payload:
|
|
821
|
+
payload: i,
|
|
662
822
|
url: t.url
|
|
663
823
|
});
|
|
664
824
|
}
|
|
665
825
|
if (!t.ok) {
|
|
666
|
-
const
|
|
667
|
-
throw new Error(`create_session failed: ${t.status} ${JSON.stringify(
|
|
826
|
+
const i = await t.json().catch(() => null);
|
|
827
|
+
throw new Error(`create_session failed: ${t.status} ${JSON.stringify(i)}`);
|
|
668
828
|
}
|
|
669
829
|
return t.json();
|
|
670
830
|
}
|
|
671
831
|
async initSession(e) {
|
|
672
|
-
this.session =
|
|
832
|
+
this.session = T({
|
|
673
833
|
serverUrl: e.session_details.server_url,
|
|
674
834
|
participantToken: e.session_details.participant_token,
|
|
675
835
|
agentIdentity: e.session_details.agent_identity,
|
|
676
836
|
onVideoTrack: (t) => {
|
|
677
|
-
console.log("[PersonaEmbed] Setting video track", t.readyState, t.enabled), this._video.srcObject = new MediaStream([t]), this._video.play().catch((
|
|
837
|
+
console.log("[PersonaEmbed] Setting video track", t.readyState, t.enabled), this._video.srcObject = new MediaStream([t]), this._video.play().catch((i) => console.warn("[PersonaEmbed] Video play failed:", i));
|
|
678
838
|
},
|
|
679
839
|
onAudioTrack: (t) => {
|
|
680
840
|
this._audio.srcObject = new MediaStream([t]), this._audio.play().catch(() => {
|
|
681
841
|
});
|
|
682
842
|
},
|
|
683
843
|
onStateChange: (t) => {
|
|
684
|
-
this.mounted && t === "disconnected" && (this.
|
|
844
|
+
this.mounted && t === "disconnected" && (this.disconnect(), this.callbacks.onDisconnect?.());
|
|
685
845
|
},
|
|
686
846
|
onAgentStateChange: (t) => {
|
|
687
847
|
this.mounted && this.setAgentState(t);
|
|
@@ -692,10 +852,10 @@ class L {
|
|
|
692
852
|
onClose: () => {
|
|
693
853
|
this.mounted && this.callbacks.onDisconnect?.();
|
|
694
854
|
}
|
|
695
|
-
}), this.agent =
|
|
855
|
+
}), 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
856
|
this.session?.endAudioTurn(), this.session?.interrupt();
|
|
697
857
|
}), this.agent.on("closed", () => {
|
|
698
|
-
this.mounted && this.callbacks.onDisconnect?.();
|
|
858
|
+
this.mounted && (this.disconnect(), this.callbacks.onDisconnect?.());
|
|
699
859
|
}), this.agent.on("emotion", (t) => this.session?.setEmotion(t)), await this.session.connect();
|
|
700
860
|
}
|
|
701
861
|
async initMicrophone() {
|
|
@@ -703,34 +863,45 @@ class L {
|
|
|
703
863
|
audio: { sampleRate: 16e3, echoCancellation: !0, noiseSuppression: !0 }
|
|
704
864
|
}), this.audioContext = new AudioContext({ sampleRate: 16e3 });
|
|
705
865
|
const e = this.audioContext.createMediaStreamSource(this.stream);
|
|
706
|
-
this.processor = this.audioContext
|
|
866
|
+
this.processor = await x(this.audioContext), this.processor.port.onmessage = (t) => {
|
|
707
867
|
if (!this._isMuted) {
|
|
708
|
-
const
|
|
709
|
-
this.agent?.sendAudio(
|
|
868
|
+
const i = A(t.data);
|
|
869
|
+
this.agent?.sendAudio(i);
|
|
710
870
|
}
|
|
711
871
|
}, e.connect(this.processor), this.processor.connect(this.audioContext.destination);
|
|
712
872
|
}
|
|
713
873
|
async connectAgent(e) {
|
|
714
874
|
if (!this.agent) return;
|
|
715
875
|
const t = { inputSampleRate: 16e3 };
|
|
716
|
-
e.type === "elevenlabs"
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
876
|
+
if (e.type === "elevenlabs")
|
|
877
|
+
await this.agent.connect({
|
|
878
|
+
...t,
|
|
879
|
+
agentId: e.agent_id,
|
|
880
|
+
signedUrl: e.signed_url
|
|
881
|
+
});
|
|
882
|
+
else if (e.type === "openai")
|
|
883
|
+
await this.agent.connect({
|
|
884
|
+
...t,
|
|
885
|
+
apiKey: e.token,
|
|
886
|
+
systemPrompt: e.system_prompt,
|
|
887
|
+
voice: e.voice,
|
|
888
|
+
turnDetection: e.turn_detection
|
|
889
|
+
});
|
|
890
|
+
else if (e.type === "kfl") {
|
|
891
|
+
if (!e.signed_url)
|
|
892
|
+
throw new Error("KFL voice agent requires signed_url");
|
|
893
|
+
await this.agent.connect({
|
|
894
|
+
...t,
|
|
895
|
+
wsUrl: e.signed_url
|
|
896
|
+
});
|
|
897
|
+
}
|
|
727
898
|
}
|
|
728
899
|
cleanup() {
|
|
729
900
|
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
901
|
}
|
|
731
902
|
}
|
|
732
|
-
const
|
|
733
|
-
class
|
|
903
|
+
const l = /* @__PURE__ */ new Set();
|
|
904
|
+
class be {
|
|
734
905
|
voiceAgentDetails;
|
|
735
906
|
sessionDetails;
|
|
736
907
|
callbacks;
|
|
@@ -774,24 +945,24 @@ class $ {
|
|
|
774
945
|
}
|
|
775
946
|
/** Connect to the session */
|
|
776
947
|
async connect() {
|
|
777
|
-
if (
|
|
948
|
+
if (l.has(this.connectionId)) {
|
|
778
949
|
console.log("[PersonaView] Connection already in progress, skipping");
|
|
779
950
|
return;
|
|
780
951
|
}
|
|
781
|
-
|
|
952
|
+
l.add(this.connectionId), this.mounted = !0, this.setStatus("connecting");
|
|
782
953
|
try {
|
|
783
954
|
if (await this.initSession(), await this.initMicrophone(), await this.connectAgent(), !this.mounted) {
|
|
784
|
-
this.cleanup(),
|
|
955
|
+
this.cleanup(), l.delete(this.connectionId);
|
|
785
956
|
return;
|
|
786
957
|
}
|
|
787
958
|
this.setStatus("connected");
|
|
788
959
|
} catch (e) {
|
|
789
|
-
|
|
960
|
+
l.delete(this.connectionId), console.error("[PersonaView]", e), this.mounted && (this.setStatus("error"), this.callbacks.onError?.(e));
|
|
790
961
|
}
|
|
791
962
|
}
|
|
792
963
|
/** Disconnect and cleanup */
|
|
793
964
|
disconnect() {
|
|
794
|
-
this.mounted = !1,
|
|
965
|
+
this.mounted = !1, l.delete(this.connectionId), this.cleanup(), this.setStatus("disconnected");
|
|
795
966
|
}
|
|
796
967
|
/** Toggle microphone mute */
|
|
797
968
|
toggleMute() {
|
|
@@ -804,7 +975,7 @@ class $ {
|
|
|
804
975
|
this._agentState !== e && (this._agentState = e, this.callbacks.onAgentStateChange?.(e));
|
|
805
976
|
}
|
|
806
977
|
async initSession() {
|
|
807
|
-
this.session =
|
|
978
|
+
this.session = T({
|
|
808
979
|
serverUrl: this.sessionDetails.server_url,
|
|
809
980
|
participantToken: this.sessionDetails.participant_token,
|
|
810
981
|
agentIdentity: this.sessionDetails.agent_identity,
|
|
@@ -816,7 +987,7 @@ class $ {
|
|
|
816
987
|
});
|
|
817
988
|
},
|
|
818
989
|
onStateChange: (e) => {
|
|
819
|
-
this.mounted && e === "disconnected" && (this.
|
|
990
|
+
this.mounted && e === "disconnected" && (this.disconnect(), this.callbacks.onDisconnect?.());
|
|
820
991
|
},
|
|
821
992
|
onAgentStateChange: (e) => {
|
|
822
993
|
this.mounted && this.setAgentState(e);
|
|
@@ -827,10 +998,10 @@ class $ {
|
|
|
827
998
|
onClose: () => {
|
|
828
999
|
this.mounted && this.callbacks.onDisconnect?.();
|
|
829
1000
|
}
|
|
830
|
-
}), this.agent =
|
|
1001
|
+
}), 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
1002
|
this.session?.endAudioTurn(), this.session?.interrupt();
|
|
832
1003
|
}), this.agent.on("closed", () => {
|
|
833
|
-
this.mounted && this.callbacks.onDisconnect?.();
|
|
1004
|
+
this.mounted && (this.disconnect(), this.callbacks.onDisconnect?.());
|
|
834
1005
|
}), this.agent.on("emotion", (e) => this.session?.setEmotion(e)), await this.session.connect();
|
|
835
1006
|
}
|
|
836
1007
|
async initMicrophone() {
|
|
@@ -838,46 +1009,614 @@ class $ {
|
|
|
838
1009
|
audio: { sampleRate: 16e3, echoCancellation: !0, noiseSuppression: !0 }
|
|
839
1010
|
}), this.audioContext = new AudioContext({ sampleRate: 16e3 });
|
|
840
1011
|
const e = this.audioContext.createMediaStreamSource(this.stream);
|
|
841
|
-
this.processor = this.audioContext
|
|
1012
|
+
this.processor = await x(this.audioContext), this.processor.port.onmessage = (t) => {
|
|
842
1013
|
if (!this._isMuted) {
|
|
843
|
-
const
|
|
844
|
-
this.agent?.sendAudio(
|
|
1014
|
+
const i = A(t.data);
|
|
1015
|
+
this.agent?.sendAudio(i);
|
|
845
1016
|
}
|
|
846
1017
|
}, e.connect(this.processor), this.processor.connect(this.audioContext.destination);
|
|
847
1018
|
}
|
|
848
1019
|
async connectAgent() {
|
|
849
1020
|
if (!this.agent) return;
|
|
850
1021
|
const e = this.voiceAgentDetails, t = { inputSampleRate: 16e3 };
|
|
851
|
-
e.type === "elevenlabs"
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
1022
|
+
if (e.type === "elevenlabs")
|
|
1023
|
+
await this.agent.connect({
|
|
1024
|
+
...t,
|
|
1025
|
+
agentId: e.agent_id,
|
|
1026
|
+
signedUrl: e.signed_url
|
|
1027
|
+
});
|
|
1028
|
+
else if (e.type === "openai")
|
|
1029
|
+
await this.agent.connect({
|
|
1030
|
+
...t,
|
|
1031
|
+
apiKey: e.token,
|
|
1032
|
+
systemPrompt: e.system_prompt,
|
|
1033
|
+
voice: e.voice,
|
|
1034
|
+
turnDetection: e.turn_detection
|
|
1035
|
+
});
|
|
1036
|
+
else if (e.type === "kfl") {
|
|
1037
|
+
if (!e.signed_url)
|
|
1038
|
+
throw new Error("KFL voice agent requires signed_url");
|
|
1039
|
+
await this.agent.connect({
|
|
1040
|
+
...t,
|
|
1041
|
+
wsUrl: e.signed_url
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
862
1044
|
}
|
|
863
1045
|
cleanup() {
|
|
864
1046
|
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
1047
|
}
|
|
866
1048
|
}
|
|
1049
|
+
const f = ["minimized", "active", "hidden"], Z = ["bottom-right", "bottom-left", "top-right", "top-left"], X = [
|
|
1050
|
+
"publishable-key",
|
|
1051
|
+
"api-base-url",
|
|
1052
|
+
"initial-state",
|
|
1053
|
+
"controlled-widget-state",
|
|
1054
|
+
"active-width",
|
|
1055
|
+
"active-height",
|
|
1056
|
+
"minimized-width",
|
|
1057
|
+
"minimized-height",
|
|
1058
|
+
"inline",
|
|
1059
|
+
"corner",
|
|
1060
|
+
"hide-ui",
|
|
1061
|
+
"show-minimize-button",
|
|
1062
|
+
"controlled-show-minimize-button",
|
|
1063
|
+
"button-color",
|
|
1064
|
+
"button-color-opacity",
|
|
1065
|
+
"video-fit",
|
|
1066
|
+
"preview-image",
|
|
1067
|
+
"call-to-action"
|
|
1068
|
+
], E = "https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap";
|
|
1069
|
+
function Q() {
|
|
1070
|
+
if (document.querySelector(`link[href="${E}"]`)) return;
|
|
1071
|
+
const s = document.createElement("link");
|
|
1072
|
+
s.rel = "stylesheet", s.href = E, document.head.appendChild(s);
|
|
1073
|
+
}
|
|
1074
|
+
const ee = `
|
|
1075
|
+
:host {
|
|
1076
|
+
display: block;
|
|
1077
|
+
font-family: 'Noto Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
.kfl-widget {
|
|
1081
|
+
position: fixed;
|
|
1082
|
+
z-index: 2147483647;
|
|
1083
|
+
transition: width 0.2s ease, height 0.2s ease;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
:host([inline]) .kfl-widget {
|
|
1087
|
+
position: relative;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
.kfl-widget--hidden .kfl-widget-inner {
|
|
1091
|
+
display: none;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
.kfl-reveal-btn {
|
|
1095
|
+
position: absolute;
|
|
1096
|
+
width: 40px;
|
|
1097
|
+
height: 40px;
|
|
1098
|
+
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
1099
|
+
background: rgba(30, 30, 30, 0.95);
|
|
1100
|
+
color: rgba(255, 255, 255, 0.7);
|
|
1101
|
+
cursor: pointer;
|
|
1102
|
+
display: none;
|
|
1103
|
+
align-items: center;
|
|
1104
|
+
justify-content: center;
|
|
1105
|
+
transition: background 0.15s, color 0.15s;
|
|
1106
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
.kfl-reveal-btn:hover {
|
|
1110
|
+
background: rgba(50, 50, 50, 0.95);
|
|
1111
|
+
color: #fff;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
.kfl-reveal-btn svg {
|
|
1115
|
+
width: 16px;
|
|
1116
|
+
height: 16px;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
.kfl-reveal-btn--left {
|
|
1120
|
+
border-radius: 0 50% 50% 0;
|
|
1121
|
+
left: 0;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
.kfl-reveal-btn--right {
|
|
1125
|
+
border-radius: 50% 0 0 50%;
|
|
1126
|
+
right: 0;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
.kfl-widget--hidden .kfl-reveal-btn {
|
|
1130
|
+
display: flex;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
.kfl-widget-inner {
|
|
1134
|
+
width: 100%;
|
|
1135
|
+
height: 100%;
|
|
1136
|
+
border-radius: 12px;
|
|
1137
|
+
overflow: hidden;
|
|
1138
|
+
position: relative;
|
|
1139
|
+
background: #000;
|
|
1140
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
.kfl-join-btn {
|
|
1144
|
+
position: absolute;
|
|
1145
|
+
bottom: 8px;
|
|
1146
|
+
left: 8px;
|
|
1147
|
+
right: 8px;
|
|
1148
|
+
z-index: 3;
|
|
1149
|
+
display: flex;
|
|
1150
|
+
align-items: center;
|
|
1151
|
+
justify-content: center;
|
|
1152
|
+
gap: 6px;
|
|
1153
|
+
padding: 6px 12px;
|
|
1154
|
+
border-radius: 999px;
|
|
1155
|
+
border: none;
|
|
1156
|
+
color: #fff;
|
|
1157
|
+
font-size: 12px;
|
|
1158
|
+
font-weight: 500;
|
|
1159
|
+
cursor: pointer;
|
|
1160
|
+
transition: opacity 0.15s;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
.kfl-join-btn:hover {
|
|
1164
|
+
opacity: 0.9;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
.kfl-join-btn svg {
|
|
1168
|
+
width: 14px;
|
|
1169
|
+
height: 14px;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
.kfl-toolbar {
|
|
1173
|
+
position: absolute;
|
|
1174
|
+
bottom: 12px;
|
|
1175
|
+
left: 50%;
|
|
1176
|
+
transform: translateX(-50%);
|
|
1177
|
+
z-index: 3;
|
|
1178
|
+
display: none;
|
|
1179
|
+
align-items: center;
|
|
1180
|
+
justify-content: center;
|
|
1181
|
+
gap: 8px;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
.kfl-mic-btn {
|
|
1185
|
+
display: flex;
|
|
1186
|
+
align-items: center;
|
|
1187
|
+
justify-content: center;
|
|
1188
|
+
width: 40px;
|
|
1189
|
+
height: 40px;
|
|
1190
|
+
border-radius: 9999px;
|
|
1191
|
+
border: none;
|
|
1192
|
+
background: rgba(0, 0, 0, 0.4);
|
|
1193
|
+
backdrop-filter: blur(4px);
|
|
1194
|
+
color: rgba(255, 255, 255, 0.8);
|
|
1195
|
+
cursor: pointer;
|
|
1196
|
+
transition: background 0.15s, color 0.15s;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
.kfl-mic-btn:hover {
|
|
1200
|
+
background: rgba(0, 0, 0, 0.6);
|
|
1201
|
+
color: #fff;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
.kfl-mic-btn svg {
|
|
1205
|
+
width: 20px;
|
|
1206
|
+
height: 20px;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
.kfl-endcall-btn {
|
|
1210
|
+
display: flex;
|
|
1211
|
+
align-items: center;
|
|
1212
|
+
justify-content: center;
|
|
1213
|
+
width: 40px;
|
|
1214
|
+
height: 40px;
|
|
1215
|
+
border-radius: 9999px;
|
|
1216
|
+
border: none;
|
|
1217
|
+
background-color: #dc2626;
|
|
1218
|
+
color: #fff;
|
|
1219
|
+
cursor: pointer;
|
|
1220
|
+
box-shadow: 0 10px 15px -3px rgba(127, 29, 29, 0.2);
|
|
1221
|
+
transition: background-color 0.15s, box-shadow 0.15s;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
.kfl-endcall-btn:hover {
|
|
1225
|
+
background-color: #b91c1c;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
.kfl-endcall-btn svg {
|
|
1229
|
+
width: 20px;
|
|
1230
|
+
height: 20px;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
.kfl-toggle-btn {
|
|
1234
|
+
position: absolute;
|
|
1235
|
+
top: 6px;
|
|
1236
|
+
right: 6px;
|
|
1237
|
+
z-index: 3;
|
|
1238
|
+
width: 24px;
|
|
1239
|
+
height: 24px;
|
|
1240
|
+
border-radius: 50%;
|
|
1241
|
+
background: rgba(0, 0, 0, 0.4);
|
|
1242
|
+
backdrop-filter: blur(4px);
|
|
1243
|
+
border: none;
|
|
1244
|
+
color: rgba(255, 255, 255, 0.8);
|
|
1245
|
+
cursor: pointer;
|
|
1246
|
+
display: flex;
|
|
1247
|
+
align-items: center;
|
|
1248
|
+
justify-content: center;
|
|
1249
|
+
transition: background 0.15s, color 0.15s;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
.kfl-toggle-btn:hover {
|
|
1253
|
+
background: rgba(0, 0, 0, 0.6);
|
|
1254
|
+
color: #fff;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
.kfl-toggle-btn svg {
|
|
1258
|
+
width: 12px;
|
|
1259
|
+
height: 12px;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
.kfl-embed-container {
|
|
1263
|
+
position: relative;
|
|
1264
|
+
width: 100%;
|
|
1265
|
+
height: 100%;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
.kfl-preview-img {
|
|
1269
|
+
position: absolute;
|
|
1270
|
+
inset: 0;
|
|
1271
|
+
width: 100%;
|
|
1272
|
+
height: 100%;
|
|
1273
|
+
object-fit: cover;
|
|
1274
|
+
pointer-events: none;
|
|
1275
|
+
z-index: 1;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
.kfl-spinner {
|
|
1279
|
+
position: absolute;
|
|
1280
|
+
inset: 0;
|
|
1281
|
+
z-index: 2;
|
|
1282
|
+
display: none;
|
|
1283
|
+
align-items: center;
|
|
1284
|
+
justify-content: center;
|
|
1285
|
+
background: rgba(0, 0, 0, 0.3);
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
.kfl-spinner--visible {
|
|
1289
|
+
display: flex;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
.kfl-spinner-ring {
|
|
1293
|
+
width: 28px;
|
|
1294
|
+
height: 28px;
|
|
1295
|
+
border: 2.5px solid rgba(255, 255, 255, 0.2);
|
|
1296
|
+
border-top-color: #fff;
|
|
1297
|
+
border-radius: 50%;
|
|
1298
|
+
animation: kfl-spin 0.7s linear infinite;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
@keyframes kfl-spin {
|
|
1302
|
+
to { transform: rotate(360deg); }
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
.kfl-error-toast {
|
|
1306
|
+
position: absolute;
|
|
1307
|
+
bottom: 44px;
|
|
1308
|
+
left: 8px;
|
|
1309
|
+
right: 8px;
|
|
1310
|
+
padding: 6px 10px;
|
|
1311
|
+
border-radius: 8px;
|
|
1312
|
+
background: rgba(220, 38, 38, 0.85);
|
|
1313
|
+
backdrop-filter: blur(4px);
|
|
1314
|
+
color: #fff;
|
|
1315
|
+
font-size: 11px;
|
|
1316
|
+
line-height: 1.4;
|
|
1317
|
+
text-align: center;
|
|
1318
|
+
opacity: 0;
|
|
1319
|
+
transform: translateY(4px);
|
|
1320
|
+
transition: opacity 0.2s, transform 0.2s;
|
|
1321
|
+
pointer-events: none;
|
|
1322
|
+
z-index: 5;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
.kfl-error-toast--visible {
|
|
1326
|
+
opacity: 1;
|
|
1327
|
+
transform: translateY(0);
|
|
1328
|
+
}
|
|
1329
|
+
`, 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 = `
|
|
1330
|
+
<div class="kfl-widget">
|
|
1331
|
+
<div class="kfl-widget-inner">
|
|
1332
|
+
<img class="kfl-preview-img" alt="Persona preview" draggable="false" />
|
|
1333
|
+
<div class="kfl-embed-container"></div>
|
|
1334
|
+
<div class="kfl-spinner"><div class="kfl-spinner-ring"></div></div>
|
|
1335
|
+
<div class="kfl-error-toast"></div>
|
|
1336
|
+
<button type="button" class="kfl-join-btn"></button>
|
|
1337
|
+
<div class="kfl-toolbar">
|
|
1338
|
+
<button type="button" class="kfl-mic-btn"></button>
|
|
1339
|
+
<button type="button" class="kfl-endcall-btn"></button>
|
|
1340
|
+
</div>
|
|
1341
|
+
<button type="button" class="kfl-toggle-btn"></button>
|
|
1342
|
+
</div>
|
|
1343
|
+
<button type="button" class="kfl-reveal-btn"></button>
|
|
1344
|
+
</div>
|
|
1345
|
+
`;
|
|
1346
|
+
class ve extends HTMLElement {
|
|
1347
|
+
static get observedAttributes() {
|
|
1348
|
+
return [...X];
|
|
1349
|
+
}
|
|
1350
|
+
shadow;
|
|
1351
|
+
embed = null;
|
|
1352
|
+
_widgetState = "minimized";
|
|
1353
|
+
_connected = !1;
|
|
1354
|
+
_connecting = !1;
|
|
1355
|
+
_initialized = !1;
|
|
1356
|
+
widgetEl;
|
|
1357
|
+
innerEl;
|
|
1358
|
+
containerEl;
|
|
1359
|
+
previewImg;
|
|
1360
|
+
spinnerEl;
|
|
1361
|
+
errorToast;
|
|
1362
|
+
errorTimer = null;
|
|
1363
|
+
joinBtn;
|
|
1364
|
+
endCallBtn;
|
|
1365
|
+
toggleBtn;
|
|
1366
|
+
revealBtn;
|
|
1367
|
+
toolbarEl;
|
|
1368
|
+
micBtn;
|
|
1369
|
+
_onJoinClick = (e) => {
|
|
1370
|
+
e.stopPropagation(), this._handleJoinCall();
|
|
1371
|
+
};
|
|
1372
|
+
_onEndCallClick = () => this._handleEndCall();
|
|
1373
|
+
_onMicClick = () => this._handleMicToggle();
|
|
1374
|
+
_onToggleClick = (e) => {
|
|
1375
|
+
e.stopPropagation(), this._handleToggle();
|
|
1376
|
+
};
|
|
1377
|
+
_onRevealClick = () => this._handleReveal();
|
|
1378
|
+
_onInnerClick = (e) => this._handleInnerClick(e);
|
|
1379
|
+
constructor() {
|
|
1380
|
+
super(), this.shadow = this.attachShadow({ mode: "open" });
|
|
1381
|
+
}
|
|
1382
|
+
connectedCallback() {
|
|
1383
|
+
Q(), this._initialized || (this._initDom(), this._initialized = !0), this._widgetState = this._resolveInitialState(), this._applyState();
|
|
1384
|
+
}
|
|
1385
|
+
disconnectedCallback() {
|
|
1386
|
+
this.errorTimer && clearTimeout(this.errorTimer), this._disconnectSession(), this._setWidgetState("minimized", { emit: !1 });
|
|
1387
|
+
}
|
|
1388
|
+
attributeChangedCallback(e, t, i) {
|
|
1389
|
+
if (this._initialized) {
|
|
1390
|
+
if (e === "call-to-action") {
|
|
1391
|
+
this._renderJoinLabel();
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
if (e === "preview-image") {
|
|
1395
|
+
this._applyPreviewImage(i);
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
if (e === "controlled-widget-state") {
|
|
1399
|
+
i && f.includes(i) ? this._setWidgetState(i, { emit: !1 }) : this._applyState();
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
this._applyState();
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
// --- Public API ---
|
|
1406
|
+
async mute() {
|
|
1407
|
+
this.embed && (this.embed.audioElement.muted = !0);
|
|
1408
|
+
}
|
|
1409
|
+
async unmute() {
|
|
1410
|
+
this.embed && (this.embed.audioElement.muted = !1);
|
|
1411
|
+
}
|
|
1412
|
+
isMuted() {
|
|
1413
|
+
return this.embed?.audioElement.muted ?? !1;
|
|
1414
|
+
}
|
|
1415
|
+
canUnmute() {
|
|
1416
|
+
return this._connected;
|
|
1417
|
+
}
|
|
1418
|
+
async micOn() {
|
|
1419
|
+
this._connected || (this._connecting = !0, this._setWidgetState("active", { emit: !1 }), await this._connect()), this.embed && this.embed.isMuted && this.embed.toggleMute(), this._updateMicIcon();
|
|
1420
|
+
}
|
|
1421
|
+
async micOff() {
|
|
1422
|
+
this.embed && !this.embed.isMuted && this.embed.toggleMute(), this._updateMicIcon();
|
|
1423
|
+
}
|
|
1424
|
+
isMicOn() {
|
|
1425
|
+
return this.embed ? !this.embed.isMuted : !0;
|
|
1426
|
+
}
|
|
1427
|
+
canTurnOnMic() {
|
|
1428
|
+
return !!this.getAttribute("publishable-key");
|
|
1429
|
+
}
|
|
1430
|
+
setEmotion(e) {
|
|
1431
|
+
console.warn("[kfl-embed] setEmotion requires direct session access (not yet exposed by PersonaEmbed)");
|
|
1432
|
+
}
|
|
1433
|
+
// --- Internals ---
|
|
1434
|
+
_initDom() {
|
|
1435
|
+
const e = document.createElement("style");
|
|
1436
|
+
e.textContent = ee, this.shadow.appendChild(e);
|
|
1437
|
+
const t = document.createElement("template");
|
|
1438
|
+
t.innerHTML = me;
|
|
1439
|
+
const i = t.content.cloneNode(!0);
|
|
1440
|
+
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);
|
|
1441
|
+
}
|
|
1442
|
+
_requireElement(e, t) {
|
|
1443
|
+
const i = e.querySelector(t);
|
|
1444
|
+
if (!i) throw new Error(`[kfl-embed] Missing template node: ${t}`);
|
|
1445
|
+
return i;
|
|
1446
|
+
}
|
|
1447
|
+
_resolveInitialState() {
|
|
1448
|
+
const e = this.getAttribute("controlled-widget-state");
|
|
1449
|
+
if (e && f.includes(e))
|
|
1450
|
+
return e;
|
|
1451
|
+
const t = this.getAttribute("initial-state");
|
|
1452
|
+
return t && f.includes(t) ? t : "minimized";
|
|
1453
|
+
}
|
|
1454
|
+
_renderJoinLabel() {
|
|
1455
|
+
this.joinBtn.innerHTML = `${te} ${this.getAttribute("call-to-action") || le}`;
|
|
1456
|
+
}
|
|
1457
|
+
_applyPreviewImage(e) {
|
|
1458
|
+
if (e) {
|
|
1459
|
+
this.previewImg.src = e, this.previewImg.style.display = this._connected ? "none" : "block";
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
this.previewImg.removeAttribute("src"), this.previewImg.style.display = "none";
|
|
1463
|
+
}
|
|
1464
|
+
_readRenderConfig() {
|
|
1465
|
+
const e = this._widgetState === "minimized", t = this._widgetState === "hidden";
|
|
1466
|
+
return {
|
|
1467
|
+
hideUI: this.hasAttribute("hide-ui"),
|
|
1468
|
+
isInline: this.hasAttribute("inline"),
|
|
1469
|
+
showMin: this._shouldShowMinimizeButton(),
|
|
1470
|
+
isMinimized: e,
|
|
1471
|
+
isHidden: t,
|
|
1472
|
+
isActive: this._widgetState === "active",
|
|
1473
|
+
canExpandFromMinimized: e && this._hasLiveSession(),
|
|
1474
|
+
corner: this._getCorner(),
|
|
1475
|
+
buttonColor: this.getAttribute("button-color") || pe,
|
|
1476
|
+
buttonOpacity: this._getAttrNum("button-color-opacity", ge)
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
_getAttrNum(e, t) {
|
|
1480
|
+
const i = this.getAttribute(e);
|
|
1481
|
+
if (!i) return t;
|
|
1482
|
+
const n = parseFloat(i);
|
|
1483
|
+
return isNaN(n) ? t : n;
|
|
1484
|
+
}
|
|
1485
|
+
_getCorner() {
|
|
1486
|
+
const e = this.getAttribute("corner");
|
|
1487
|
+
return e && Z.includes(e) ? e : "bottom-right";
|
|
1488
|
+
}
|
|
1489
|
+
_applyLayout(e) {
|
|
1490
|
+
if (!this.widgetEl) return;
|
|
1491
|
+
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);
|
|
1492
|
+
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));
|
|
1493
|
+
}
|
|
1494
|
+
_applyState() {
|
|
1495
|
+
if (!this.widgetEl) return;
|
|
1496
|
+
const e = this._readRenderConfig();
|
|
1497
|
+
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);
|
|
1498
|
+
}
|
|
1499
|
+
_renderJoinButton(e) {
|
|
1500
|
+
this.joinBtn.style.display = this._connected || this._connecting || e.isHidden ? "none" : "flex";
|
|
1501
|
+
}
|
|
1502
|
+
_renderToolbar(e) {
|
|
1503
|
+
this.toolbarEl.style.display = this._connected && !e.hideUI && !e.isMinimized && !e.isHidden ? "flex" : "none";
|
|
1504
|
+
}
|
|
1505
|
+
_renderToggleButton(e) {
|
|
1506
|
+
!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";
|
|
1507
|
+
}
|
|
1508
|
+
_renderRevealButton(e) {
|
|
1509
|
+
const t = e.corner.includes("right");
|
|
1510
|
+
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);
|
|
1511
|
+
}
|
|
1512
|
+
_renderJoinButtonColor(e) {
|
|
1513
|
+
const t = Math.max(0, Math.min(1, e.buttonOpacity)), i = Math.round(t * 255).toString(16).padStart(2, "0");
|
|
1514
|
+
this.joinBtn.style.backgroundColor = `${e.buttonColor}${i}`;
|
|
1515
|
+
}
|
|
1516
|
+
_shouldShowMinimizeButton() {
|
|
1517
|
+
const e = this.getAttribute("controlled-show-minimize-button");
|
|
1518
|
+
return e !== null ? e !== "false" : this.getAttribute("show-minimize-button") !== "false";
|
|
1519
|
+
}
|
|
1520
|
+
_handleJoinCall() {
|
|
1521
|
+
this._connecting = !0, this._setWidgetState("active", { emit: !1 }), this._connect();
|
|
1522
|
+
}
|
|
1523
|
+
_handleToggle() {
|
|
1524
|
+
if (this._widgetState === "minimized") {
|
|
1525
|
+
if (this.hasAttribute("inline")) return;
|
|
1526
|
+
this._hasLiveSession() && this._disconnectSession(), this._setWidgetState("hidden");
|
|
1527
|
+
} else this._widgetState === "active" && this._setWidgetState("minimized");
|
|
1528
|
+
}
|
|
1529
|
+
_handleInnerClick(e) {
|
|
1530
|
+
if (this._widgetState !== "minimized" || !this._hasLiveSession()) return;
|
|
1531
|
+
const t = e.composedPath();
|
|
1532
|
+
t.includes(this.joinBtn) || t.includes(this.toggleBtn) || this._setWidgetState("active");
|
|
1533
|
+
}
|
|
1534
|
+
_handleEndCall() {
|
|
1535
|
+
this._widgetState === "active" && (this._disconnectSession(), this._setWidgetState("minimized"));
|
|
1536
|
+
}
|
|
1537
|
+
_handleReveal() {
|
|
1538
|
+
this._setWidgetState("minimized");
|
|
1539
|
+
}
|
|
1540
|
+
_setWidgetState(e, t) {
|
|
1541
|
+
const i = t?.emit ?? !0, n = this._widgetState !== e;
|
|
1542
|
+
this._widgetState = e, n && i && this.dispatchEvent(new CustomEvent("widgetstatechange", { detail: { state: e } })), this._applyState();
|
|
1543
|
+
}
|
|
1544
|
+
_handleMicToggle() {
|
|
1545
|
+
this.embed && (this.embed.toggleMute(), this._updateMicIcon());
|
|
1546
|
+
}
|
|
1547
|
+
_updateMicIcon() {
|
|
1548
|
+
if (!this.micBtn) return;
|
|
1549
|
+
const e = this.embed ? this.embed.isMuted : !1;
|
|
1550
|
+
this.micBtn.innerHTML = e ? ae : S;
|
|
1551
|
+
}
|
|
1552
|
+
_hasLiveSession() {
|
|
1553
|
+
return this._connected || this._connecting || !!this.embed;
|
|
1554
|
+
}
|
|
1555
|
+
_disconnectSession() {
|
|
1556
|
+
try {
|
|
1557
|
+
this.embed?.disconnect();
|
|
1558
|
+
} catch (e) {
|
|
1559
|
+
console.warn("[kfl-embed] Disconnect error:", e);
|
|
1560
|
+
}
|
|
1561
|
+
this._connected = !1, this._connecting = !1, this.embed = null, this.containerEl.innerHTML = "", this.containerEl.removeAttribute("style"), this._applyPreviewImage(this.getAttribute("preview-image"));
|
|
1562
|
+
}
|
|
1563
|
+
_resetToMinimized() {
|
|
1564
|
+
this._disconnectSession(), this._setWidgetState("minimized", { emit: !1 });
|
|
1565
|
+
}
|
|
1566
|
+
_showError(e) {
|
|
1567
|
+
this.errorTimer && clearTimeout(this.errorTimer), this.errorToast.textContent = e, this.errorToast.classList.add("kfl-error-toast--visible"), this.errorTimer = setTimeout(() => {
|
|
1568
|
+
this.errorToast.classList.remove("kfl-error-toast--visible"), this.errorTimer = null;
|
|
1569
|
+
}, 4e3);
|
|
1570
|
+
}
|
|
1571
|
+
async _connect() {
|
|
1572
|
+
const e = this.getAttribute("publishable-key");
|
|
1573
|
+
if (!e) {
|
|
1574
|
+
console.error("[kfl-embed] publishable-key attribute is required"), this._resetToMinimized(), this._showError("Missing publishable key");
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
if (this._connected || this.embed) return;
|
|
1578
|
+
const t = this.getAttribute("api-base-url") || void 0, i = this.getAttribute("video-fit") || "cover";
|
|
1579
|
+
this.previewImg.style.display = "none";
|
|
1580
|
+
try {
|
|
1581
|
+
this.embed = new Y({
|
|
1582
|
+
container: this.containerEl,
|
|
1583
|
+
publishableKey: e,
|
|
1584
|
+
apiBaseUrl: t,
|
|
1585
|
+
videoFit: i,
|
|
1586
|
+
onStateChange: (n) => {
|
|
1587
|
+
n === "connected" && (this._connected = !0, this._connecting = !1, this._applyState()), this.dispatchEvent(new CustomEvent("statechange", { detail: { status: n } }));
|
|
1588
|
+
},
|
|
1589
|
+
onDisconnect: () => {
|
|
1590
|
+
this._resetToMinimized(), this.dispatchEvent(new CustomEvent("disconnected"));
|
|
1591
|
+
},
|
|
1592
|
+
onError: (n) => {
|
|
1593
|
+
console.error("[kfl-embed] Error:", n), this._resetToMinimized(), this._showError("Unable to connect"), this.dispatchEvent(new CustomEvent("error", { detail: { error: n } }));
|
|
1594
|
+
},
|
|
1595
|
+
onAgentStateChange: (n) => {
|
|
1596
|
+
this.dispatchEvent(new CustomEvent("agentstatechange", { detail: { state: n } }));
|
|
1597
|
+
}
|
|
1598
|
+
}), await this.embed.connect();
|
|
1599
|
+
} catch (n) {
|
|
1600
|
+
console.error("[kfl-embed] Connection failed:", n), this._resetToMinimized(), this._showError("Unable to connect"), this.dispatchEvent(new CustomEvent("error", { detail: { error: n } }));
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
867
1604
|
export {
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
$ as
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
1605
|
+
q as AGENT_REGISTRY,
|
|
1606
|
+
v as BaseAgent,
|
|
1607
|
+
F as ElevenLabsAgent,
|
|
1608
|
+
G as KeyframeApiError,
|
|
1609
|
+
J as KflAgent,
|
|
1610
|
+
ve as KflEmbedElement,
|
|
1611
|
+
$ as OpenAIRealtimeAgent,
|
|
1612
|
+
Y as PersonaEmbed,
|
|
1613
|
+
be as PersonaView,
|
|
1614
|
+
h as SAMPLE_RATE,
|
|
1615
|
+
_ as base64ToBytes,
|
|
1616
|
+
b as bytesToBase64,
|
|
1617
|
+
R as createAgent,
|
|
1618
|
+
L as createEventEmitter,
|
|
1619
|
+
A as floatTo16BitPCM,
|
|
1620
|
+
_e as getAgentInfo,
|
|
1621
|
+
u as resamplePcm
|
|
883
1622
|
};
|