@craftedxp/voice-js 0.3.1 → 0.4.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.
@@ -1,4 +1,28 @@
1
- "use strict";var VoxlineEmbedBundle=(()=>{function V(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 F(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 O=`// AudioWorklet \u2014 runs off the main thread in the audio rendering graph.
1
+ 'use strict'
2
+ var VoissiaEmbedBundle = (() => {
3
+ function B(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 V(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 W = `// AudioWorklet \u2014 runs off the main thread in the audio rendering graph.
2
26
  //
3
27
  // Input: Float32 samples at the AudioContext's native sampleRate (typically
4
28
  // 48000 Hz on desktop, 44100 Hz on some iOS devices).
@@ -79,14 +103,958 @@ class MicDownsampler extends AudioWorkletProcessor {
79
103
  }
80
104
 
81
105
  registerProcessor('mic-downsampler', MicDownsampler)
82
- `;var X=100,W=n=>{let e=null,t=null,r=null,o=null,a=null,i=null,l=!1,d=!1,h=s=>{let p=0;for(let g=0;g<s.length;g++)p+=s[g]*s[g];let y=Math.sqrt(p/s.length);return Math.min(1,y*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 s=new Blob([O],{type:"application/javascript"}),p=URL.createObjectURL(s);try{await e.audioWorklet.addModule(p)}finally{URL.revokeObjectURL(p)}if(r=e.createMediaStreamSource(t),o=new AudioWorkletNode(e,"mic-downsampler"),o.port.onmessage=g=>{l||n.onChunk(g.data)},n.onVolume){a=e.createAnalyser(),a.fftSize=256,r.connect(a);let g=new Float32Array(a.fftSize);i=setInterval(()=>{a&&(a.getFloatTimeDomainData(g),n.onVolume?.(h(g)))},X)}r.connect(o);let y=e.createGain();y.gain.value=0,o.connect(y).connect(e.destination),d=!0}catch(s){let p=s instanceof Error?s:new Error(typeof s=="string"?s:"capture failed");throw n.onError?.(p),p}},stop:()=>{if(d){d=!1,i&&(clearInterval(i),i=null);try{o?.disconnect(),a?.disconnect(),r?.disconnect()}catch{}if(o=null,a=null,r=null,t){for(let s of t.getTracks())s.stop();t=null}e&&e.state!=="closed"&&e.close().catch(()=>{}),e=null}},mute:s=>{l=s},isCapturing:()=>d}};var B=(n={})=>{let e=n.sampleRate??16e3,t=null,r=null,o=null,a=null,i=0,l=[],d=!1,h=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 c=new Float32Array(o.fftSize);a=setInterval(()=>{if(!o)return;o.getFloatTimeDomainData(c);let f=0;for(let C=0;C<c.length;C++)f+=c[C]*c[C];let k=Math.sqrt(f/c.length);n.onVolume?.(Math.min(1,k*1.8))},100)}r.connect(t.destination),i=t.currentTime},m=c=>{c!==d&&(d=c,n.onSpeakingChange?.(c))},u=()=>{let c=t?.currentTime??0;l=l.filter(f=>(f._endsAt??0)>c),l.length===0&&m(!1)},s=c=>{if(!t){h().then(()=>s(c));return}if(!t||!r)return;let f=new Int16Array(c);if(f.length===0)return;let k=t.createBuffer(1,f.length,e),C=k.getChannelData(0);for(let S=0;S<f.length;S++)C[S]=f[S]/32768;let b=t.createBufferSource();b.buffer=k,b.connect(r);let G=t.currentTime,w=Math.max(G,i);b.start(w);let I=f.length/e;b._endsAt=w+I,i=w+I,l.push(b),m(!0),b.onended=()=>u()},p=()=>{if(!(!t||!r)){for(let c of l)try{c.stop()}catch{}l=[],r.disconnect(),r=t.createGain(),o&&(o.disconnect(),r.connect(o)),r.connect(t.destination),i=t.currentTime,m(!1)}};return{enqueue:s,flush:p,close:()=>{p(),a&&(clearInterval(a),a=null),t&&t.state!=="closed"&&t.close().catch(()=>{}),t=null,r=null,o=null},resume:async()=>{await h()}}};var P=(n,e)=>{let t=n.maxRetries??3,r=n.initialBackoffMs??500,o=n.maxBackoffMs??8e3,a=null,i=!1,l=0,d=r,h=null,m=()=>{a=n.wsFactory(n.url),a.binaryType="arraybuffer",a.onopen=()=>{e(l===0?{type:"open"}:{type:"reconnected"}),l=0,d=r},a.onmessage=u=>{e({type:"message",data:u.data})},a.onerror=()=>{e({type:"error",error:new Error("WebSocket error")})},a.onclose=u=>{if(a=null,!(!i&&l<t)){e({type:"close",code:u.code,reason:u.reason,permanent:!0});return}e({type:"close",code:u.code,reason:u.reason,permanent:!1}),l++;let p=Math.min(d,o);d=Math.min(d*2,o),h=setTimeout(m,p)}};return m(),{send:u=>{a&&a.readyState===1&&a.send(u)},close:(u=1e3,s="client-requested")=>{i=!0,h&&(clearTimeout(h),h=null);try{a?.close(u,s)}catch{}},readyState:()=>a?.readyState??3}};var L=()=>({state:"idle",transcript:[],agentBubbleId:null,idCounter:0,endReason:null}),Y=n=>n==="agent_ended"?"agent_ended":n==="caller_hung_up"?"user_hangup":n==="silence_timeout"||n==="max_duration"?"timeout":"error";function N(n,e,t){let r;try{r=JSON.parse(n)}catch{return}switch(r.type){case"connected":t.onConnected(),x(e,"listening",t);return;case"transcript":{let o=r.text??"";if(!o)return;let a=!!r.isFinal;a||x(e,"user_speaking",t),K(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),t.onAgentTurnStart(),x(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(i=>i.id===a&&i.role==="agent"?{...i,text:i.text+o}:i),t.onTranscript(e.transcript);return}case"agent_turn_end":e.agentBubbleId=null,x(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(i=>i.id===a&&i.role==="agent"?{...i,text:o,interrupted:!0}:i):e.transcript=e.transcript.filter(i=>i.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??""),i=r.args??{};if(!o||!a)return;t.onClientToolCall({toolCallId:o,name:a,args:i});return}case"call_end":{let o=String(r.reason??""),a=Y(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 x=(n,e,t)=>{n.state!==e&&(n.state=e,t.onState(e))},K=(n,e,t)=>{let r=-1;for(let i=n.transcript.length-1;i>=0;i--){let l=n.transcript[i];if(l.role==="user"&&l.committed===!1){r=i;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 $(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 Z=/^[a-zA-Z_][a-zA-Z0-9_]*$/;var D=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(!Z.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]`)}},U=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}:{}}))}),z=(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 v=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=U(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"?N(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:()=>this.args.options.onAgentTurnStart?.(),onCallEnd:t=>this.teardown(t),onConnected:()=>this.sendClientToolsRegister(),onClientToolCall:t=>z(r=>this.rws?.send(JSON.stringify(r)),this.args.options.clientTools??{},t)}):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=W({onChunk:e=>{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=>{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=L(),D(e.options.clientTools)}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=$({apiBase:this.args.config.apiBase,agentId:this.args.options.agentId,token:this.args.token,bargeIn:this.args.options.bargeIn});this.playback=B({onVolume:t=>{this.outputVolume=t,this.args.options.onVolume?.({input:this.inputVolume,output:t})}});try{await this.playback.resume()}catch{}this.rws=P({url:e,wsFactory:this.args.wsFactory,maxRetries:3},t=>this.handleSocketEvent(t))}};var Q=n=>new globalThis.WebSocket(n),T=class{constructor(e){this.startCall=async e=>{if(!e.agentId)throw new Error("startCall: agentId is required");let{context:t,metadata:r}=F(this.config,e),o={agentId:e.agentId,userId:e.userId,context:t,metadata:r},a;if(e.token)a=e.token;else if(a=await this.config.fetchToken(o),!a)throw new Error("configureVoiceClient.fetchToken returned empty token");let i=new v({config:this.config,options:{...e,context:t,metadata:r},token:a,wsFactory:Q});return await i.start(),i};this.config=e}};function j(n){return new T(V(n))}var H="voice-agent-embed-root",ee="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",A="http://www.w3.org/2000/svg",q=n=>{let e=document.createElementNS(A,"svg");if(e.setAttribute("viewBox","0 0 24 24"),e.setAttribute("class","icon"),n.rect){let t=document.createElementNS(A,"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(A,"path");t.setAttribute("d",n.path),e.appendChild(t)}return e},te=n=>{try{return JSON.parse(n)}catch{return}},re=n=>{if(n)try{return new URL(n.src,location.href).origin}catch{}return location.origin},ne=()=>{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||re(e),token:t.token,agentId:t.agentId,vars:t.vars?te(t.vars):void 0,label:t.label||"Call agent",primaryColor:t.primaryColor||"#3d7dd0",position:t.position==="bottom-left"?"bottom-left":"bottom-right"}}return null},M=class{constructor(e){this.call=null;this.cfg=e,document.getElementById(H)?.remove(),this.host=document.createElement("div"),this.host.id=H,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`
106
+ `
107
+ var K = 100,
108
+ P = (n) => {
109
+ let e = null,
110
+ t = null,
111
+ r = null,
112
+ o = null,
113
+ a = null,
114
+ s = null,
115
+ l = !1,
116
+ f = !1,
117
+ m = (p) => {
118
+ let g = 0
119
+ for (let i = 0; i < p.length; i++) g += p[i] * p[i]
120
+ let b = Math.sqrt(g / p.length)
121
+ return Math.min(1, b * 1.8)
122
+ }
123
+ return {
124
+ start: async () => {
125
+ if (!f)
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 p = new Blob([W], { type: 'application/javascript' }),
138
+ g = URL.createObjectURL(p)
139
+ try {
140
+ await e.audioWorklet.addModule(g)
141
+ } finally {
142
+ URL.revokeObjectURL(g)
143
+ }
144
+ if (
145
+ ((r = e.createMediaStreamSource(t)),
146
+ (o = new AudioWorkletNode(e, 'mic-downsampler')),
147
+ (o.port.onmessage = (i) => {
148
+ l || n.onChunk(i.data)
149
+ }),
150
+ n.onVolume)
151
+ ) {
152
+ ;((a = e.createAnalyser()), (a.fftSize = 256), r.connect(a))
153
+ let i = new Float32Array(a.fftSize)
154
+ s = setInterval(() => {
155
+ a && (a.getFloatTimeDomainData(i), n.onVolume?.(m(i)))
156
+ }, K)
157
+ }
158
+ r.connect(o)
159
+ let b = e.createGain()
160
+ ;((b.gain.value = 0), o.connect(b).connect(e.destination), (f = !0))
161
+ } catch (p) {
162
+ let g =
163
+ p instanceof Error ? p : new Error(typeof p == 'string' ? p : 'capture failed')
164
+ throw (n.onError?.(g), g)
165
+ }
166
+ },
167
+ stop: () => {
168
+ if (f) {
169
+ ;((f = !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 p of t.getTracks()) p.stop()
175
+ t = null
176
+ }
177
+ ;(e && e.state !== 'closed' && e.close().catch(() => {}), (e = null))
178
+ }
179
+ },
180
+ mute: (p) => {
181
+ l = p
182
+ },
183
+ isCapturing: () => f,
184
+ }
185
+ }
186
+ var N = (n = {}) => {
187
+ let e = n.sampleRate ?? 16e3,
188
+ t = null,
189
+ r = null,
190
+ o = null,
191
+ a = null,
192
+ s = 0,
193
+ l = [],
194
+ f = !1,
195
+ m = 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 u = new Float32Array(o.fftSize)
203
+ a = setInterval(() => {
204
+ if (!o) return
205
+ o.getFloatTimeDomainData(u)
206
+ let h = 0
207
+ for (let y = 0; y < u.length; y++) h += u[y] * u[y]
208
+ let C = Math.sqrt(h / u.length)
209
+ n.onVolume?.(Math.min(1, C * 1.8))
210
+ }, 100)
211
+ }
212
+ ;(r.connect(t.destination), (s = t.currentTime))
213
+ },
214
+ c = (u) => {
215
+ u !== f && ((f = u), n.onSpeakingChange?.(u))
216
+ },
217
+ d = () => {
218
+ let u = t?.currentTime ?? 0
219
+ ;((l = l.filter((h) => (h._endsAt ?? 0) > u)), l.length === 0 && c(!1))
220
+ },
221
+ p = (u) => {
222
+ if (!t) {
223
+ m().then(() => p(u))
224
+ return
225
+ }
226
+ if (!t || !r) return
227
+ let h = new Int16Array(u)
228
+ if (h.length === 0) return
229
+ let C = t.createBuffer(1, h.length, e),
230
+ y = C.getChannelData(0)
231
+ for (let w = 0; w < h.length; w++) y[w] = h[w] / 32768
232
+ let k = t.createBufferSource()
233
+ ;((k.buffer = C), k.connect(r))
234
+ let X = t.currentTime,
235
+ A = Math.max(X, s)
236
+ k.start(A)
237
+ let F = h.length / e
238
+ ;((k._endsAt = A + F), (s = A + F), l.push(k), c(!0), (k.onended = () => d()))
239
+ },
240
+ g = () => {
241
+ if (!(!t || !r)) {
242
+ for (let u of l)
243
+ try {
244
+ u.stop()
245
+ } catch {}
246
+ ;((l = []),
247
+ r.disconnect(),
248
+ (r = t.createGain()),
249
+ o && (o.disconnect(), r.connect(o)),
250
+ r.connect(t.destination),
251
+ (s = t.currentTime),
252
+ c(!1))
253
+ }
254
+ }
255
+ return {
256
+ enqueue: p,
257
+ flush: g,
258
+ close: () => {
259
+ ;(g(),
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 m()
268
+ },
269
+ }
270
+ }
271
+ var L = (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
+ l = 0,
278
+ f = r,
279
+ m = null,
280
+ c = () => {
281
+ ;((a = n.wsFactory(n.url)),
282
+ (a.binaryType = 'arraybuffer'),
283
+ (a.onopen = () => {
284
+ ;(e(l === 0 ? { type: 'open' } : { type: 'reconnected' }), (l = 0), (f = r))
285
+ }),
286
+ (a.onmessage = (d) => {
287
+ e({ type: 'message', data: d.data })
288
+ }),
289
+ (a.onerror = () => {
290
+ e({ type: 'error', error: new Error('WebSocket error') })
291
+ }),
292
+ (a.onclose = (d) => {
293
+ if (((a = null), !(!s && l < t))) {
294
+ e({ type: 'close', code: d.code, reason: d.reason, permanent: !0 })
295
+ return
296
+ }
297
+ ;(e({ type: 'close', code: d.code, reason: d.reason, permanent: !1 }), l++)
298
+ let g = Math.min(f, o)
299
+ ;((f = Math.min(f * 2, o)), (m = setTimeout(c, g)))
300
+ }))
301
+ }
302
+ return (
303
+ c(),
304
+ {
305
+ send: (d) => {
306
+ a && a.readyState === 1 && a.send(d)
307
+ },
308
+ close: (d = 1e3, p = 'client-requested') => {
309
+ ;((s = !0), m && (clearTimeout(m), (m = null)))
310
+ try {
311
+ a?.close(d, p)
312
+ } catch {}
313
+ },
314
+ readyState: () => a?.readyState ?? 3,
315
+ }
316
+ )
317
+ }
318
+ var E = () => ({
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(), S(e, 'listening', t))
343
+ return
344
+ case 'transcript': {
345
+ let o = r.text ?? ''
346
+ if (!o) return
347
+ let a = !!r.isFinal
348
+ ;(a || S(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), S(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), S(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 S = (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 l = n.transcript[s]
450
+ if (l.role === 'user' && l.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 $(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 U = (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
+ D = (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
+ j = (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 = (c) => {
546
+ let d = r.get(c)
547
+ if (!d || !d.ended) return
548
+ let p = {}
549
+ ;(d.firstOutboundAt !== null &&
550
+ d.firstAudibleAt !== null &&
551
+ (p.client_mic_to_first_audible_ms = d.firstAudibleAt - d.firstOutboundAt),
552
+ n.send({ type: 'client_marks', seq: c, marks: p, clientNow: Date.now() }),
553
+ r.delete(c))
554
+ }
555
+ return {
556
+ markFirstOutboundAudio: () => {
557
+ t === null && (t = e())
558
+ },
559
+ markFirstAudibleOutput: () => {
560
+ let c
561
+ for (let d of r.values()) d.ended || (c = d)
562
+ c && c.firstAudibleAt === null && (c.firstAudibleAt = e())
563
+ },
564
+ onAgentTurnStart: (c) => {
565
+ ;(r.set(c, { firstOutboundAt: t, firstAudibleAt: null, ended: !1 }), (t = null))
566
+ },
567
+ onAgentTurnEnd: (c) => {
568
+ let d = r.get(c)
569
+ if (!d) {
570
+ n.send({ type: 'client_marks', seq: c, marks: {}, clientNow: Date.now() })
571
+ return
572
+ }
573
+ ;((d.ended = !0), o(c))
574
+ },
575
+ flush: () => {
576
+ for (let c of [...r.keys()]) {
577
+ let d = r.get(c)
578
+ ;((d.ended = !0), o(c))
579
+ }
580
+ t = null
581
+ },
582
+ }
583
+ }
584
+ var v = 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 = D(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
+ j(
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 = P({
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 = E()),
712
+ U(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 = $({
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 = N({
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 = L({ url: e, wsFactory: this.args.wsFactory, maxRetries: 3 }, (t) =>
748
+ this.handleSocketEvent(t),
749
+ )
750
+ }
751
+ }
752
+ async function q(n) {
753
+ let e = E(),
754
+ t = !1,
755
+ r = !1,
756
+ o = (i) => {
757
+ e.state !== i && ((e.state = i), n.onStateChange?.(i))
758
+ },
759
+ a = (i) => {
760
+ x(i, e, {
761
+ onState: o,
762
+ onTranscript: (u) => n.onTranscript?.(u),
763
+ onError: (u) => n.onError?.(u),
764
+ onInterrupt: () => n.onInterrupt?.(),
765
+ onAgentTurnStart: () => n.onAgentTurnStart?.(),
766
+ onAgentTurnEnd: () => {},
767
+ onCallEnd: () => b(),
768
+ onConnected: () => {},
769
+ onClientToolCall: () => {},
770
+ })
771
+ }
772
+ o('connecting')
773
+ let s = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] }),
774
+ l = document.createElement('audio')
775
+ ;((l.autoplay = !0),
776
+ (l.style.display = 'none'),
777
+ document.body.appendChild(l),
778
+ (s.ontrack = (i) => {
779
+ l.srcObject = i.streams[0] ?? new MediaStream([i.track])
780
+ }))
781
+ let f
782
+ try {
783
+ f = await navigator.mediaDevices.getUserMedia({ audio: !0 })
784
+ } catch (i) {
785
+ let u =
786
+ i instanceof DOMException && i.name === 'NotAllowedError'
787
+ ? 'mic_denied'
788
+ : 'mic_start_failed'
789
+ throw (
790
+ n.onError?.({ code: u, message: i instanceof Error ? i.message : 'getUserMedia failed' }),
791
+ o('error'),
792
+ s.close(),
793
+ l.remove(),
794
+ i
795
+ )
796
+ }
797
+ for (let i of f.getAudioTracks()) s.addTrack(i, f)
798
+ let m = s.createDataChannel('control', { ordered: !0 })
799
+ ;((m.onmessage = (i) => {
800
+ typeof i.data == 'string' && a(i.data)
801
+ }),
802
+ (m.onerror = () => {
803
+ n.onError?.({ code: 'socket_error', message: 'control channel error' })
804
+ }))
805
+ let c = n.webrtcGatewayBase || '',
806
+ d = c
807
+ ? `${c}/webrtc/offer?token=${encodeURIComponent(n.token)}`
808
+ : `${n.apiBase}/v1/agents/${encodeURIComponent(n.agentId)}/webrtc/offer?token=${encodeURIComponent(n.token)}`,
809
+ p = c
810
+ ? `${c}/webrtc/ice?token=${encodeURIComponent(n.token)}`
811
+ : `${n.apiBase}/v1/agents/${encodeURIComponent(n.agentId)}/webrtc/ice?token=${encodeURIComponent(n.token)}`
812
+ await s.setLocalDescription(await s.createOffer())
813
+ let g
814
+ try {
815
+ let i = await fetch(d, {
816
+ method: 'POST',
817
+ headers: { 'content-type': 'application/json' },
818
+ body: JSON.stringify({ sdp: s.localDescription.sdp, type: 'offer', agentId: n.agentId }),
819
+ })
820
+ if (!i.ok) {
821
+ let h = i.status === 401 ? 'unauthorized' : 'server_error'
822
+ throw (
823
+ n.onError?.({ code: h, message: `signaling failed: HTTP ${i.status}` }),
824
+ o('error'),
825
+ f.getTracks().forEach((C) => C.stop()),
826
+ s.close(),
827
+ l.remove(),
828
+ new Error(`webrtc offer failed: ${i.status}`)
829
+ )
830
+ }
831
+ let u = await i.json()
832
+ ;((g = u.callId), await s.setRemoteDescription({ type: 'answer', sdp: u.sdp }))
833
+ } catch (i) {
834
+ throw (
835
+ r ||
836
+ (n.onError?.({
837
+ code: 'network_unreachable',
838
+ message: i instanceof Error ? i.message : 'signaling failed',
839
+ }),
840
+ o('error'),
841
+ f.getTracks().forEach((u) => u.stop()),
842
+ s.close(),
843
+ l.remove()),
844
+ i
845
+ )
846
+ }
847
+ ;((s.onicecandidate = (i) => {
848
+ i.candidate &&
849
+ fetch(p, {
850
+ method: 'POST',
851
+ headers: { 'content-type': 'application/json' },
852
+ body: JSON.stringify({ callId: g, candidate: i.candidate }),
853
+ }).catch(() => {})
854
+ }),
855
+ (s.onconnectionstatechange = () => {
856
+ let i = s.connectionState
857
+ ;(i === 'connected' && o('listening'),
858
+ (i === 'failed' || i === 'disconnected') &&
859
+ (n.onError?.({ code: 'socket_error', message: `webrtc connection ${i}` }), b()),
860
+ i === 'closed' && !r && b())
861
+ }))
862
+ let b = () => {
863
+ if (!r) {
864
+ r = !0
865
+ try {
866
+ f.getTracks().forEach((i) => i.stop())
867
+ } catch {}
868
+ try {
869
+ s.close()
870
+ } catch {}
871
+ try {
872
+ l.remove()
873
+ } catch {}
874
+ ;(o('ended'), n.onEnd?.())
875
+ }
876
+ }
877
+ return {
878
+ get state() {
879
+ return e.state
880
+ },
881
+ get transcript() {
882
+ return e.transcript.slice()
883
+ },
884
+ get isMuted() {
885
+ return t
886
+ },
887
+ end: () => b(),
888
+ mute: () => {
889
+ t || ((t = !0), f.getAudioTracks().forEach((i) => (i.enabled = !1)))
890
+ },
891
+ unmute: () => {
892
+ t && ((t = !1), f.getAudioTracks().forEach((i) => (i.enabled = !0)))
893
+ },
894
+ }
895
+ }
896
+ var te = (n) => new globalThis.WebSocket(n),
897
+ M = class {
898
+ constructor(e) {
899
+ this.startCall = async (e) => {
900
+ if (!e.agentId) throw new Error('startCall: agentId is required')
901
+ let { context: t, metadata: r } = V(this.config, e),
902
+ o = { agentId: e.agentId, userId: e.userId, context: t, metadata: r },
903
+ a
904
+ if (e.token) a = { token: e.token, transport: 'ws' }
905
+ else {
906
+ let l = await this.config.fetchToken(o)
907
+ if (!l) throw new Error('configureVoiceClient.fetchToken returned empty token')
908
+ if (((a = typeof l == 'string' ? { token: l, transport: 'ws' } : l), !a.token))
909
+ throw new Error('configureVoiceClient.fetchToken returned an object without `token`')
910
+ }
911
+ if (a.transport === 'webrtc')
912
+ return q({
913
+ agentId: e.agentId,
914
+ apiBase: this.config.apiBase,
915
+ token: a.token,
916
+ webrtcGatewayBase: a.webrtcGatewayBase,
917
+ onStateChange: e.onStateChange,
918
+ onTranscript: e.onTranscript,
919
+ onError: e.onError,
920
+ onEnd: e.onEnd ? () => e.onEnd({ reason: 'agent_ended', durationMs: 0 }) : void 0,
921
+ onInterrupt: e.onInterrupt,
922
+ onAgentTurnStart: e.onAgentTurnStart,
923
+ })
924
+ let s = new v({
925
+ config: this.config,
926
+ options: { ...e, context: t, metadata: r },
927
+ token: a.token,
928
+ wsFactory: te,
929
+ })
930
+ return (await s.start(), s)
931
+ }
932
+ this.config = e
933
+ }
934
+ }
935
+ function G(n) {
936
+ return new M(B(n))
937
+ }
938
+ var H = 'voice-agent-embed-root',
939
+ re =
940
+ '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',
941
+ R = 'http://www.w3.org/2000/svg',
942
+ J = (n) => {
943
+ let e = document.createElementNS(R, 'svg')
944
+ if ((e.setAttribute('viewBox', '0 0 24 24'), e.setAttribute('class', 'icon'), n.rect)) {
945
+ let t = document.createElementNS(R, 'rect')
946
+ ;(t.setAttribute('x', '7'),
947
+ t.setAttribute('y', '7'),
948
+ t.setAttribute('width', '10'),
949
+ t.setAttribute('height', '10'),
950
+ t.setAttribute('rx', '1.5'),
951
+ e.appendChild(t))
952
+ } else if (n.path) {
953
+ let t = document.createElementNS(R, 'path')
954
+ ;(t.setAttribute('d', n.path), e.appendChild(t))
955
+ }
956
+ return e
957
+ },
958
+ ne = (n) => {
959
+ try {
960
+ return JSON.parse(n)
961
+ } catch {
962
+ return
963
+ }
964
+ },
965
+ oe = (n) => {
966
+ if (n)
967
+ try {
968
+ return new URL(n.src, location.href).origin
969
+ } catch {}
970
+ return location.origin
971
+ },
972
+ ae = () => {
973
+ let n = document.querySelectorAll('script[src*="embed.js"]')
974
+ for (let e of Array.from(n)) {
975
+ let t = e.dataset
976
+ if (
977
+ (t.apiKey &&
978
+ console.error(
979
+ '[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.',
980
+ ),
981
+ t.agentId && t.token)
982
+ )
983
+ return {
984
+ apiBase: t.apiBase || oe(e),
985
+ token: t.token,
986
+ agentId: t.agentId,
987
+ vars: t.vars ? ne(t.vars) : void 0,
988
+ label: t.label || 'Call agent',
989
+ primaryColor: t.primaryColor || '#3d7dd0',
990
+ position: t.position === 'bottom-left' ? 'bottom-left' : 'bottom-right',
991
+ transport: t.transport || void 0,
992
+ webrtcGatewayBase: t.webrtcGatewayBase || void 0,
993
+ }
994
+ }
995
+ return null
996
+ },
997
+ I = class {
998
+ constructor(e) {
999
+ this.call = null
1000
+ ;((this.cfg = e),
1001
+ document.getElementById(H)?.remove(),
1002
+ (this.host = document.createElement('div')),
1003
+ (this.host.id = H),
1004
+ document.body.appendChild(this.host),
1005
+ (this.shadow = this.host.attachShadow({ mode: 'open' })),
1006
+ this.mount())
1007
+ }
1008
+ mount() {
1009
+ let e = document.createElement('style')
1010
+ ;((e.textContent = this.css()), this.shadow.appendChild(e))
1011
+ let t = document.createElement('div')
1012
+ ;((t.className = `wrap ${this.cfg.position ?? 'bottom-right'}`),
1013
+ (this.panel = document.createElement('div')),
1014
+ (this.panel.className = 'panel'),
1015
+ (this.panel.style.display = 'none'))
1016
+ let r = document.createElement('div')
1017
+ r.className = 'panel-head'
1018
+ let o = document.createElement('div')
1019
+ ;((o.className = 'title'),
1020
+ (o.textContent = this.cfg.label ?? 'Call agent'),
1021
+ (this.statusEl = document.createElement('div')),
1022
+ (this.statusEl.className = 'status'),
1023
+ (this.statusEl.textContent = 'idle'),
1024
+ r.appendChild(o),
1025
+ r.appendChild(this.statusEl),
1026
+ (this.transcriptEl = document.createElement('div')),
1027
+ (this.transcriptEl.className = 'transcript'),
1028
+ this.panel.appendChild(r),
1029
+ this.panel.appendChild(this.transcriptEl),
1030
+ t.appendChild(this.panel),
1031
+ (this.fab = document.createElement('button')),
1032
+ (this.fab.className = 'fab'),
1033
+ (this.fab.type = 'button'),
1034
+ this.setFabState('idle'),
1035
+ this.fab.addEventListener('click', () => this.toggle()),
1036
+ t.appendChild(this.fab),
1037
+ this.shadow.appendChild(t),
1038
+ (!this.cfg.agentId || !this.cfg.token) &&
1039
+ (this.setStatus(
1040
+ 'Widget not configured \u2014 data-token + data-agent-id are required',
1041
+ 'warn',
1042
+ ),
1043
+ (this.fab.disabled = !0),
1044
+ (this.fab.title = 'Missing data-token or data-agent-id')))
1045
+ }
1046
+ destroy() {
1047
+ ;(this.call?.end(), (this.call = null), this.host.remove())
1048
+ }
1049
+ css() {
1050
+ return `
83
1051
  :host, * { box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
84
1052
  .wrap { position: fixed; z-index: 2147483000; display: flex; flex-direction: column; align-items: flex-end; gap: 10px; }
85
1053
  .wrap.bottom-right { bottom: 24px; right: 24px; }
86
1054
  .wrap.bottom-left { bottom: 24px; left: 24px; align-items: flex-start; }
87
1055
  .fab {
88
1056
  width: 58px; height: 58px; border-radius: 50%;
89
- background: ${this.cfg.primaryColor??"#3d7dd0"}; color: white; border: 0;
1057
+ background: ${this.cfg.primaryColor ?? '#3d7dd0'}; color: white; border: 0;
90
1058
  cursor: pointer; box-shadow: 0 6px 18px rgba(0,0,0,0.25);
91
1059
  display: flex; align-items: center; justify-content: center;
92
1060
  transition: transform 0.15s ease, background 0.2s ease;
@@ -122,4 +1090,126 @@ registerProcessor('mic-downsampler', MicDownsampler)
122
1090
  .line.user.interim { opacity: 0.55; font-style: italic; }
123
1091
  .line.agent.interrupted { opacity: 0.7; }
124
1092
  .line.agent.interrupted::after { content: ' (interrupted)'; color: #e8b660; font-style: italic; font-size: 11px; }
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(q(e==="live"?{rect:!0}:{path:ee})),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=j({apiBase:this.cfg.apiBase??location.origin,fetchToken:async()=>{throw new Error("embed widget: fetchToken should not be invoked \u2014 token is supplied via data-token")}});try{let t=await e.startCall({agentId:this.cfg.agentId,token:this.cfg.token,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}},E=null,_=n=>{E?.destroy(),E=new M(n)},oe=()=>{E?.destroy(),E=null};window.PlatformWidget={init:_,destroy:oe};var R=ne();if(R){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"?_(R):document.addEventListener("DOMContentLoaded",()=>_(R)))}})();
1093
+ `
1094
+ }
1095
+ setStatus(e, t = '') {
1096
+ ;((this.statusEl.className = `status ${t}`.trim()), (this.statusEl.textContent = e))
1097
+ }
1098
+ setFabState(e) {
1099
+ for (
1100
+ this.fab.classList.remove('live', 'connecting'),
1101
+ e === 'live'
1102
+ ? this.fab.classList.add('live')
1103
+ : e === 'connecting' && this.fab.classList.add('connecting');
1104
+ this.fab.firstChild;
1105
+ )
1106
+ this.fab.removeChild(this.fab.firstChild)
1107
+ ;(this.fab.appendChild(J(e === 'live' ? { rect: !0 } : { path: re })),
1108
+ this.fab.setAttribute('aria-label', e === 'live' ? 'End call' : 'Start call'))
1109
+ }
1110
+ async toggle() {
1111
+ if (this.call) {
1112
+ this.call.end()
1113
+ return
1114
+ }
1115
+ if (!this.cfg.token) {
1116
+ this.setStatus('data-token="ct_..." is required', 'err')
1117
+ return
1118
+ }
1119
+ ;((this.panel.style.display = 'flex'),
1120
+ this.setFabState('connecting'),
1121
+ this.setStatus('connecting\u2026'))
1122
+ let e = G({
1123
+ apiBase: this.cfg.apiBase ?? location.origin,
1124
+ fetchToken: async () => ({
1125
+ token: this.cfg.token,
1126
+ transport: this.cfg.transport ?? 'ws',
1127
+ webrtcGatewayBase: this.cfg.webrtcGatewayBase,
1128
+ }),
1129
+ })
1130
+ try {
1131
+ let t = await e.startCall({
1132
+ agentId: this.cfg.agentId,
1133
+ context: this.cfg.vars,
1134
+ onStateChange: (r) => this.onState(r),
1135
+ onTranscript: (r) => this.renderTranscript(r),
1136
+ onError: (r) => {
1137
+ this.setStatus(`${r.code}: ${r.message}`, 'err')
1138
+ },
1139
+ onEnd: () => {
1140
+ ;(this.setFabState('idle'), (this.call = null))
1141
+ },
1142
+ })
1143
+ this.call = t
1144
+ } catch (t) {
1145
+ ;(this.setStatus(t instanceof Error ? t.message : 'connect failed', 'err'),
1146
+ this.setFabState('idle'),
1147
+ (this.call = null))
1148
+ }
1149
+ }
1150
+ onState(e) {
1151
+ switch (e) {
1152
+ case 'connecting':
1153
+ ;(this.setFabState('connecting'), this.setStatus('connecting\u2026'))
1154
+ break
1155
+ case 'listening':
1156
+ ;(this.setFabState('live'), this.setStatus('listening', 'ok'))
1157
+ break
1158
+ case 'user_speaking':
1159
+ this.setStatus('you: \u2026speaking', 'ok')
1160
+ break
1161
+ case 'agent_speaking':
1162
+ this.setStatus('agent speaking', 'ok')
1163
+ break
1164
+ case 'ended':
1165
+ ;(this.setFabState('idle'), this.setStatus('ended'))
1166
+ break
1167
+ case 'error':
1168
+ this.setFabState('idle')
1169
+ break
1170
+ }
1171
+ }
1172
+ renderTranscript(e) {
1173
+ for (; this.transcriptEl.firstChild; )
1174
+ this.transcriptEl.removeChild(this.transcriptEl.firstChild)
1175
+ for (let t of e) {
1176
+ let r = document.createElement('div')
1177
+ if (((r.className = 'line'), t.role === 'user')) {
1178
+ ;(r.classList.add('user'), t.committed || r.classList.add('interim'))
1179
+ let o = document.createElement('span')
1180
+ ;((o.className = 'label'),
1181
+ (o.textContent = 'You:'),
1182
+ r.appendChild(o),
1183
+ r.append(' ' + t.text))
1184
+ } else if (t.role === 'agent') {
1185
+ ;(r.classList.add('agent'), t.interrupted && r.classList.add('interrupted'))
1186
+ let o = document.createElement('span')
1187
+ ;((o.className = 'label'),
1188
+ (o.textContent = 'Agent:'),
1189
+ r.appendChild(o),
1190
+ r.append(' ' + t.text))
1191
+ } else (t.role, r.classList.add('sys'), (r.textContent = t.text))
1192
+ this.transcriptEl.appendChild(r)
1193
+ }
1194
+ this.transcriptEl.scrollTop = this.transcriptEl.scrollHeight
1195
+ }
1196
+ },
1197
+ T = null,
1198
+ O = (n) => {
1199
+ ;(T?.destroy(), (T = new I(n)))
1200
+ },
1201
+ ie = () => {
1202
+ ;(T?.destroy(), (T = null))
1203
+ }
1204
+ window.PlatformWidget = { init: O, destroy: ie }
1205
+ var _ = ae()
1206
+ if (_) {
1207
+ let n = document.querySelectorAll('script[src*="embed.js"]'),
1208
+ e = !0
1209
+ for (let t of Array.from(n)) t.dataset.autoInit === 'false' && (e = !1)
1210
+ e &&
1211
+ (document.readyState !== 'loading'
1212
+ ? O(_)
1213
+ : document.addEventListener('DOMContentLoaded', () => O(_)))
1214
+ }
1215
+ })()