@moveo-ai/web-client 0.123.0 → 0.124.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.
Files changed (48) hide show
  1. package/README.md +11 -8
  2. package/dist/63.min.js.LICENSE.txt +1 -1
  3. package/dist/752.min.js.LICENSE.txt +1 -1
  4. package/dist/882.min.js +1 -1
  5. package/dist/882.min.js.LICENSE.txt +1 -1
  6. package/dist/929.min.js.LICENSE.txt +1 -1
  7. package/dist/audio-client.min.js +1 -1
  8. package/dist/audio-client.min.js.LICENSE.txt +1 -1
  9. package/dist/continue/index.html +1 -1
  10. package/dist/iframe.min.js +1 -1
  11. package/dist/iframe.min.js.LICENSE.txt +1 -1
  12. package/dist/index.html +1 -1
  13. package/dist/preview/index.html +2 -2
  14. package/dist/preview.css.min.js.LICENSE.txt +1 -1
  15. package/dist/public/locales/ar.json +1 -0
  16. package/dist/public/locales/bg.json +1 -0
  17. package/dist/public/locales/cs.json +1 -0
  18. package/dist/public/locales/da.json +1 -0
  19. package/dist/public/locales/de.json +1 -0
  20. package/dist/public/locales/el.json +1 -0
  21. package/dist/public/locales/en.json +1 -0
  22. package/dist/public/locales/es.json +1 -0
  23. package/dist/public/locales/fr.json +1 -0
  24. package/dist/public/locales/id.json +1 -0
  25. package/dist/public/locales/it.json +1 -0
  26. package/dist/public/locales/ja.json +1 -0
  27. package/dist/public/locales/me.json +1 -0
  28. package/dist/public/locales/nl-be.json +1 -0
  29. package/dist/public/locales/nl.json +1 -0
  30. package/dist/public/locales/pl.json +1 -0
  31. package/dist/public/locales/pt-br.json +1 -0
  32. package/dist/public/locales/pt.json +1 -0
  33. package/dist/public/locales/ro.json +1 -0
  34. package/dist/public/locales/ru.json +1 -0
  35. package/dist/public/locales/sq.json +1 -0
  36. package/dist/public/locales/sr.json +1 -0
  37. package/dist/public/locales/sv.json +1 -0
  38. package/dist/public/locales/tl.json +1 -0
  39. package/dist/public/locales/tr.json +1 -0
  40. package/dist/public/locales/zh.json +1 -0
  41. package/dist/telnyx-client.min.js +1 -1
  42. package/dist/telnyx-client.min.js.LICENSE.txt +1 -1
  43. package/dist/web-client.min.js +1 -1
  44. package/dist/web-client.min.js.LICENSE.txt +1 -1
  45. package/dist/webrtc-client.min.js +1 -1
  46. package/dist/webrtc-client.min.js.LICENSE.txt +1 -1
  47. package/package.json +4 -2
  48. package/web-client.d.ts +14 -2
