@yak-io/javascript 0.7.0 → 0.9.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/embed.js CHANGED
@@ -1,5 +1,21 @@
1
1
  import { YakClient } from "./client.js";
2
2
  import { logger } from "./logger.js";
3
+ import { INITIAL_VOICE_MACHINE } from "./voice-machine.js";
4
+ import { YakVoiceSession, } from "./voice-session.js";
5
+ // Single source of truth for the default trigger + panel corner.
6
+ const DEFAULT_POSITION = "bottom-left";
7
+ // ── Inline SVG icons (lucide) ───────────────────────────────────────────────
8
+ const MESSAGE_CIRCLE_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/></svg>`;
9
+ const AUDIO_LINES_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 10v3"/><path d="M6 6v11"/><path d="M10 3v18"/><path d="M14 8v7"/><path d="M18 5v13"/><path d="M22 10v3"/></svg>`;
10
+ const STOP_SVG = `<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>`;
11
+ const VOICE_STATE_ARIA = {
12
+ idle: "Start voice mode",
13
+ connecting: "Connecting voice session",
14
+ listening: "Voice listening — tap to stop",
15
+ thinking: "Voice thinking — tap to stop",
16
+ speaking: "Voice speaking — tap to stop",
17
+ error: "Voice error — tap to retry",
18
+ };
3
19
  // ── CSS ─────────────────────────────────────────────────────────────────────
