@yak-io/javascript 0.7.0 → 0.8.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. */
@@ -372,10 +492,42 @@ export class YakEmbed {
372
492
  this.stateListeners.delete(listener);
373
493
  };
374
494
  }
495
+ // ── Public voice API ────────────────────────────────────────────────────
496
+ /** Start a voice session. Must be invoked from a user gesture. */
497
+ voiceStart() {
498
+ return this.voice ? this.voice.start() : Promise.resolve();
499
+ }
500
+ /** Stop the current voice session. */
501
+ voiceStop() {
502
+ return this.voice ? this.voice.stop() : Promise.resolve();
503
+ }
504
+ /** Toggle: start if idle/error, stop if active. */
505
+ async voiceToggle() {
506
+ if (!this.voice)
507
+ return;
508
+ const state = this.voice.getState().state;
509
+ if (state === "idle" || state === "error") {
510
+ await this.voice.start();
511
+ }
512
+ else if (state === "listening" || state === "speaking" || state === "thinking") {
513
+ await this.voice.stop();
514
+ }
515
+ }
516
+ /** Current voice machine snapshot. */
517
+ getVoiceState() {
518
+ return this.voice ? this.voice.getState() : INITIAL_VOICE_MACHINE;
519
+ }
520
+ /** Subscribe to voice state changes. */
521
+ onVoiceStateChange(listener) {
522
+ this.voiceListeners.add(listener);
523
+ return () => {
524
+ this.voiceListeners.delete(listener);
525
+ };
526
+ }
375
527
  // ── DOM creation ────────────────────────────────────────────────────────
