@lokutor/sdk 1.1.19 → 1.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +77 -1
- package/dist/index.d.ts +77 -1
- package/dist/index.js +578 -0
- package/dist/index.mjs +577 -0
- package/package.json +1 -1
- package/src/client.ts +1 -0
- package/src/conversational-panel.ts +609 -0
- package/src/index.ts +2 -0
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
const PANEL_CSS = /*css*/ `
|
|
2
|
+
.cv-panel {
|
|
3
|
+
position: relative;
|
|
4
|
+
width: 100%;
|
|
5
|
+
aspect-ratio: 16 / 9;
|
|
6
|
+
max-height: 800px;
|
|
7
|
+
display: flex;
|
|
8
|
+
flex-direction: column;
|
|
9
|
+
align-items: center;
|
|
10
|
+
justify-content: center;
|
|
11
|
+
padding: 2rem 0;
|
|
12
|
+
margin: 0;
|
|
13
|
+
background: var(--cv-bg, #0a0a0a);
|
|
14
|
+
box-shadow: inset 0 10px 40px rgba(0, 0, 0, 0.1), inset 0 0 100px rgba(0, 0, 0, 0.05);
|
|
15
|
+
border-radius: 40px;
|
|
16
|
+
overflow: hidden;
|
|
17
|
+
container-type: inline-size;
|
|
18
|
+
}
|
|
19
|
+
.cv-curtain {
|
|
20
|
+
position: absolute;
|
|
21
|
+
inset: 0;
|
|
22
|
+
display: flex;
|
|
23
|
+
flex-direction: column;
|
|
24
|
+
align-items: center;
|
|
25
|
+
justify-content: center;
|
|
26
|
+
background: var(--cv-bg, #0a0a0a);
|
|
27
|
+
z-index: 100;
|
|
28
|
+
transition: transform 1s cubic-bezier(0.16, 1, 0.3, 1);
|
|
29
|
+
}
|
|
30
|
+
.cv-curtain .cv-curtain-bg {
|
|
31
|
+
position: absolute;
|
|
32
|
+
inset: 0;
|
|
33
|
+
background: url('/background_gradient.jpeg') center / cover no-repeat;
|
|
34
|
+
z-index: -1;
|
|
35
|
+
}
|
|
36
|
+
.cv-curtain .cv-curtain-overlay {
|
|
37
|
+
position: absolute;
|
|
38
|
+
inset: 0;
|
|
39
|
+
background: var(--cv-accent);
|
|
40
|
+
opacity: 0.35;
|
|
41
|
+
z-index: -1;
|
|
42
|
+
}
|
|
43
|
+
.cv-curtain.is-up { transform: translateY(-100%); }
|
|
44
|
+
.cv-curtain-content {
|
|
45
|
+
display: flex;
|
|
46
|
+
flex-direction: column;
|
|
47
|
+
align-items: center;
|
|
48
|
+
gap: clamp(0.75rem, 1.5cqi, 1.5rem);
|
|
49
|
+
color: #fff;
|
|
50
|
+
text-align: center;
|
|
51
|
+
z-index: 2;
|
|
52
|
+
padding: clamp(1rem, 2cqi, 2rem);
|
|
53
|
+
}
|
|
54
|
+
.cv-curtain-title {
|
|
55
|
+
font-size: clamp(1.2rem, 4cqi, 2.5rem);
|
|
56
|
+
font-weight: 800;
|
|
57
|
+
letter-spacing: -0.03em;
|
|
58
|
+
margin: 0;
|
|
59
|
+
text-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
|
60
|
+
}
|
|
61
|
+
.cv-curtain-desc {
|
|
62
|
+
font-size: clamp(0.75rem, 2cqi, 1.1rem);
|
|
63
|
+
opacity: 0.8;
|
|
64
|
+
max-width: 400px;
|
|
65
|
+
margin: 0;
|
|
66
|
+
line-height: 1.5;
|
|
67
|
+
}
|
|
68
|
+
.cv-curtain-btn {
|
|
69
|
+
margin-top: 1rem;
|
|
70
|
+
padding: clamp(0.6rem, 1.5cqi, 1rem) clamp(1.5rem, 3cqi, 2.5rem);
|
|
71
|
+
border-radius: 100px;
|
|
72
|
+
background: #fff;
|
|
73
|
+
color: #000;
|
|
74
|
+
border: none;
|
|
75
|
+
font-weight: 700;
|
|
76
|
+
font-size: clamp(0.8rem, 1.5cqi, 1rem);
|
|
77
|
+
display: flex;
|
|
78
|
+
align-items: center;
|
|
79
|
+
gap: 0.75rem;
|
|
80
|
+
cursor: pointer;
|
|
81
|
+
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
|
82
|
+
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
|
|
83
|
+
}
|
|
84
|
+
.cv-curtain-btn:hover {
|
|
85
|
+
transform: scale(1.05);
|
|
86
|
+
background: #f0f0f0;
|
|
87
|
+
box-shadow: 0 15px 40px rgba(0,0,0,0.3);
|
|
88
|
+
}
|
|
89
|
+
.cv-curtain-btn svg { transition: transform 0.3s ease; }
|
|
90
|
+
.cv-curtain-btn:hover svg { transform: translateX(4px); }
|
|
91
|
+
.cv-header {
|
|
92
|
+
width: 100%;
|
|
93
|
+
display: flex;
|
|
94
|
+
flex-direction: column;
|
|
95
|
+
align-items: center;
|
|
96
|
+
gap: 0.25rem;
|
|
97
|
+
z-index: 20;
|
|
98
|
+
margin-bottom: auto;
|
|
99
|
+
}
|
|
100
|
+
.cv-title {
|
|
101
|
+
font-size: clamp(1rem, 2.5cqi, 1.75rem);
|
|
102
|
+
font-weight: 700;
|
|
103
|
+
color: #e0e0e0;
|
|
104
|
+
display: flex;
|
|
105
|
+
align-items: center;
|
|
106
|
+
gap: 1rem;
|
|
107
|
+
letter-spacing: -0.02em;
|
|
108
|
+
}
|
|
109
|
+
.cv-title .cv-timer {
|
|
110
|
+
font-variant-numeric: tabular-nums;
|
|
111
|
+
color: var(--cv-accent);
|
|
112
|
+
font-weight: 400;
|
|
113
|
+
opacity: 0.8;
|
|
114
|
+
}
|
|
115
|
+
.cv-visualizer-wrap {
|
|
116
|
+
position: absolute;
|
|
117
|
+
top: 50%;
|
|
118
|
+
left: 50%;
|
|
119
|
+
transform: translate(-50%, -50%);
|
|
120
|
+
width: clamp(140px, 40cqi, 280px);
|
|
121
|
+
height: clamp(140px, 40cqi, 280px);
|
|
122
|
+
display: flex;
|
|
123
|
+
align-items: center;
|
|
124
|
+
justify-content: center;
|
|
125
|
+
z-index: 10;
|
|
126
|
+
transition: all 0.5s cubic-bezier(0.16, 1, 0.3, 1);
|
|
127
|
+
}
|
|
128
|
+
.cv-is-speaking .cv-visualizer-wrap {
|
|
129
|
+
animation: cv-pulse 2.5s infinite ease-in-out;
|
|
130
|
+
}
|
|
131
|
+
.cv-is-thinking .cv-visualizer-wrap {
|
|
132
|
+
opacity: 0.5;
|
|
133
|
+
transform: translate(-50%, -50%) scale(0.9);
|
|
134
|
+
}
|
|
135
|
+
@keyframes cv-pulse {
|
|
136
|
+
0%, 100% { transform: translate(-50%, -50%) scale(1); }
|
|
137
|
+
50% { transform: translate(-50%, -50%) scale(1.05); }
|
|
138
|
+
}
|
|
139
|
+
.cv-canvas {
|
|
140
|
+
width: 100% !important;
|
|
141
|
+
height: 100% !important;
|
|
142
|
+
position: relative;
|
|
143
|
+
z-index: 0;
|
|
144
|
+
}
|
|
145
|
+
.cv-canvas {
|
|
146
|
+
width: 100% !important;
|
|
147
|
+
height: 100% !important;
|
|
148
|
+
position: relative;
|
|
149
|
+
z-index: 0;
|
|
150
|
+
}
|
|
151
|
+
.cv-controls {
|
|
152
|
+
width: 100%;
|
|
153
|
+
display: flex;
|
|
154
|
+
flex-direction: column;
|
|
155
|
+
align-items: center;
|
|
156
|
+
gap: 1.5rem;
|
|
157
|
+
padding-top: 1rem;
|
|
158
|
+
margin-top: auto;
|
|
159
|
+
z-index: 20;
|
|
160
|
+
}
|
|
161
|
+
.cv-pill {
|
|
162
|
+
display: flex;
|
|
163
|
+
align-items: center;
|
|
164
|
+
gap: 0.75rem;
|
|
165
|
+
background: rgba(255, 255, 255, 0.03);
|
|
166
|
+
backdrop-filter: blur(30px);
|
|
167
|
+
-webkit-backdrop-filter: blur(30px);
|
|
168
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
169
|
+
padding: 0.3rem 0.75rem;
|
|
170
|
+
border-radius: 100px;
|
|
171
|
+
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.3);
|
|
172
|
+
}
|
|
173
|
+
.cv-btn {
|
|
174
|
+
display: flex;
|
|
175
|
+
align-items: center;
|
|
176
|
+
gap: 0.5rem;
|
|
177
|
+
background: transparent;
|
|
178
|
+
border: none;
|
|
179
|
+
color: rgba(255,255,255,0.55);
|
|
180
|
+
font-weight: 600;
|
|
181
|
+
font-size: 0.75rem;
|
|
182
|
+
cursor: pointer;
|
|
183
|
+
transition: all 0.2s ease;
|
|
184
|
+
padding: 0.35rem 0.75rem;
|
|
185
|
+
border-radius: 50px;
|
|
186
|
+
}
|
|
187
|
+
.cv-btn:hover { color: #fff; background: rgba(255,255,255,0.05); }
|
|
188
|
+
.cv-btn--end { color: #ff4444; }
|
|
189
|
+
.cv-btn--end .cv-btn-box {
|
|
190
|
+
background: #ff4444;
|
|
191
|
+
width: 8px;
|
|
192
|
+
height: 8px;
|
|
193
|
+
border-radius: 2px;
|
|
194
|
+
}
|
|
195
|
+
.cv-btn.is-muted { color: var(--cv-accent); }
|
|
196
|
+
.cv-error {
|
|
197
|
+
position: absolute;
|
|
198
|
+
bottom: 1.25rem;
|
|
199
|
+
left: 50%;
|
|
200
|
+
transform: translateX(-50%);
|
|
201
|
+
background: #0a0a0a;
|
|
202
|
+
color: #e0e0e0;
|
|
203
|
+
padding: 0.75rem 1.5rem;
|
|
204
|
+
border-radius: 12px;
|
|
205
|
+
font-size: 0.875rem;
|
|
206
|
+
font-weight: 500;
|
|
207
|
+
display: none;
|
|
208
|
+
align-items: center;
|
|
209
|
+
gap: 0.75rem;
|
|
210
|
+
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
|
211
|
+
z-index: 1000;
|
|
212
|
+
border: 1px solid rgba(255, 255, 255, 0.06);
|
|
213
|
+
backdrop-filter: blur(20px);
|
|
214
|
+
}
|
|
215
|
+
.cv-error.is-visible { display: flex; }
|
|
216
|
+
.cv-error-icon { color: var(--cv-accent); flex-shrink: 0; }
|
|
217
|
+
`;
|
|
218
|
+
|
|
219
|
+
let styleInjected = false;
|
|
220
|
+
|
|
221
|
+
function injectStyles() {
|
|
222
|
+
if (styleInjected) return;
|
|
223
|
+
const style = document.createElement('style');
|
|
224
|
+
style.textContent = PANEL_CSS;
|
|
225
|
+
document.head.appendChild(style);
|
|
226
|
+
styleInjected = true;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export interface ConversationalPanelConfig {
|
|
230
|
+
container: HTMLElement;
|
|
231
|
+
title: string;
|
|
232
|
+
description: string;
|
|
233
|
+
prompt: string;
|
|
234
|
+
voice?: string;
|
|
235
|
+
language?: string;
|
|
236
|
+
/** Accent color for the sphere glow and highlights, e.g. '#e74c3c' */
|
|
237
|
+
accentColor?: string;
|
|
238
|
+
/** Background color, e.g. '#0a0a0a' */
|
|
239
|
+
backgroundColor?: string;
|
|
240
|
+
/** API key for connecting to the voice agent */
|
|
241
|
+
apiKey: string;
|
|
242
|
+
/** Reference to the ConvoAgent class */
|
|
243
|
+
ConvoAgent: any;
|
|
244
|
+
/** Reference to the SphereVisualizer class */
|
|
245
|
+
SphereVisualizer: any;
|
|
246
|
+
/** URL for the connecting sound asset */
|
|
247
|
+
connectingSoundSrc?: string;
|
|
248
|
+
/** Background image for the curtain, e.g. '/background_gradient.jpeg' */
|
|
249
|
+
curtainBgSrc?: string;
|
|
250
|
+
/** Tool definitions for LLM function calling */
|
|
251
|
+
tools?: any[];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export class ConversationalPanel {
|
|
255
|
+
private cfg: ConversationalPanelConfig;
|
|
256
|
+
private container: HTMLElement;
|
|
257
|
+
private agent: any = null;
|
|
258
|
+
private visualizer: any = null;
|
|
259
|
+
private timerTicker: number | null = null;
|
|
260
|
+
private connectingSound: HTMLAudioElement | null = null;
|
|
261
|
+
private connectingFadeTimer: number | null = null;
|
|
262
|
+
private isRunning = false;
|
|
263
|
+
|
|
264
|
+
// Cached DOM refs
|
|
265
|
+
private el!: HTMLElement;
|
|
266
|
+
private curtain!: HTMLElement;
|
|
267
|
+
private curtainTitle!: HTMLElement;
|
|
268
|
+
private curtainDesc!: HTMLElement;
|
|
269
|
+
private startBtn!: HTMLButtonElement;
|
|
270
|
+
private errorEl!: HTMLElement;
|
|
271
|
+
private errorText!: HTMLElement;
|
|
272
|
+
private timerEl!: HTMLElement;
|
|
273
|
+
private canvas!: HTMLCanvasElement;
|
|
274
|
+
private muteBtn!: HTMLElement;
|
|
275
|
+
private stopBtn!: HTMLElement;
|
|
276
|
+
private visualizerWrap!: HTMLElement;
|
|
277
|
+
|
|
278
|
+
// Callbacks
|
|
279
|
+
onTranscription?: (text: string) => void;
|
|
280
|
+
onResponse?: (text: string) => void;
|
|
281
|
+
onStart?: () => void;
|
|
282
|
+
onStop?: () => void;
|
|
283
|
+
onError?: (err: any) => void;
|
|
284
|
+
|
|
285
|
+
constructor(cfg: ConversationalPanelConfig) {
|
|
286
|
+
this.cfg = cfg;
|
|
287
|
+
this.container = cfg.container;
|
|
288
|
+
injectStyles();
|
|
289
|
+
this.buildDOM();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private buildDOM() {
|
|
293
|
+
const accent = this.cfg.accentColor || '#a25a6b';
|
|
294
|
+
const bg = this.cfg.backgroundColor || '#0a0a0a';
|
|
295
|
+
|
|
296
|
+
this.container.style.setProperty('--cv-accent', accent);
|
|
297
|
+
this.container.style.setProperty('--cv-bg', bg);
|
|
298
|
+
|
|
299
|
+
this.el = document.createElement('div');
|
|
300
|
+
this.el.className = 'cv-panel';
|
|
301
|
+
this.el.innerHTML = `
|
|
302
|
+
<div class="cv-curtain">
|
|
303
|
+
<div class="cv-curtain-bg"></div>
|
|
304
|
+
<div class="cv-curtain-overlay"></div>
|
|
305
|
+
<div class="cv-curtain-content">
|
|
306
|
+
<h3 class="cv-curtain-title">${this.esc(this.cfg.title)}</h3>
|
|
307
|
+
<p class="cv-curtain-desc">${this.esc(this.cfg.description)}</p>
|
|
308
|
+
<button class="cv-curtain-btn">
|
|
309
|
+
<span>Start Conversation</span>
|
|
310
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="20" height="20">
|
|
311
|
+
<path d="M5 12h14M12 5l7 7-7 7"/>
|
|
312
|
+
</svg>
|
|
313
|
+
</button>
|
|
314
|
+
</div>
|
|
315
|
+
<div class="cv-error">
|
|
316
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
|
317
|
+
width="20" height="20" class="cv-error-icon">
|
|
318
|
+
<circle cx="12" cy="12" r="10"></circle>
|
|
319
|
+
<path d="M12 8v4"></path>
|
|
320
|
+
<path d="M12 16h.01"></path>
|
|
321
|
+
</svg>
|
|
322
|
+
<span class="cv-error-text"></span>
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
<div class="cv-header">
|
|
326
|
+
<h2 class="cv-title">${this.esc(this.cfg.title)} <span class="cv-timer">00:00</span></h2>
|
|
327
|
+
</div>
|
|
328
|
+
<div class="cv-visualizer-wrap">
|
|
329
|
+
<canvas class="cv-canvas"></canvas>
|
|
330
|
+
</div>
|
|
331
|
+
<div class="cv-controls">
|
|
332
|
+
<div class="cv-pill">
|
|
333
|
+
<button class="cv-btn cv-btn--mute">
|
|
334
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="18" height="18">
|
|
335
|
+
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/>
|
|
336
|
+
<path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
|
|
337
|
+
</svg>
|
|
338
|
+
<span>Mute</span>
|
|
339
|
+
</button>
|
|
340
|
+
<button class="cv-btn cv-btn--end">
|
|
341
|
+
<div class="cv-btn-box"></div>
|
|
342
|
+
<span>End call</span>
|
|
343
|
+
</button>
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
`;
|
|
347
|
+
|
|
348
|
+
this.container.appendChild(this.el);
|
|
349
|
+
|
|
350
|
+
// Cache refs
|
|
351
|
+
this.curtain = this.el.querySelector('.cv-curtain')!;
|
|
352
|
+
if (this.cfg.curtainBgSrc) {
|
|
353
|
+
this.curtain.style.backgroundImage = `url('${this.cfg.curtainBgSrc}')`;
|
|
354
|
+
}
|
|
355
|
+
this.curtainTitle = this.el.querySelector('.cv-curtain-title')!;
|
|
356
|
+
this.curtainDesc = this.el.querySelector('.cv-curtain-desc')!;
|
|
357
|
+
this.startBtn = this.el.querySelector('.cv-curtain-btn')!;
|
|
358
|
+
this.errorEl = this.el.querySelector('.cv-error')!;
|
|
359
|
+
this.errorText = this.el.querySelector('.cv-error-text')!;
|
|
360
|
+
this.timerEl = this.el.querySelector('.cv-timer')!;
|
|
361
|
+
this.canvas = this.el.querySelector('.cv-canvas')!;
|
|
362
|
+
this.muteBtn = this.el.querySelector('.cv-btn--mute')!;
|
|
363
|
+
this.stopBtn = this.el.querySelector('.cv-btn--end')!;
|
|
364
|
+
this.visualizerWrap = this.el.querySelector('.cv-visualizer-wrap')!;
|
|
365
|
+
|
|
366
|
+
// Events
|
|
367
|
+
this.startBtn.addEventListener('click', () => this.start());
|
|
368
|
+
this.stopBtn.addEventListener('click', () => this.stop());
|
|
369
|
+
this.muteBtn.addEventListener('click', () => this.toggleMute());
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
private esc(s: string) {
|
|
373
|
+
const d = document.createElement('div');
|
|
374
|
+
d.textContent = s;
|
|
375
|
+
return d.innerHTML;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** Start the conversation (called when user clicks "Start Conversation" or externally) */
|
|
379
|
+
async start() {
|
|
380
|
+
if (this.isRunning) return;
|
|
381
|
+
this.isRunning = true;
|
|
382
|
+
this.startBtn.disabled = true;
|
|
383
|
+
this.errorEl.classList.remove('is-visible');
|
|
384
|
+
this.playConnectingSound();
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
const ConvoAgent = this.cfg.ConvoAgent;
|
|
388
|
+
const SphereVisualizer = this.cfg.SphereVisualizer;
|
|
389
|
+
|
|
390
|
+
const visualizer = new SphereVisualizer(this.canvas);
|
|
391
|
+
visualizer.resize(280, 280);
|
|
392
|
+
this.visualizer = visualizer;
|
|
393
|
+
|
|
394
|
+
// Pass accent color to the visualizer shader
|
|
395
|
+
if (this.cfg.accentColor && typeof visualizer.setAccentColor === 'function') {
|
|
396
|
+
const hex = this.cfg.accentColor;
|
|
397
|
+
visualizer.setAccentColor(
|
|
398
|
+
parseInt(hex.slice(1, 3), 16) / 255,
|
|
399
|
+
parseInt(hex.slice(3, 5), 16) / 255,
|
|
400
|
+
parseInt(hex.slice(5, 7), 16) / 255
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const agentHandle = new ConvoAgent({
|
|
405
|
+
apiKey: this.cfg.apiKey,
|
|
406
|
+
tools: this.cfg.tools,
|
|
407
|
+
prompt: this.cfg.prompt,
|
|
408
|
+
voice: this.cfg.voice || 'M1',
|
|
409
|
+
language: this.cfg.language || 'en',
|
|
410
|
+
onStatusChange: (status: string) => {
|
|
411
|
+
this.el.classList.remove('cv-is-speaking', 'cv-is-thinking');
|
|
412
|
+
if (status === 'speaking') this.el.classList.add('cv-is-speaking');
|
|
413
|
+
else if (status === 'thinking') this.el.classList.add('cv-is-thinking');
|
|
414
|
+
},
|
|
415
|
+
onTranscription: (text: string) => {
|
|
416
|
+
this.onTranscription?.(text);
|
|
417
|
+
},
|
|
418
|
+
onResponse: (text: string) => {
|
|
419
|
+
this.onResponse?.(text);
|
|
420
|
+
},
|
|
421
|
+
onError: (err: any) => {
|
|
422
|
+
if (!this.isRunning) return;
|
|
423
|
+
this.onError?.(err);
|
|
424
|
+
this.stop();
|
|
425
|
+
this.showError('Connection issue. Try again in a moment.');
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
this.agent = agentHandle;
|
|
430
|
+
visualizer.setAudioClient(agentHandle);
|
|
431
|
+
|
|
432
|
+
// Lift curtain immediately for responsive UI
|
|
433
|
+
this.curtain.classList.add('is-up');
|
|
434
|
+
try { agentHandle.unlockAudioForMobile?.(); } catch (_) { /* ignore */ }
|
|
435
|
+
|
|
436
|
+
const ok = await agentHandle.connect();
|
|
437
|
+
if (ok) {
|
|
438
|
+
this.fadeOutConnectingSound();
|
|
439
|
+
// Resize canvas to container
|
|
440
|
+
const wrap = this.visualizerWrap;
|
|
441
|
+
if (wrap) {
|
|
442
|
+
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
443
|
+
const w = Math.floor(wrap.clientWidth * dpr);
|
|
444
|
+
const h = Math.floor(wrap.clientHeight * dpr);
|
|
445
|
+
visualizer.resize(w, h);
|
|
446
|
+
}
|
|
447
|
+
visualizer.start();
|
|
448
|
+
this.startTimer();
|
|
449
|
+
this.onStart?.();
|
|
450
|
+
if ((window as any).lucide) (window as any).lucide.createIcons();
|
|
451
|
+
} else {
|
|
452
|
+
this.fadeOutConnectingSound();
|
|
453
|
+
this.playErrorTone();
|
|
454
|
+
this.curtain.classList.remove('is-up');
|
|
455
|
+
this.showError('Could not connect. Please try again.');
|
|
456
|
+
this.isRunning = false;
|
|
457
|
+
}
|
|
458
|
+
} catch (err: any) {
|
|
459
|
+
this.fadeOutConnectingSound();
|
|
460
|
+
this.playErrorTone();
|
|
461
|
+
console.error('ConversationalPanel start error:', err);
|
|
462
|
+
this.stop();
|
|
463
|
+
this.showError('Something went wrong. Please try again.');
|
|
464
|
+
} finally {
|
|
465
|
+
this.startBtn.disabled = false;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** Stop / disconnect the conversation */
|
|
470
|
+
stop() {
|
|
471
|
+
if (!this.isRunning && !this.agent && !this.visualizer) return;
|
|
472
|
+
this.isRunning = false;
|
|
473
|
+
this.fadeOutConnectingSound();
|
|
474
|
+
this.errorEl.classList.remove('is-visible');
|
|
475
|
+
if (this.agent) {
|
|
476
|
+
try { this.agent.disconnect(); } catch (_) {}
|
|
477
|
+
this.agent = null;
|
|
478
|
+
}
|
|
479
|
+
if (this.timerTicker) { clearInterval(this.timerTicker); this.timerTicker = null; }
|
|
480
|
+
if (this.visualizer) { this.visualizer.stop(); this.visualizer = null; }
|
|
481
|
+
this.curtain.classList.remove('is-up');
|
|
482
|
+
this.el.classList.remove('cv-is-speaking', 'cv-is-thinking');
|
|
483
|
+
this.muteBtn.classList.remove('is-muted');
|
|
484
|
+
const span = this.muteBtn.querySelector('span');
|
|
485
|
+
if (span) span.textContent = 'Mute';
|
|
486
|
+
this.onStop?.();
|
|
487
|
+
|
|
488
|
+
// Ensure start button is enabled
|
|
489
|
+
this.startBtn.disabled = false;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/** Update accent color dynamically */
|
|
493
|
+
setColor(color: string) {
|
|
494
|
+
this.cfg.accentColor = color;
|
|
495
|
+
this.container.style.setProperty('--cv-accent', color);
|
|
496
|
+
if (this.visualizer && typeof this.visualizer.setAccentColor === 'function') {
|
|
497
|
+
const r = parseInt(color.slice(1, 3), 16) / 255;
|
|
498
|
+
const g = parseInt(color.slice(3, 5), 16) / 255;
|
|
499
|
+
const b = parseInt(color.slice(5, 7), 16) / 255;
|
|
500
|
+
this.visualizer.setAccentColor(r, g, b);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/** Update title text */
|
|
505
|
+
setTitle(title: string) {
|
|
506
|
+
this.cfg.title = title;
|
|
507
|
+
this.curtainTitle.textContent = title;
|
|
508
|
+
const h2 = this.el.querySelector('.cv-title');
|
|
509
|
+
if (h2) h2.innerHTML = `${this.esc(title)} <span class="cv-timer">${this.timerEl?.textContent || '00:00'}</span>`;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/** Update description text */
|
|
513
|
+
setDescription(desc: string) {
|
|
514
|
+
this.cfg.description = desc;
|
|
515
|
+
this.curtainDesc.textContent = desc;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/** Update prompt (only takes effect on next start()) */
|
|
519
|
+
setPrompt(prompt: string) {
|
|
520
|
+
this.cfg.prompt = prompt;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/** Show an error message */
|
|
524
|
+
showError(text: string) {
|
|
525
|
+
this.errorText.textContent = text;
|
|
526
|
+
this.errorEl.classList.add('is-visible');
|
|
527
|
+
setTimeout(() => {
|
|
528
|
+
this.errorEl.classList.remove('is-visible');
|
|
529
|
+
}, 5000);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** Destroy the component, removing all DOM and stopping any active session */
|
|
533
|
+
destroy() {
|
|
534
|
+
this.stop();
|
|
535
|
+
this.el.remove();
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// ─── internal helpers ────────────────────────────────────
|
|
539
|
+
|
|
540
|
+
private startTimer() {
|
|
541
|
+
let elapsed = 0;
|
|
542
|
+
this.timerEl.textContent = '00:00';
|
|
543
|
+
this.timerTicker = window.setInterval(() => {
|
|
544
|
+
elapsed++;
|
|
545
|
+
const m = Math.floor(elapsed / 60).toString().padStart(2, '0');
|
|
546
|
+
const s = (elapsed % 60).toString().padStart(2, '0');
|
|
547
|
+
this.timerEl.textContent = `${m}:${s}`;
|
|
548
|
+
}, 1000);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
private toggleMute() {
|
|
552
|
+
if (!this.agent) return;
|
|
553
|
+
try {
|
|
554
|
+
const muted = this.agent.toggleMute();
|
|
555
|
+
this.muteBtn.classList.toggle('is-muted', muted);
|
|
556
|
+
const span = this.muteBtn.querySelector('span');
|
|
557
|
+
if (span) span.textContent = muted ? 'Unmute' : 'Mute';
|
|
558
|
+
if (muted) {
|
|
559
|
+
this.agent.pauseAudio?.();
|
|
560
|
+
this.visualizer?.setDeactivated?.(true);
|
|
561
|
+
} else {
|
|
562
|
+
this.agent.resumeAudio?.();
|
|
563
|
+
this.visualizer?.setDeactivated?.(false);
|
|
564
|
+
}
|
|
565
|
+
} catch (_) {}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
private playConnectingSound() {
|
|
569
|
+
this.fadeOutConnectingSound();
|
|
570
|
+
const src = this.cfg.connectingSoundSrc || '/connecting.mp3';
|
|
571
|
+
this.connectingSound = new Audio(src);
|
|
572
|
+
this.connectingSound.volume = 0.6;
|
|
573
|
+
this.connectingSound.play().catch(() => {});
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
private fadeOutConnectingSound() {
|
|
577
|
+
if (this.connectingFadeTimer) { clearInterval(this.connectingFadeTimer); this.connectingFadeTimer = null; }
|
|
578
|
+
if (!this.connectingSound) return;
|
|
579
|
+
const startVol = this.connectingSound.volume;
|
|
580
|
+
const steps = 15;
|
|
581
|
+
let step = 0;
|
|
582
|
+
this.connectingFadeTimer = window.setInterval(() => {
|
|
583
|
+
step++;
|
|
584
|
+
const progress = step / steps;
|
|
585
|
+
if (this.connectingSound) this.connectingSound.volume = startVol * Math.max(0, 1 - progress);
|
|
586
|
+
if (step >= steps) {
|
|
587
|
+
if (this.connectingFadeTimer) { clearInterval(this.connectingFadeTimer); this.connectingFadeTimer = null; }
|
|
588
|
+
if (this.connectingSound) { this.connectingSound.pause(); this.connectingSound.currentTime = 0; this.connectingSound = null; }
|
|
589
|
+
}
|
|
590
|
+
}, 100);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
private playErrorTone() {
|
|
594
|
+
try {
|
|
595
|
+
const ctx = new ((window as any).AudioContext || (window as any).webkitAudioContext)();
|
|
596
|
+
const now = ctx.currentTime;
|
|
597
|
+
[0, 0.3].forEach((offset, i) => {
|
|
598
|
+
const osc = ctx.createOscillator();
|
|
599
|
+
const gain = ctx.createGain();
|
|
600
|
+
osc.connect(gain); gain.connect(ctx.destination);
|
|
601
|
+
osc.type = 'sine';
|
|
602
|
+
osc.frequency.setValueAtTime(600 - i * 200, now + offset);
|
|
603
|
+
gain.gain.setValueAtTime(0.25, now + offset);
|
|
604
|
+
gain.gain.exponentialRampToValueAtTime(0.001, now + offset + 0.35);
|
|
605
|
+
osc.start(now + offset); osc.stop(now + offset + 0.4);
|
|
606
|
+
});
|
|
607
|
+
} catch (_) {}
|
|
608
|
+
}
|
|
609
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,8 @@ export * from './audio-utils';
|
|
|
4
4
|
export * from './browser-audio';
|
|
5
5
|
export { VoiceAgentClient, TTSClient, simpleConversation, simpleTTS } from './client';
|
|
6
6
|
export { BrowserAudioManager } from './browser-audio';
|
|
7
|
+
export { ConversationalPanel } from './conversational-panel';
|
|
8
|
+
export type { ConversationalPanelConfig } from './conversational-panel';
|
|
7
9
|
export {
|
|
8
10
|
pcm16ToFloat32,
|
|
9
11
|
float32ToPcm16,
|