4
20
  function getPanelStyles() {
5
21
  return `
@@ -93,41 +109,77 @@ function getTriggerStyles() {
93
109
  return `
94
110
  .yak-widget-trigger {
95
111
  position: fixed; z-index: 9997;
96
- display: flex; align-items: center; gap: 12px;
112
+ display: inline-flex; align-items: center; gap: 8px;
97
113
  border: none; border-radius: 30px;
98
- padding: 0 5px 0 20px; height: 45px; min-width: 45px; width: auto;
99
- cursor: pointer;
114
+ padding: 5px; height: 45px;
100
115
  transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
101
- overflow: hidden;
102
116
  background-color: #000; color: #fff;
103
117
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
104
118
  font-family: system-ui, -apple-system, sans-serif;
105
119
  }
106
120
 
107
- .yak-widget-trigger[data-position="top-left"] { top: 28px; left: 28px; flex-direction: row-reverse; }
121
+ .yak-widget-trigger[data-position="top-left"] { top: 28px; left: 28px; }
108
122
  .yak-widget-trigger[data-position="top-center"] { top: 28px; left: 50%; transform: translateX(-50%); }
109
123
  .yak-widget-trigger[data-position="top-right"] { top: 28px; right: 28px; }
110
- .yak-widget-trigger[data-position="left-center"] { top: 50%; left: 28px; transform: translateY(-50%); flex-direction: row-reverse; }
124
+ .yak-widget-trigger[data-position="left-center"] { top: 50%; left: 28px; transform: translateY(-50%); }
111
125
  .yak-widget-trigger[data-position="right-center"] { top: 50%; right: 28px; transform: translateY(-50%); }
112
- .yak-widget-trigger[data-position="bottom-left"] { bottom: 28px; left: 28px; flex-direction: row-reverse; }
126
+ .yak-widget-trigger[data-position="bottom-left"] { bottom: 28px; left: 28px; }
113
127
  .yak-widget-trigger[data-position="bottom-center"] { bottom: 28px; left: 50%; transform: translateX(-50%); }
114
128
  .yak-widget-trigger[data-position="bottom-right"] { bottom: 28px; right: 28px; }
115
129
 
116
- .yak-widget-trigger-label { font-size: 14px; font-weight: 600; white-space: nowrap; }
117
-
118
130
  .yak-widget-icon-bg {
119
131
  display: flex; align-items: center; justify-content: center;
120
132
  width: 36px; height: 36px; border-radius: 50%;
121
133
  background-color: rgba(255, 255, 255, 0.1);
134
+ flex-shrink: 0;
122
135
  }
123
136
 
124
137
  .yak-widget-icon { width: 20px; height: 20px; color: currentColor; }
125
138
 
139
+ .yak-widget-trigger-icon-btn {
140
+ display: inline-flex; align-items: center; justify-content: center;
141
+ width: 36px; height: 36px; border-radius: 50%;
142
+ border: none; padding: 0;
143
+ background-color: transparent;
144
+ color: inherit;
145
+ cursor: pointer;
146
+ position: relative;
147
+ transition: background-color 0.15s ease;
148
+ flex-shrink: 0;
149
+ }
150
+ .yak-widget-trigger-icon-btn:hover { background-color: rgba(255, 255, 255, 0.12); }
151
+ .yak-widget-trigger-icon-btn:disabled { cursor: wait; opacity: 0.7; }
152
+ .yak-widget-trigger-icon-btn svg { width: 20px; height: 20px; display: block; }
153
+
154
+ .yak-widget-trigger-icon-btn[data-action="voice"][data-state="error"] {
155
+ background-color: rgba(185, 28, 28, 0.18);
156
+ }
157
+ .yak-widget-trigger-icon-btn[data-action="voice"][data-state="listening"]::after,
158
+ .yak-widget-trigger-icon-btn[data-action="voice"][data-state="speaking"]::after {
159
+ content: "";
160
+ position: absolute; inset: 2px;
161
+ border-radius: 50%;
162
+ border: 2px solid currentColor;
163
+ pointer-events: none;
164
+ }
165
+ .yak-widget-trigger-icon-btn[data-action="voice"][data-state="listening"]::after {
166
+ opacity: 0.4; animation: yak-widget-pulse 1.2s ease-out infinite;
167
+ }
168
+ .yak-widget-trigger-icon-btn[data-action="voice"][data-state="speaking"]::after {
169
+ opacity: 0.5; animation: yak-widget-wave 0.8s ease-in-out infinite;
170
+ }
171
+
126
172
  @media (prefers-color-scheme: dark) {
127
173
  .yak-widget-trigger:not(.yak-widget-light) .yak-widget-icon { filter: invert(1); }
174
+ .yak-widget-trigger:not(.yak-widget-light) .yak-widget-trigger-icon-btn:hover {
175
+ background-color: rgba(255, 255, 255, 0.12);
176
+ }
128
177
  }
129
178
  .yak-widget-trigger.yak-widget-dark .yak-widget-icon { filter: invert(1); }
130
179
  .yak-widget-trigger.yak-widget-light .yak-widget-icon { filter: none; }
180
+ .yak-widget-trigger.yak-widget-light .yak-widget-trigger-icon-btn:hover {
181
+ background-color: rgba(0, 0, 0, 0.06);
182
+ }
131
183
 
132
184
  .yak-widget-spinner {
133
185
  width: 20px; height: 20px;
@@ -135,8 +187,14 @@ function getTriggerStyles() {
135
187
  animation: yak-widget-spin 0.8s linear infinite;
136
188
  }
137
189
  @keyframes yak-widget-spin { to { transform: rotate(360deg); } }
138
-
139
- .yak-widget-trigger:disabled { cursor: wait; }
190
+ @keyframes yak-widget-pulse {
191
+ 0% { transform: scale(1); opacity: 0.5; }
192
+ 100% { transform: scale(1.45); opacity: 0; }
193
+ }
194
+ @keyframes yak-widget-wave {
195
+ 0%, 100% { transform: scale(1); opacity: 0.5; }
196
+ 50% { transform: scale(1.25); opacity: 0.9; }
197
+ }
140
198
 
141
199
  .yak-widget-trigger.yak-widget-custom-light {
142
200
  background-color: var(--yak-btn-light-bg, #fff); color: var(--yak-btn-light-color, #000);
@@ -203,14 +261,16 @@ function getTriggerStyles() {
203
261
  }
204
262
  // ── YakEmbed class ──────────────────────────────────────────────────────────
205
263
  /**
206
- * Drop-in widget that renders the yak chat iframe + optional trigger button.
207
- * Wraps YakClient with DOM rendering, lazy iframe mounting, and consistent styling.
264
+ * Drop-in widget that renders the yak trigger pill plus, depending on mode,
265
+ * the chat iframe panel and/or a WebRTC voice session. Composes both
266
+ * `YakClient` (chat) and `YakVoiceSession` (voice) under one trigger.
208
267
  *
209
268
  * @example
210
269
  * ```ts
211
270
  * const embed = new YakEmbed({
212
271
  * appId: "my-app",
213
- * theme: { position: "bottom-right" },
272
+ * mode: "both",
273
+ * theme: { position: "bottom-left" },
214
274
  * onToolCall: async (name, args) => { ... },
215
275
  * });
216
276
  * embed.mount();
@@ -218,13 +278,17 @@ function getTriggerStyles() {
218
278
  */
219
279
  export class YakEmbed {
220
280
  client;
281
+ voice;
221
282
  config;
283
+ mode;
222
284
  // DOM elements
223
285
  styleEl = null;
224
286
  panelRoot = null;
225
287
  container = null;
226
288
  iframe = null;
227
- triggerButton = null;
289
+ triggerEl = null;
290
+ chatButton = null;
291
+ voiceButton = null;
228
292
  // State
229
293
  isOpen = false;
230
294
  isReady = false;
@@ -232,23 +296,31 @@ export class YakEmbed {
232
296
  hasBeenOpened = false;
233
297
  pendingPrompt = null;
234
298
  mounted = false;
299
+ voiceMachine = INITIAL_VOICE_MACHINE;
235
300
  // Listeners
236
301
  stateListeners = new Set();
302
+ voiceListeners = new Set();
303
+ unsubscribeVoice = null;
237
304
  mobileQuery = null;
238
305
  mobileHandler = null;
239
306
  expandHandler = null;
240
307
  constructor(config) {
241
308
  this.config = config;
309
+ this.mode = config.mode ?? "chat";
242
310
  // Wrap callbacks to integrate with our state
243
311
  this.client = new YakClient({
244
312
  ...config,
245
313
  onReady: () => {
246
314
  this.isReady = true;
247
315
  this.updatePanelState();
248
- this.updateTriggerState();
316
+ this.updateChatButtonState();
249
317
  this.sendPendingPrompt();
250
318
  this.sendFocusIfOpen();
251
319
  this.notifyMobileState();
320
+ // Surface the ready transition to framework subscribers (React/Vue/
321
+ // Svelte/Angular all derive their loading state from this). Without
322
+ // it `isReady` stays false forever and consumers spin indefinitely.
323
+ this.notifyListeners();
252
324
  config.onReady?.();
253
325
  },
254
326
  onClose: () => {
@@ -256,16 +328,39 @@ export class YakEmbed {
256
328
  config.onClose?.();
257
329
  },
258
330
  });
331
+ if (this.mode !== "chat") {
332
+ const voiceConfig = {
333
+ appId: config.appId,
334
+ getConfig: config.getConfig,
335
+ chatConfig: config.chatConfig,
336
+ onToolCall: config.onToolCall,
337
+ onGraphQLSchemaCall: config.onGraphQLSchemaCall,
338
+ onRESTSchemaCall: config.onRESTSchemaCall,
339
+ onRedirect: config.onRedirect,
340
+ };
341
+ this.voice = new YakVoiceSession(voiceConfig);
342
+ }
343
+ else {
344
+ this.voice = null;
345
+ }
259
346
  }
260
347
  /** The underlying headless YakClient for advanced usage */
261
348
  getClient() {
262
349
  return this.client;
263
350
  }
351
+ /** The underlying voice session — null when mode === "chat". */
352
+ getVoiceSession() {
353
+ return this.voice;
354
+ }
355
+ /** Current widget mode (immutable for the lifetime of the embed). */
356
+ getMode() {
357
+ return this.mode;
358
+ }
264
359
  // ── Lifecycle ───────────────────────────────────────────────────────────
265
360
  /**
266
361
  * Mount the widget into the DOM. Call once after construction.
267
- * Inserts styles and trigger button (if enabled). The iframe is lazily
268
- * created on the first call to open().
362
+ * Inserts styles and trigger button (if enabled). The chat iframe is
363
+ * lazily created on the first call to open().
269
364
  */
270
365
  mount(target) {
271
366
  if (this.mounted)
@@ -276,7 +371,7 @@ export class YakEmbed {
276
371
  this.styleEl = document.createElement("style");
277
372
  this.styleEl.textContent = getPanelStyles() + getTriggerStyles();
278
373
  parent.appendChild(this.styleEl);
279
- // Create trigger button
374
+ // Create trigger
280
375
  if (this.config.trigger !== false) {
281
376
  this.createTrigger(parent);
282
377
  }
@@ -291,6 +386,23 @@ export class YakEmbed {
291
386
  window.addEventListener("message", this.expandHandler);
292
387
  // Start the client's message listeners
293
388
  this.client.mount();
389
+ // Subscribe to voice state for trigger updates + fan-out to listeners
390
+ if (this.voice) {
391
+ this.voiceMachine = this.voice.getState();
392
+ this.unsubscribeVoice = this.voice.onStateChange((machine) => {
393
+ this.voiceMachine = machine;
394
+ this.updateVoiceButtonState();
395
+ for (const listener of this.voiceListeners) {
396
+ try {
397
+ listener(machine);
398
+ }
399
+ catch (err) {
400
+ logger.warn("Error in voice listener:", err);
401
+ }
402
+ }
403
+ });
404
+ this.updateVoiceButtonState();
405
+ }
294
406
  }
295
407
  /** Remove all DOM elements and event listeners. */
296
408
  destroy() {
@@ -298,6 +410,11 @@ export class YakEmbed {
298
410
  return;
299
411
  this.mounted = false;
300
412
  this.client.unmount();
413
+ if (this.unsubscribeVoice) {
414
+ this.unsubscribeVoice();
415
+ this.unsubscribeVoice = null;
416
+ }
417
+ this.voice?.destroy();
301
418
  if (this.expandHandler) {
302
419
  window.removeEventListener("message", this.expandHandler);
303
420
  this.expandHandler = null;
@@ -308,20 +425,23 @@ export class YakEmbed {
308
425
  this.mobileHandler = null;
309
426
  }
310
427
  this.panelRoot?.remove();
311
- this.triggerButton?.remove();
428
+ this.triggerEl?.remove();
312
429
  this.styleEl?.remove();
313
430
  this.panelRoot = null;
314
431
  this.container = null;
315
432
  this.iframe = null;
316
- this.triggerButton = null;
433
+ this.triggerEl = null;
434
+ this.chatButton = null;
435
+ this.voiceButton = null;
317
436
  this.styleEl = null;
318
437
  this.isOpen = false;
319
438
  this.isReady = false;
320
439
  this.isExpanded = false;
321
440
  this.hasBeenOpened = false;
322
441
  this.stateListeners.clear();
442
+ this.voiceListeners.clear();
323
443
  }
324
- // ── Public API ──────────────────────────────────────────────────────────
444
+ // ── Public chat API ─────────────────────────────────────────────────────
325
445
  /** Open the chat widget. Creates the iframe on first call (lazy mount). */
326
446
  open() {
327
447
  if (!this.mounted)
@@ -334,7 +454,7 @@ export class YakEmbed {
334
454
  this.isOpen = true;
335
455
  this.client.setWidgetOpen(true);
336
456
  this.updatePanelState();
337
- this.updateTriggerState();
457
+ this.updateChatButtonState();
338
458
  this.sendFocusIfOpen();
339
459
  this.notifyListeners();
340
460
  }
@@ -343,7 +463,7 @@ export class YakEmbed {
343
463
  this.isOpen = false;
344
464
  this.client.setWidgetOpen(false);
345
465
  this.updatePanelState();
346
- this.updateTriggerState();
466
+ this.updateChatButtonState();
347
467
  this.notifyListeners();
348
468
  }
349
469
  /** Toggle the chat widget open/closed. */
@@ -363,7 +483,12 @@ export class YakEmbed {
363
483
  }
364
484
  /** Get the current widget state. */
365
485
  getState() {
366
- return { isOpen: this.isOpen, isReady: this.isReady, isExpanded: this.isExpanded };
486
+ return {
487
+ isOpen: this.isOpen,
488
+ isReady: this.isReady,
489
+ isLoading: this.isOpen && !this.isReady,
490
+ isExpanded: this.isExpanded,
491
+ };
367
492
  }
368
493
  /** Subscribe to state changes. Returns an unsubscribe function. */
369
494
  onStateChange(listener) {
@@ -372,10 +497,42 @@ export class YakEmbed {
372
497
  this.stateListeners.delete(listener);
373
498
  };
374
499
  }
500
+ // ── Public voice API ────────────────────────────────────────────────────
501
+ /** Start a voice session. Must be invoked from a user gesture. */
502
+ voiceStart() {
503
+ return this.voice ? this.voice.start() : Promise.resolve();
504
+ }
505
+ /** Stop the current voice session. */
506
+ voiceStop() {
507
+ return this.voice ? this.voice.stop() : Promise.resolve();
508
+ }
509
+ /** Toggle: start if idle/error, stop if active. */
510
+ async voiceToggle() {
511
+ if (!this.voice)
512
+ return;
513
+ const state = this.voice.getState().state;
514
+ if (state === "idle" || state === "error") {
515
+ await this.voice.start();
516
+ }
517
+ else if (state === "listening" || state === "speaking" || state === "thinking") {
518
+ await this.voice.stop();
519
+ }
520
+ }
521
+ /** Current voice machine snapshot. */
522
+ getVoiceState() {
523
+ return this.voice ? this.voice.getState() : INITIAL_VOICE_MACHINE;
524
+ }
525
+ /** Subscribe to voice state changes. */
526
+ onVoiceStateChange(listener) {
527
+ this.voiceListeners.add(listener);
528
+ return () => {
529
+ this.voiceListeners.delete(listener);
530
+ };
531
+ }
375
532
  // ── DOM creation ────────────────────────────────────────────────────────
376
533
  createPanel(parent) {
377
534
  const theme = this.config.theme;
378
- const position = theme?.position ?? "bottom-right";
535
+ const position = theme?.position ?? DEFAULT_POSITION;
379
536
  const colorMode = theme?.colorMode;
380
537
  const displayMode = theme?.displayMode ?? "chatbox";
381
538
  const isDrawer = displayMode === "drawer";
@@ -393,12 +550,13 @@ export class YakEmbed {
393
550
  classes.push("yak-panel-dark");
394
551
  this.container.className = classes.join(" ");
395
552
  this.container.dataset.position = position;
396
- // Iframe
553
+ // Iframe — set `allow` and `title` BEFORE `src` (some browsers
554
+ // persist the pre-load Permissions-Policy otherwise).
397
555
  this.iframe = document.createElement("iframe");
398
- this.iframe.src = this.client.getEmbedUrl();
399
- this.iframe.className = "yak-panel-iframe";
400
- this.iframe.title = "yak-chat-host";
401
556
  this.iframe.allow = "clipboard-write";
557
+ this.iframe.title = "yak-chat-host";
558
+ this.iframe.className = "yak-panel-iframe";
559
+ this.iframe.src = this.client.getEmbedUrl();
402
560
  this.iframe.addEventListener("load", () => {
403
561
  this.client.setIframeWindow(this.iframe?.contentWindow ?? null);
404
562
  });
@@ -414,20 +572,15 @@ export class YakEmbed {
414
572
  }
415
573
  createTrigger(parent) {
416
574
  const theme = this.config.theme;
417
- const position = theme?.position ?? "bottom-right";
575
+ const position = theme?.position ?? DEFAULT_POSITION;
418
576
  const colorMode = theme?.colorMode;
419
577
  const triggerConfig = typeof this.config.trigger === "object" ? this.config.trigger : {};
420
- const label = triggerConfig.label ?? "Ask with AI";
421
- this.triggerButton = document.createElement("button");
422
- this.triggerButton.type = "button";
423
- this.triggerButton.setAttribute("aria-label", "Open chat");
424
- this.triggerButton.dataset.position = position;
425
- this.triggerButton.className = this.buildTriggerClasses(colorMode, triggerConfig);
578
+ this.triggerEl = document.createElement("div");
579
+ this.triggerEl.dataset.position = position;
580
+ this.triggerEl.dataset.mode = this.mode;
581
+ this.triggerEl.className = this.buildTriggerClasses(colorMode, triggerConfig);
426
582
  this.applyTriggerCustomColors(triggerConfig);
427
- // Inner content
428
- const labelEl = document.createElement("span");
429
- labelEl.className = "yak-widget-trigger-label";
430
- labelEl.textContent = label;
583
+ // Logo circle on the left
431
584
  const iconBg = document.createElement("div");
432
585
  iconBg.className = "yak-widget-icon-bg";
433
586
  const logoImg = document.createElement("img");
@@ -437,12 +590,33 @@ export class YakEmbed {
437
590
  logoImg.height = 20;
438
591
  logoImg.className = "yak-widget-icon";
439
592
  iconBg.appendChild(logoImg);
440
- this.triggerButton.appendChild(labelEl);
441
- this.triggerButton.appendChild(iconBg);
442
- this.triggerButton.addEventListener("click", () => {
443
- this.open();
444
- });
445
- parent.appendChild(this.triggerButton);
593
+ this.triggerEl.appendChild(iconBg);
594
+ // Chat icon button
595
+ if (this.mode === "chat" || this.mode === "both") {
596
+ this.chatButton = document.createElement("button");
597
+ this.chatButton.type = "button";
598
+ this.chatButton.className = "yak-widget-trigger-icon-btn";
599
+ this.chatButton.dataset.action = "chat";
600
+ this.chatButton.setAttribute("aria-label", "Open chat");
601
+ this.chatButton.innerHTML = MESSAGE_CIRCLE_SVG;
602
+ this.chatButton.addEventListener("click", () => this.open());
603
+ this.triggerEl.appendChild(this.chatButton);
604
+ }
605
+ // Voice icon button
606
+ if (this.mode === "voice" || this.mode === "both") {
607
+ this.voiceButton = document.createElement("button");
608
+ this.voiceButton.type = "button";
609
+ this.voiceButton.className = "yak-widget-trigger-icon-btn";
610
+ this.voiceButton.dataset.action = "voice";
611
+ this.voiceButton.dataset.state = "idle";
612
+ this.voiceButton.setAttribute("aria-label", VOICE_STATE_ARIA.idle);
613
+ this.voiceButton.innerHTML = AUDIO_LINES_SVG;
614
+ this.voiceButton.addEventListener("click", () => {
615
+ void this.voiceToggle();
616
+ });
617
+ this.triggerEl.appendChild(this.voiceButton);
618
+ }
619
+ parent.appendChild(this.triggerEl);
446
620
  }
447
621
  buildTriggerClasses(colorMode, triggerConfig) {
448
622
  const classes = ["yak-widget-trigger"];
@@ -463,7 +637,7 @@ export class YakEmbed {
463
637
  return classes.join(" ");
464
638
  }
465
639
  applyTriggerCustomColors(triggerConfig) {
466
- if (!this.triggerButton)
640
+ if (!this.triggerEl)
467
641
  return;
468
642
  const { lightButton, darkButton } = triggerConfig;
469
643
  const hasLightCustom = lightButton?.background || lightButton?.color || lightButton?.border;
@@ -479,13 +653,13 @@ export class YakEmbed {
479
653
  ];
480
654
  for (const [prop, value] of vars) {
481
655
  if (value)
482
- this.triggerButton.style.setProperty(prop, value);
656
+ this.triggerEl.style.setProperty(prop, value);
483
657
  }
484
658
  }
485
659
  if (hasLightCustom)
486
- this.triggerButton.dataset.hasLightCustom = "true";
660
+ this.triggerEl.dataset.hasLightCustom = "true";
487
661
  if (hasDarkCustom)
488
- this.triggerButton.dataset.hasDarkCustom = "true";
662
+ this.triggerEl.dataset.hasDarkCustom = "true";
489
663
  }
490
664
  // ── Internal state management ───────────────────────────────────────────
491
665
  updatePanelState() {
@@ -497,32 +671,36 @@ export class YakEmbed {
497
671
  this.panelRoot.dataset.expanded = String(this.isExpanded);
498
672
  }
499
673
  }
500
- updateTriggerState() {
501
- if (!this.triggerButton)
674
+ updateChatButtonState() {
675
+ if (!this.chatButton)
502
676
  return;
503
677
  const isLoading = this.isOpen && !this.isReady;
504
- this.triggerButton.disabled = isLoading;
505
- this.triggerButton.setAttribute("aria-label", isLoading ? "Loading chat" : "Open chat");
506
- // Swap icon content: spinner vs logo
507
- const iconBg = this.triggerButton.querySelector(".yak-widget-icon-bg");
508
- if (!iconBg)
509
- return;
678
+ this.chatButton.disabled = isLoading;
679
+ this.chatButton.setAttribute("aria-label", isLoading ? "Loading chat" : "Open chat");
510
680
  if (isLoading) {
511
- iconBg.innerHTML = '<div class="yak-widget-spinner" aria-hidden="true"></div>';
681
+ this.chatButton.innerHTML = `<span class="yak-widget-spinner" aria-hidden="true"></span>`;
512
682
  }
513
683
  else {
514
- const existing = iconBg.querySelector(".yak-widget-icon");
515
- if (!existing) {
516
- const logoImg = document.createElement("img");
517
- logoImg.src = `${this.client.getIframeOrigin()}/logo.svg`;
518
- logoImg.alt = "";
519
- logoImg.width = 20;
520
- logoImg.height = 20;
521
- logoImg.className = "yak-widget-icon";
522
- iconBg.innerHTML = "";
523
- iconBg.appendChild(logoImg);
524
- }
684
+ this.chatButton.innerHTML = MESSAGE_CIRCLE_SVG;
685
+ }
686
+ }
687
+ updateVoiceButtonState() {
688
+ if (!this.voiceButton)
689
+ return;
690
+ const state = this.voiceMachine.state;
691
+ this.voiceButton.dataset.state = state;
692
+ this.voiceButton.setAttribute("aria-label", VOICE_STATE_ARIA[state]);
693
+ this.voiceButton.disabled = state === "connecting";
694
+ this.voiceButton.innerHTML = this.iconForVoiceState(state);
695
+ }
696
+ iconForVoiceState(state) {
697
+ if (state === "connecting") {
698
+ return `<span class="yak-widget-spinner" aria-hidden="true"></span>`;
699
+ }
700
+ if (state === "listening" || state === "speaking" || state === "thinking") {
701
+ return STOP_SVG;
525
702
  }
703
+ return AUDIO_LINES_SVG;
526
704
  }
527
705
  sendPendingPrompt() {
528
706
  if (!this.pendingPrompt || !this.isReady)
package/dist/index.d.ts CHANGED
@@ -1,12 +1,16 @@
1
+ export type { YakClientConfig } from "./client.js";
2
+ export { YakClient } from "./client.js";
3
+ export type { TriggerButtonConfig, WidgetMode, YakEmbedConfig, YakEmbedState, YakEmbedStateListener, } from "./embed.js";
4
+ export { YakEmbed } from "./embed.js";
5
+ export { disableYakLogging, enableYakLogging, isYakLoggingEnabled, logger } from "./logger.js";
1
6
  export * from "./types/config.js";
2
7
  export * from "./types/messaging.js";
3
8
  export * from "./types/routes.js";
4
9
  export * from "./types/tools.js";
5
- export { EMBED_PROTOCOL_VERSION } from "./version.js";
6
10
  export type { EmbedProtocolVersion } from "./version.js";
7
- export { YakClient } from "./client.js";
8
- export type { YakClientConfig } from "./client.js";
9
- export { YakEmbed } from "./embed.js";
10
- export type { YakEmbedConfig, YakEmbedState, YakEmbedStateListener, TriggerButtonConfig, } from "./embed.js";
11
- export { enableYakLogging, disableYakLogging, isYakLoggingEnabled, logger } from "./logger.js";
11
+ export { EMBED_PROTOCOL_VERSION } from "./version.js";
12
+ export type { VoiceEvent, VoiceMachine, VoiceState } from "./voice-machine.js";
13
+ export { handleRealtimeMessage, INITIAL_VOICE_MACHINE, voiceReducer, } from "./voice-machine.js";
14
+ export type { VoiceStateListener, YakVoiceSessionConfig } from "./voice-session.js";
15
+ export { YakVoiceSession } from "./voice-session.js";
12
16
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AAGjC,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AACtD,YAAY,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAGzD,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAGnD,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,YAAY,EACV,cAAc,EACd,aAAa,EACb,qBAAqB,EACrB,mBAAmB,GACpB,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,YAAY,EACV,mBAAmB,EACnB,UAAU,EACV,cAAc,EACd,aAAa,EACb,qBAAqB,GACtB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtC,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC/F,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,YAAY,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAEzD,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AACtD,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAC/E,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,YAAY,GACb,MAAM,oBAAoB,CAAC;AAE5B,YAAY,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AACpF,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC"}
package/dist/index.js CHANGED
@@ -1,13 +1,15 @@
1
1
  // Public types
2
+ // Public client API
3
+ export { YakClient } from "./client.js";
4
+ // Embed (DOM rendering layer — chat + voice)
5
+ export { YakEmbed } from "./embed.js";
6
+ // Logging utilities
7
+ export { disableYakLogging, enableYakLogging, isYakLoggingEnabled, logger } from "./logger.js";
2
8
  export * from "./types/config.js";
3
9
  export * from "./types/messaging.js";
4
10
  export * from "./types/routes.js";
5
11
  export * from "./types/tools.js";
6
12
  // Version
7
13
  export { EMBED_PROTOCOL_VERSION } from "./version.js";
8
- // Public client API
9
- export { YakClient } from "./client.js";
10
- // Embed (DOM rendering layer)
11
- export { YakEmbed } from "./embed.js";
12
- // Logging utilities
13
- export { enableYakLogging, disableYakLogging, isYakLoggingEnabled, logger } from "./logger.js";
14
+ export { handleRealtimeMessage, INITIAL_VOICE_MACHINE, voiceReducer, } from "./voice-machine.js";
15
+ export { YakVoiceSession } from "./voice-session.js";
@@ -1 +1 @@
1
- {"version":3,"file":"createYakHandler.d.ts","sourceRoot":"","sources":["../../src/server/createYakHandler.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,gBAAgB,EAAe,eAAe,EAAc,MAAM,cAAc,CAAC;AAE/F,MAAM,MAAM,gBAAgB,GAAG;IAC7B,MAAM,EAAE,gBAAgB,CAAC;IACzB,KAAK,CAAC,EAAE,eAAe,CAAC;CACzB,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB;gBAIT,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC;gBAezB,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC;EA2C5E;AAED,MAAM,MAAM,sBAAsB,GAAG;IACnC,MAAM,EAAE,gBAAgB,CAAC;IACzB,KAAK,CAAC,EAAE,eAAe,CAAC;CACzB,CAAC;AAEF,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,sBAAsB,IAIhC,MAAM,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CAcrE;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,KAAK,EAAE,eAAe,CAAC;CACxB,CAAC;AAEF,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,qBAAqB,IAO/B,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CAoCnE"}
1
+ {"version":3,"file":"createYakHandler.d.ts","sourceRoot":"","sources":["../../src/server/createYakHandler.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAe,gBAAgB,EAAc,eAAe,EAAE,MAAM,cAAc,CAAC;AAG/F,MAAM,MAAM,gBAAgB,GAAG;IAC7B,MAAM,EAAE,gBAAgB,CAAC;IACzB,KAAK,CAAC,EAAE,eAAe,CAAC;CACzB,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB;gBAIT,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC;gBAezB,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC;EA2C5E;AAED,MAAM,MAAM,sBAAsB,GAAG;IACnC,MAAM,EAAE,gBAAgB,CAAC;IACzB,KAAK,CAAC,EAAE,eAAe,CAAC;CACzB,CAAC;AAEF,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,sBAAsB,IAIhC,MAAM,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CAcrE;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,KAAK,EAAE,eAAe,CAAC;CACxB,CAAC;AAEF,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,qBAAqB,IAO/B,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CAoCnE"}
@@ -1,8 +1,8 @@
1
- export { createYakHandler, createYakConfigHandler, createYakToolsHandler, } from "./createYakHandler.js";
2
- export type { YakHandlerConfig, YakConfigHandlerConfig, YakToolsHandlerConfig, } from "./createYakHandler.js";
3
- export { normalizeRouteSources, normalizeToolSources, } from "./sources.js";
4
- export type { RouteSource, RouteSourceInput, ToolSource, ToolSourceInput } from "./sources.js";
5
- export type { ToolExecutor, ToolDefinition, ToolManifest, ToolCallPayload, ToolCallResult, } from "../types/tools.js";
6
- export type { RouteInfo, RouteManifest } from "../types/routes.js";
7
1
  export type { ChatConfig } from "../types/config.js";
2
+ export type { RouteInfo, RouteManifest } from "../types/routes.js";
3
+ export type { ToolCallPayload, ToolCallResult, ToolDefinition, ToolExecutor, ToolManifest, } from "../types/tools.js";
4
+ export type { YakConfigHandlerConfig, YakHandlerConfig, YakToolsHandlerConfig, } from "./createYakHandler.js";
5
+ export { createYakConfigHandler, createYakHandler, createYakToolsHandler, } from "./createYakHandler.js";
6
+ export type { RouteSource, RouteSourceInput, ToolSource, ToolSourceInput } from "./sources.js";
7
+ export { normalizeRouteSources, normalizeToolSources, } from "./sources.js";
8
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EACV,gBAAgB,EAChB,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EACL,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,cAAc,CAAC;AACtB,YAAY,EAAE,WAAW,EAAE,gBAAgB,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/F,YAAY,EACV,YAAY,EACZ,cAAc,EACd,YAAY,EACZ,eAAe,EACf,cAAc,GACf,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnE,YAAY,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrD,YAAY,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnE,YAAY,EACV,eAAe,EACf,cAAc,EACd,cAAc,EACd,YAAY,EACZ,YAAY,GACb,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EACV,sBAAsB,EACtB,gBAAgB,EAChB,qBAAqB,GACtB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,sBAAsB,EACtB,gBAAgB,EAChB,qBAAqB,GACtB,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EAAE,WAAW,EAAE,gBAAgB,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/F,OAAO,EACL,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,cAAc,CAAC"}
@@ -1,2 +1,2 @@
1
- export { createYakHandler, createYakConfigHandler, createYakToolsHandler, } from "./createYakHandler.js";
1
+ export { createYakConfigHandler, createYakHandler, createYakToolsHandler, } from "./createYakHandler.js";
2
2
  export { normalizeRouteSources, normalizeToolSources, } from "./sources.js";
@@ -1,5 +1,5 @@
1
1
  import type { RouteInfo } from "../types/routes.js";
2
- import type { ToolDefinition, ToolManifest, ToolExecutor } from "../types/tools.js";
2
+ import type { ToolDefinition, ToolExecutor, ToolManifest } from "../types/tools.js";
3
3
  export type RouteSource = {
4
4
  id?: string;
5
5
  getRoutes: () => Promise<RouteInfo[]>;