@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/dist/index.js CHANGED
@@ -1,30 +1,30 @@
1
- import { createClient as _ } from "@keyframelabs/sdk";
2
- const u = 24e3;
3
- function f(s) {
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 n = 0; n < e.length; n++)
6
- t[n] = e.charCodeAt(n);
5
+ for (let i = 0; i < e.length; i++)
6
+ t[i] = e.charCodeAt(i);
7
7
  return t;
8
8
  }
9
- function S(s) {
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 p(s, e, t) {
15
+ function u(s, e, t) {
16
16
  if (e === t)
17
17
  return s;
18
- const n = new Int16Array(s.buffer, s.byteOffset, s.length / 2), a = e / t, i = Math.floor(n.length / a), c = new Int16Array(i);
19
- for (let l = 0; l < i; l++) {
20
- const m = l * a, d = Math.floor(m), w = Math.min(d + 1, n.length - 1), g = m - d;
21
- c[l] = Math.round(
22
- n[d] * (1 - g) + n[w] * g
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 C() {
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((n) => n(t));
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 v(s) {
44
+ function A(s) {
45
45
  const e = new Int16Array(s.length);
46
46
  for (let t = 0; t < s.length; t++) {
47
- const n = Math.max(-1, Math.min(1, s[t]));
48
- e[t] = n < 0 ? n * 32768 : n * 32767;
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 E = 16e3;
53
- class y {
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 = C();
57
- inputSampleRate = E;
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 A = ["neutral", "angry", "sad", "happy"], I = "wss://api.elevenlabs.io/v1/convai/conversation";
117
- class R extends y {
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 = `${I}?agent_id=${e.agentId}`, e.apiKey && (t += `&xi-api-key=${e.apiKey}`)), new Promise((n, a) => {
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"), n();
181
+ this.setState("listening"), i();
145
182
  }, this.ws.onerror = () => {
146
- a(new Error("Failed to connect to ElevenLabs"));
147
- }, this.ws.onclose = (i) => {
148
- this.ws = null, this.setState("idle"), this.emitClosed(i.code, i.reason);
149
- }, this.ws.onmessage = (i) => {
150
- this.handleMessage(i.data);
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 n = t.agent_output_audio_format.match(/pcm_(\d+)/);
188
- n && (this.outputSampleRate = parseInt(n[1], 10));
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 n = t.user_input_audio_format.match(/pcm_(\d+)/);
192
- n && (this.expectedInputSampleRate = parseInt(n[1], 10));
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 a = f(t.audio_base_64);
209
- this.outputSampleRate !== u && (a = p(a, this.outputSampleRate, u)), this.events.emit("audio", a);
210
- const i = a.length / 2 / u * 1e3;
211
- this.turnStartTime === 0 && (this.turnStartTime = Date.now()), this.accumulatedDurationMs += i, console.debug(
212
- `[ElevenLabs] audio chunk: ${a.length} bytes, +${i.toFixed(0)}ms, totalDuration=${this.accumulatedDurationMs.toFixed(0)}ms, agentResponse=${this.agentResponseReceived}`
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 n = t.parameters?.emotion?.toLowerCase();
256
- n && A.includes(n) && this.events.emit("emotion", n);
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 = p(e, this.sourceInputSampleRate, this.expectedInputSampleRate)), this.ws.send(JSON.stringify({
277
- user_audio_chunk: S(t)
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 T = ["neutral", "angry", "sad", "happy"], k = "wss://api.openai.com/v1/realtime", M = "gpt-realtime", h = 24e3, O = {
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 P extends y {
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 ?? M;
345
- return this.initialSessionUpdate = this.buildSessionUpdate(e, t), new Promise((n, a) => {
346
- this.connectResolve = n, this.connectReject = a, this.connectTimeout = setTimeout(() => {
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
- `${k}?model=${encodeURIComponent(t)}`,
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 = (i) => {
391
+ }, this.ws.onclose = (o) => {
355
392
  if (this.clearConnectTimeout(), this.connectReject) {
356
- const c = i.reason ? `: ${i.reason}` : "";
357
- this.rejectPendingConnect(new Error(`OpenAI Realtime closed before initialization (${i.code}${c})`));
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(i.code, i.reason);
360
- }, this.ws.onmessage = (i) => {
361
- this.handleMessage(i.data);
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 n = performance.now() - this.pendingFunctionCallStartedAtMs;
417
+ const i = performance.now() - this.pendingFunctionCallStartedAtMs;
381
418
  console.debug("[OpenAIRealtime] Function call latency", {
382
419
  calls: this.pendingFunctionCallNames,
383
- latencyMs: Math.round(n)
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", f(t.delta));
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 n = t.error?.message ?? t.message ?? "Unknown OpenAI Realtime error";
416
- this.rejectPendingConnect(new Error(n)), console.error("[OpenAIRealtime] Server error:", t);
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 !== h && (t = p(e, this.sourceInputSampleRate, h)), this.ws.send(JSON.stringify({
464
+ this.sourceInputSampleRate !== m && (t = u(e, this.sourceInputSampleRate, m)), this.ws.send(JSON.stringify({
428
465
  type: "input_audio_buffer.append",
429
- audio: S(t)
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 n = e.turnDetection ?? { type: "semantic_vad", eagerness: "high" };
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: h
485
+ rate: m
449
486
  },
450
- turn_detection: n
487
+ turn_detection: i
451
488
  },
452
489
  output: {
453
490
  format: {
454
491
  type: "audio/pcm",
455
- rate: u
492
+ rate: h
456
493
  },
457
494
  ...e.voice ? { voice: e.voice } : {}
458
495
  }
459
496
  },
460
- tools: [O],
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(x);
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 n = [];
483
- for (const a of e) {
484
- if (!a.call_id || this.handledFunctionCallIds.has(a.call_id))
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(a.call_id), n.push(a.name ?? "unknown");
487
- const i = this.handleFunctionCall(a);
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: a.call_id,
493
- output: JSON.stringify(i)
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 = n, console.debug("[OpenAIRealtime] Function call received", {
498
- calls: n
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 n = (e.arguments ? JSON.parse(e.arguments) : {}).emotion?.toLowerCase();
506
- return n && T.includes(n) ? (this.events.emit("emotion", n), { result: "ok" }) : { error: "Invalid emotion" };
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 x(s) {
573
+ function H(s) {
537
574
  return s.type === "function_call";
538
575
  }
539
- const D = [
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 b(s) {
701
+ function R(s) {
544
702
  switch (s) {
545
703
  case "elevenlabs":
546
- return new R();
704
+ return new F();
547
705
  case "openai":
548
- return new P();
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 N(s) {
554
- return D.find((e) => e.id === s);
713
+ function _e(s) {
714
+ return q.find((e) => e.id === s);
555
715
  }
556
- class F extends Error {
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 o = /* @__PURE__ */ new Set();
565
- class L {
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 (o.has(this.publishableKey)) {
769
+ if (r.has(this.publishableKey)) {
610
770
  console.log("[PersonaEmbed] Connection already in progress, skipping");
611
771
  return;
612
772
  }
613
- o.add(this.publishableKey), this.mounted = !0, this.abortController = new AbortController(), this.setStatus("connecting");
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
- o.delete(this.publishableKey);
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(), o.delete(this.publishableKey);
781
+ this.cleanup(), r.delete(this.publishableKey);
622
782
  return;
623
783
  }
624
784
  this.setStatus("connected");
625
785
  } catch (e) {
626
- if (o.delete(this.publishableKey), e instanceof Error && e.name === "AbortError")
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, o.delete(this.publishableKey), this.cleanup(), this.setStatus("disconnected");
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 n;
813
+ let i;
654
814
  try {
655
- n = await t.json();
815
+ i = await t.json();
656
816
  } catch {
657
817
  }
658
- throw new F({
659
- message: n?.message ?? "create_session failed",
818
+ throw new G({
819
+ message: i?.message ?? "create_session failed",
660
820
  status: t.status,
661
- payload: n,
821
+ payload: i,
662
822
  url: t.url
663
823
  });
664
824
  }
665
825
  if (!t.ok) {
666
- const n = await t.json().catch(() => null);
667
- throw new Error(`create_session failed: ${t.status} ${JSON.stringify(n)}`);
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((n) => console.warn("[PersonaEmbed] Video play failed:", n));
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.setStatus("disconnected"), this.callbacks.onDisconnect?.());
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 = b(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", () => {
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.createScriptProcessor(4096, 1, 1), this.processor.onaudioprocess = (t) => {
866
+ this.processor = await x(this.audioContext), this.processor.port.onmessage = (t) => {
707
867
  if (!this._isMuted) {
708
- const n = v(t.inputBuffer.getChannelData(0));
709
- this.agent?.sendAudio(n);
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" ? await this.agent.connect({
717
- ...t,
718
- agentId: e.agent_id,
719
- signedUrl: e.signed_url
720
- }) : e.type === "openai" && await this.agent.connect({
721
- ...t,
722
- apiKey: e.token,
723
- systemPrompt: e.system_prompt,
724
- voice: e.voice,
725
- turnDetection: e.turn_detection
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 r = /* @__PURE__ */ new Set();
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 (r.has(this.connectionId)) {
948
+ if (l.has(this.connectionId)) {
778
949
  console.log("[PersonaView] Connection already in progress, skipping");
779
950
  return;
780
951
  }
781
- r.add(this.connectionId), this.mounted = !0, this.setStatus("connecting");
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(), r.delete(this.connectionId);
955
+ this.cleanup(), l.delete(this.connectionId);
785
956
  return;
786
957
  }
787
958
  this.setStatus("connected");
788
959
  } catch (e) {
789
- r.delete(this.connectionId), console.error("[PersonaView]", e), this.mounted && (this.setStatus("error"), this.callbacks.onError?.(e));
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, r.delete(this.connectionId), this.cleanup(), this.setStatus("disconnected");
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.setStatus("disconnected"), this.callbacks.onDisconnect?.());
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 = b(this.voiceAgentDetails.type), this.agent.on("audio", (e) => this.session?.sendAudio(e)), this.agent.on("turnEnd", () => this.session?.endAudioTurn()), this.agent.on("interrupted", () => {
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.createScriptProcessor(4096, 1, 1), this.processor.onaudioprocess = (t) => {
1012
+ this.processor = await x(this.audioContext), this.processor.port.onmessage = (t) => {
842
1013
  if (!this._isMuted) {
843
- const n = v(t.inputBuffer.getChannelData(0));
844
- this.agent?.sendAudio(n);
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" ? await this.agent.connect({
852
- ...t,
853
- agentId: e.agent_id,
854
- signedUrl: e.signed_url
855
- }) : e.type === "openai" && await this.agent.connect({
856
- ...t,
857
- apiKey: e.token,
858
- systemPrompt: e.system_prompt,
859
- voice: e.voice,
860
- turnDetection: e.turn_detection
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
- D as AGENT_REGISTRY,
869
- y as BaseAgent,
870
- R as ElevenLabsAgent,
871
- F as KeyframeApiError,
872
- P as OpenAIRealtimeAgent,
873
- L as PersonaEmbed,
874
- $ as PersonaView,
875
- u as SAMPLE_RATE,
876
- f as base64ToBytes,
877
- S as bytesToBase64,
878
- b as createAgent,
879
- C as createEventEmitter,
880
- v as floatTo16BitPCM,
881
- N as getAgentInfo,
882
- p as resamplePcm
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
  };