@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/client.d.ts +24 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +84 -4
- package/dist/embed.d.ts +50 -9
- package/dist/embed.d.ts.map +1 -1
- package/dist/embed.js +243 -70
- 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 +69 -0
- package/dist/voice-machine.d.ts.map +1 -0
- package/dist/voice-machine.js +163 -0
- package/dist/voice-session.d.ts +102 -0
- package/dist/voice-session.d.ts.map +1 -0
- package/dist/voice-session.js +530 -0
- package/package.json +4 -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. */
|
|
@@ -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 ??
|
|
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 ??
|
|
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
|
-
|
|
421
|
-
this.
|
|
422
|
-
this.
|
|
423
|
-
this.
|
|
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
|
-
//
|
|
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.
|
|
441
|
-
|
|
442
|
-
this.
|
|
443
|
-
this.
|
|
444
|
-
|
|
445
|
-
|
|
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.
|
|
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.
|
|
651
|
+
this.triggerEl.style.setProperty(prop, value);
|
|
483
652
|
}
|
|
484
653
|
}
|
|
485
654
|
if (hasLightCustom)
|
|
486
|
-
this.
|
|
655
|
+
this.triggerEl.dataset.hasLightCustom = "true";
|
|
487
656
|
if (hasDarkCustom)
|
|
488
|
-
this.
|
|
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
|
-
|
|
501
|
-
if (!this.
|
|
669
|
+
updateChatButtonState() {
|
|
670
|
+
if (!this.chatButton)
|
|
502
671
|
return;
|
|
503
672
|
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;
|
|
673
|
+
this.chatButton.disabled = isLoading;
|
|
674
|
+
this.chatButton.setAttribute("aria-label", isLoading ? "Loading chat" : "Open chat");
|
|
510
675
|
if (isLoading) {
|
|
511
|
-
|
|
676
|
+
this.chatButton.innerHTML = `<span class="yak-widget-spinner" aria-hidden="true"></span>`;
|
|
512
677
|
}
|
|
513
678
|
else {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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 {
|
|
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[]>;
|
|
@@ -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"}
|