@craftedxp/voice-js 0.4.1 → 0.4.2

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.
@@ -1,28 +1,4 @@
1
- 'use strict'
2
- var VoissiaEmbedBundle = (() => {
3
- function P(n) {
4
- if (!n) throw new Error('configureVoiceClient: config is required')
5
- if ('apiKey' in n)
6
- throw new Error(
7
- 'configureVoiceClient: `apiKey` is no longer supported. Embedding sk_ in JS code ships server-grade credentials to every client. Pass `fetchToken: async ({ agentId }) => { /* call YOUR backend mint */ }` instead \u2014 see the @craftedxp/voice-js README for the migration recipe.',
8
- )
9
- if (!n.apiBase) throw new Error('configureVoiceClient: apiBase is required')
10
- if (typeof n.fetchToken != 'function')
11
- throw new Error('configureVoiceClient: fetchToken must be a function')
12
- return { ...n, apiBase: n.apiBase.replace(/\/+$/, '') }
13
- }
14
- function N(n, e) {
15
- let t =
16
- n.defaultContext || e.context
17
- ? { ...(n.defaultContext ?? {}), ...(e.context ?? {}) }
18
- : void 0,
19
- r =
20
- n.defaultMetadata || e.metadata
21
- ? { ...(n.defaultMetadata ?? {}), ...(e.metadata ?? {}) }
22
- : void 0
23
- return { context: t, metadata: r }
24
- }
25
- var L = `// AudioWorklet \u2014 runs off the main thread in the audio rendering graph.
1
+ "use strict";var VoissiaEmbedBundle=(()=>{function N(n){if(!n)throw new Error("configureVoiceClient: config is required");if("apiKey"in n)throw new Error("configureVoiceClient: `apiKey` is no longer supported. Embedding sk_ in JS code ships server-grade credentials to every client. Pass `fetchToken: async ({ agentId }) => { /* call YOUR backend mint */ }` instead \u2014 see the @craftedxp/voice-js README for the migration recipe.");if(!n.apiBase)throw new Error("configureVoiceClient: apiBase is required");if(typeof n.fetchToken!="function")throw new Error("configureVoiceClient: fetchToken must be a function");return{...n,apiBase:n.apiBase.replace(/\/+$/,"")}}function L(n,e){let t=n.defaultContext||e.context?{...n.defaultContext??{},...e.context??{}}:void 0,r=n.defaultMetadata||e.metadata?{...n.defaultMetadata??{},...e.metadata??{}}:void 0;return{context:t,metadata:r}}var $=`// AudioWorklet \u2014 runs off the main thread in the audio rendering graph.
26
2
  //
27
3
  // Input: Float32 samples at the AudioContext's native sampleRate (typically
28
4
  // 48000 Hz on desktop, 44100 Hz on some iOS devices).
@@ -103,974 +79,14 @@ class MicDownsampler extends AudioWorkletProcessor {
103
79
  }
104
80
 
105
81
  registerProcessor('mic-downsampler', MicDownsampler)
106
- `
107
- var K = 100,
108
- $ = (n) => {
109
- let e = null,
110
- t = null,
111
- r = null,
112
- o = null,
113
- a = null,
114
- s = null,
115
- p = !1,
116
- d = !1,
117
- g = (u) => {
118
- let h = 0
119
- for (let b = 0; b < u.length; b++) h += u[b] * u[b]
120
- let k = Math.sqrt(h / u.length)
121
- return Math.min(1, k * 1.8)
122
- }
123
- return {
124
- start: async () => {
125
- if (!d)
126
- try {
127
- ;((t = await navigator.mediaDevices.getUserMedia({
128
- audio: {
129
- echoCancellation: !0,
130
- noiseSuppression: !0,
131
- autoGainControl: !0,
132
- channelCount: 1,
133
- },
134
- })),
135
- (e = new AudioContext()),
136
- e.state === 'suspended' && (await e.resume()))
137
- let u = new Blob([L], { type: 'application/javascript' }),
138
- h = URL.createObjectURL(u)
139
- try {
140
- await e.audioWorklet.addModule(h)
141
- } finally {
142
- URL.revokeObjectURL(h)
143
- }
144
- if (
145
- ((r = e.createMediaStreamSource(t)),
146
- (o = new AudioWorkletNode(e, 'mic-downsampler')),
147
- (o.port.onmessage = (b) => {
148
- p || n.onChunk(b.data)
149
- }),
150
- n.onVolume)
151
- ) {
152
- ;((a = e.createAnalyser()), (a.fftSize = 256), r.connect(a))
153
- let b = new Float32Array(a.fftSize)
154
- s = setInterval(() => {
155
- a && (a.getFloatTimeDomainData(b), n.onVolume?.(g(b)))
156
- }, K)
157
- }
158
- r.connect(o)
159
- let k = e.createGain()
160
- ;((k.gain.value = 0), o.connect(k).connect(e.destination), (d = !0))
161
- } catch (u) {
162
- let h =
163
- u instanceof Error ? u : new Error(typeof u == 'string' ? u : 'capture failed')
164
- throw (n.onError?.(h), h)
165
- }
166
- },
167
- stop: () => {
168
- if (d) {
169
- ;((d = !1), s && (clearInterval(s), (s = null)))
170
- try {
171
- ;(o?.disconnect(), a?.disconnect(), r?.disconnect())
172
- } catch {}
173
- if (((o = null), (a = null), (r = null), t)) {
174
- for (let u of t.getTracks()) u.stop()
175
- t = null
176
- }
177
- ;(e && e.state !== 'closed' && e.close().catch(() => {}), (e = null))
178
- }
179
- },
180
- mute: (u) => {
181
- p = u
182
- },
183
- isCapturing: () => d,
184
- }
185
- }
186
- var U = (n = {}) => {
187
- let e = n.sampleRate ?? 16e3,
188
- t = null,
189
- r = null,
190
- o = null,
191
- a = null,
192
- s = 0,
193
- p = [],
194
- d = !1,
195
- g = async () => {
196
- if (t) {
197
- t.state === 'suspended' && (await t.resume())
198
- return
199
- }
200
- if (((t = new AudioContext({ sampleRate: e })), (r = t.createGain()), n.onVolume)) {
201
- ;((o = t.createAnalyser()), (o.fftSize = 256), r.connect(o))
202
- let f = new Float32Array(o.fftSize)
203
- a = setInterval(() => {
204
- if (!o) return
205
- o.getFloatTimeDomainData(f)
206
- let i = 0
207
- for (let C = 0; C < f.length; C++) i += f[C] * f[C]
208
- let m = Math.sqrt(i / f.length)
209
- n.onVolume?.(Math.min(1, m * 1.8))
210
- }, 100)
211
- }
212
- ;(r.connect(t.destination), (s = t.currentTime))
213
- },
214
- l = (f) => {
215
- f !== d && ((d = f), n.onSpeakingChange?.(f))
216
- },
217
- c = () => {
218
- let f = t?.currentTime ?? 0
219
- ;((p = p.filter((i) => (i._endsAt ?? 0) > f)), p.length === 0 && l(!1))
220
- },
221
- u = (f) => {
222
- if (!t) {
223
- g().then(() => u(f))
224
- return
225
- }
226
- if (!t || !r) return
227
- let i = new Int16Array(f)
228
- if (i.length === 0) return
229
- let m = t.createBuffer(1, i.length, e),
230
- C = m.getChannelData(0)
231
- for (let w = 0; w < i.length; w++) C[w] = i[w] / 32768
232
- let y = t.createBufferSource()
233
- ;((y.buffer = m), y.connect(r))
234
- let X = t.currentTime,
235
- _ = Math.max(X, s)
236
- y.start(_)
237
- let W = i.length / e
238
- ;((y._endsAt = _ + W), (s = _ + W), p.push(y), l(!0), (y.onended = () => c()))
239
- },
240
- h = () => {
241
- if (!(!t || !r)) {
242
- for (let f of p)
243
- try {
244
- f.stop()
245
- } catch {}
246
- ;((p = []),
247
- r.disconnect(),
248
- (r = t.createGain()),
249
- o && (o.disconnect(), r.connect(o)),
250
- r.connect(t.destination),
251
- (s = t.currentTime),
252
- l(!1))
253
- }
254
- }
255
- return {
256
- enqueue: u,
257
- flush: h,
258
- close: () => {
259
- ;(h(),
260
- a && (clearInterval(a), (a = null)),
261
- t && t.state !== 'closed' && t.close().catch(() => {}),
262
- (t = null),
263
- (r = null),
264
- (o = null))
265
- },
266
- resume: async () => {
267
- await g()
268
- },
269
- }
270
- }
271
- var D = (n, e) => {
272
- let t = n.maxRetries ?? 3,
273
- r = n.initialBackoffMs ?? 500,
274
- o = n.maxBackoffMs ?? 8e3,
275
- a = null,
276
- s = !1,
277
- p = 0,
278
- d = r,
279
- g = null,
280
- l = () => {
281
- ;((a = n.wsFactory(n.url)),
282
- (a.binaryType = 'arraybuffer'),
283
- (a.onopen = () => {
284
- ;(e(p === 0 ? { type: 'open' } : { type: 'reconnected' }), (p = 0), (d = r))
285
- }),
286
- (a.onmessage = (c) => {
287
- e({ type: 'message', data: c.data })
288
- }),
289
- (a.onerror = () => {
290
- e({ type: 'error', error: new Error('WebSocket error') })
291
- }),
292
- (a.onclose = (c) => {
293
- if (((a = null), !(!s && p < t))) {
294
- e({ type: 'close', code: c.code, reason: c.reason, permanent: !0 })
295
- return
296
- }
297
- ;(e({ type: 'close', code: c.code, reason: c.reason, permanent: !1 }), p++)
298
- let h = Math.min(d, o)
299
- ;((d = Math.min(d * 2, o)), (g = setTimeout(l, h)))
300
- }))
301
- }
302
- return (
303
- l(),
304
- {
305
- send: (c) => {
306
- a && a.readyState === 1 && a.send(c)
307
- },
308
- close: (c = 1e3, u = 'client-requested') => {
309
- ;((s = !0), g && (clearTimeout(g), (g = null)))
310
- try {
311
- a?.close(c, u)
312
- } catch {}
313
- },
314
- readyState: () => a?.readyState ?? 3,
315
- }
316
- )
317
- }
318
- var v = () => ({
319
- state: 'idle',
320
- transcript: [],
321
- agentBubbleId: null,
322
- idCounter: 0,
323
- endReason: null,
324
- }),
325
- Z = (n) =>
326
- n === 'agent_ended'
327
- ? 'agent_ended'
328
- : n === 'caller_hung_up'
329
- ? 'user_hangup'
330
- : n === 'silence_timeout' || n === 'max_duration'
331
- ? 'timeout'
332
- : 'error'
333
- function x(n, e, t) {
334
- let r
335
- try {
336
- r = JSON.parse(n)
337
- } catch {
338
- return
339
- }
340
- switch (r.type) {
341
- case 'connected':
342
- ;(t.onConnected(), E(e, 'listening', t))
343
- return
344
- case 'transcript': {
345
- let o = r.text ?? ''
346
- if (!o) return
347
- let a = !!r.isFinal
348
- ;(a || E(e, 'user_speaking', t), Q(e, o, a), t.onTranscript(e.transcript))
349
- return
350
- }
351
- case 'agent_turn_start': {
352
- let o = `m${e.idCounter++}`
353
- ;((e.agentBubbleId = o),
354
- (e.transcript = [...e.transcript, { id: o, role: 'agent', text: '' }]),
355
- t.onTranscript(e.transcript))
356
- let a = typeof r.seq == 'number' ? r.seq : void 0
357
- ;(t.onAgentTurnStart(a), E(e, 'agent_speaking', t))
358
- return
359
- }
360
- case 'agent_text': {
361
- let o = r.text ?? ''
362
- if (!o || !e.agentBubbleId) return
363
- let a = e.agentBubbleId
364
- ;((e.transcript = e.transcript.map((s) =>
365
- s.id === a && s.role === 'agent' ? { ...s, text: s.text + o } : s,
366
- )),
367
- t.onTranscript(e.transcript))
368
- return
369
- }
370
- case 'agent_turn_end': {
371
- e.agentBubbleId = null
372
- let o = typeof r.seq == 'number' ? r.seq : void 0
373
- ;(t.onAgentTurnEnd(o), E(e, 'listening', t))
374
- return
375
- }
376
- case 'interrupt':
377
- t.onInterrupt()
378
- return
379
- case 'agent_turn_abort': {
380
- let o = (r.committedText ?? '').trim()
381
- if (e.agentBubbleId) {
382
- let a = e.agentBubbleId
383
- ;(o
384
- ? (e.transcript = e.transcript.map((s) =>
385
- s.id === a && s.role === 'agent' ? { ...s, text: o, interrupted: !0 } : s,
386
- ))
387
- : (e.transcript = e.transcript.filter((s) => s.id !== a)),
388
- t.onTranscript(e.transcript))
389
- }
390
- e.agentBubbleId = null
391
- return
392
- }
393
- case 'tool_call':
394
- ;((e.transcript = [
395
- ...e.transcript,
396
- {
397
- id: `m${e.idCounter++}`,
398
- role: 'tool',
399
- text: `\u2192 ${String(r.tool ?? '?')}(${r.args ? JSON.stringify(r.args) : ''})`,
400
- },
401
- ]),
402
- t.onTranscript(e.transcript))
403
- return
404
- case 'tool_result':
405
- ;((e.transcript = [
406
- ...e.transcript,
407
- {
408
- id: `m${e.idCounter++}`,
409
- role: 'tool',
410
- text: `${r.ok ? '\u2713' : '\u2717'} ${String(r.tool ?? '?')}`,
411
- },
412
- ]),
413
- t.onTranscript(e.transcript))
414
- return
415
- case 'client_tool_call': {
416
- let o = String(r.toolCallId ?? ''),
417
- a = String(r.name ?? ''),
418
- s = r.args ?? {}
419
- if (!o || !a) return
420
- t.onClientToolCall({ toolCallId: o, name: a, args: s })
421
- return
422
- }
423
- case 'call_end': {
424
- let o = String(r.reason ?? ''),
425
- a = Z(o)
426
- ;((e.endReason = a),
427
- (e.transcript = [
428
- ...e.transcript,
429
- { id: `m${e.idCounter++}`, role: 'system', text: `call ended${o ? ` (${o})` : ''}` },
430
- ]),
431
- t.onTranscript(e.transcript),
432
- t.onCallEnd(a))
433
- return
434
- }
435
- case 'error': {
436
- let o = r.code ?? 'server_error',
437
- a = r.message ?? 'server error'
438
- t.onError({ code: o, message: a })
439
- return
440
- }
441
- }
442
- }
443
- var E = (n, e, t) => {
444
- n.state !== e && t.onState(e)
445
- },
446
- Q = (n, e, t) => {
447
- let r = -1
448
- for (let s = n.transcript.length - 1; s >= 0; s--) {
449
- let p = n.transcript[s]
450
- if (p.role === 'user' && p.committed === !1) {
451
- r = s
452
- break
453
- }
454
- }
455
- if (r === -1) {
456
- n.transcript = [
457
- ...n.transcript,
458
- { id: `m${n.idCounter++}`, role: 'user', text: e, committed: t },
459
- ]
460
- return
461
- }
462
- let o = n.transcript[r],
463
- a = [...n.transcript]
464
- ;((a[r] = { ...o, text: e, committed: t }), (n.transcript = a))
465
- }
466
- function j(n) {
467
- let e = new URL(n.apiBase),
468
- t = e.protocol === 'https:' ? 'wss:' : 'ws:',
469
- r = n.bargeIn === !1 ? '&barge=off' : ''
470
- return `${t}//${e.host}/v1/agents/${encodeURIComponent(n.agentId)}/call?token=${encodeURIComponent(n.token)}${r}`
471
- }
472
- var ee = /^[a-zA-Z_][a-zA-Z0-9_]*$/
473
- var T = (n) => {
474
- if (n === void 0) return
475
- if (typeof n != 'object' || n === null || Array.isArray(n))
476
- throw new Error('clientTools must be an object keyed by tool name')
477
- let e = Object.entries(n)
478
- if (e.length > 64)
479
- throw new Error(`clientTools may declare at most 64 tools (got ${e.length})`)
480
- for (let [t, r] of e) {
481
- if (!ee.test(t))
482
- throw new Error(
483
- `clientTools["${t}"]: name must be a valid identifier (^[a-zA-Z_][a-zA-Z0-9_]*$)`,
484
- )
485
- if (!r || typeof r != 'object') throw new Error(`clientTools["${t}"]: must be an object`)
486
- if (typeof r.description != 'string' || r.description.length === 0)
487
- throw new Error(`clientTools["${t}"]: must have a description`)
488
- if (typeof r.handler != 'function')
489
- throw new Error(`clientTools["${t}"]: must have a handler function`)
490
- if (r.usage !== void 0 && r.usage.length > 500)
491
- throw new Error(`clientTools["${t}"]: usage must be \u2264500 chars`)
492
- if (
493
- r.timeoutMs !== void 0 &&
494
- (!Number.isFinite(r.timeoutMs) || r.timeoutMs <= 0 || r.timeoutMs > 3e4)
495
- )
496
- throw new Error(`clientTools["${t}"]: timeoutMs must be in (0, 30000]`)
497
- }
498
- },
499
- S = (n) => ({
500
- type: 'client_tools_register',
501
- tools: Object.entries(n).map(([e, t]) => ({
502
- name: e,
503
- description: t.description,
504
- parameters: t.parameters,
505
- ...(t.usage !== void 0 ? { usage: t.usage } : {}),
506
- ...(t.timeoutMs !== void 0 ? { timeoutMs: t.timeoutMs } : {}),
507
- })),
508
- }),
509
- A = (n, e, t) => {
510
- let r = (a) => {
511
- try {
512
- n(a)
513
- } catch {}
514
- },
515
- o = e[t.name]
516
- if (!o) {
517
- r({
518
- type: 'client_tool_result',
519
- toolCallId: t.toolCallId,
520
- error: `No handler for ${t.name}`,
521
- })
522
- return
523
- }
524
- ;(async () => {
525
- try {
526
- let a = await o.handler(t.args)
527
- r({
528
- type: 'client_tool_result',
529
- toolCallId: t.toolCallId,
530
- result: typeof a == 'string' ? a : JSON.stringify(a),
531
- })
532
- } catch (a) {
533
- r({
534
- type: 'client_tool_result',
535
- toolCallId: t.toolCallId,
536
- error: a instanceof Error ? a.message : String(a),
537
- })
538
- }
539
- })()
540
- }
541
- var z = (n) => {
542
- let e = n.now ?? (() => performance.now()),
543
- t = null,
544
- r = new Map(),
545
- o = (l) => {
546
- let c = r.get(l)
547
- if (!c || !c.ended) return
548
- let u = {}
549
- ;(c.firstOutboundAt !== null &&
550
- c.firstAudibleAt !== null &&
551
- (u.client_mic_to_first_audible_ms = c.firstAudibleAt - c.firstOutboundAt),
552
- n.send({ type: 'client_marks', seq: l, marks: u, clientNow: Date.now() }),
553
- r.delete(l))
554
- }
555
- return {
556
- markFirstOutboundAudio: () => {
557
- t === null && (t = e())
558
- },
559
- markFirstAudibleOutput: () => {
560
- let l
561
- for (let c of r.values()) c.ended || (l = c)
562
- l && l.firstAudibleAt === null && (l.firstAudibleAt = e())
563
- },
564
- onAgentTurnStart: (l) => {
565
- ;(r.set(l, { firstOutboundAt: t, firstAudibleAt: null, ended: !1 }), (t = null))
566
- },
567
- onAgentTurnEnd: (l) => {
568
- let c = r.get(l)
569
- if (!c) {
570
- n.send({ type: 'client_marks', seq: l, marks: {}, clientNow: Date.now() })
571
- return
572
- }
573
- ;((c.ended = !0), o(l))
574
- },
575
- flush: () => {
576
- for (let l of [...r.keys()]) {
577
- let c = r.get(l)
578
- ;((c.ended = !0), o(l))
579
- }
580
- t = null
581
- },
582
- }
583
- }
584
- var M = class {
585
- constructor(e) {
586
- this.rws = null
587
- this.capture = null
588
- this.playback = null
589
- this.muted = !1
590
- this.inputVolume = 0
591
- this.outputVolume = 0
592
- this.startedAt = null
593
- this.endedFired = !1
594
- this.lastError = null
595
- this.end = () => {
596
- this.teardown('user_hangup')
597
- }
598
- this.mute = () => {
599
- this.muted || ((this.muted = !0), this.capture?.mute(!0))
600
- }
601
- this.unmute = () => {
602
- this.muted && ((this.muted = !1), this.capture?.mute(!1))
603
- }
604
- this.sendClientToolsRegister = () => {
605
- let e = S(this.args.options.clientTools ?? {})
606
- this.rws?.send(JSON.stringify(e))
607
- }
608
- this.setState = (e) => {
609
- this.proto.state !== e && ((this.proto.state = e), this.args.options.onStateChange?.(e))
610
- }
611
- this.emitError = (e) => {
612
- ;((this.lastError = e), this.args.options.onError?.(e))
613
- }
614
- this.handleSocketEvent = (e) => {
615
- switch (e.type) {
616
- case 'open':
617
- this.startCapture()
618
- break
619
- case 'reconnected':
620
- ;((this.proto.transcript = []),
621
- (this.proto.agentBubbleId = null),
622
- this.args.options.onTranscript?.(this.proto.transcript),
623
- this.startCapture(),
624
- this.setState('listening'))
625
- break
626
- case 'message':
627
- typeof e.data == 'string'
628
- ? x(e.data, this.proto, {
629
- onState: this.setState,
630
- onTranscript: (t) => this.args.options.onTranscript?.(t),
631
- onError: this.emitError,
632
- onInterrupt: () => {
633
- ;(this.playback?.flush(), this.args.options.onInterrupt?.())
634
- },
635
- onAgentTurnStart: (t) => {
636
- ;(typeof t == 'number' && this.marks.onAgentTurnStart(t),
637
- this.args.options.onAgentTurnStart?.())
638
- },
639
- onAgentTurnEnd: (t) => {
640
- typeof t == 'number' && this.marks.onAgentTurnEnd(t)
641
- },
642
- onCallEnd: (t) => this.teardown(t),
643
- onConnected: () => this.sendClientToolsRegister(),
644
- onClientToolCall: (t) =>
645
- A(
646
- (r) => this.rws?.send(JSON.stringify(r)),
647
- this.args.options.clientTools ?? {},
648
- t,
649
- ),
650
- })
651
- : (this.marks.markFirstAudibleOutput(), this.playback?.enqueue(e.data))
652
- break
653
- case 'close':
654
- if (e.permanent) {
655
- let t = this.proto.endReason ?? (this.lastError ? 'error' : 'user_hangup')
656
- this.teardown(t)
657
- }
658
- break
659
- case 'error':
660
- this.emitError({ code: 'socket_error', message: e.error.message })
661
- break
662
- }
663
- }
664
- this.startCapture = async () => {
665
- if (!this.capture?.isCapturing()) {
666
- ;((this.capture = $({
667
- onChunk: (e) => {
668
- ;(this.marks.markFirstOutboundAudio(), this.rws?.send(e))
669
- },
670
- onVolume: (e) => {
671
- ;((this.inputVolume = e),
672
- this.args.options.onVolume?.({ input: e, output: this.outputVolume }))
673
- },
674
- onError: (e) => {
675
- this.emitError({
676
- code: e.name === 'NotAllowedError' ? 'mic_denied' : 'mic_start_failed',
677
- message: e.message,
678
- })
679
- },
680
- })),
681
- this.muted && this.capture.mute(!0))
682
- try {
683
- await this.capture.start()
684
- } catch {}
685
- }
686
- }
687
- this.teardown = (e) => {
688
- try {
689
- this.marks.flush()
690
- } catch {}
691
- ;(this.capture?.stop(),
692
- (this.capture = null),
693
- this.playback?.close(),
694
- (this.playback = null))
695
- try {
696
- this.rws?.close(1e3, e)
697
- } catch {}
698
- ;((this.rws = null), this.setState('ended'), this.fireEndOnce(e))
699
- }
700
- this.fireEndOnce = (e) => {
701
- if (this.endedFired) return
702
- this.endedFired = !0
703
- let t = this.startedAt ?? Date.now()
704
- this.args.options.onEnd?.({
705
- reason: e,
706
- errorCode: e === 'error' ? this.lastError?.code : void 0,
707
- durationMs: Date.now() - t,
708
- })
709
- }
710
- ;((this.args = e),
711
- (this.proto = v()),
712
- T(e.options.clientTools),
713
- (this.marks = z({
714
- send: (t) => {
715
- try {
716
- this.rws?.send(JSON.stringify(t))
717
- } catch {}
718
- },
719
- })))
720
- }
721
- get state() {
722
- return this.proto.state
723
- }
724
- get transcript() {
725
- return this.proto.transcript.slice()
726
- }
727
- get isMuted() {
728
- return this.muted
729
- }
730
- async start() {
731
- ;(this.setState('connecting'), (this.startedAt = Date.now()))
732
- let e = j({
733
- apiBase: this.args.config.apiBase,
734
- agentId: this.args.options.agentId,
735
- token: this.args.token,
736
- bargeIn: this.args.options.bargeIn,
737
- })
738
- this.playback = U({
739
- onVolume: (t) => {
740
- ;((this.outputVolume = t),
741
- this.args.options.onVolume?.({ input: this.inputVolume, output: t }))
742
- },
743
- })
744
- try {
745
- await this.playback.resume()
746
- } catch {}
747
- this.rws = D({ url: e, wsFactory: this.args.wsFactory, maxRetries: 3 }, (t) =>
748
- this.handleSocketEvent(t),
749
- )
750
- }
751
- }
752
- async function q(n) {
753
- T(n.clientTools)
754
- let e = v(),
755
- t = !1,
756
- r = !1,
757
- o = n.clientTools ?? {},
758
- a = (i) => {
759
- if (c?.readyState === 'open')
760
- try {
761
- c.send(JSON.stringify(i))
762
- } catch {}
763
- },
764
- s = (i) => {
765
- e.state !== i && ((e.state = i), n.onStateChange?.(i))
766
- },
767
- p = (i) => {
768
- x(i, e, {
769
- onState: s,
770
- onTranscript: (m) => n.onTranscript?.(m),
771
- onError: (m) => n.onError?.(m),
772
- onInterrupt: () => n.onInterrupt?.(),
773
- onAgentTurnStart: () => n.onAgentTurnStart?.(),
774
- onAgentTurnEnd: () => {},
775
- onCallEnd: () => f(),
776
- onConnected: () => {
777
- Object.keys(o).length > 0 && a(S(o))
778
- },
779
- onClientToolCall: (m) => {
780
- A(a, o, m)
781
- },
782
- })
783
- }
784
- s('connecting')
785
- let d = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] }),
786
- g = document.createElement('audio')
787
- ;((g.autoplay = !0),
788
- (g.style.display = 'none'),
789
- document.body.appendChild(g),
790
- (d.ontrack = (i) => {
791
- g.srcObject = i.streams[0] ?? new MediaStream([i.track])
792
- }))
793
- let l
794
- try {
795
- l = await navigator.mediaDevices.getUserMedia({ audio: !0 })
796
- } catch (i) {
797
- let m =
798
- i instanceof DOMException && i.name === 'NotAllowedError'
799
- ? 'mic_denied'
800
- : 'mic_start_failed'
801
- throw (
802
- n.onError?.({ code: m, message: i instanceof Error ? i.message : 'getUserMedia failed' }),
803
- s('error'),
804
- d.close(),
805
- g.remove(),
806
- i
807
- )
808
- }
809
- for (let i of l.getAudioTracks()) d.addTrack(i, l)
810
- let c = d.createDataChannel('control', { ordered: !0 })
811
- ;((c.onmessage = (i) => {
812
- typeof i.data == 'string' && p(i.data)
813
- }),
814
- (c.onerror = () => {
815
- n.onError?.({ code: 'socket_error', message: 'control channel error' })
816
- }),
817
- (c.onopen = () => {
818
- Object.keys(o).length > 0 && a(S(o))
819
- }))
820
- let u = n.webrtcGatewayBase || '',
821
- h = u
822
- ? `${u}/webrtc/offer?token=${encodeURIComponent(n.token)}`
823
- : `${n.apiBase}/v1/agents/${encodeURIComponent(n.agentId)}/webrtc/offer?token=${encodeURIComponent(n.token)}`,
824
- k = u
825
- ? `${u}/webrtc/ice?token=${encodeURIComponent(n.token)}`
826
- : `${n.apiBase}/v1/agents/${encodeURIComponent(n.agentId)}/webrtc/ice?token=${encodeURIComponent(n.token)}`
827
- await d.setLocalDescription(await d.createOffer())
828
- let b
829
- try {
830
- let i = await fetch(h, {
831
- method: 'POST',
832
- headers: { 'content-type': 'application/json' },
833
- body: JSON.stringify({ sdp: d.localDescription.sdp, type: 'offer', agentId: n.agentId }),
834
- })
835
- if (!i.ok) {
836
- let C = i.status === 401 ? 'unauthorized' : 'server_error'
837
- throw (
838
- n.onError?.({ code: C, message: `signaling failed: HTTP ${i.status}` }),
839
- s('error'),
840
- l.getTracks().forEach((y) => y.stop()),
841
- d.close(),
842
- g.remove(),
843
- new Error(`webrtc offer failed: ${i.status}`)
844
- )
845
- }
846
- let m = await i.json()
847
- ;((b = m.callId), await d.setRemoteDescription({ type: 'answer', sdp: m.sdp }))
848
- } catch (i) {
849
- throw (
850
- r ||
851
- (n.onError?.({
852
- code: 'network_unreachable',
853
- message: i instanceof Error ? i.message : 'signaling failed',
854
- }),
855
- s('error'),
856
- l.getTracks().forEach((m) => m.stop()),
857
- d.close(),
858
- g.remove()),
859
- i
860
- )
861
- }
862
- ;((d.onicecandidate = (i) => {
863
- i.candidate &&
864
- fetch(k, {
865
- method: 'POST',
866
- headers: { 'content-type': 'application/json' },
867
- body: JSON.stringify({ callId: b, candidate: i.candidate }),
868
- }).catch(() => {})
869
- }),
870
- (d.onconnectionstatechange = () => {
871
- let i = d.connectionState
872
- ;(i === 'connected' && s('listening'),
873
- (i === 'failed' || i === 'disconnected') &&
874
- (n.onError?.({ code: 'socket_error', message: `webrtc connection ${i}` }), f()),
875
- i === 'closed' && !r && f())
876
- }))
877
- let f = () => {
878
- if (!r) {
879
- r = !0
880
- try {
881
- l.getTracks().forEach((i) => i.stop())
882
- } catch {}
883
- try {
884
- d.close()
885
- } catch {}
886
- try {
887
- g.remove()
888
- } catch {}
889
- ;(s('ended'), n.onEnd?.())
890
- }
891
- }
892
- return {
893
- get state() {
894
- return e.state
895
- },
896
- get transcript() {
897
- return e.transcript.slice()
898
- },
899
- get isMuted() {
900
- return t
901
- },
902
- end: () => f(),
903
- mute: () => {
904
- t || ((t = !0), l.getAudioTracks().forEach((i) => (i.enabled = !1)))
905
- },
906
- unmute: () => {
907
- t && ((t = !1), l.getAudioTracks().forEach((i) => (i.enabled = !0)))
908
- },
909
- }
910
- }
911
- var te = (n) => new globalThis.WebSocket(n),
912
- I = class {
913
- constructor(e) {
914
- this.startCall = async (e) => {
915
- if (!e.agentId) throw new Error('startCall: agentId is required')
916
- let { context: t, metadata: r } = N(this.config, e),
917
- o = { agentId: e.agentId, userId: e.userId, context: t, metadata: r },
918
- a
919
- if (e.token) a = { token: e.token, transport: 'ws' }
920
- else {
921
- let p = await this.config.fetchToken(o)
922
- if (!p) throw new Error('configureVoiceClient.fetchToken returned empty token')
923
- if (((a = typeof p == 'string' ? { token: p, transport: 'ws' } : p), !a.token))
924
- throw new Error('configureVoiceClient.fetchToken returned an object without `token`')
925
- }
926
- if (a.transport === 'webrtc')
927
- return q({
928
- agentId: e.agentId,
929
- apiBase: this.config.apiBase,
930
- token: a.token,
931
- webrtcGatewayBase: a.webrtcGatewayBase,
932
- onStateChange: e.onStateChange,
933
- onTranscript: e.onTranscript,
934
- onError: e.onError,
935
- onEnd: e.onEnd ? () => e.onEnd({ reason: 'agent_ended', durationMs: 0 }) : void 0,
936
- onInterrupt: e.onInterrupt,
937
- onAgentTurnStart: e.onAgentTurnStart,
938
- clientTools: e.clientTools,
939
- })
940
- let s = new M({
941
- config: this.config,
942
- options: { ...e, context: t, metadata: r },
943
- token: a.token,
944
- wsFactory: te,
945
- })
946
- return (await s.start(), s)
947
- }
948
- this.config = e
949
- }
950
- }
951
- function G(n) {
952
- return new I(P(n))
953
- }
954
- var H = 'voice-agent-embed-root',
955
- re =
956
- 'M12 14a3 3 0 0 0 3-3V5a3 3 0 0 0-6 0v6a3 3 0 0 0 3 3zm5.3-3a.7.7 0 0 1 1.4 0 6.7 6.7 0 0 1-6 6.66V21h-1.4v-3.34A6.7 6.7 0 0 1 5.3 11a.7.7 0 0 1 1.4 0 5.3 5.3 0 0 0 10.6 0z',
957
- O = 'http://www.w3.org/2000/svg',
958
- J = (n) => {
959
- let e = document.createElementNS(O, 'svg')
960
- if ((e.setAttribute('viewBox', '0 0 24 24'), e.setAttribute('class', 'icon'), n.rect)) {
961
- let t = document.createElementNS(O, 'rect')
962
- ;(t.setAttribute('x', '7'),
963
- t.setAttribute('y', '7'),
964
- t.setAttribute('width', '10'),
965
- t.setAttribute('height', '10'),
966
- t.setAttribute('rx', '1.5'),
967
- e.appendChild(t))
968
- } else if (n.path) {
969
- let t = document.createElementNS(O, 'path')
970
- ;(t.setAttribute('d', n.path), e.appendChild(t))
971
- }
972
- return e
973
- },
974
- ne = (n) => {
975
- try {
976
- return JSON.parse(n)
977
- } catch {
978
- return
979
- }
980
- },
981
- oe = (n) => {
982
- if (n)
983
- try {
984
- return new URL(n.src, location.href).origin
985
- } catch {}
986
- return location.origin
987
- },
988
- ae = () => {
989
- let n = document.querySelectorAll('script[src*="embed.js"]')
990
- for (let e of Array.from(n)) {
991
- let t = e.dataset
992
- if (
993
- (t.apiKey &&
994
- console.error(
995
- '[craftedxp/voice-js] data-api-key on the embed <script> is no longer supported. Mint a ct_ server-side via @craftedxp/sdk-node and inject it into data-token instead.',
996
- ),
997
- t.agentId && t.token)
998
- )
999
- return {
1000
- apiBase: t.apiBase || oe(e),
1001
- token: t.token,
1002
- agentId: t.agentId,
1003
- vars: t.vars ? ne(t.vars) : void 0,
1004
- label: t.label || 'Call agent',
1005
- primaryColor: t.primaryColor || '#3d7dd0',
1006
- position: t.position === 'bottom-left' ? 'bottom-left' : 'bottom-right',
1007
- transport: t.transport || void 0,
1008
- webrtcGatewayBase: t.webrtcGatewayBase || void 0,
1009
- }
1010
- }
1011
- return null
1012
- },
1013
- B = class {
1014
- constructor(e) {
1015
- this.call = null
1016
- ;((this.cfg = e),
1017
- document.getElementById(H)?.remove(),
1018
- (this.host = document.createElement('div')),
1019
- (this.host.id = H),
1020
- document.body.appendChild(this.host),
1021
- (this.shadow = this.host.attachShadow({ mode: 'open' })),
1022
- this.mount())
1023
- }
1024
- mount() {
1025
- let e = document.createElement('style')
1026
- ;((e.textContent = this.css()), this.shadow.appendChild(e))
1027
- let t = document.createElement('div')
1028
- ;((t.className = `wrap ${this.cfg.position ?? 'bottom-right'}`),
1029
- (this.panel = document.createElement('div')),
1030
- (this.panel.className = 'panel'),
1031
- (this.panel.style.display = 'none'))
1032
- let r = document.createElement('div')
1033
- r.className = 'panel-head'
1034
- let o = document.createElement('div')
1035
- ;((o.className = 'title'),
1036
- (o.textContent = this.cfg.label ?? 'Call agent'),
1037
- (this.statusEl = document.createElement('div')),
1038
- (this.statusEl.className = 'status'),
1039
- (this.statusEl.textContent = 'idle'),
1040
- r.appendChild(o),
1041
- r.appendChild(this.statusEl),
1042
- (this.transcriptEl = document.createElement('div')),
1043
- (this.transcriptEl.className = 'transcript'),
1044
- this.panel.appendChild(r),
1045
- this.panel.appendChild(this.transcriptEl),
1046
- t.appendChild(this.panel),
1047
- (this.fab = document.createElement('button')),
1048
- (this.fab.className = 'fab'),
1049
- (this.fab.type = 'button'),
1050
- this.setFabState('idle'),
1051
- this.fab.addEventListener('click', () => this.toggle()),
1052
- t.appendChild(this.fab),
1053
- this.shadow.appendChild(t),
1054
- (!this.cfg.agentId || !this.cfg.token) &&
1055
- (this.setStatus(
1056
- 'Widget not configured \u2014 data-token + data-agent-id are required',
1057
- 'warn',
1058
- ),
1059
- (this.fab.disabled = !0),
1060
- (this.fab.title = 'Missing data-token or data-agent-id')))
1061
- }
1062
- destroy() {
1063
- ;(this.call?.end(), (this.call = null), this.host.remove())
1064
- }
1065
- css() {
1066
- return `
82
+ `;var K=100,U=n=>{let e=null,t=null,r=null,o=null,a=null,s=null,p=!1,d=!1,g=u=>{let h=0;for(let b=0;b<u.length;b++)h+=u[b]*u[b];let k=Math.sqrt(h/u.length);return Math.min(1,k*1.8)};return{start:async()=>{if(!d)try{t=await navigator.mediaDevices.getUserMedia({audio:{echoCancellation:!0,noiseSuppression:!0,autoGainControl:!0,channelCount:1}}),e=new AudioContext,e.state==="suspended"&&await e.resume();let u=new Blob([$],{type:"application/javascript"}),h=URL.createObjectURL(u);try{await e.audioWorklet.addModule(h)}finally{URL.revokeObjectURL(h)}if(r=e.createMediaStreamSource(t),o=new AudioWorkletNode(e,"mic-downsampler"),o.port.onmessage=b=>{p||n.onChunk(b.data)},n.onVolume){a=e.createAnalyser(),a.fftSize=256,r.connect(a);let b=new Float32Array(a.fftSize);s=setInterval(()=>{a&&(a.getFloatTimeDomainData(b),n.onVolume?.(g(b)))},K)}r.connect(o);let k=e.createGain();k.gain.value=0,o.connect(k).connect(e.destination),d=!0}catch(u){let h=u instanceof Error?u:new Error(typeof u=="string"?u:"capture failed");throw n.onError?.(h),h}},stop:()=>{if(d){d=!1,s&&(clearInterval(s),s=null);try{o?.disconnect(),a?.disconnect(),r?.disconnect()}catch{}if(o=null,a=null,r=null,t){for(let u of t.getTracks())u.stop();t=null}e&&e.state!=="closed"&&e.close().catch(()=>{}),e=null}},mute:u=>{p=u},isCapturing:()=>d}};var D=(n={})=>{let e=n.sampleRate??16e3,t=null,r=null,o=null,a=null,s=0,p=[],d=!1,g=async()=>{if(t){t.state==="suspended"&&await t.resume();return}if(t=new AudioContext({sampleRate:e}),r=t.createGain(),n.onVolume){o=t.createAnalyser(),o.fftSize=256,r.connect(o);let f=new Float32Array(o.fftSize);a=setInterval(()=>{if(!o)return;o.getFloatTimeDomainData(f);let C=0;for(let i=0;i<f.length;i++)C+=f[i]*f[i];let y=Math.sqrt(C/f.length);n.onVolume?.(Math.min(1,y*1.8))},100)}r.connect(t.destination),s=t.currentTime},l=f=>{f!==d&&(d=f,n.onSpeakingChange?.(f))},c=()=>{let f=t?.currentTime??0;p=p.filter(C=>(C._endsAt??0)>f),p.length===0&&l(!1)},u=f=>{if(!t){g().then(()=>u(f));return}if(!t||!r)return;let C=new Int16Array(f);if(C.length===0)return;let y=t.createBuffer(1,C.length,e),i=y.getChannelData(0);for(let E=0;E<C.length;E++)i[E]=C[E]/32768;let m=t.createBufferSource();m.buffer=y,m.connect(r);let I=t.currentTime,S=Math.max(I,s);m.start(S);let P=C.length/e;m._endsAt=S+P,s=S+P,p.push(m),l(!0),m.onended=()=>c()},h=()=>{if(!(!t||!r)){for(let f of p)try{f.stop()}catch{}p=[],r.disconnect(),r=t.createGain(),o&&(o.disconnect(),r.connect(o)),r.connect(t.destination),s=t.currentTime,l(!1)}};return{enqueue:u,flush:h,close:()=>{h(),a&&(clearInterval(a),a=null),t&&t.state!=="closed"&&t.close().catch(()=>{}),t=null,r=null,o=null},resume:async()=>{await g()}}};var j=(n,e)=>{let t=n.maxRetries??3,r=n.initialBackoffMs??500,o=n.maxBackoffMs??8e3,a=null,s=!1,p=0,d=r,g=null,l=()=>{a=n.wsFactory(n.url),a.binaryType="arraybuffer",a.onopen=()=>{e(p===0?{type:"open"}:{type:"reconnected"}),p=0,d=r},a.onmessage=c=>{e({type:"message",data:c.data})},a.onerror=()=>{e({type:"error",error:new Error("WebSocket error")})},a.onclose=c=>{if(a=null,!(!s&&p<t)){e({type:"close",code:c.code,reason:c.reason,permanent:!0});return}e({type:"close",code:c.code,reason:c.reason,permanent:!1}),p++;let h=Math.min(d,o);d=Math.min(d*2,o),g=setTimeout(l,h)}};return l(),{send:c=>{a&&a.readyState===1&&a.send(c)},close:(c=1e3,u="client-requested")=>{s=!0,g&&(clearTimeout(g),g=null);try{a?.close(c,u)}catch{}},readyState:()=>a?.readyState??3}};var x=()=>({state:"idle",transcript:[],agentBubbleId:null,idCounter:0,endReason:null}),Z=n=>n==="agent_ended"?"agent_ended":n==="caller_hung_up"?"user_hangup":n==="silence_timeout"||n==="max_duration"?"timeout":"error";function T(n,e,t){let r;try{r=JSON.parse(n)}catch{return}switch(r.type){case"connected":t.onConnected(),v(e,"listening",t);return;case"transcript":{let o=r.text??"";if(!o)return;let a=!!r.isFinal;a||v(e,"user_speaking",t),Q(e,o,a),t.onTranscript(e.transcript);return}case"agent_turn_start":{let o=`m${e.idCounter++}`;e.agentBubbleId=o,e.transcript=[...e.transcript,{id:o,role:"agent",text:""}],t.onTranscript(e.transcript);let a=typeof r.seq=="number"?r.seq:void 0;t.onAgentTurnStart(a),v(e,"agent_speaking",t);return}case"agent_text":{let o=r.text??"";if(!o||!e.agentBubbleId)return;let a=e.agentBubbleId;e.transcript=e.transcript.map(s=>s.id===a&&s.role==="agent"?{...s,text:s.text+o}:s),t.onTranscript(e.transcript);return}case"agent_turn_end":{e.agentBubbleId=null;let o=typeof r.seq=="number"?r.seq:void 0;t.onAgentTurnEnd(o),v(e,"listening",t);return}case"interrupt":t.onInterrupt();return;case"agent_turn_abort":{let o=(r.committedText??"").trim();if(e.agentBubbleId){let a=e.agentBubbleId;o?e.transcript=e.transcript.map(s=>s.id===a&&s.role==="agent"?{...s,text:o,interrupted:!0}:s):e.transcript=e.transcript.filter(s=>s.id!==a),t.onTranscript(e.transcript)}e.agentBubbleId=null;return}case"tool_call":e.transcript=[...e.transcript,{id:`m${e.idCounter++}`,role:"tool",text:`\u2192 ${String(r.tool??"?")}(${r.args?JSON.stringify(r.args):""})`}],t.onTranscript(e.transcript);return;case"tool_result":e.transcript=[...e.transcript,{id:`m${e.idCounter++}`,role:"tool",text:`${r.ok?"\u2713":"\u2717"} ${String(r.tool??"?")}`}],t.onTranscript(e.transcript);return;case"client_tool_call":{let o=String(r.toolCallId??""),a=String(r.name??""),s=r.args??{};if(!o||!a)return;t.onClientToolCall({toolCallId:o,name:a,args:s});return}case"call_end":{let o=String(r.reason??""),a=Z(o);e.endReason=a,e.transcript=[...e.transcript,{id:`m${e.idCounter++}`,role:"system",text:`call ended${o?` (${o})`:""}`}],t.onTranscript(e.transcript),t.onCallEnd(a);return}case"error":{let o=r.code??"server_error",a=r.message??"server error";t.onError({code:o,message:a});return}}}var v=(n,e,t)=>{n.state!==e&&t.onState(e)},Q=(n,e,t)=>{let r=-1;for(let s=n.transcript.length-1;s>=0;s--){let p=n.transcript[s];if(p.role==="user"&&p.committed===!1){r=s;break}}if(r===-1){n.transcript=[...n.transcript,{id:`m${n.idCounter++}`,role:"user",text:e,committed:t}];return}let o=n.transcript[r],a=[...n.transcript];a[r]={...o,text:e,committed:t},n.transcript=a};function z(n){let e=new URL(n.apiBase),t=e.protocol==="https:"?"wss:":"ws:",r=n.bargeIn===!1?"&barge=off":"";return`${t}//${e.host}/v1/agents/${encodeURIComponent(n.agentId)}/call?token=${encodeURIComponent(n.token)}${r}`}var ee=/^[a-zA-Z_][a-zA-Z0-9_]*$/;var A=n=>{if(n===void 0)return;if(typeof n!="object"||n===null||Array.isArray(n))throw new Error("clientTools must be an object keyed by tool name");let e=Object.entries(n);if(e.length>64)throw new Error(`clientTools may declare at most 64 tools (got ${e.length})`);for(let[t,r]of e){if(!ee.test(t))throw new Error(`clientTools["${t}"]: name must be a valid identifier (^[a-zA-Z_][a-zA-Z0-9_]*$)`);if(!r||typeof r!="object")throw new Error(`clientTools["${t}"]: must be an object`);if(typeof r.description!="string"||r.description.length===0)throw new Error(`clientTools["${t}"]: must have a description`);if(typeof r.handler!="function")throw new Error(`clientTools["${t}"]: must have a handler function`);if(r.usage!==void 0&&r.usage.length>500)throw new Error(`clientTools["${t}"]: usage must be \u2264500 chars`);if(r.timeoutMs!==void 0&&(!Number.isFinite(r.timeoutMs)||r.timeoutMs<=0||r.timeoutMs>3e4))throw new Error(`clientTools["${t}"]: timeoutMs must be in (0, 30000]`)}},w=n=>({type:"client_tools_register",tools:Object.entries(n).map(([e,t])=>({name:e,description:t.description,parameters:t.parameters,...t.usage!==void 0?{usage:t.usage}:{},...t.timeoutMs!==void 0?{timeoutMs:t.timeoutMs}:{}}))}),M=(n,e,t)=>{let r=a=>{try{n(a)}catch{}},o=e[t.name];if(!o){r({type:"client_tool_result",toolCallId:t.toolCallId,error:`No handler for ${t.name}`});return}(async()=>{try{let a=await o.handler(t.args);r({type:"client_tool_result",toolCallId:t.toolCallId,result:typeof a=="string"?a:JSON.stringify(a)})}catch(a){r({type:"client_tool_result",toolCallId:t.toolCallId,error:a instanceof Error?a.message:String(a)})}})()};var q=n=>{let e=n.now??(()=>performance.now()),t=null,r=new Map,o=l=>{let c=r.get(l);if(!c||!c.ended)return;let u={};c.firstOutboundAt!==null&&c.firstAudibleAt!==null&&(u.client_mic_to_first_audible_ms=c.firstAudibleAt-c.firstOutboundAt),n.send({type:"client_marks",seq:l,marks:u,clientNow:Date.now()}),r.delete(l)};return{markFirstOutboundAudio:()=>{t===null&&(t=e())},markFirstAudibleOutput:()=>{let l;for(let c of r.values())c.ended||(l=c);l&&l.firstAudibleAt===null&&(l.firstAudibleAt=e())},onAgentTurnStart:l=>{r.set(l,{firstOutboundAt:t,firstAudibleAt:null,ended:!1}),t=null},onAgentTurnEnd:l=>{let c=r.get(l);if(!c){n.send({type:"client_marks",seq:l,marks:{},clientNow:Date.now()});return}c.ended=!0,o(l)},flush:()=>{for(let l of[...r.keys()]){let c=r.get(l);c.ended=!0,o(l)}t=null}}};var R=class{constructor(e){this.rws=null;this.capture=null;this.playback=null;this.muted=!1;this.inputVolume=0;this.outputVolume=0;this.startedAt=null;this.endedFired=!1;this.lastError=null;this.end=()=>{this.teardown("user_hangup")};this.mute=()=>{this.muted||(this.muted=!0,this.capture?.mute(!0))};this.unmute=()=>{this.muted&&(this.muted=!1,this.capture?.mute(!1))};this.sendClientToolsRegister=()=>{let e=w(this.args.options.clientTools??{});this.rws?.send(JSON.stringify(e))};this.setState=e=>{this.proto.state!==e&&(this.proto.state=e,this.args.options.onStateChange?.(e))};this.emitError=e=>{this.lastError=e,this.args.options.onError?.(e)};this.handleSocketEvent=e=>{switch(e.type){case"open":this.startCapture();break;case"reconnected":this.proto.transcript=[],this.proto.agentBubbleId=null,this.args.options.onTranscript?.(this.proto.transcript),this.startCapture(),this.setState("listening");break;case"message":typeof e.data=="string"?T(e.data,this.proto,{onState:this.setState,onTranscript:t=>this.args.options.onTranscript?.(t),onError:this.emitError,onInterrupt:()=>{this.playback?.flush(),this.args.options.onInterrupt?.()},onAgentTurnStart:t=>{typeof t=="number"&&this.marks.onAgentTurnStart(t),this.args.options.onAgentTurnStart?.()},onAgentTurnEnd:t=>{typeof t=="number"&&this.marks.onAgentTurnEnd(t)},onCallEnd:t=>this.teardown(t),onConnected:()=>this.sendClientToolsRegister(),onClientToolCall:t=>M(r=>this.rws?.send(JSON.stringify(r)),this.args.options.clientTools??{},t)}):(this.marks.markFirstAudibleOutput(),this.playback?.enqueue(e.data));break;case"close":if(e.permanent){let t=this.proto.endReason??(this.lastError?"error":"user_hangup");this.teardown(t)}break;case"error":this.emitError({code:"socket_error",message:e.error.message});break}};this.startCapture=async()=>{if(!this.capture?.isCapturing()){this.capture=U({onChunk:e=>{this.marks.markFirstOutboundAudio(),this.rws?.send(e)},onVolume:e=>{this.inputVolume=e,this.args.options.onVolume?.({input:e,output:this.outputVolume})},onError:e=>{this.emitError({code:e.name==="NotAllowedError"?"mic_denied":"mic_start_failed",message:e.message})}}),this.muted&&this.capture.mute(!0);try{await this.capture.start()}catch{}}};this.teardown=e=>{try{this.marks.flush()}catch{}this.capture?.stop(),this.capture=null,this.playback?.close(),this.playback=null;try{this.rws?.close(1e3,e)}catch{}this.rws=null,this.setState("ended"),this.fireEndOnce(e)};this.fireEndOnce=e=>{if(this.endedFired)return;this.endedFired=!0;let t=this.startedAt??Date.now();this.args.options.onEnd?.({reason:e,errorCode:e==="error"?this.lastError?.code:void 0,durationMs:Date.now()-t})};this.args=e,this.proto=x(),A(e.options.clientTools),this.marks=q({send:t=>{try{this.rws?.send(JSON.stringify(t))}catch{}}})}get state(){return this.proto.state}get transcript(){return this.proto.transcript.slice()}get isMuted(){return this.muted}async start(){this.setState("connecting"),this.startedAt=Date.now();let e=z({apiBase:this.args.config.apiBase,agentId:this.args.options.agentId,token:this.args.token,bargeIn:this.args.options.bargeIn});this.playback=D({onVolume:t=>{this.outputVolume=t,this.args.options.onVolume?.({input:this.inputVolume,output:t})}});try{await this.playback.resume()}catch{}this.rws=j({url:e,wsFactory:this.args.wsFactory,maxRetries:3},t=>this.handleSocketEvent(t))}};async function G(n){A(n.clientTools);let e=x(),t=!1,r=!1,o=n.clientTools??{},a=i=>{if(c?.readyState==="open")try{c.send(JSON.stringify(i))}catch{}},s=i=>{e.state!==i&&(e.state=i,n.onStateChange?.(i))},p=i=>{T(i,e,{onState:s,onTranscript:m=>n.onTranscript?.(m),onError:m=>n.onError?.(m),onInterrupt:()=>n.onInterrupt?.(),onAgentTurnStart:()=>n.onAgentTurnStart?.(),onAgentTurnEnd:()=>{},onCallEnd:()=>b(),onConnected:()=>{Object.keys(o).length>0&&a(w(o))},onClientToolCall:m=>{M(a,o,m)}})};s("connecting");let d=new RTCPeerConnection({iceServers:[{urls:"stun:stun.l.google.com:19302"}]}),g=document.createElement("audio");g.autoplay=!0,g.style.display="none",document.body.appendChild(g),d.ontrack=i=>{g.srcObject=i.streams[0]??new MediaStream([i.track])};let l;try{l=await navigator.mediaDevices.getUserMedia({audio:!0})}catch(i){let m=i instanceof DOMException&&i.name==="NotAllowedError"?"mic_denied":"mic_start_failed";throw n.onError?.({code:m,message:i instanceof Error?i.message:"getUserMedia failed"}),s("error"),d.close(),g.remove(),i}for(let i of l.getAudioTracks())d.addTrack(i,l);let c=d.createDataChannel("control",{ordered:!0});c.onmessage=i=>{typeof i.data=="string"&&p(i.data)},c.onerror=()=>{n.onError?.({code:"socket_error",message:"control channel error"})},c.onopen=()=>{Object.keys(o).length>0&&a(w(o))};let u=n.webrtcGatewayBase||"",h=u?`${u}/webrtc/offer?token=${encodeURIComponent(n.token)}`:`${n.apiBase}/v1/agents/${encodeURIComponent(n.agentId)}/webrtc/offer?token=${encodeURIComponent(n.token)}`,k=u?`${u}/webrtc/ice?token=${encodeURIComponent(n.token)}`:`${n.apiBase}/v1/agents/${encodeURIComponent(n.agentId)}/webrtc/ice?token=${encodeURIComponent(n.token)}`,b=()=>{if(!r){r=!0;try{l.getTracks().forEach(i=>i.stop())}catch{}try{d.close()}catch{}try{g.remove()}catch{}s("ended"),n.onEnd?.()}},f=null,C=[],y=i=>{fetch(k,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({callId:f,candidate:i})}).catch(()=>{})};d.onicecandidate=i=>{i.candidate&&(f?y(i.candidate):C.push(i.candidate))},d.onconnectionstatechange=()=>{let i=d.connectionState;i==="connected"&&s("listening"),(i==="failed"||i==="disconnected")&&(n.onError?.({code:"socket_error",message:`webrtc connection ${i}`}),b()),i==="closed"&&!r&&b()},await d.setLocalDescription(await d.createOffer());try{let i=await fetch(h,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({sdp:d.localDescription.sdp,type:"offer",agentId:n.agentId})});if(!i.ok){let I=i.status===401?"unauthorized":"server_error";throw n.onError?.({code:I,message:`signaling failed: HTTP ${i.status}`}),s("error"),l.getTracks().forEach(S=>S.stop()),d.close(),g.remove(),new Error(`webrtc offer failed: ${i.status}`)}let m=await i.json();for(f=m.callId,await d.setRemoteDescription({type:"answer",sdp:m.sdp});C.length>0;)y(C.shift())}catch(i){throw r||(n.onError?.({code:"network_unreachable",message:i instanceof Error?i.message:"signaling failed"}),s("error"),l.getTracks().forEach(m=>m.stop()),d.close(),g.remove()),i}return{get state(){return e.state},get transcript(){return e.transcript.slice()},get isMuted(){return t},end:()=>b(),mute:()=>{t||(t=!0,l.getAudioTracks().forEach(i=>i.enabled=!1))},unmute:()=>{t&&(t=!1,l.getAudioTracks().forEach(i=>i.enabled=!0))}}}var te=n=>new globalThis.WebSocket(n),O=class{constructor(e){this.startCall=async e=>{if(!e.agentId)throw new Error("startCall: agentId is required");let{context:t,metadata:r}=L(this.config,e),o={agentId:e.agentId,userId:e.userId,context:t,metadata:r},a;if(e.token)a={token:e.token,transport:"ws"};else{let p=await this.config.fetchToken(o);if(!p)throw new Error("configureVoiceClient.fetchToken returned empty token");if(a=typeof p=="string"?{token:p,transport:"ws"}:p,!a.token)throw new Error("configureVoiceClient.fetchToken returned an object without `token`")}if(a.transport==="webrtc")return G({agentId:e.agentId,apiBase:this.config.apiBase,token:a.token,webrtcGatewayBase:a.webrtcGatewayBase,onStateChange:e.onStateChange,onTranscript:e.onTranscript,onError:e.onError,onEnd:e.onEnd?()=>e.onEnd({reason:"agent_ended",durationMs:0}):void 0,onInterrupt:e.onInterrupt,onAgentTurnStart:e.onAgentTurnStart,clientTools:e.clientTools});let s=new R({config:this.config,options:{...e,context:t,metadata:r},token:a.token,wsFactory:te});return await s.start(),s};this.config=e}};function H(n){return new O(N(n))}var J="voice-agent-embed-root",re="M12 14a3 3 0 0 0 3-3V5a3 3 0 0 0-6 0v6a3 3 0 0 0 3 3zm5.3-3a.7.7 0 0 1 1.4 0 6.7 6.7 0 0 1-6 6.66V21h-1.4v-3.34A6.7 6.7 0 0 1 5.3 11a.7.7 0 0 1 1.4 0 5.3 5.3 0 0 0 10.6 0z",F="http://www.w3.org/2000/svg",X=n=>{let e=document.createElementNS(F,"svg");if(e.setAttribute("viewBox","0 0 24 24"),e.setAttribute("class","icon"),n.rect){let t=document.createElementNS(F,"rect");t.setAttribute("x","7"),t.setAttribute("y","7"),t.setAttribute("width","10"),t.setAttribute("height","10"),t.setAttribute("rx","1.5"),e.appendChild(t)}else if(n.path){let t=document.createElementNS(F,"path");t.setAttribute("d",n.path),e.appendChild(t)}return e},ne=n=>{try{return JSON.parse(n)}catch{return}},oe=n=>{if(n)try{return new URL(n.src,location.href).origin}catch{}return location.origin},ae=()=>{let n=document.querySelectorAll('script[src*="embed.js"]');for(let e of Array.from(n)){let t=e.dataset;if(t.apiKey&&console.error("[craftedxp/voice-js] data-api-key on the embed <script> is no longer supported. Mint a ct_ server-side via @craftedxp/sdk-node and inject it into data-token instead."),t.agentId&&t.token)return{apiBase:t.apiBase||oe(e),token:t.token,agentId:t.agentId,vars:t.vars?ne(t.vars):void 0,label:t.label||"Call agent",primaryColor:t.primaryColor||"#3d7dd0",position:t.position==="bottom-left"?"bottom-left":"bottom-right",transport:t.transport||void 0,webrtcGatewayBase:t.webrtcGatewayBase||void 0}}return null},V=class{constructor(e){this.call=null;this.cfg=e,document.getElementById(J)?.remove(),this.host=document.createElement("div"),this.host.id=J,document.body.appendChild(this.host),this.shadow=this.host.attachShadow({mode:"open"}),this.mount()}mount(){let e=document.createElement("style");e.textContent=this.css(),this.shadow.appendChild(e);let t=document.createElement("div");t.className=`wrap ${this.cfg.position??"bottom-right"}`,this.panel=document.createElement("div"),this.panel.className="panel",this.panel.style.display="none";let r=document.createElement("div");r.className="panel-head";let o=document.createElement("div");o.className="title",o.textContent=this.cfg.label??"Call agent",this.statusEl=document.createElement("div"),this.statusEl.className="status",this.statusEl.textContent="idle",r.appendChild(o),r.appendChild(this.statusEl),this.transcriptEl=document.createElement("div"),this.transcriptEl.className="transcript",this.panel.appendChild(r),this.panel.appendChild(this.transcriptEl),t.appendChild(this.panel),this.fab=document.createElement("button"),this.fab.className="fab",this.fab.type="button",this.setFabState("idle"),this.fab.addEventListener("click",()=>this.toggle()),t.appendChild(this.fab),this.shadow.appendChild(t),(!this.cfg.agentId||!this.cfg.token)&&(this.setStatus("Widget not configured \u2014 data-token + data-agent-id are required","warn"),this.fab.disabled=!0,this.fab.title="Missing data-token or data-agent-id")}destroy(){this.call?.end(),this.call=null,this.host.remove()}css(){return`
1067
83
  :host, * { box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
1068
84
  .wrap { position: fixed; z-index: 2147483000; display: flex; flex-direction: column; align-items: flex-end; gap: 10px; }
1069
85
  .wrap.bottom-right { bottom: 24px; right: 24px; }
1070
86
  .wrap.bottom-left { bottom: 24px; left: 24px; align-items: flex-start; }
1071
87
  .fab {
1072
88
  width: 58px; height: 58px; border-radius: 50%;
1073
- background: ${this.cfg.primaryColor ?? '#3d7dd0'}; color: white; border: 0;
89
+ background: ${this.cfg.primaryColor??"#3d7dd0"}; color: white; border: 0;
1074
90
  cursor: pointer; box-shadow: 0 6px 18px rgba(0,0,0,0.25);
1075
91
  display: flex; align-items: center; justify-content: center;
1076
92
  transition: transform 0.15s ease, background 0.2s ease;
@@ -1106,126 +122,4 @@ registerProcessor('mic-downsampler', MicDownsampler)
1106
122
  .line.user.interim { opacity: 0.55; font-style: italic; }
1107
123
  .line.agent.interrupted { opacity: 0.7; }
1108
124
  .line.agent.interrupted::after { content: ' (interrupted)'; color: #e8b660; font-style: italic; font-size: 11px; }
1109
- `
1110
- }
1111
- setStatus(e, t = '') {
1112
- ;((this.statusEl.className = `status ${t}`.trim()), (this.statusEl.textContent = e))
1113
- }
1114
- setFabState(e) {
1115
- for (
1116
- this.fab.classList.remove('live', 'connecting'),
1117
- e === 'live'
1118
- ? this.fab.classList.add('live')
1119
- : e === 'connecting' && this.fab.classList.add('connecting');
1120
- this.fab.firstChild;
1121
- )
1122
- this.fab.removeChild(this.fab.firstChild)
1123
- ;(this.fab.appendChild(J(e === 'live' ? { rect: !0 } : { path: re })),
1124
- this.fab.setAttribute('aria-label', e === 'live' ? 'End call' : 'Start call'))
1125
- }
1126
- async toggle() {
1127
- if (this.call) {
1128
- this.call.end()
1129
- return
1130
- }
1131
- if (!this.cfg.token) {
1132
- this.setStatus('data-token="ct_..." is required', 'err')
1133
- return
1134
- }
1135
- ;((this.panel.style.display = 'flex'),
1136
- this.setFabState('connecting'),
1137
- this.setStatus('connecting\u2026'))
1138
- let e = G({
1139
- apiBase: this.cfg.apiBase ?? location.origin,
1140
- fetchToken: async () => ({
1141
- token: this.cfg.token,
1142
- transport: this.cfg.transport ?? 'ws',
1143
- webrtcGatewayBase: this.cfg.webrtcGatewayBase,
1144
- }),
1145
- })
1146
- try {
1147
- let t = await e.startCall({
1148
- agentId: this.cfg.agentId,
1149
- context: this.cfg.vars,
1150
- onStateChange: (r) => this.onState(r),
1151
- onTranscript: (r) => this.renderTranscript(r),
1152
- onError: (r) => {
1153
- this.setStatus(`${r.code}: ${r.message}`, 'err')
1154
- },
1155
- onEnd: () => {
1156
- ;(this.setFabState('idle'), (this.call = null))
1157
- },
1158
- })
1159
- this.call = t
1160
- } catch (t) {
1161
- ;(this.setStatus(t instanceof Error ? t.message : 'connect failed', 'err'),
1162
- this.setFabState('idle'),
1163
- (this.call = null))
1164
- }
1165
- }
1166
- onState(e) {
1167
- switch (e) {
1168
- case 'connecting':
1169
- ;(this.setFabState('connecting'), this.setStatus('connecting\u2026'))
1170
- break
1171
- case 'listening':
1172
- ;(this.setFabState('live'), this.setStatus('listening', 'ok'))
1173
- break
1174
- case 'user_speaking':
1175
- this.setStatus('you: \u2026speaking', 'ok')
1176
- break
1177
- case 'agent_speaking':
1178
- this.setStatus('agent speaking', 'ok')
1179
- break
1180
- case 'ended':
1181
- ;(this.setFabState('idle'), this.setStatus('ended'))
1182
- break
1183
- case 'error':
1184
- this.setFabState('idle')
1185
- break
1186
- }
1187
- }
1188
- renderTranscript(e) {
1189
- for (; this.transcriptEl.firstChild; )
1190
- this.transcriptEl.removeChild(this.transcriptEl.firstChild)
1191
- for (let t of e) {
1192
- let r = document.createElement('div')
1193
- if (((r.className = 'line'), t.role === 'user')) {
1194
- ;(r.classList.add('user'), t.committed || r.classList.add('interim'))
1195
- let o = document.createElement('span')
1196
- ;((o.className = 'label'),
1197
- (o.textContent = 'You:'),
1198
- r.appendChild(o),
1199
- r.append(' ' + t.text))
1200
- } else if (t.role === 'agent') {
1201
- ;(r.classList.add('agent'), t.interrupted && r.classList.add('interrupted'))
1202
- let o = document.createElement('span')
1203
- ;((o.className = 'label'),
1204
- (o.textContent = 'Agent:'),
1205
- r.appendChild(o),
1206
- r.append(' ' + t.text))
1207
- } else (t.role, r.classList.add('sys'), (r.textContent = t.text))
1208
- this.transcriptEl.appendChild(r)
1209
- }
1210
- this.transcriptEl.scrollTop = this.transcriptEl.scrollHeight
1211
- }
1212
- },
1213
- R = null,
1214
- V = (n) => {
1215
- ;(R?.destroy(), (R = new B(n)))
1216
- },
1217
- ie = () => {
1218
- ;(R?.destroy(), (R = null))
1219
- }
1220
- window.PlatformWidget = { init: V, destroy: ie }
1221
- var F = ae()
1222
- if (F) {
1223
- let n = document.querySelectorAll('script[src*="embed.js"]'),
1224
- e = !0
1225
- for (let t of Array.from(n)) t.dataset.autoInit === 'false' && (e = !1)
1226
- e &&
1227
- (document.readyState !== 'loading'
1228
- ? V(F)
1229
- : document.addEventListener('DOMContentLoaded', () => V(F)))
1230
- }
1231
- })()
125
+ `}setStatus(e,t=""){this.statusEl.className=`status ${t}`.trim(),this.statusEl.textContent=e}setFabState(e){for(this.fab.classList.remove("live","connecting"),e==="live"?this.fab.classList.add("live"):e==="connecting"&&this.fab.classList.add("connecting");this.fab.firstChild;)this.fab.removeChild(this.fab.firstChild);this.fab.appendChild(X(e==="live"?{rect:!0}:{path:re})),this.fab.setAttribute("aria-label",e==="live"?"End call":"Start call")}async toggle(){if(this.call){this.call.end();return}if(!this.cfg.token){this.setStatus('data-token="ct_..." is required',"err");return}this.panel.style.display="flex",this.setFabState("connecting"),this.setStatus("connecting\u2026");let e=H({apiBase:this.cfg.apiBase??location.origin,fetchToken:async()=>({token:this.cfg.token,transport:this.cfg.transport??"ws",webrtcGatewayBase:this.cfg.webrtcGatewayBase})});try{let t=await e.startCall({agentId:this.cfg.agentId,context:this.cfg.vars,onStateChange:r=>this.onState(r),onTranscript:r=>this.renderTranscript(r),onError:r=>{this.setStatus(`${r.code}: ${r.message}`,"err")},onEnd:()=>{this.setFabState("idle"),this.call=null}});this.call=t}catch(t){this.setStatus(t instanceof Error?t.message:"connect failed","err"),this.setFabState("idle"),this.call=null}}onState(e){switch(e){case"connecting":this.setFabState("connecting"),this.setStatus("connecting\u2026");break;case"listening":this.setFabState("live"),this.setStatus("listening","ok");break;case"user_speaking":this.setStatus("you: \u2026speaking","ok");break;case"agent_speaking":this.setStatus("agent speaking","ok");break;case"ended":this.setFabState("idle"),this.setStatus("ended");break;case"error":this.setFabState("idle");break}}renderTranscript(e){for(;this.transcriptEl.firstChild;)this.transcriptEl.removeChild(this.transcriptEl.firstChild);for(let t of e){let r=document.createElement("div");if(r.className="line",t.role==="user"){r.classList.add("user"),t.committed||r.classList.add("interim");let o=document.createElement("span");o.className="label",o.textContent="You:",r.appendChild(o),r.append(" "+t.text)}else if(t.role==="agent"){r.classList.add("agent"),t.interrupted&&r.classList.add("interrupted");let o=document.createElement("span");o.className="label",o.textContent="Agent:",r.appendChild(o),r.append(" "+t.text)}else t.role,r.classList.add("sys"),r.textContent=t.text;this.transcriptEl.appendChild(r)}this.transcriptEl.scrollTop=this.transcriptEl.scrollHeight}},_=null,W=n=>{_?.destroy(),_=new V(n)},ie=()=>{_?.destroy(),_=null};window.PlatformWidget={init:W,destroy:ie};var B=ae();if(B){let n=document.querySelectorAll('script[src*="embed.js"]'),e=!0;for(let t of Array.from(n))t.dataset.autoInit==="false"&&(e=!1);e&&(document.readyState!=="loading"?W(B):document.addEventListener("DOMContentLoaded",()=>W(B)))}})();