376
528
  createPanel(parent) {
377
529
  const theme = this.config.theme;
378
- const position = theme?.position ?? "bottom-right";
530
+ const position = theme?.position ?? DEFAULT_POSITION;
379
531
  const colorMode = theme?.colorMode;
380
532
  const displayMode = theme?.displayMode ?? "chatbox";
381
533
  const isDrawer = displayMode === "drawer";
@@ -393,12 +545,13 @@ export class YakEmbed {
393
545
  classes.push("yak-panel-dark");
394
546
  this.container.className = classes.join(" ");
395
547
  this.container.dataset.position = position;
396
- // Iframe
548
+ // Iframe — set `allow` and `title` BEFORE `src` (some browsers
549
+ // persist the pre-load Permissions-Policy otherwise).
397
550
  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
551
  this.iframe.allow = "clipboard-write";
552
+ this.iframe.title = "yak-chat-host";
553
+ this.iframe.className = "yak-panel-iframe";
554
+ this.iframe.src = this.client.getEmbedUrl();
402
555
  this.iframe.addEventListener("load", () => {
403
556
  this.client.setIframeWindow(this.iframe?.contentWindow ?? null);
404
557
  });
@@ -414,20 +567,15 @@ export class YakEmbed {
414
567
  }
415
568
  createTrigger(parent) {
416
569
  const theme = this.config.theme;
417
- const position = theme?.position ?? "bottom-right";
570
+ const position = theme?.position ?? DEFAULT_POSITION;
418
571
  const colorMode = theme?.colorMode;
419
572
  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);
573
+ this.triggerEl = document.createElement("div");
574
+ this.triggerEl.dataset.position = position;
575
+ this.triggerEl.dataset.mode = this.mode;
576
+ this.triggerEl.className = this.buildTriggerClasses(colorMode, triggerConfig);
426
577
  this.applyTriggerCustomColors(triggerConfig);
427
- // Inner content
428
- const labelEl = document.createElement("span");
429
- labelEl.className = "yak-widget-trigger-label";
430
- labelEl.textContent = label;
578
+ // Logo circle on the left
431
579
  const iconBg = document.createElement("div");
432
580
  iconBg.className = "yak-widget-icon-bg";
433
581
  const logoImg = document.createElement("img");
@@ -437,12 +585,33 @@ export class YakEmbed {
437
585
  logoImg.height = 20;
438
586
  logoImg.className = "yak-widget-icon";
439
587
  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);
588
+ this.triggerEl.appendChild(iconBg);
589
+ // Chat icon button
590
+ if (this.mode === "chat" || this.mode === "both") {
591
+ this.chatButton = document.createElement("button");
592
+ this.chatButton.type = "button";
593
+ this.chatButton.className = "yak-widget-trigger-icon-btn";
594
+ this.chatButton.dataset.action = "chat";
595
+ this.chatButton.setAttribute("aria-label", "Open chat");
596
+ this.chatButton.innerHTML = MESSAGE_CIRCLE_SVG;
597
+ this.chatButton.addEventListener("click", () => this.open());
598
+ this.triggerEl.appendChild(this.chatButton);
599
+ }
600
+ // Voice icon button
601
+ if (this.mode === "voice" || this.mode === "both") {
602
+ this.voiceButton = document.createElement("button");
603
+ this.voiceButton.type = "button";
604
+ this.voiceButton.className = "yak-widget-trigger-icon-btn";
605
+ this.voiceButton.dataset.action = "voice";
606
+ this.voiceButton.dataset.state = "idle";
607
+ this.voiceButton.setAttribute("aria-label", VOICE_STATE_ARIA.idle);
608
+ this.voiceButton.innerHTML = AUDIO_LINES_SVG;
609
+ this.voiceButton.addEventListener("click", () => {
610
+ void this.voiceToggle();
611
+ });
612
+ this.triggerEl.appendChild(this.voiceButton);
613
+ }
614
+ parent.appendChild(this.triggerEl);
446
615
  }
447
616
  buildTriggerClasses(colorMode, triggerConfig) {
448
617
  const classes = ["yak-widget-trigger"];
@@ -463,7 +632,7 @@ export class YakEmbed {
463
632
  return classes.join(" ");
464
633
  }
465
634
  applyTriggerCustomColors(triggerConfig) {
466
- if (!this.triggerButton)
635
+ if (!this.triggerEl)
467
636
  return;
468
637
  const { lightButton, darkButton } = triggerConfig;
469
638
  const hasLightCustom = lightButton?.background || lightButton?.color || lightButton?.border;
@@ -479,13 +648,13 @@ export class YakEmbed {
479
648
  ];
480
649
  for (const [prop, value] of vars) {
481
650
  if (value)
482
- this.triggerButton.style.setProperty(prop, value);
651
+ this.triggerEl.style.setProperty(prop, value);
483
652
  }
484
653
  }
485
654
  if (hasLightCustom)
486
- this.triggerButton.dataset.hasLightCustom = "true";
655
+ this.triggerEl.dataset.hasLightCustom = "true";
487
656
  if (hasDarkCustom)
488
- this.triggerButton.dataset.hasDarkCustom = "true";
657
+ this.triggerEl.dataset.hasDarkCustom = "true";
489
658
  }
490
659
  // ── Internal state management ───────────────────────────────────────────
491
660
  updatePanelState() {
@@ -497,32 +666,36 @@ export class YakEmbed {
497
666
  this.panelRoot.dataset.expanded = String(this.isExpanded);
498
667
  }
499
668
  }
500
- updateTriggerState() {
501
- if (!this.triggerButton)
669
+ updateChatButtonState() {
670
+ if (!this.chatButton)
502
671
  return;
503
672
  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;
673
+ this.chatButton.disabled = isLoading;
674
+ this.chatButton.setAttribute("aria-label", isLoading ? "Loading chat" : "Open chat");
510
675
  if (isLoading) {
511
- iconBg.innerHTML = '<div class="yak-widget-spinner" aria-hidden="true"></div>';
676
+ this.chatButton.innerHTML = `<span class="yak-widget-spinner" aria-hidden="true"></span>`;
512
677
  }
513
678
  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
- }
679
+ this.chatButton.innerHTML = MESSAGE_CIRCLE_SVG;
680
+ }
681
+ }
682
+ updateVoiceButtonState() {
683
+ if (!this.voiceButton)
684
+ return;
685
+ const state = this.voiceMachine.state;
686
+ this.voiceButton.dataset.state = state;
687
+ this.voiceButton.setAttribute("aria-label", VOICE_STATE_ARIA[state]);
688
+ this.voiceButton.disabled = state === "connecting";
689
+ this.voiceButton.innerHTML = this.iconForVoiceState(state);
690
+ }
691
+ iconForVoiceState(state) {
692
+ if (state === "connecting") {
693
+ return `<span class="yak-widget-spinner" aria-hidden="true"></span>`;
694
+ }
695
+ if (state === "listening" || state === "speaking" || state === "thinking") {
696
+ return STOP_SVG;
525
697
  }
698
+ return AUDIO_LINES_SVG;
526
699
  }
527
700
  sendPendingPrompt() {
528
701
  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[]>;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Generates a tool ID from a tool name using a 32-bit hash.
3
+ * Format: `yt_<8-char-hex-hash>`.
4
+ *
5
+ * The hash is deterministic so chat and voice always derive the same id for
6
+ * the same tool name — this is what lets the mint route, the iframe, and the
7
+ * SDK all agree on which decorated id maps back to which host tool name.
8
+ */
9
+ export declare function generateToolId(originalName: string): string;
10
+ //# sourceMappingURL=tool-name.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-name.d.ts","sourceRoot":"","sources":["../src/tool-name.ts"],"names":[],"mappings":"AAcA;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAG3D"}