@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/README.md +129 -104
- package/dist/client.d.ts +24 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +84 -4
- package/dist/embed.d.ts +65 -9
- package/dist/embed.d.ts.map +1 -1
- package/dist/embed.js +249 -71
- package/dist/index.d.ts +10 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -6
- package/dist/server/createYakHandler.d.ts.map +1 -1
- package/dist/server/index.d.ts +6 -6
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/sources.d.ts +1 -1
- package/dist/tool-name.d.ts +10 -0
- package/dist/tool-name.d.ts.map +1 -0
- package/dist/tool-name.js +24 -0
- package/dist/types/config.d.ts +1 -1
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/messaging.d.ts +55 -2
- package/dist/types/messaging.d.ts.map +1 -1
- package/dist/voice-machine.d.ts +85 -0
- package/dist/voice-machine.d.ts.map +1 -0
- package/dist/voice-machine.js +168 -0
- package/dist/voice-session.d.ts +102 -0
- package/dist/voice-session.d.ts.map +1 -0
- package/dist/voice-session.js +541 -0
- package/package.json +6 -2
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:
|
|
112
|
+
display: inline-flex; align-items: center; gap: 8px;
|
|
97
113
|
border: none; border-radius: 30px;
|
|
98
|
-
padding:
|
|
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;
|
|
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%);
|
|
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;
|
|
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
|
-
|
|
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
|
|
207
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 {
|
|
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 ??
|
|
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 ??
|
|
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
|
-
|
|
421
|
-
this.
|
|
422
|
-
this.
|
|
423
|
-
this.
|
|
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
|
-
//
|
|
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.
|
|
441
|
-
|
|
442
|
-
this.
|
|
443
|
-
this.
|
|
444
|
-
|
|
445
|
-
|
|
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.
|
|
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.
|
|
656
|
+
this.triggerEl.style.setProperty(prop, value);
|
|
483
657
|
}
|
|
484
658
|
}
|
|
485
659
|
if (hasLightCustom)
|
|
486
|
-
this.
|
|
660
|
+
this.triggerEl.dataset.hasLightCustom = "true";
|
|
487
661
|
if (hasDarkCustom)
|
|
488
|
-
this.
|
|
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
|
-
|
|
501
|
-
if (!this.
|
|
674
|
+
updateChatButtonState() {
|
|
675
|
+
if (!this.chatButton)
|
|
502
676
|
return;
|
|
503
677
|
const isLoading = this.isOpen && !this.isReady;
|
|
504
|
-
this.
|
|
505
|
-
this.
|
|
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
|
-
|
|
681
|
+
this.chatButton.innerHTML = `<span class="yak-widget-spinner" aria-hidden="true"></span>`;
|
|
512
682
|
}
|
|
513
683
|
else {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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 {
|
|
8
|
-
export type {
|
|
9
|
-
export {
|
|
10
|
-
export type {
|
|
11
|
-
export {
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
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
|
-
|
|
9
|
-
export {
|
|
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":"
|
|
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"}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/server/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { createYakConfigHandler, createYakHandler, createYakToolsHandler, } from "./createYakHandler.js";
|
|
2
2
|
export { normalizeRouteSources, normalizeToolSources, } from "./sources.js";
|
package/dist/server/sources.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RouteInfo } from "../types/routes.js";
|
|
2
|
-
import type { ToolDefinition,
|
|
2
|
+
import type { ToolDefinition, ToolExecutor, ToolManifest } from "../types/tools.js";
|
|
3
3
|
export type RouteSource = {
|
|
4
4
|
id?: string;
|
|
5
5
|
getRoutes: () => Promise<RouteInfo[]>;
|