package/README.md CHANGED
@@ -103,6 +103,9 @@ MoveoAI.init({ integrationId: 'YOUR_INTEGRATION_ID' }).then((controller) => {
103
103
  // Send a message programmatically
104
104
  controller.sendMessage({ text: 'Hello!' });
105
105
 
106
+ // Run a dialog node directly, with no text
107
+ controller.sendMessage({ trigger_node_id: 'YOUR_NODE_ID' });
108
+
106
109
  // Update the conversation context
107
110
  controller.updateContext({ custom_field: 'custom_value' });
108
111
 
@@ -176,7 +179,7 @@ Initializes the chat widget.
176
179
  | `host` | `string` | No | Regional channels host, when it differs from the default |
177
180
  | `element` | `HTMLElement \| string` | No | Embed target, required for the `embed` widget position |
178
181
  | `identityToken` | `string` | No | Signed token that verifies the user's identity on the session |
179
- | `voiceIntegrationId` | `string` | No | Voice pairing override for the in-widget call |
182
+ | `voiceIntegrationId` | `string` | No | Deprecated and ignored: the call uses the desk voice pairing |
180
183
  | `variant` | `'default' \| 'nbg' \| 'alpha' \| 'fiserv'` | No | Visual variant |
181
184
 
182
185
  Any other key passes through as a widget-config override for the loaded integration.
@@ -191,6 +194,7 @@ Any other key passes through as a widget-config override for the loaded integrat
191
194
  | `closeWindow()` | Closes the chat window |
192
195
  | `closeConversation({ hide })` | Ends the conversation; the widget hides unless `hide: false` |
193
196
  | `sendMessage({ text })` | Sends a message to the agent |
197
+ | `sendMessage({ trigger_node_id })` | Runs a dialog node directly. Send it without text |
194
198
  | `showTeaserMessage(message?)` | Shows the teaser bubble |
195
199
  | `updateContext(ctx)` | Updates the conversation context |
196
200
  | `setCSSVariables(vars)` | Overrides widget CSS variables |
@@ -256,9 +260,9 @@ runs the end-of-chat survey when the account has one. On an empty chat, or
256
260
  while a prompt or webview owns the panel, it just hides the panel. Set
257
261
  `close_minimizes_window: true` and the button always minimizes and never ends.
258
262
 
259
- **Restart**, in the header title menu, does not ask. It ends the conversation
260
- and opens the next one in place, with the panel still open, and shows no
261
- end-of-chat survey.
263
+ **New conversation**, in the header title menu, does not ask. It ends the
264
+ conversation and opens the next one in place, with the panel still open, and
265
+ shows no end-of-chat survey.
262
266
 
263
267
  During a voice call the close button asks first, then ends the call and hides
264
268
  the panel. The conversation stays open, and the call's turns stay in the thread.
@@ -270,7 +274,7 @@ Which event to count:
270
274
  | `close_icon_clicked` | The visitor presses X, before they answer the confirmation | No |
271
275
  | `popover_end_chat_clicked` | They confirm **End conversation** | Yes |
272
276
  | `popover_end_chat_continue_clicked` | They pick **Minimize** instead | No |
273
- | `restart_conversation_clicked` | They pick **Restart** from the menu | Yes |
277
+ | `restart_conversation_clicked` | They pick **New conversation** from the menu | Yes |
274
278
 
275
279
  Add `popover_end_chat_clicked` and `restart_conversation_clicked` for the
276
280
  number of conversations visitors ended themselves. `close_icon_clicked` counts
@@ -512,9 +516,8 @@ teaser steps aside while the panel is up, and a visitor who opens it, reads
512
516
  whatever the bot opened with and closes it again has said nothing — so the
513
517
  teaser comes back, waiting out `show_after` once more. It goes away for good
514
518
  the moment the visitor sends anything of their own, a tapped chip included. On
515
- a sticky session that reload does not tell us anything until they open the
516
- chat, so a returning visitor sees the teaser again and loses it as soon as
517
- their earlier conversation loads.
519
+ a sticky session the reload waits for the earlier conversation to load before
520
+ the teaser shows, so a visitor who already talked never sees it flash.
518
521
 
519
522
  **Sections belong to their page.** A single-page-app navigation rebuilds the
520
523
  watch against the new page's sections, and clears the current section so
@@ -1,6 +1,6 @@
1
1
  /*!
2
2
  *
3
- * @moveo-ai/web-client v0.123.0
3
+ * @moveo-ai/web-client v0.124.0
4
4
  * Copyright (c) Moveo.ai (TM)
5
5
  *
6
6
  */
@@ -1,6 +1,6 @@
1
1
  /*!
2
2
  *
3
- * @moveo-ai/web-client v0.123.0
3
+ * @moveo-ai/web-client v0.124.0
4
4
  * Copyright (c) Moveo.ai (TM)
5
5
  *
6
6
  */
package/dist/882.min.js CHANGED
@@ -1,2 +1,2 @@
1
1
  /*! For license information please see 882.min.js.LICENSE.txt */
2
- "use strict";(this.webpackChunkMoveoAI||=[]).push([[882],{2229(t,e,i){i.d(e,{h:()=>s});class s{constructor(){this.analyserNode=null,this.audioSource=null,this.animationId=null,this.audioContext=null}start(t,e){try{this.audioContext=new AudioContext,this.audioSource=this.audioContext.createMediaStreamSource(t),this.analyserNode=this.audioContext.createAnalyser(),this.analyserNode.fftSize=256,this.analyserNode.smoothingTimeConstant=.3,this.audioSource.connect(this.analyserNode);const i=new Float32Array(this.analyserNode.fftSize),s=()=>{if(!this.analyserNode)return void this.stop();this.analyserNode.getFloatTimeDomainData(i);let t=0;for(const e of i)t+=e*e;const n=Math.sqrt(t/i.length),r=Math.min(1,10*n);e((r>.01?r:0).toFixed(3)),this.animationId=requestAnimationFrame(s)};s()}catch{}}stop(){if(null!==this.animationId&&(cancelAnimationFrame(this.animationId),this.animationId=null),this.audioSource){try{this.audioSource.disconnect()}catch{}this.audioSource=null}this.audioContext&&(this.audioContext.close().catch(()=>{}),this.audioContext=null),this.analyserNode=null}}},6815(t,e,i){i.d(e,["n",0,{NotAllowedError:"Microphone permission denied",NotFoundError:"No microphone found",NotReadableError:"Microphone is in use by another application",OverconstrainedError:"Microphone does not support required settings"}])},3564(t,e,i){i.d(e,{M:()=>r});var s=i(2076);const n={"high-rtt":"High network latency detected","low-mos":"Poor call quality detected","high-jitter":"Network instability detected","high-packet-loss":"Packet loss detected"};class r{constructor(t,e,i,s=5e3){this.pc=t,this.onWarning=e,this.onWarningCleared=i,this.pollIntervalMs=s,this.interval=null,this.prevStats=null,this.activeWarnings=new Set}start(){this.interval=setInterval(()=>{this.pollStats()},this.pollIntervalMs)}stop(){null!==this.interval&&(clearInterval(this.interval),this.interval=null),this.prevStats=null,this.activeWarnings.clear()}async pollStats(){try{const t=await this.pc.getStats();let e=null,i=0,n=0;const r=(0,s._)(t),o=null!=r?.currentRoundTripTime?1e3*r.currentRoundTripTime:null;t.forEach(t=>{if("inbound-rtp"===t.type&&"audio"===t.kind){const s=t;e=s.jitter??null,i=s.packetsReceived??0,n=s.packetsLost??0}});let a=0;if(this.prevStats){const t=i-this.prevStats.packetsReceived,e=n-this.prevStats.packetsLost,s=t+e;a=s>0?e/s:0}if(this.prevStats={packetsReceived:i,packetsLost:n},this.checkWarning("high-rtt",null!==o&&o>400),this.checkWarning("high-jitter",null!==e&&e>.03),this.checkWarning("high-packet-loss",a>.03),null!==o&&null!==e){const t=93.2-1e3*e*2-100*a*40-.1*Math.max(0,o-150),i=1+.035*t+t*(t-60)*(100-t)*7e-6;this.checkWarning("low-mos",i<3)}}catch{}}checkWarning(t,e){e&&!this.activeWarnings.has(t)?(this.activeWarnings.add(t),this.onWarning(t,n[t]||t)):!e&&this.activeWarnings.has(t)&&(this.activeWarnings.delete(t),this.onWarningCleared(t))}}},6845(t,e,i){i.d(e,{PA:()=>s,TM:()=>r,Xq:()=>n});class s extends Error{constructor(t="Voice service is busy, please try again."){super(t),this.name="ServerBusyError"}}class n extends Error{constructor(t){super(t),this.name="ConfigUnavailableError"}}class r extends Error{constructor(t="Too many attempts. Please try again in a moment."){super(t),this.name="RateLimitedError"}}},7943(t,e,i){i.d(e,{$:()=>o});var s=i(7833),n=i.n(s),r=i(2076);class o{constructor(t,e,i,s=1e3,r=3,o=!function(t=navigator.userAgent){return/\b(iPad|iPhone|iPod)\b/.test(t)||/AppleWebKit/.test(t)&&!/Chrome|Chromium|CriOS|Edg|OPR/.test(t)}()){this.pc=t,this.onStall=e,this.onRecover=i,this.pollIntervalMs=s,this.flatPollsThreshold=r,this.escalate=o,this.log=n()("moveo:webrtc:media"),this.interval=null,this.prevCounts=null,this.flatPolls=0,this.stalled=!1,this.cannotAssessLogged=!1}start(){this.log("started (escalate=%s)",this.escalate),this.interval=setInterval(()=>{this.poll()},this.pollIntervalMs)}stop(){null!==this.interval&&(clearInterval(this.interval),this.interval=null),this.prevCounts=null,this.flatPolls=0,this.stalled=!1,this.cannotAssessLogged=!1}async poll(){try{const t=await this.pc.getStats(),e=function(t){const e=new Map;return t.forEach(t=>{if("inbound-rtp"===t.type){const i=t;"audio"===i.kind&&void 0!==i.packetsReceived&&e.set(i.id,i.packetsReceived)}}),e}(t);if(0===e.size)return void(this.cannotAssessLogged||(this.cannotAssessLogged=!0,this.log("cannot assess: no audio inbound-rtp with packetsReceived")));this.cannotAssessLogged=!1;const i=this.prevCounts;if(this.prevCounts=e,null===i)return void this.log("assessing %d audio inbound-rtp report(s)",e.size);let s=!1,n=!1,r=0,o=!1;if(e.forEach((t,e)=>{const a=i.get(e);void 0!==a?(r++,t>a?s=!0:t<a&&(n=!0)):o=!0}),s)return this.stalled&&(this.stalled=!1,this.log("recovered%s (pair %s)",this.escalate?"":" (log-only)",this.describeSelectedPair(t)),this.escalate&&this.onRecover()),void(this.flatPolls=0);if(0===r||n||o)return this.log("inbound-rtp reports reseeded (comparable=%d new=%s)",r,o),void(this.flatPolls=0);this.flatPolls++,this.log("flat poll reports=%d flat=%d",e.size,this.flatPolls),this.flatPolls>=this.flatPollsThreshold&&!this.stalled&&(this.stalled=!0,this.log("stall declared%s after %d flat polls (pair %s)",this.escalate?"":" (log-only, escalation disabled)",this.flatPolls,this.describeSelectedPair(t)),this.escalate&&this.onStall())}catch{}}describeSelectedPair(t){const e=(0,r._)(t);return e?(0,r.t)(t,e):"?"}}},2076(t,e,i){function s(t){let e;const i=[];t.forEach(t=>{if("transport"===t.type){const{selectedCandidatePairId:i}=t;i&&!e&&(e=i)}else"candidate-pair"===t.type&&i.push(t)});const s=e&&i.find(t=>t.id===e)||i.find(t=>!0===t.selected);if(s)return s;let n;for(const t of i){if(!t.nominated)continue;if(void 0===n){n=t;continue}const e=n.lastPacketReceivedTimestamp,i=t.lastPacketReceivedTimestamp;void 0!==i&&(void 0===e||i>e)&&(n=t)}return n}function n(t,e){const i=e=>{if(!e)return"?";const i=t.get(e);if(!i)return"?";const s=`${i.candidateType??"?"}/${i.protocol??"?"}`;return i.relayProtocol?`${s}(${i.relayProtocol})`:s};return`${i(e.localCandidateId)} -> ${i(e.remoteCandidateId)}`}i.d(e,{_:()=>s,t:()=>n})},4623(t,e,i){i.d(e,{g:()=>r});var s=i(7833),n=i.n(s);class r{constructor(t,e,i,s,r){this.url=t,this.sessionToken=e,this.onMessage=i,this.onClose=s,this.onError=r,this.log=n()("moveo:webrtc:media"),this.ws=null}connect(){return new Promise((t,e)=>{this.log("connecting to %s",this.url);const i=`${this.url}?token=${encodeURIComponent(this.sessionToken)}`;this.ws=new WebSocket(i);const s=setTimeout(()=>{this.close(),e(new Error("Signaling connection timed out"))},1e4);this.ws.onopen=()=>{},this.ws.onmessage=e=>{let i;try{i=JSON.parse(e.data)}catch{return}this.log("recv %s",i.type),"welcome"===i.type&&(clearTimeout(s),t()),this.onMessage(i)},this.ws.onerror=t=>{clearTimeout(s),this.log("websocket error"),this.onError(t),e(new Error("Signaling WebSocket error"))},this.ws.onclose=t=>{clearTimeout(s),this.log("closed code=%d reason=%s",t.code,t.reason),e(new Error("Signaling WebSocket closed before connection established")),this.onClose(t.code,t.reason)}})}connectWithFirstFrame(t,e){const i="attach"===t.type?"attach_ack":"reconnect_ack";return new Promise((s,n)=>{this.log("%s to %s",t.type,this.url);const r=`${this.url}?token=${encodeURIComponent(this.sessionToken)}`;this.ws=new WebSocket(r);let o=!1;const a=setTimeout(()=>{o||(o=!0,"reconnect"===t.type?this.close(4001,"Resume timed out"):this.close(),n(new Error(`Signaling ${t.type} timed out`)))},1e4);this.ws.onopen=()=>{this.ws?.send(JSON.stringify(t)),e?.()},this.ws.onmessage=e=>{let r;try{r=JSON.parse(e.data)}catch{return}this.log("recv %s",r.type),this.onMessage(r),o||(r.type===i?(o=!0,clearTimeout(a),s()):"error"===r.type&&(o=!0,clearTimeout(a),n(new Error(`${t.type} rejected: ${r.error.code}`))))},this.ws.onerror=t=>{this.log("websocket error"),this.onError(t),o||(o=!0,clearTimeout(a),n(new Error("Signaling WebSocket error")))},this.ws.onclose=e=>{this.log("closed code=%d reason=%s",e.code,e.reason),o||(o=!0,clearTimeout(a),n(new Error(`Signaling WebSocket closed before ${t.type} established`))),this.onClose(e.code,e.reason)}})}send(t){this.ws&&this.ws.readyState===WebSocket.OPEN&&(this.log("send %s",t.type),this.ws.send(JSON.stringify(t)))}close(t=1e3,e="Client closed"){this.ws&&(this.ws.onopen=null,this.ws.onclose=null,this.ws.onerror=null,this.ws.onmessage=null,this.ws.close(t,e),this.ws=null)}get isConnected(){return this.ws?.readyState===WebSocket.OPEN}}},5882(t,e,i){i.r(e),i.d(e,{WebRTCProvider:()=>w}),i(6752);var s=i(7833),n=i.n(s),r=i(2229),o=i(6815),a=i(3564),c=i(6845),h=i(7943),l=i(4623);const d=500,u=new Set(["session_expired","session_inactive","invalid_credential","max_recovery_exceeded","unexpected_first_frame"]),g={session_expired:"Your session has expired.",session_inactive:"The call has ended.",invalid_credential:"Could not resume the call.",max_recovery_exceeded:"Could not resume the call after several attempts.",unexpected_first_frame:"Could not resume the call."},m={invalid_code:"Could not start the call.",used:"A call is already running in this conversation.",session_ended:"This conversation has ended.",attach_unsupported:"Calls are not available in this conversation.",internal_error:"Could not start the call."},p=t=>t instanceof Error?t.message:"Signaling connection failed",f={...o.n,setRemoteDescription:"Connection setup failed",setLocalDescription:"Connection setup failed",addIceCandidate:"Network configuration failed",createOffer:"Browser audio setup failed"};class w{constructor(t,e){this.wsUrl=t,this.fetchConfig=e,this.log=n()("moveo:webrtc:media"),this.pc=null,this.signaling=null,this.localStream=null,this.remoteStream=null,this.remoteAudio=null,this.remoteAudioMonitor=null,this.audioMonitor=null,this.qualityMonitor=null,this.stallDetector=null,this.pendingCandidates=[],this.state="idle",this.eventListeners=new Map,this.iceRestarting=!1,this.iceRestartAttempts=0,this.iceRestartTimeout=null,this.iceRestartAnswerTimeout=null,this.connectResolve=null,this.connectReject=null,this.resuming=!1,this.reconnectAttempts=0,this.reconnectErrorCode=null,this.attaching=!1,this.attachErrorCode=null,this.ended=!1,this.hangupReason=null,this.attachSent=!1,this.attachSignaling=null,this.brainResponseChunks=new Map,this.interruptedRequests=new Map}get serverHangupReason(){return this.hangupReason}get attached(){return this.attachSent}setConfig(t){this.config=t}async connect(t){try{return await this.runConnect(t)}catch(t){if("connecting"===this.state){const t="Could not start the call.";throw this.cleanup(),this.emitError(t),new Error(t)}throw t}}async runConnect(t){if(!this.config)throw new Error("Voice config not set; call setConfig() before connect().");this.ended=!1,this.hangupReason=null,this.attachSent=!1,this.setState("connecting");try{this.localStream=await navigator.mediaDevices.getUserMedia({audio:{echoCancellation:!0,noiseSuppression:!0,autoGainControl:!0}})}catch(t){const e=t instanceof Error?t.name:"",i=f[e]||"Microphone access failed";throw this.emitError(i),new Error(i)}if(t)this.signaling=await this.openAttachedSignaling(t);else{this.signaling=this.createSignaling();try{await this.signaling.connect()}catch(t){this.failSignaling(p(t))}}this.pc=new RTCPeerConnection({iceServers:this.config.iceServers,iceTransportPolicy:this.config.iceTransportPolicy||"relay"});const e=this.localStream.getAudioTracks()[0];this.pc.addTrack(e,this.localStream),this.pc.ontrack=this.handleRemoteTrack.bind(this),this.pc.onicecandidate=t=>{t.candidate&&this.signaling?.send({type:"ice",candidate:t.candidate.toJSON()})},this.pc.onconnectionstatechange=this.handleConnectionStateChange.bind(this);try{const t=await this.pc.createOffer();if(!t.sdp)throw new Error("createOffer returned no SDP");await this.pc.setLocalDescription(t),this.signaling.send({type:"offer",sdp:t.sdp})}catch(t){this.cleanup();const e=t instanceof Error?t.name:"createOffer",i=f[e]||"Failed to create call offer";throw this.emitError(i),new Error(i)}return this.audioMonitor=new r.h,this.audioMonitor.start(this.localStream,t=>{this.emit("audio-level",t,Number(t))}),new Promise((t,e)=>{this.connectResolve=t,this.connectReject=e})}disconnect(){this.disconnectInternal(!1)}createSignaling(){return new l.g(this.wsUrl,this.config.sessionToken,this.handleSignalingMessage.bind(this),this.handleSignalingClose.bind(this),this.handleSignalingError.bind(this))}async openAttachedSignaling(t){for(let e=1;;e++){this.attachErrorCode=null;let i=null;try{const e=await t.getCode();if(this.ended)throw new Error("Call ended");if(i=this.createSignaling(),this.attachSignaling=i,this.attaching=!0,await i.connectWithFirstFrame({type:"attach",code:e},()=>{this.attachSent=!0}),this.ended)throw new Error("Call ended");return i}catch(t){i?.close();const s=this.attachErrorCode;if(s&&(this.attachSent=!1),"internal_error"===s&&e<2)continue;this.failSignaling(s?m[s]??"Could not start the call.":p(t))}finally{this.attaching=!1,this.attachSignaling=null}}}failSignaling(t){throw this.cleanupLocalStream(),this.emitError(t),new Error(t)}disconnectInternal(t){if(this.ended=!0,this.attachSent||this.attachSignaling?.close(),!t)try{this.signaling?.send({type:"hangup"})}catch{}try{this.qualityMonitor?.stop(),this.qualityMonitor=null}catch{}try{this.stallDetector?.stop(),this.stallDetector=null}catch{}try{this.audioMonitor?.stop(),this.audioMonitor=null}catch{}try{this.remoteAudioMonitor?.stop(),this.remoteAudioMonitor=null}catch{}try{this.pc?.close(),this.pc=null}catch{}this.cleanupLocalStream();try{this.signaling?.close(),this.signaling=null}catch{}try{this.remoteAudio&&(this.remoteAudio.srcObject=null,this.remoteAudio.remove(),this.remoteAudio=null),this.remoteStream=null}catch{}this.clearIceRestart(),this.iceRestartAttempts=0,this.connectReject&&this.connectReject(new Error("Call disconnected before connection completed")),this.connectResolve=null,this.connectReject=null,this.resuming=!1,this.reconnectAttempts=0,this.reconnectErrorCode=null,this.originalSessionID=void 0,this.originalConnectionID=void 0,this.pendingCandidates=[],this.brainResponseChunks.clear(),this.interruptedRequests.clear(),this.setState("idle")}toggleMute(){const t=this.localStream?.getAudioTracks()[0];if(t){t.enabled=!t.enabled;const e=!t.enabled;return this.signaling?.send({type:"mute",muted:e}),e}return!1}isMuted(){return!(this.localStream?.getAudioTracks()[0]?.enabled??1)}getLocalStream(){return this.localStream}getRemoteStream(){return this.remoteStream}sendDigits(t){for(const e of t)this.signaling?.send({type:"dtmf",digit:e})}on(t,e){const i=this.eventListeners.get(t)??new Set;return i.add(e),this.eventListeners.set(t,i),()=>this.off(t,e)}off(t,e){const i=this.eventListeners.get(t);i&&(i.delete(e),0===i.size&&this.eventListeners.delete(t))}handleRemoteTrack(t){const[e]=t.streams;this.remoteStream=e??null,this.remoteAudio=document.createElement("audio"),this.remoteAudio.srcObject=e,this.remoteAudio.autoplay=!0,document.body.appendChild(this.remoteAudio),this.remoteAudio.play().catch(()=>{}),this.remoteAudioMonitor=new r.h,this.remoteAudioMonitor.start(e,t=>{Number(t)>.02&&this.emit("remote-audio",t,Number(t))})}handleConnectionStateChange(){if(this.pc)switch(this.log("connectionState %s",this.pc.connectionState),this.pc.connectionState){case"connecting":this.setState("connecting");break;case"connected":{const t="reconnecting"===this.state;this.setState("connected"),this.clearIceRestart(),this.iceRestartAttempts=0,t&&this.emit("warning-cleared",void 0,"reconnect"),this.qualityMonitor?.stop(),this.qualityMonitor=new a.M(this.pc,(t,e)=>this.emit("warning",e,t),t=>this.emit("warning-cleared",void 0,t)),this.qualityMonitor.start(),this.stallDetector?.stop(),this.stallDetector=new h.$(this.pc,()=>{this.log("media stall -> ICE restart"),this.triggerIceRestart()},()=>{this.log("media recovered"),this.clearIceRestart(),this.iceRestartAttempts=0,"reconnecting"===this.state&&this.setState("connected"),this.emit("warning-cleared",void 0,"reconnect")}),this.stallDetector.start(),this.connectResolve?.(),this.connectResolve=null,this.connectReject=null;break}case"disconnected":if(this.resuming)break;this.triggerIceRestart();break;case"failed":if(this.resuming){this.connectReject?.(new Error("ICE connection failed")),this.connectResolve=null,this.connectReject=null;break}if(this.connectReject){this.connectReject(new Error("ICE connection failed")),this.connectResolve=null,this.connectReject=null,this.emitError("Connection failed"),this.cleanup();break}this.iceRestarting?this.onRestartAttemptExpired():this.triggerIceRestart();break;case"closed":this.cleanup()}}async triggerIceRestart(){if(this.resuming||!this.pc||!this.signaling)return;if(this.iceRestarting||this.iceRestartAttempts>=2)return;if(!this.signaling.isConnected)return this.isResumeEligible()?void this.beginResume(d):(this.emitError("Connection lost"),void this.cleanup());const t=this.pc,e=this.signaling;this.iceRestarting=!0,this.iceRestartAttempts++,this.log("ICE restart attempt %d/%d",this.iceRestartAttempts,2),this.setState("reconnecting"),this.emit("warning","Reconnecting…","reconnect");try{const i=await t.createOffer({iceRestart:!0});if(!i.sdp)throw new Error("createOffer returned no SDP");await t.setLocalDescription(i),e.send({type:"offer",sdp:i.sdp})}catch{if(this.resuming||!this.iceRestarting||this.pc!==t)return;return this.iceRestarting=!1,this.emitError("Connection lost"),void this.cleanup()}this.iceRestarting&&this.pc===t&&(this.iceRestartTimeout=setTimeout(()=>{this.onRestartAttemptExpired()},1e4),this.iceRestartAnswerTimeout=setTimeout(()=>{this.onIceRestartUnanswered()},3e3))}onIceRestartUnanswered(){this.iceRestartAnswerTimeout=null,this.iceRestarting&&this.isResumeEligible()&&(this.log("ICE restart offer unanswered -> resume"),this.beginResume(d))}onRestartAttemptExpired(){this.log("ICE restart attempt expired (attempts=%d)",this.iceRestartAttempts),this.iceRestartTimeout&&(clearTimeout(this.iceRestartTimeout),this.iceRestartTimeout=null),this.iceRestartAnswerTimeout&&(clearTimeout(this.iceRestartAnswerTimeout),this.iceRestartAnswerTimeout=null),this.iceRestarting=!1,this.iceRestartAttempts<2?this.triggerIceRestart():this.isResumeEligible()?this.beginResume(d):(this.emitError("Connection lost"),this.cleanup())}clearIceRestart(){this.iceRestarting&&this.log("ICE restart cleared"),this.iceRestartTimeout&&(clearTimeout(this.iceRestartTimeout),this.iceRestartTimeout=null),this.iceRestartAnswerTimeout&&(clearTimeout(this.iceRestartAnswerTimeout),this.iceRestartAnswerTimeout=null),this.iceRestarting=!1}handleSignalingMessage(t){switch(t.type){case"answer":this.handleAnswer(t.sdp);break;case"ice":this.addIceCandidate(t.candidate);break;case"transcript":this.emit("transcript",t.text,{text:t.text,request_id:t.request_id,timestamp:t.timestamp});break;case"brain_response":this.handleBrainResponse(t);break;case"interrupt":this.handleInterrupt(t);break;case"hangup":this.hangupReason=t.reason??"",this.disconnectInternal(!0);break;case"session_ready":this.originalSessionID=t.session_id,this.originalConnectionID=t.connection_id;break;case"reconnect_ack":this.originalConnectionID=t.connection_id;break;case"drain":this.handleDrain(t);break;case"error":this.attaching?this.attachErrorCode=t.error.code:this.resuming?(this.reconnectErrorCode=t.error.code,this.connectReject?.(new Error(`reconnect rejected: ${t.error.code}`))):this.emitError(t.error.message)}}handleSignalingClose(t,e){this.resuming?this.connectReject?.(new Error("Signaling closed during resume")):this.isResumeEligible()?this.beginResume(d):"idle"!==this.state&&this.disconnect()}handleSignalingError(t){this.resuming||this.isResumeEligible()||"connected"===this.state&&this.emitError("Signaling error")}isResumeEligible(){return!this.resuming&&("connected"===this.state||"reconnecting"===this.state)&&void 0!==this.originalSessionID&&void 0!==this.originalConnectionID}async beginResume(t=0){this.resuming=!0,this.setState("reconnecting"),this.clearIceRestart(),this.emit("warning","Reconnecting…","reconnect"),t>0&&(await this.sleep(Math.random()*t),!this.resuming)||await this.reconnect()}async handleDrain(t){if(!this.resuming)return this.isResumeEligible()?void await this.beginResume(Math.max(0,t.reconnect_delay_ms||0)):(this.emitError("Could not resume the call."),void this.disconnectInternal(!0))}async reconnect(){try{this.pc?.getSenders().forEach(t=>{t.track&&this.pc?.removeTrack(t)})}catch{}const t=this.localStream?.getAudioTracks()[0];if(!t||"live"!==t.readyState)return this.emitError("Microphone is no longer available."),void this.disconnectInternal(!0);for(this.teardownForReconnect();this.reconnectAttempts<3;){this.reconnectErrorCode=null;try{if(this.config=await this.fetchConfig(),!this.resuming)return;return this.signaling=this.createSignaling(),await this.signaling.connectWithFirstFrame({type:"reconnect",session_id:this.originalSessionID,connection_id:this.originalConnectionID}),await this.buildReconnectPeerConnection(t),this.reconnectAttempts=0,this.resuming=!1,void this.emit("warning-cleared",void 0,"reconnect")}catch(t){const e=this.reconnectErrorCode;if(e&&u.has(e))return this.emitError(g[e]??"Could not resume the call."),void this.disconnectInternal(!0);if(t instanceof c.Xq)return this.emitError(t.message),void this.disconnectInternal(!0);const i=t instanceof c.PA?t.message:"Reconnecting…";if(this.emit("warning",i,"reconnect"),this.teardownForReconnect(),this.reconnectAttempts++,this.reconnectAttempts>=3)break;if(await this.sleep(500*this.reconnectAttempts),!this.resuming)return}}this.emitError("Could not resume the call."),this.disconnectInternal(!0)}buildReconnectPeerConnection(t){return this.pc=new RTCPeerConnection({iceServers:this.config.iceServers,iceTransportPolicy:this.config.iceTransportPolicy||"relay"}),this.pc.addTrack(t,this.localStream),this.pc.ontrack=this.handleRemoteTrack.bind(this),this.pc.onicecandidate=t=>{t.candidate&&this.signaling?.send({type:"ice",candidate:t.candidate.toJSON()})},this.pc.onconnectionstatechange=this.handleConnectionStateChange.bind(this),this.audioMonitor=new r.h,this.audioMonitor.start(this.localStream,t=>{this.emit("audio-level",t,Number(t))}),new Promise((t,e)=>{const i=setTimeout(()=>{this.connectResolve=null,this.connectReject=null,e(new Error("Reconnect media resume timed out"))},15e3);this.connectResolve=()=>{clearTimeout(i),t()},this.connectReject=t=>{clearTimeout(i),e(t)},(async()=>{try{const t=await this.pc.createOffer();if(!t.sdp)throw new Error("createOffer returned no SDP");await this.pc.setLocalDescription(t),this.signaling.send({type:"offer",sdp:t.sdp})}catch(t){const e=t instanceof Error?t.name:"createOffer";this.connectReject?.(new Error(f[e]||"Failed to create call offer"))}})()})}teardownForReconnect(){try{this.qualityMonitor?.stop(),this.qualityMonitor=null}catch{}try{this.stallDetector?.stop(),this.stallDetector=null}catch{}try{this.audioMonitor?.stop(),this.audioMonitor=null}catch{}try{this.remoteAudioMonitor?.stop(),this.remoteAudioMonitor=null}catch{}try{this.pc?.close(),this.pc=null}catch{}try{this.signaling?.close(4001,"Resuming"),this.signaling=null}catch{}try{this.remoteAudio&&(this.remoteAudio.srcObject=null,this.remoteAudio.remove(),this.remoteAudio=null),this.remoteStream=null}catch{}this.clearIceRestart(),this.iceRestartAttempts=0,this.connectResolve=null,this.connectReject=null,this.pendingCandidates=[]}sleep(t){return new Promise(e=>setTimeout(e,t))}async handleAnswer(t){if(this.iceRestartAnswerTimeout&&(clearTimeout(this.iceRestartAnswerTimeout),this.iceRestartAnswerTimeout=null),this.pc)try{await this.pc.setRemoteDescription({type:"answer",sdp:t}),await this.flushPendingCandidates()}catch{this.emitError(f.setRemoteDescription)}}async addIceCandidate(t){if(this.pc?.remoteDescription)try{await this.pc.addIceCandidate(t)}catch{}else this.pendingCandidates.push(t)}async flushPendingCandidates(){for(const t of this.pendingCandidates){if(!this.pc)break;try{await this.pc.addIceCandidate(t)}catch{}}this.pendingCandidates=[]}cleanup(){this.disconnect()}cleanupLocalStream(){try{this.localStream?.getTracks().forEach(t=>t.stop()),this.localStream=null}catch{}}setState(t){this.state=t,this.emit("state-change",t,t)}handleBrainResponse(t){const e=t.request_id;if(this.interruptedRequests.has(e))return;if(t.final){const t=this.assembleBrainResponse(e);return void this.emit("brain-response",t,{text:t,request_id:e,final:!0})}let i=this.brainResponseChunks.get(e);i||(i=new Map,this.brainResponseChunks.set(e,i)),i.set(t.index,t.text);const s=this.assembleBrainResponse(e);this.emit("brain-response",s,{text:s,request_id:e,final:!1})}assembleBrainResponse(t){const e=this.brainResponseChunks.get(t);return e?[...e.entries()].sort((t,e)=>t[0]-e[0]).map(([,t])=>t).join(" "):""}handleInterrupt(t){const e=this.interruptedRequests.get(t.request_id),i=void 0===e?t.heard_index:Math.min(e,t.heard_index);this.interruptedRequests.set(t.request_id,i);const s=this.brainResponseChunks.get(t.request_id),n=s?[...s.entries()].sort((t,e)=>t[0]-e[0]):[],r=t=>n.filter(([e])=>t(e)).map(([,t])=>t).join(" "),o=r(t=>t<=i),a=r(t=>t>i);this.emit("interrupt",o,{request_id:t.request_id,heard_index:i,timestamp:t.timestamp,heardText:o,unheardText:a})}emitError(t){this.emit("error",t),this.setState("error")}emit(t,e,i){const s={state:t,message:e,data:i},n=t=>{const e=this.eventListeners.get(t);e&&e.forEach(t=>{try{t(s)}catch{}})};n(t),n("any")}}}}]);
2
+ "use strict";(this.webpackChunkMoveoAI||=[]).push([[882],{2229(t,e,i){i.d(e,{h:()=>s});class s{constructor(){this.analyserNode=null,this.audioSource=null,this.animationId=null,this.audioContext=null}start(t,e){try{this.audioContext=new AudioContext,this.audioSource=this.audioContext.createMediaStreamSource(t),this.analyserNode=this.audioContext.createAnalyser(),this.analyserNode.fftSize=256,this.analyserNode.smoothingTimeConstant=.3,this.audioSource.connect(this.analyserNode);const i=new Float32Array(this.analyserNode.fftSize),s=()=>{if(!this.analyserNode)return void this.stop();this.analyserNode.getFloatTimeDomainData(i);let t=0;for(const e of i)t+=e*e;const n=Math.sqrt(t/i.length),r=Math.min(1,10*n);e((r>.01?r:0).toFixed(3)),this.animationId=requestAnimationFrame(s)};s()}catch{}}stop(){if(null!==this.animationId&&(cancelAnimationFrame(this.animationId),this.animationId=null),this.audioSource){try{this.audioSource.disconnect()}catch{}this.audioSource=null}this.audioContext&&(this.audioContext.close().catch(()=>{}),this.audioContext=null),this.analyserNode=null}}},6815(t,e,i){i.d(e,["n",0,{NotAllowedError:"Microphone permission denied",NotFoundError:"No microphone found",NotReadableError:"Microphone is in use by another application",OverconstrainedError:"Microphone does not support required settings"}])},3564(t,e,i){i.d(e,{M:()=>r});var s=i(2076);const n={"high-rtt":"High network latency detected","low-mos":"Poor call quality detected","high-jitter":"Network instability detected","high-packet-loss":"Packet loss detected"};class r{constructor(t,e,i,s=5e3){this.pc=t,this.onWarning=e,this.onWarningCleared=i,this.pollIntervalMs=s,this.interval=null,this.prevStats=null,this.activeWarnings=new Set}start(){this.interval=setInterval(()=>{this.pollStats()},this.pollIntervalMs)}stop(){null!==this.interval&&(clearInterval(this.interval),this.interval=null),this.prevStats=null,this.activeWarnings.clear()}async pollStats(){try{const t=await this.pc.getStats();let e=null,i=0,n=0;const r=(0,s._)(t),o=null!=r?.currentRoundTripTime?1e3*r.currentRoundTripTime:null;t.forEach(t=>{if("inbound-rtp"===t.type&&"audio"===t.kind){const s=t;e=s.jitter??null,i=s.packetsReceived??0,n=s.packetsLost??0}});let a=0;if(this.prevStats){const t=i-this.prevStats.packetsReceived,e=n-this.prevStats.packetsLost,s=t+e;a=s>0?e/s:0}if(this.prevStats={packetsReceived:i,packetsLost:n},this.checkWarning("high-rtt",null!==o&&o>400),this.checkWarning("high-jitter",null!==e&&e>.03),this.checkWarning("high-packet-loss",a>.03),null!==o&&null!==e){const t=93.2-1e3*e*2-100*a*40-.1*Math.max(0,o-150),i=1+.035*t+t*(t-60)*(100-t)*7e-6;this.checkWarning("low-mos",i<3)}}catch{}}checkWarning(t,e){e&&!this.activeWarnings.has(t)?(this.activeWarnings.add(t),this.onWarning(t,n[t]||t)):!e&&this.activeWarnings.has(t)&&(this.activeWarnings.delete(t),this.onWarningCleared(t))}}},6845(t,e,i){i.d(e,{PA:()=>s,TM:()=>r,Xq:()=>n});class s extends Error{constructor(t="Voice service is busy, please try again."){super(t),this.name="ServerBusyError"}}class n extends Error{constructor(t){super(t),this.name="ConfigUnavailableError"}}class r extends Error{constructor(t="Too many attempts. Please try again in a moment."){super(t),this.name="RateLimitedError"}}},7943(t,e,i){i.d(e,{$:()=>o});var s=i(7833),n=i.n(s),r=i(2076);class o{constructor(t,e,i,s=1e3,r=3,o=!function(t=navigator.userAgent){return/\b(iPad|iPhone|iPod)\b/.test(t)||/AppleWebKit/.test(t)&&!/Chrome|Chromium|CriOS|Edg|OPR/.test(t)}()){this.pc=t,this.onStall=e,this.onRecover=i,this.pollIntervalMs=s,this.flatPollsThreshold=r,this.escalate=o,this.log=n()("moveo:webrtc:media"),this.interval=null,this.prevCounts=null,this.flatPolls=0,this.stalled=!1,this.cannotAssessLogged=!1}start(){this.log("started (escalate=%s)",this.escalate),this.interval=setInterval(()=>{this.poll()},this.pollIntervalMs)}stop(){null!==this.interval&&(clearInterval(this.interval),this.interval=null),this.prevCounts=null,this.flatPolls=0,this.stalled=!1,this.cannotAssessLogged=!1}async poll(){try{const t=await this.pc.getStats(),e=function(t){const e=new Map;return t.forEach(t=>{if("inbound-rtp"===t.type){const i=t;"audio"===i.kind&&void 0!==i.packetsReceived&&e.set(i.id,i.packetsReceived)}}),e}(t);if(0===e.size)return void(this.cannotAssessLogged||(this.cannotAssessLogged=!0,this.log("cannot assess: no audio inbound-rtp with packetsReceived")));this.cannotAssessLogged=!1;const i=this.prevCounts;if(this.prevCounts=e,null===i)return void this.log("assessing %d audio inbound-rtp report(s)",e.size);let s=!1,n=!1,r=0,o=!1;if(e.forEach((t,e)=>{const a=i.get(e);void 0!==a?(r++,t>a?s=!0:t<a&&(n=!0)):o=!0}),s)return this.stalled&&(this.stalled=!1,this.log("recovered%s (pair %s)",this.escalate?"":" (log-only)",this.describeSelectedPair(t)),this.escalate&&this.onRecover()),void(this.flatPolls=0);if(0===r||n||o)return this.log("inbound-rtp reports reseeded (comparable=%d new=%s)",r,o),void(this.flatPolls=0);this.flatPolls++,this.log("flat poll reports=%d flat=%d",e.size,this.flatPolls),this.flatPolls>=this.flatPollsThreshold&&!this.stalled&&(this.stalled=!0,this.log("stall declared%s after %d flat polls (pair %s)",this.escalate?"":" (log-only, escalation disabled)",this.flatPolls,this.describeSelectedPair(t)),this.escalate&&this.onStall())}catch{}}describeSelectedPair(t){const e=(0,r._)(t);return e?(0,r.t)(t,e):"?"}}},2076(t,e,i){function s(t){let e;const i=[];t.forEach(t=>{if("transport"===t.type){const{selectedCandidatePairId:i}=t;i&&!e&&(e=i)}else"candidate-pair"===t.type&&i.push(t)});const s=e&&i.find(t=>t.id===e)||i.find(t=>!0===t.selected);if(s)return s;let n;for(const t of i){if(!t.nominated)continue;if(void 0===n){n=t;continue}const e=n.lastPacketReceivedTimestamp,i=t.lastPacketReceivedTimestamp;void 0!==i&&(void 0===e||i>e)&&(n=t)}return n}function n(t,e){const i=e=>{if(!e)return"?";const i=t.get(e);if(!i)return"?";const s=`${i.candidateType??"?"}/${i.protocol??"?"}`;return i.relayProtocol?`${s}(${i.relayProtocol})`:s};return`${i(e.localCandidateId)} -> ${i(e.remoteCandidateId)}`}i.d(e,{_:()=>s,t:()=>n})},4623(t,e,i){i.d(e,{g:()=>r});var s=i(7833),n=i.n(s);class r{constructor(t,e,i,s,r){this.url=t,this.sessionToken=e,this.onMessage=i,this.onClose=s,this.onError=r,this.log=n()("moveo:webrtc:media"),this.ws=null}connect(){return new Promise((t,e)=>{this.log("connecting to %s",this.url);const i=`${this.url}?token=${encodeURIComponent(this.sessionToken)}`;this.ws=new WebSocket(i);const s=setTimeout(()=>{this.close(),e(new Error("Signaling connection timed out"))},1e4);this.ws.onopen=()=>{},this.ws.onmessage=e=>{let i;try{i=JSON.parse(e.data)}catch{return}this.log("recv %s",i.type),"welcome"===i.type&&(clearTimeout(s),t()),this.onMessage(i)},this.ws.onerror=t=>{clearTimeout(s),this.log("websocket error"),this.onError(t),e(new Error("Signaling WebSocket error"))},this.ws.onclose=t=>{clearTimeout(s),this.log("closed code=%d reason=%s",t.code,t.reason),e(new Error("Signaling WebSocket closed before connection established")),this.onClose(t.code,t.reason)}})}connectWithFirstFrame(t,e){const i="attach"===t.type?"attach_ack":"reconnect_ack";return new Promise((s,n)=>{this.log("%s to %s",t.type,this.url);const r=`${this.url}?token=${encodeURIComponent(this.sessionToken)}`;this.ws=new WebSocket(r);let o=!1;const a=setTimeout(()=>{o||(o=!0,"reconnect"===t.type?this.close(4001,"Resume timed out"):this.close(),n(new Error(`Signaling ${t.type} timed out`)))},1e4);this.ws.onopen=()=>{this.ws?.send(JSON.stringify(t)),e?.()},this.ws.onmessage=e=>{let r;try{r=JSON.parse(e.data)}catch{return}this.log("recv %s",r.type),this.onMessage(r),o||(r.type===i?(o=!0,clearTimeout(a),s()):"error"===r.type&&(o=!0,clearTimeout(a),n(new Error(`${t.type} rejected: ${r.error.code}`))))},this.ws.onerror=t=>{this.log("websocket error"),this.onError(t),o||(o=!0,clearTimeout(a),n(new Error("Signaling WebSocket error")))},this.ws.onclose=e=>{this.log("closed code=%d reason=%s",e.code,e.reason),o||(o=!0,clearTimeout(a),n(new Error(`Signaling WebSocket closed before ${t.type} established`))),this.onClose(e.code,e.reason)}})}send(t){this.ws&&this.ws.readyState===WebSocket.OPEN&&(this.log("send %s",t.type),this.ws.send(JSON.stringify(t)))}close(t=1e3,e="Client closed"){this.ws&&(this.ws.onopen=null,this.ws.onclose=null,this.ws.onerror=null,this.ws.onmessage=null,this.ws.close(t,e),this.ws=null)}get isConnected(){return this.ws?.readyState===WebSocket.OPEN}}},5882(t,e,i){i.r(e),i.d(e,{WebRTCProvider:()=>w}),i(6752);var s=i(7833),n=i.n(s),r=i(2229),o=i(6815),a=i(3564),c=i(6845),h=i(7943),l=i(4623);const d=500,u=new Set(["session_expired","session_inactive","invalid_credential","max_recovery_exceeded","unexpected_first_frame"]),g={session_expired:"Your session has expired.",session_inactive:"The call has ended.",invalid_credential:"Could not resume the call.",max_recovery_exceeded:"Could not resume the call after several attempts.",unexpected_first_frame:"Could not resume the call."},m={invalid_code:"Could not start the call.",used:"A call is already running in this conversation.",session_ended:"This conversation has ended.",attach_unsupported:"Calls are not available in this conversation.",internal_error:"Could not start the call."},p=t=>t instanceof Error?t.message:"Signaling connection failed",f={...o.n,setRemoteDescription:"Connection setup failed",setLocalDescription:"Connection setup failed",addIceCandidate:"Network configuration failed",createOffer:"Browser audio setup failed"};class w{constructor(t,e){this.wsUrl=t,this.fetchConfig=e,this.log=n()("moveo:webrtc:media"),this.pc=null,this.signaling=null,this.localStream=null,this.remoteStream=null,this.remoteAudio=null,this.remoteAudioMonitor=null,this.audioMonitor=null,this.qualityMonitor=null,this.stallDetector=null,this.pendingCandidates=[],this.state="idle",this.eventListeners=new Map,this.iceRestarting=!1,this.iceRestartAttempts=0,this.iceRestartTimeout=null,this.iceRestartAnswerTimeout=null,this.connectResolve=null,this.connectReject=null,this.resuming=!1,this.reconnectAttempts=0,this.reconnectErrorCode=null,this.attaching=!1,this.attachErrorCode=null,this.ended=!1,this.hangupReason=null,this.attachSent=!1,this.attachSignaling=null,this.brainResponseChunks=new Map,this.interruptedRequests=new Map}get serverHangupReason(){return this.hangupReason}get attached(){return this.attachSent}setConfig(t){this.config=t}async connect(t){try{return await this.runConnect(t)}catch(t){if("connecting"===this.state){const t="Could not start the call.";throw this.cleanup(),this.emitError(t),new Error(t)}throw t}}async runConnect(t){if(!this.config)throw new Error("Voice config not set; call setConfig() before connect().");this.ended=!1,this.hangupReason=null,this.attachSent=!1,this.setState("connecting");try{this.localStream=await navigator.mediaDevices.getUserMedia({audio:{echoCancellation:!0,noiseSuppression:!0,autoGainControl:!0}})}catch(t){const e=t instanceof Error?t.name:"",i=f[e]||"Microphone access failed";throw this.emitError(i),new Error(i)}if(this.ended)throw this.cleanupLocalStream(),new Error("Call ended");if(t)this.signaling=await this.openAttachedSignaling(t);else{this.signaling=this.createSignaling();try{await this.signaling.connect()}catch(t){this.failSignaling(p(t))}}this.pc=new RTCPeerConnection({iceServers:this.config.iceServers,iceTransportPolicy:this.config.iceTransportPolicy||"relay"});const e=this.localStream.getAudioTracks()[0];this.pc.addTrack(e,this.localStream),this.pc.ontrack=this.handleRemoteTrack.bind(this),this.pc.onicecandidate=t=>{t.candidate&&this.signaling?.send({type:"ice",candidate:t.candidate.toJSON()})},this.pc.onconnectionstatechange=this.handleConnectionStateChange.bind(this);try{const t=await this.pc.createOffer();if(!t.sdp)throw new Error("createOffer returned no SDP");await this.pc.setLocalDescription(t),this.signaling.send({type:"offer",sdp:t.sdp})}catch(t){this.cleanup();const e=t instanceof Error?t.name:"createOffer",i=f[e]||"Failed to create call offer";throw this.emitError(i),new Error(i)}return this.audioMonitor=new r.h,this.audioMonitor.start(this.localStream,t=>{this.emit("audio-level",t,Number(t))}),new Promise((t,e)=>{this.connectResolve=t,this.connectReject=e})}disconnect(){this.disconnectInternal(!1)}createSignaling(){return new l.g(this.wsUrl,this.config.sessionToken,this.handleSignalingMessage.bind(this),this.handleSignalingClose.bind(this),this.handleSignalingError.bind(this))}async openAttachedSignaling(t){for(let e=1;;e++){this.attachErrorCode=null;let i=null;try{const e=await t.getCode();if(this.ended)throw new Error("Call ended");if(i=this.createSignaling(),this.attachSignaling=i,this.attaching=!0,await i.connectWithFirstFrame({type:"attach",code:e},()=>{this.attachSent=!0}),this.ended)throw new Error("Call ended");return i}catch(t){i?.close();const s=this.attachErrorCode;if(s&&(this.attachSent=!1),"internal_error"===s&&e<2)continue;this.failSignaling(s?m[s]??"Could not start the call.":p(t))}finally{this.attaching=!1,this.attachSignaling=null}}}failSignaling(t){throw this.cleanupLocalStream(),this.emitError(t),new Error(t)}disconnectInternal(t){if(this.ended=!0,this.attachSent||this.attachSignaling?.close(),!t)try{this.signaling?.send({type:"hangup"})}catch{}try{this.qualityMonitor?.stop(),this.qualityMonitor=null}catch{}try{this.stallDetector?.stop(),this.stallDetector=null}catch{}try{this.audioMonitor?.stop(),this.audioMonitor=null}catch{}try{this.remoteAudioMonitor?.stop(),this.remoteAudioMonitor=null}catch{}try{this.pc?.close(),this.pc=null}catch{}this.cleanupLocalStream();try{this.signaling?.close(),this.signaling=null}catch{}try{this.remoteAudio&&(this.remoteAudio.srcObject=null,this.remoteAudio.remove(),this.remoteAudio=null),this.remoteStream=null}catch{}this.clearIceRestart(),this.iceRestartAttempts=0,this.connectReject&&this.connectReject(new Error("Call disconnected before connection completed")),this.connectResolve=null,this.connectReject=null,this.resuming=!1,this.reconnectAttempts=0,this.reconnectErrorCode=null,this.originalSessionID=void 0,this.originalConnectionID=void 0,this.pendingCandidates=[],this.brainResponseChunks.clear(),this.interruptedRequests.clear(),this.setState("idle")}toggleMute(){const t=this.localStream?.getAudioTracks()[0];if(t){t.enabled=!t.enabled;const e=!t.enabled;return this.signaling?.send({type:"mute",muted:e}),e}return!1}isMuted(){return!(this.localStream?.getAudioTracks()[0]?.enabled??1)}getLocalStream(){return this.localStream}getRemoteStream(){return this.remoteStream}sendDigits(t){for(const e of t)this.signaling?.send({type:"dtmf",digit:e})}on(t,e){const i=this.eventListeners.get(t)??new Set;return i.add(e),this.eventListeners.set(t,i),()=>this.off(t,e)}off(t,e){const i=this.eventListeners.get(t);i&&(i.delete(e),0===i.size&&this.eventListeners.delete(t))}handleRemoteTrack(t){const[e]=t.streams;this.remoteStream=e??null,this.remoteAudio=document.createElement("audio"),this.remoteAudio.srcObject=e,this.remoteAudio.autoplay=!0,document.body.appendChild(this.remoteAudio),this.remoteAudio.play().catch(()=>{}),this.remoteAudioMonitor=new r.h,this.remoteAudioMonitor.start(e,t=>{Number(t)>.02&&this.emit("remote-audio",t,Number(t))})}handleConnectionStateChange(){if(this.pc)switch(this.log("connectionState %s",this.pc.connectionState),this.pc.connectionState){case"connecting":this.setState("connecting");break;case"connected":{const t="reconnecting"===this.state;this.setState("connected"),this.clearIceRestart(),this.iceRestartAttempts=0,t&&this.emit("warning-cleared",void 0,"reconnect"),this.qualityMonitor?.stop(),this.qualityMonitor=new a.M(this.pc,(t,e)=>this.emit("warning",e,t),t=>this.emit("warning-cleared",void 0,t)),this.qualityMonitor.start(),this.stallDetector?.stop(),this.stallDetector=new h.$(this.pc,()=>{this.log("media stall -> ICE restart"),this.triggerIceRestart()},()=>{this.log("media recovered"),this.clearIceRestart(),this.iceRestartAttempts=0,"reconnecting"===this.state&&this.setState("connected"),this.emit("warning-cleared",void 0,"reconnect")}),this.stallDetector.start(),this.connectResolve?.(),this.connectResolve=null,this.connectReject=null;break}case"disconnected":if(this.resuming)break;this.triggerIceRestart();break;case"failed":if(this.resuming){this.connectReject?.(new Error("ICE connection failed")),this.connectResolve=null,this.connectReject=null;break}if(this.connectReject){this.connectReject(new Error("ICE connection failed")),this.connectResolve=null,this.connectReject=null,this.emitError("Connection failed"),this.cleanup();break}this.iceRestarting?this.onRestartAttemptExpired():this.triggerIceRestart();break;case"closed":this.cleanup()}}async triggerIceRestart(){if(this.resuming||!this.pc||!this.signaling)return;if(this.iceRestarting||this.iceRestartAttempts>=2)return;if(!this.signaling.isConnected)return this.isResumeEligible()?void this.beginResume(d):(this.emitError("Connection lost"),void this.cleanup());const t=this.pc,e=this.signaling;this.iceRestarting=!0,this.iceRestartAttempts++,this.log("ICE restart attempt %d/%d",this.iceRestartAttempts,2),this.setState("reconnecting"),this.emit("warning","Reconnecting…","reconnect");try{const i=await t.createOffer({iceRestart:!0});if(!i.sdp)throw new Error("createOffer returned no SDP");await t.setLocalDescription(i),e.send({type:"offer",sdp:i.sdp})}catch{if(this.resuming||!this.iceRestarting||this.pc!==t)return;return this.iceRestarting=!1,this.emitError("Connection lost"),void this.cleanup()}this.iceRestarting&&this.pc===t&&(this.iceRestartTimeout=setTimeout(()=>{this.onRestartAttemptExpired()},1e4),this.iceRestartAnswerTimeout=setTimeout(()=>{this.onIceRestartUnanswered()},3e3))}onIceRestartUnanswered(){this.iceRestartAnswerTimeout=null,this.iceRestarting&&this.isResumeEligible()&&(this.log("ICE restart offer unanswered -> resume"),this.beginResume(d))}onRestartAttemptExpired(){this.log("ICE restart attempt expired (attempts=%d)",this.iceRestartAttempts),this.iceRestartTimeout&&(clearTimeout(this.iceRestartTimeout),this.iceRestartTimeout=null),this.iceRestartAnswerTimeout&&(clearTimeout(this.iceRestartAnswerTimeout),this.iceRestartAnswerTimeout=null),this.iceRestarting=!1,this.iceRestartAttempts<2?this.triggerIceRestart():this.isResumeEligible()?this.beginResume(d):(this.emitError("Connection lost"),this.cleanup())}clearIceRestart(){this.iceRestarting&&this.log("ICE restart cleared"),this.iceRestartTimeout&&(clearTimeout(this.iceRestartTimeout),this.iceRestartTimeout=null),this.iceRestartAnswerTimeout&&(clearTimeout(this.iceRestartAnswerTimeout),this.iceRestartAnswerTimeout=null),this.iceRestarting=!1}handleSignalingMessage(t){switch(t.type){case"answer":this.handleAnswer(t.sdp);break;case"ice":this.addIceCandidate(t.candidate);break;case"transcript":this.emit("transcript",t.text,{text:t.text,request_id:t.request_id,timestamp:t.timestamp});break;case"brain_response":this.handleBrainResponse(t);break;case"interrupt":this.handleInterrupt(t);break;case"hangup":this.hangupReason=t.reason??"",this.disconnectInternal(!0);break;case"session_ready":this.originalSessionID=t.session_id,this.originalConnectionID=t.connection_id;break;case"reconnect_ack":this.originalConnectionID=t.connection_id;break;case"drain":this.handleDrain(t);break;case"error":this.attaching?this.attachErrorCode=t.error.code:this.resuming?(this.reconnectErrorCode=t.error.code,this.connectReject?.(new Error(`reconnect rejected: ${t.error.code}`))):this.emitError(t.error.message)}}handleSignalingClose(t,e){this.resuming?this.connectReject?.(new Error("Signaling closed during resume")):this.isResumeEligible()?this.beginResume(d):"idle"!==this.state&&this.disconnect()}handleSignalingError(t){this.resuming||this.isResumeEligible()||"connected"===this.state&&this.emitError("Signaling error")}isResumeEligible(){return!this.resuming&&("connected"===this.state||"reconnecting"===this.state)&&void 0!==this.originalSessionID&&void 0!==this.originalConnectionID}async beginResume(t=0){this.resuming=!0,this.setState("reconnecting"),this.clearIceRestart(),this.emit("warning","Reconnecting…","reconnect"),t>0&&(await this.sleep(Math.random()*t),!this.resuming)||await this.reconnect()}async handleDrain(t){if(!this.resuming)return this.isResumeEligible()?void await this.beginResume(Math.max(0,t.reconnect_delay_ms||0)):(this.emitError("Could not resume the call."),void this.disconnectInternal(!0))}async reconnect(){try{this.pc?.getSenders().forEach(t=>{t.track&&this.pc?.removeTrack(t)})}catch{}const t=this.localStream?.getAudioTracks()[0];if(!t||"live"!==t.readyState)return this.emitError("Microphone is no longer available."),void this.disconnectInternal(!0);for(this.teardownForReconnect();this.reconnectAttempts<3;){this.reconnectErrorCode=null;try{if(this.config=await this.fetchConfig(),!this.resuming)return;return this.signaling=this.createSignaling(),await this.signaling.connectWithFirstFrame({type:"reconnect",session_id:this.originalSessionID,connection_id:this.originalConnectionID}),await this.buildReconnectPeerConnection(t),this.reconnectAttempts=0,this.resuming=!1,void this.emit("warning-cleared",void 0,"reconnect")}catch(t){const e=this.reconnectErrorCode;if(e&&u.has(e))return this.emitError(g[e]??"Could not resume the call."),void this.disconnectInternal(!0);if(t instanceof c.Xq)return this.emitError(t.message),void this.disconnectInternal(!0);const i=t instanceof c.PA?t.message:"Reconnecting…";if(this.emit("warning",i,"reconnect"),this.teardownForReconnect(),this.reconnectAttempts++,this.reconnectAttempts>=3)break;if(await this.sleep(500*this.reconnectAttempts),!this.resuming)return}}this.emitError("Could not resume the call."),this.disconnectInternal(!0)}buildReconnectPeerConnection(t){return this.pc=new RTCPeerConnection({iceServers:this.config.iceServers,iceTransportPolicy:this.config.iceTransportPolicy||"relay"}),this.pc.addTrack(t,this.localStream),this.pc.ontrack=this.handleRemoteTrack.bind(this),this.pc.onicecandidate=t=>{t.candidate&&this.signaling?.send({type:"ice",candidate:t.candidate.toJSON()})},this.pc.onconnectionstatechange=this.handleConnectionStateChange.bind(this),this.audioMonitor=new r.h,this.audioMonitor.start(this.localStream,t=>{this.emit("audio-level",t,Number(t))}),new Promise((t,e)=>{const i=setTimeout(()=>{this.connectResolve=null,this.connectReject=null,e(new Error("Reconnect media resume timed out"))},15e3);this.connectResolve=()=>{clearTimeout(i),t()},this.connectReject=t=>{clearTimeout(i),e(t)},(async()=>{try{const t=await this.pc.createOffer();if(!t.sdp)throw new Error("createOffer returned no SDP");await this.pc.setLocalDescription(t),this.signaling.send({type:"offer",sdp:t.sdp})}catch(t){const e=t instanceof Error?t.name:"createOffer";this.connectReject?.(new Error(f[e]||"Failed to create call offer"))}})()})}teardownForReconnect(){try{this.qualityMonitor?.stop(),this.qualityMonitor=null}catch{}try{this.stallDetector?.stop(),this.stallDetector=null}catch{}try{this.audioMonitor?.stop(),this.audioMonitor=null}catch{}try{this.remoteAudioMonitor?.stop(),this.remoteAudioMonitor=null}catch{}try{this.pc?.close(),this.pc=null}catch{}try{this.signaling?.close(4001,"Resuming"),this.signaling=null}catch{}try{this.remoteAudio&&(this.remoteAudio.srcObject=null,this.remoteAudio.remove(),this.remoteAudio=null),this.remoteStream=null}catch{}this.clearIceRestart(),this.iceRestartAttempts=0,this.connectResolve=null,this.connectReject=null,this.pendingCandidates=[]}sleep(t){return new Promise(e=>setTimeout(e,t))}async handleAnswer(t){if(this.iceRestartAnswerTimeout&&(clearTimeout(this.iceRestartAnswerTimeout),this.iceRestartAnswerTimeout=null),this.pc)try{await this.pc.setRemoteDescription({type:"answer",sdp:t}),await this.flushPendingCandidates()}catch{this.emitError(f.setRemoteDescription)}}async addIceCandidate(t){if(this.pc?.remoteDescription)try{await this.pc.addIceCandidate(t)}catch{}else this.pendingCandidates.push(t)}async flushPendingCandidates(){for(const t of this.pendingCandidates){if(!this.pc)break;try{await this.pc.addIceCandidate(t)}catch{}}this.pendingCandidates=[]}cleanup(){this.disconnect()}cleanupLocalStream(){try{this.localStream?.getTracks().forEach(t=>t.stop()),this.localStream=null}catch{}}setState(t){this.state=t,this.emit("state-change",t,t)}handleBrainResponse(t){const e=t.request_id;if(this.interruptedRequests.has(e))return;if(t.final){const t=this.assembleBrainResponse(e);return void this.emit("brain-response",t,{text:t,request_id:e,final:!0})}let i=this.brainResponseChunks.get(e);i||(i=new Map,this.brainResponseChunks.set(e,i)),i.set(t.index,t.text);const s=this.assembleBrainResponse(e);this.emit("brain-response",s,{text:s,request_id:e,final:!1})}assembleBrainResponse(t){const e=this.brainResponseChunks.get(t);return e?[...e.entries()].sort((t,e)=>t[0]-e[0]).map(([,t])=>t).join(" "):""}handleInterrupt(t){const e=this.interruptedRequests.get(t.request_id),i=void 0===e?t.heard_index:Math.min(e,t.heard_index);this.interruptedRequests.set(t.request_id,i);const s=this.brainResponseChunks.get(t.request_id),n=s?[...s.entries()].sort((t,e)=>t[0]-e[0]):[],r=t=>n.filter(([e])=>t(e)).map(([,t])=>t).join(" "),o=r(t=>t<=i),a=r(t=>t>i);this.emit("interrupt",o,{request_id:t.request_id,heard_index:i,timestamp:t.timestamp,heardText:o,unheardText:a})}emitError(t){this.emit("error",t),this.setState("error")}emit(t,e,i){const s={state:t,message:e,data:i},n=t=>{const e=this.eventListeners.get(t);e&&e.forEach(t=>{try{t(s)}catch{}})};n(t),n("any")}}}}]);
@@ -1,6 +1,6 @@
1
1
  /*!
2
2
  *
3
- * @moveo-ai/web-client v0.123.0
3
+ * @moveo-ai/web-client v0.124.0
4
4
  * Copyright (c) Moveo.ai (TM)
5
5
  *
6
6
  */
@@ -1,6 +1,6 @@
1
1
  /*!
2
2
  *
3
- * @moveo-ai/web-client v0.123.0
3
+ * @moveo-ai/web-client v0.124.0
4
4
  * Copyright (c) Moveo.ai (TM)
5
5
  *
6
6
  */