@1interface/voice-core 0.5.4 → 0.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("react"),t=require("react-redux"),r=require("@reduxjs/toolkit"),n=require("@1interface/shared-core"),s={DISCONNECTED:"disconnected",CONNECTING:"connecting",CONNECTED:"connected"},o={IDLE:"idle",LISTENING:"listening",PROCESSING:"processing",SPEAKING:"speaking"},i={CASCADE:"cascade",REALTIME:"realtime"},a={OFFLINE:"offline",CONNECTING:"connecting",READY:"ready",IDLE:"idle",LISTENING:"listening",MUTED:"muted",PROCESSING:"processing",SPEAKING:"speaking"},c=[/rejected/i,/unauthori[sz]ed/i,/forbidden/i,/\b40[13]\b/],u=[/\b402\b/,/\b429\b/,/rate[\s-]?limit/i],l={SELECTED_MIC_ID:"voice_selectedMicId",SELECTED_SPEAKER_ID:"voice_selectedSpeakerId"};function d(){return{connectionStatus:s.DISCONNECTED,pipelineStatus:o.IDLE,listeningEnabled:!1,muted:!1,serverMuted:!1,statusText:null,isAgentWorking:!1,responseStartTime:null,interimTranscript:null,botTurn:{active:!1,chatMsgId:null,segments:[]},sessionEndedSignal:0,selectedMicId:"",selectedSpeakerId:"",microphones:[],speakers:[]}}const g=d(),p=r.createSlice({name:"voice",initialState:g,reducers:{setConnectionStatus(e,t){e.connectionStatus=t.payload},setPipelineStatus(e,t){e.pipelineStatus=t.payload},setListeningEnabled(e,t){e.listeningEnabled=t.payload},setStatusText(e,t){e.statusText=t.payload},setIsAgentWorking(e,t){e.isAgentWorking=t.payload},setResponseStartTime(e,t){e.responseStartTime=t.payload},setInterimTranscript(e,t){e.interimTranscript=t.payload},openBotTurn(e,t){e.botTurn={active:!0,chatMsgId:t.payload?.chatMsgId??null,segments:[]}},upsertBotSegment(e,t){const{id:r,text:n,spokenChars:s}=t.payload;e.botTurn.active=!0;const o=e.botTurn.segments.find(e=>e.id===r);o?(o.text=n,o.spokenChars=Math.max(o.spokenChars,s)):(e.botTurn.segments.push({id:r,text:n,spokenChars:s}),e.botTurn.segments.sort((e,t)=>e.id-t.id))},clearBotTurn(e){e.botTurn={active:!1,chatMsgId:null,segments:[]}},setMuted(e,t){e.muted=t.payload},setServerMuted(e,t){e.serverMuted=t.payload},setSelectedMicId(e,t){e.selectedMicId=t.payload},setSelectedSpeakerId(e,t){e.selectedSpeakerId=t.payload},setMicrophones(e,t){e.microphones=t.payload},setSpeakers(e,t){e.speakers=t.payload},signalSessionEnded(e){e.sessionEndedSignal+=1},resetVoiceState:()=>d(),resetIndicatorState(e){e.statusText=null,e.isAgentWorking=!1,e.responseStartTime=null,e.interimTranscript=null,e.serverMuted=!1,e.botTurn={active:!1,chatMsgId:null,segments:[]}}}}),{setConnectionStatus:m,setPipelineStatus:v,setListeningEnabled:I,setMuted:S,setServerMuted:E,setStatusText:_,setIsAgentWorking:T,setResponseStartTime:f,setInterimTranscript:C,openBotTurn:b,upsertBotSegment:h,clearBotTurn:k,setSelectedMicId:R,setSelectedSpeakerId:V,setMicrophones:x,setSpeakers:y,signalSessionEnded:M,resetVoiceState:N,resetIndicatorState:w}=p.actions,A=p.reducer,D=e.createContext(null),O=()=>{const t=e.useContext(D);if(!t)throw new Error("useAudioPlatform must be used inside <AudioPlatformProvider>. <VoiceProvider> mounts one by default; if you replaced VoiceProvider, you must wire your own.");return t};function P(){const e={};for(const[t,r]of Object.entries(n.getCustomHeaders()))r&&(e[t]=r);const t=n.readAccessToken();t&&(e[n.AUTH_HEADERS.ACCESS_TOKEN]=t);const r=n.readDeviceToken();return r&&(e[n.DEVICE_TOKEN_HEADER]=r),e}async function L(e){try{const t=await fetch(n.API_ENDPOINTS.WEBRTC_ICE(e),{headers:P()});return t.ok?(await t.json()).iceServers??[]:(n.logger.warn({message:"Voice ICE fetch failed — continuing without TURN",api_name:"voice-webrtc-ice",status:t.status}),[])}catch(e){return n.logger.warn({message:"Voice ICE fetch error — continuing without TURN",api_name:"voice-webrtc-ice",error_message:e instanceof Error?e.message:String(e)}),[]}}const G="voice-user-",U=(e,t=`${G}${n.generateUniqueId()}`)=>({id:t,sender:"user",message:e,timestamp:(new Date).toISOString()}),B=(e,t)=>{const r=`${e}${n.generateUniqueId()}`;return{id:r,msg_id:r,sender:"agent",message:"",timestamp:(new Date).toISOString(),...t}};function q(){const e=t.useSelector(e=>e.voice.connectionStatus),r=t.useSelector(e=>e.voice.pipelineStatus),n=t.useSelector(e=>e.voice.listeningEnabled),i=t.useSelector(e=>e.voice.muted);return e===s.CONNECTING?a.CONNECTING:e!==s.CONNECTED?a.OFFLINE:n?i?a.MUTED:r===o.PROCESSING?a.PROCESSING:r===o.SPEAKING?a.SPEAKING:r===o.LISTENING?a.LISTENING:a.IDLE:a.READY}const F=e.createContext(null),K=e.createContext({strategyActive:!1,voiceSlot:null,onEnterVoice:null,onFloatingOrbClick:null,voiceInputHidden:!1,headerCenterSlot:null,floatOrbPositionRef:null}),W=r.createListenerMiddleware();W.startListening({actionCreator:R,effect:async e=>{const t=n.getStorageRef();t&&await t.setItem(l.SELECTED_MIC_ID,e.payload)}}),W.startListening({actionCreator:V,effect:async e=>{const t=n.getStorageRef();t&&await t.setItem(l.SELECTED_SPEAKER_ID,e.payload)}}),W.startListening({matcher:r.isAnyOf(n.clearSession,n.sessionExpired),effect:async(e,t)=>{const r=n.getStorageRef();r&&await Promise.all([r.removeItem(l.SELECTED_MIC_ID),r.removeItem(l.SELECTED_SPEAKER_ID)]),t.dispatch(N())}});const j=e=>{const t=document.createElement("audio");return t.autoplay=!0,t.style.display="none",t.srcObject=new MediaStream([e]),document.body.appendChild(t),()=>{try{t.pause(),t.srcObject=null,t.remove()}catch{}}},H={attachBotAudioTrack:j,requestMicPermission:async function(){try{const e=await(navigator.permissions?.query({name:"microphone"}).catch(()=>null));return n.logger.debug({message:"Voice mic permission probe",api_name:"voice-mic",permission_state:e?.state??"unavailable"}),"granted"===e?.state||(await navigator.mediaDevices.getUserMedia({audio:!0})).getTracks().forEach(e=>e.stop()),{granted:!0}}catch(e){return n.logger.warn({message:"Voice mic permission denied",api_name:"voice-mic",error_message:e instanceof Error?e.message:String(e)}),{granted:!1,error:e}}}};exports.AudioPlatformContext=D,exports.ORB_STATE_CONFIG={offline:{opacity:.15,playbackRate:.3,scale:1},connecting:{opacity:.4,playbackRate:.5,scale:1},ready:{opacity:.4,playbackRate:.4,scale:1},idle:{opacity:.6,playbackRate:.6,scale:1},listening:{opacity:.7,playbackRate:.7,scale:1},muted:{opacity:.7,playbackRate:.5,scale:1},processing:{opacity:1,playbackRate:1,scale:1},speaking:{opacity:1,playbackRate:1,scale:1.3}},exports.VOICE_CONNECTION_STATUSES=s,exports.VOICE_CONNECT_TIMEOUT_MS=15e3,exports.VOICE_DISPLAY_STATES=a,exports.VOICE_PIPELINE_MODES=i,exports.VOICE_PIPELINE_STATUSES=o,exports.VOICE_STORAGE_KEYS=l,exports.VoiceContext=F,exports.VoiceUIContext=K,exports.attachBrowserBotAudioTrack=j,exports.browserAudioPlatform=H,exports.clearBotTurn=k,exports.hydrateVoice=e=>async t=>{const[r,n]=await Promise.all([e.getItem(l.SELECTED_MIC_ID),e.getItem(l.SELECTED_SPEAKER_ID)]);r&&t(p.actions.setSelectedMicId(r)),n&&t(p.actions.setSelectedSpeakerId(n))},exports.openBotTurn=b,exports.resetIndicatorState=w,exports.setConnectionStatus=m,exports.setInterimTranscript=C,exports.setIsAgentWorking=T,exports.setListeningEnabled=I,exports.setMuted=S,exports.setPipelineStatus=v,exports.setResponseStartTime=f,exports.setSelectedMicId=R,exports.setSelectedSpeakerId=V,exports.setServerMuted=E,exports.setStatusText=_,exports.upsertBotSegment=h,exports.useAudioPlatform=O,exports.useMediaDevices=function(){return{microphones:t.useSelector(e=>e.voice.microphones),speakers:t.useSelector(e=>e.voice.speakers),selectedMicId:t.useSelector(e=>e.voice.selectedMicId),selectedSpeakerId:t.useSelector(e=>e.voice.selectedSpeakerId)}},exports.useVoice=()=>{const t=e.useContext(F);if(!t)throw new Error("useVoice must be used within VoiceProvider");return t},exports.useVoiceDisplayState=q,exports.useVoiceProviderValue=({conversationId:r,voiceBridge:a,transportFactory:l,assistantStartsConversation:d})=>{const g=t.useDispatch(),p=O(),N=t.useSelector(e=>e.voice.muted),w=t.useSelector(e=>e.voice.serverMuted),A=t.useSelector(e=>e.voice.pipelineStatus),D=t.useSelector(e=>e.voice.isAgentWorking),F=t.useSelector(e=>e.voice.sessionEndedSignal),{connect:K,disconnect:W,sendText:j,setMicEnabled:H,setInputDevice:$,setOutputDevice:Y}=function(r,a,l,d){const g=t.useDispatch(),p=t.useStore(),N=O(),w=t.useSelector(e=>e.auth.accessToken),A=e.useRef(a);e.useEffect(()=>{A.current=a});const D=e.useRef(null),q=e.useRef(new Map),F=e.useRef(""),K=e.useRef(!1),W=e.useRef(!1),j=e.useRef(null),H=e.useRef(""),$=e.useRef(null),Y=e.useRef(null),Z=e.useRef(null),z=e.useRef(!0),J=e.useRef(null),Q=e.useRef(!1),X=e.useRef(""),ee=e.useRef(0),te=e.useRef(0),re=e.useRef(!1),ne=e.useRef(0),se=e.useRef(null),oe=e.useRef(null),ie=e.useRef(0),ae=e.useRef(0),ce=e.useRef(null),ue=e.useRef(null),le=e.useCallback(()=>{if(null!=se.current)return;const e=Date.now();se.current=e,g(f(e)),g(n.clearLastResponseTime())},[g]),de=e.useCallback(()=>{const e=se.current;se.current=null,g(f(null)),null!=e&&g(n.setLastResponseTime(Date.now()-e))},[g]),ge=e.useCallback(()=>{se.current=null,g(f(null))},[g]),pe=e.useCallback((e=n.INDICATOR_TIMER_ACTIONS.KEEP)=>{g(_(null)),g(T(!1)),e===n.INDICATOR_TIMER_ACTIONS.FINALIZE?de():e===n.INDICATOR_TIMER_ACTIONS.CANCEL&&ge()},[g,de,ge]),[me]=e.useState(()=>({setStatusText:e=>g(_(e||null)),setIsAgentWorking:e=>g(T(e)),appendStreamText:()=>{},pushWidget:e=>{const t=B("voice-widget-",{widget:[e],message_type:["widget"]});A.current.setMessages(e=>[...e,t])},onPayment:e=>A.current.onPaymentInit?.(e),onConversationTitle:e=>A.current.onConversationTitle?.(e),onError:e=>{n.logger.error({message:"Voice RTVI server error",api_name:"voice-rtvi",error_message:e});const t=B("voice-error-",{message:"Something went wrong. Please try again.",message_type:["error"]});A.current.setMessages(e=>[...e,t])},onStatusActive:()=>{le()}})),ve=e.useCallback(()=>{Z.current&&(clearTimeout(Z.current),Z.current=null)},[]),Ie=e.useCallback(()=>[...q.current.entries()].sort((e,t)=>e[0]-t[0]).map(([,e])=>e.text).join(" ").trim(),[]),Se=e.useCallback(()=>{D.current=null,q.current=new Map,F.current="",g(k())},[g]),Ee=e.useCallback(()=>{const e=D.current;if(e){const t=Ie();t&&A.current.updateStreamingMessage(e,t),A.current.commitStreamingMessages()}Se()},[Ie,Se]),_e=e.useCallback(e=>{ve(),J.current&&(clearTimeout(J.current),J.current=null),Q.current=!1,X.current="",g(x([])),g(y([])),Y.current?.(),Y.current=null,$.current=null,re.current=!1,g(I(!1)),g(v(o.IDLE)),g(m(s.DISCONNECTED)),g(C(null)),g(E(!1)),j.current=null,H.current="",K.current=!1,W.current=!1,e?Ee():(A.current.discardStreamingMessages(),Se()),pe(n.INDICATOR_TIMER_ACTIONS.CANCEL)},[g,pe,ve,Ee,Se]),Te=e.useCallback(()=>{if(D.current)return;const e=`voice-agent-${n.generateUniqueId()}`;D.current=e,q.current=new Map,F.current="",g(b({chatMsgId:e}))},[g]),fe=e.useCallback(e=>{const t=e.spoken_progress?(e.spoken_progress?.accumulated_text??"")+(e.spoken_progress?.remaining_text??""):e.text??"";if(!t)return;Te();const r=K.current?function(e,t){const r=(e.get(0)?.text??"")+t;return e.set(0,{text:r,completed:!1}),{id:0,text:r,spokenChars:r.length}}(q.current,t):function(e,t,r,n){const s=t.segment_id??0,o="completed"===t.spoken_status;if(o&&!e.has(s)){const t=[...e.entries()].find(([,e])=>e.text===r&&!e.completed);if(t)return t[1].completed=!0,{id:t[0],text:r,spokenChars:r.length}}const i=e.get(s);return e.set(s,{text:r,completed:o||!0===i?.completed}),{id:s,text:r,spokenChars:n&&t.spoken_progress?(t.spoken_progress.accumulated_text??"").length:r.length}}(q.current,e,t,W.current);g(h(r));const n=Ie();n&&D.current&&n!==F.current&&(F.current=n,A.current.updateStreamingMessage(D.current,n))},[g,Te,Ie]),Ce=e.useCallback(e=>{const t=e,s=t?.type,o=t?.data;switch(n.logger.debug({message:"Voice RTVI server-message received",api_name:"voice-rtvi",conversation_id:r??void 0,event_type:s??"unknown"}),s){case"status_update":n.handleSharedEvent({kind:"status_update",statusText:o?.text??""},me);break;case"started-thinking":g(T(!0)),le();break;case"stopped-thinking":pe(n.INDICATOR_TIMER_ACTIONS.FINALIZE);break;case"clear_status":break;case"widget":o&&n.handleSharedEvent({kind:"widget",widget:o},me);break;case"payment":o&&n.handleSharedEvent({kind:"payment",payment:o},me);break;case"conversation_title":{const e="string"==typeof o?.text?o.text.trim():"";e&&n.handleSharedEvent({kind:"conversation_title",title:e},me);break}case"session-ended":ue.current={reason:"string"==typeof o?.reason?o.reason:"ended"};break;default:n.logger.debug({message:"Voice RTVI unhandled server-message",api_name:"voice-rtvi",event_type:s??"unknown"})}},[me,g,le,pe,r]),be=e.useRef(()=>{}),he=e.useRef(async()=>{}),ke=e.useCallback(e=>{const t=()=>e===ee.current;return{onConnected:()=>{if(!t())return;n.logger.info({message:"Voice RTVI connected",api_name:"voice-rtvi",conversation_id:r??void 0}),g(m(s.CONNECTED)),g(I(!0)),g(v(o.IDLE)),$.current&&$.current.isMicEnabled!==z.current&&(n.logger.debug({message:"Voice re-asserting mic state on reconnect",api_name:"voice-rtvi",conversation_id:r??void 0,enabled:z.current}),$.current.enableMic(z.current));const{selectedMicId:e,selectedSpeakerId:i}=p.getState().voice;e&&$.current?.updateMic(e),i&&$.current?.updateSpeaker(i);const a=e=>t=>n.logger.warn({message:"Voice device enumeration failed",api_name:"voice-mic",conversation_id:r??void 0,device_kind:e,error_message:t instanceof Error?t.message:String(t)});$.current?.getAllMics().then(e=>{t()&&g(x(e.map(e=>({deviceId:e.deviceId,label:e.label}))))}).catch(a("mics")),$.current?.getAllSpeakers().then(e=>{t()&&g(y(e.map(e=>({deviceId:e.deviceId,label:e.label}))))}).catch(a("speakers")),Q.current=!0},onBotReady:e=>{if(!t())return;const s=e?.about;K.current=s?.pipeline_mode===i.REALTIME,W.current=!0===s?.tts_word_timestamps,ie.current=0,ae.current=0,ne.current=0,n.logger.info({message:"Voice RTVI bot-ready",api_name:"voice-rtvi",conversation_id:r??void 0,pipeline_mode:s?.pipeline_mode??"cascade",tts_word_timestamps:W.current,assistant_starts:d}),ve(),d&&(g(T(!0)),le())},onDisconnected:()=>{if(!t())return;n.logger.info({message:"Voice RTVI disconnected",api_name:"voice-rtvi",conversation_id:r??void 0,drop_outcome:ce.current??"transient",reconnect_attempts:ie.current}),_e(!0);const e=te.current,s=ce.current;ce.current=null;const o=ue.current;return ue.current=null,o?(n.logger.info({message:"Voice session ended by server — no reconnect",api_name:"voice-rtvi",conversation_id:r??void 0,reason:o.reason}),"idle_timeout"===o.reason&&n.showToast({title:"Voice ended",description:"It got quiet for a while, so we wrapped up. Start again anytime."}),void g(M())):"terminal"===s?(n.logger.warn({message:"Voice RTVI terminal error — no retry",api_name:"voice-rtvi"}),void g(M())):"auth"===s?ae.current>=1||null==r?(n.logger.warn({message:"Voice RTVI auth refresh budget exhausted — giving up",api_name:"voice-rtvi",conversation_id:r??void 0}),void g(M())):(ae.current+=1,n.logger.info({message:"Voice RTVI auth drop — refreshing token then reconnecting",api_name:"voice-rtvi",conversation_id:r??void 0,auth_refresh_count:ae.current}),void(async()=>{let t=!1;try{t=!!await(n.getTokenRefreshHandler()?.())}catch{}e===te.current&&(t?he.current():(n.logger.warn({message:"Voice RTVI token refresh failed — no reconnect",api_name:"voice-rtvi",conversation_id:r??void 0}),g(M())))})()):ie.current>=3||null==r?(ie.current>=3&&(n.logger.warn({message:"Voice RTVI auto-reconnect cap reached — giving up",api_name:"voice-rtvi",conversation_id:r??void 0,reconnect_attempts:ie.current}),n.showErrorToast("Voice connection lost. Please try again.")),void g(M())):(ie.current+=1,n.logger.info({message:"Voice RTVI scheduling auto-reconnect",api_name:"voice-rtvi",conversation_id:r??void 0,reconnect_attempts:ie.current,delay_ms:1e3}),oe.current&&clearTimeout(oe.current),void(oe.current=setTimeout(()=>{oe.current=null,he.current()},1e3)))},onError:e=>{if(!t())return;const s=e?.data,i=s?.error??s?.message??"Voice error";n.handleSharedEvent({kind:"error",error:i},me),g(v(o.IDLE)),pe(n.INDICATOR_TIMER_ACTIONS.CANCEL),s?.fatal&&(c.some(e=>e.test(i))?ce.current="auth":u.some(e=>e.test(i))&&(ce.current="terminal"),n.logger.warn({message:"Voice RTVI fatal error — closing transport",api_name:"voice-rtvi",conversation_id:r??void 0,drop_outcome:ce.current??"generic"}),$.current?.disconnect().catch(()=>{}))},onTrackStarted:(e,s)=>{t()&&(s?.local||"audio"!==e.kind||(n.logger.debug({message:"Voice RTVI bot audio track started",api_name:"voice-rtvi",conversation_id:r??void 0}),Y.current?.(),Y.current=N.attachBotAudioTrack(e)))},onAvailableMicsUpdated:e=>{t()&&(g(x(e.map(e=>({deviceId:e.deviceId,label:e.label})))),Q.current&&(J.current&&clearTimeout(J.current),J.current=setTimeout(()=>{if(J.current=null,!$.current)return;const{selectedMicId:e,microphones:t}=p.getState().voice;n.logger.info({message:"Voice audio devices changed",api_name:"voice-mic",microphones:t.map(e=>e.label).join(", ")}),n.logger.debug({message:"Voice re-acquiring mic (device change)",api_name:"voice-rtvi",enabled:z.current,device:e||"default"}),$.current.updateMic(e)},300)))},onAvailableSpeakersUpdated:e=>{if(!t())return;if(g(y(e.map(e=>({deviceId:e.deviceId,label:e.label})))),!Q.current)return;const r=e.map(e=>e.label).join(", ");r!==X.current&&(X.current=r,n.logger.info({message:"Voice audio output devices changed",api_name:"voice-mic",speakers:r}))},onMicUpdated:e=>{if(!t())return;const s=$.current;s&&s.isMicEnabled!==z.current&&(n.logger.debug({message:"Voice re-asserting mic state after track swap",api_name:"voice-rtvi",conversation_id:r??void 0,enabled:z.current,device:e?.label}),s.enableMic(z.current))},onUserMuteStarted:()=>{t()&&n.logger.debug({message:"Voice RTVI user-mute-started (ignored)",api_name:"voice-rtvi",conversation_id:r??void 0})},onUserMuteStopped:()=>{t()&&(n.logger.debug({message:"Voice RTVI user-mute-stopped",api_name:"voice-rtvi",conversation_id:r??void 0,honored:d}),d&&g(E(!1)))},onUserStartedSpeaking:()=>{t()&&(n.logger.debug({message:"Voice RTVI user started speaking",api_name:"voice-rtvi",conversation_id:r??void 0}),j.current=null,H.current="",g(C(null)),g(v(o.LISTENING)))},onUserStoppedSpeaking:()=>{if(!t())return;n.logger.debug({message:"Voice RTVI user stopped speaking",api_name:"voice-rtvi",conversation_id:r??void 0,has_interim:!!H.current.trim()}),g(v(o.PROCESSING)),g(T(!0)),g(_(null)),le();const e=H.current.trim();if(e){const t=`${G}${n.generateUniqueId()}`;j.current=t,g(C(null)),A.current.setMessages(r=>[...r,U(e,t)])}},onUserTranscript:e=>{if(!t())return;if(!e.final)return void(e.text&&(H.current=e.text,K.current||g(C(e.text))));g(C(null));const s=e.text&&e.text.trim()?e.text:"";n.logger.debug({message:"Voice RTVI user transcript (final)",api_name:"voice-rtvi",conversation_id:r??void 0,has_text:!!s});const o=j.current;j.current=null,o?s&&A.current.setMessages(e=>e.map(e=>e.id===o?{...e,message:s}:e)):s&&A.current.setMessages(e=>[...e,U(s)])},onBotStartedSpeaking:()=>{t()&&(n.logger.debug({message:"Voice RTVI bot started speaking",api_name:"voice-rtvi",conversation_id:r??void 0}),Te(),g(v(o.SPEAKING)),pe(n.INDICATOR_TIMER_ACTIONS.FINALIZE))},onBotOutput:e=>{t()&&fe(e)},onBotStoppedSpeaking:()=>{t()&&(n.logger.debug({message:"Voice RTVI bot stopped speaking",api_name:"voice-rtvi",conversation_id:r??void 0}),Ee(),g(v(o.IDLE)),pe(n.INDICATOR_TIMER_ACTIONS.FINALIZE),d&&p.getState().voice.serverMuted&&(n.logger.warn({message:"Voice greeting mute-hold released on bot-turn end (server unmute missing)",api_name:"voice-rtvi",conversation_id:r??void 0}),g(E(!1))))},onServerMessage:e=>{t()&&Ce(e)}}},[g,p,N,ve,_e,pe,le,fe,Te,Ee,Ce,me,d,r]),Re=e.useCallback(async()=>{if($.current||re.current)return;if(!r)return;if(re.current=!0,n.logger.info({message:"Voice RTVI connecting",api_name:"voice-rtvi",conversation_id:r,assistant_starts:d}),d&&(z.current=!1,g(E(!0)),n.logger.debug({message:"Voice greeting mute-hold armed",api_name:"voice-rtvi",conversation_id:r})),n.isTokenAboutToExpire(n.readTokenExpiresAt())&&ne.current<1){ne.current+=1,n.logger.debug({message:"Voice pre-connect token refresh (freshness gate)",api_name:"voice-rtvi",conversation_id:r,freshness_refresh_count:ne.current});const e=te.current;try{await Promise.resolve(n.getTokenRefreshHandler()?.())}catch{}if(e!==te.current)return}const e=++ee.current,t=te.current;ue.current=null,ce.current=null,g(m(s.CONNECTING));const o=()=>t!==te.current||e!==ee.current;try{const[{PipecatClient:t,LogLevel:s},i]=await Promise.all([Promise.resolve().then(()=>require("./index.module-DCaqEXL5.cjs")),L(r)]);if(o())return;const a=new t({transport:l(),enableMic:!0,enableCam:!1,callbacks:ke(e)});if(a.setLogLevel(s.WARN),o())return void a.disconnect().catch(()=>{});$.current=a,n.logger.debug({message:"Voice RTVI client constructed",api_name:"voice-rtvi",conversation_id:r,enable_mic:z.current}),ve(),Z.current=setTimeout(()=>{n.logger.warn({message:"Voice RTVI connect timed out (no bot-ready)",api_name:"voice-rtvi"}),n.showErrorToast("Voice connection timed out. Please try again."),g(M()),be.current()},15e3),await a.connect({webrtcRequestParams:{endpoint:n.API_ENDPOINTS.WEBRTC_OFFER(r),headers:new Headers(P())},iceConfig:{iceServers:i}})}catch(t){n.logger.error({message:"Voice RTVI connect failed",api_name:"voice-rtvi",error_message:t instanceof Error?t.message:String(t)}),e===ee.current&&(n.showErrorToast("Could not connect voice. Please try again."),_e(!1))}},[r,g,l,ke,ve,_e,d]),Ve=e.useCallback((e=!0)=>{n.logger.info({message:"Voice RTVI disconnect (intentional teardown)",api_name:"voice-rtvi",conversation_id:r??void 0,commit_turn:e}),te.current+=1,ee.current+=1,ne.current=0,oe.current&&(clearTimeout(oe.current),oe.current=null),ie.current=0,ae.current=0,ce.current=null,z.current=!0,g(S(!1));const t=$.current;t?.disconnect().catch(e=>{n.logger.warn({message:"Voice RTVI disconnect threw",api_name:"voice-rtvi",error_message:e instanceof Error?e.message:String(e)})}),_e(e)},[_e,g,r]),xe=e.useCallback(e=>{z.current=e,$.current&&(n.logger.debug({message:"Voice mic enable toggled",api_name:"voice-rtvi",enabled:e}),$.current.enableMic(e))},[]),ye=e.useCallback(e=>{g(R(e)),$.current?.updateMic(e)},[g]),Me=e.useCallback(e=>{g(V(e)),$.current?.updateSpeaker(e)},[g]),Ne=e.useCallback(e=>{const t=$.current;if(t){n.logger.info({message:"Voice RTVI send-text",api_name:"voice-rtvi",conversation_id:r??void 0,run_immediately:!0});try{t.sendText(e,{run_immediately:!0,audio_response:!0})}catch(e){return void n.logger.error({message:"Voice RTVI send-text failed",api_name:"voice-rtvi",error_message:e instanceof Error?e.message:String(e)})}D.current&&Ee(),A.current.setMessages(t=>[...t,U(e)]),g(T(!0)),g(_(null)),le(),g(v(o.PROCESSING))}},[g,le,Ee,r]);e.useEffect(()=>{be.current=Ve},[Ve]),e.useEffect(()=>{he.current=Re},[Re]);const we=e.useRef(r),Ae=e.useRef(!1);e.useEffect(()=>{const e=we.current;if(we.current=r,e===r)return;const t=re.current;t&&(Ae.current=!0),t&&Ve(!1),Ae.current&&null!=r&&(Ae.current=!1,Re())},[r,Ve,Re]);const De=e.useRef(w);return e.useEffect(()=>{const e=De.current;De.current=w,e&&!w&&re.current&&Ve(!1)},[w,Ve]),e.useEffect(()=>()=>{be.current()},[]),{connect:Re,disconnect:Ve,sendText:Ne,setMicEnabled:xe,setInputDevice:ye,setOutputDevice:Me}}(r,a,l,d);e.useEffect(()=>{H(!N&&!w)},[N,w,H]);const Z=q();e.useEffect(()=>(a.setVoiceSendText(j),()=>a.setVoiceSendText(null)),[a,j]);const z=e.useRef(!1);return{displayState:Z,pipelineStatus:A,isAgentWorking:D,enterVoiceMode:e.useCallback(async()=>{if(z.current)return n.showErrorToast("Microphone permission changed — please reload the page to apply the new setting."),!1;const e=await p.requestMicPermission();if(!e.granted){const t=e.error;return n.logger.error({message:"Mic permission denied",api_name:"voice-mic",error_message:t instanceof Error?t.message:t?String(t):void 0}),z.current=!0,n.showErrorToast("Microphone access is required for voice mode. Please allow it in your browser's site settings and reload the page."),!1}return K(),!0},[p,K]),exitVoiceMode:e.useCallback(()=>{W()},[W]),toggleMute:e.useCallback(()=>{g(S(!N))},[g,N]),sendText:j,muted:N,serverMuted:w,sessionEndedSignal:F,setInputDevice:$,setOutputDevice:Y}},exports.useVoiceUI=()=>e.useContext(K),exports.voiceListenerMiddleware=W,exports.voiceReducer=A;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("react"),t=require("react-redux"),r=require("@reduxjs/toolkit"),n=require("@1interface/shared-core"),s={DISCONNECTED:"disconnected",CONNECTING:"connecting",CONNECTED:"connected"},o={IDLE:"idle",LISTENING:"listening",PROCESSING:"processing",SPEAKING:"speaking"},i={CASCADE:"cascade",REALTIME:"realtime"},a={OFFLINE:"offline",CONNECTING:"connecting",READY:"ready",IDLE:"idle",LISTENING:"listening",MUTED:"muted",PROCESSING:"processing",SPEAKING:"speaking"},c=[/rejected/i,/unauthori[sz]ed/i,/forbidden/i,/\b40[13]\b/],u=[/\b402\b/,/\b429\b/,/rate[\s-]?limit/i],l={SELECTED_MIC_ID:"voice_selectedMicId",SELECTED_SPEAKER_ID:"voice_selectedSpeakerId"};function d(){return{connectionStatus:s.DISCONNECTED,pipelineStatus:o.IDLE,listeningEnabled:!1,muted:!1,serverMuted:!1,statusText:null,isAgentWorking:!1,responseStartTime:null,interimTranscript:null,botTurn:{active:!1,chatMsgId:null,segments:[]},sessionEndedSignal:0,selectedMicId:"",selectedSpeakerId:"",microphones:[],speakers:[]}}const g=d(),p=r.createSlice({name:"voice",initialState:g,reducers:{setConnectionStatus(e,t){e.connectionStatus=t.payload},setPipelineStatus(e,t){e.pipelineStatus=t.payload},setListeningEnabled(e,t){e.listeningEnabled=t.payload},setStatusText(e,t){e.statusText=t.payload},setIsAgentWorking(e,t){e.isAgentWorking=t.payload},setResponseStartTime(e,t){e.responseStartTime=t.payload},setInterimTranscript(e,t){e.interimTranscript=t.payload},openBotTurn(e,t){e.botTurn={active:!0,chatMsgId:t.payload?.chatMsgId??null,segments:[]}},upsertBotSegment(e,t){const{id:r,text:n,spokenChars:s}=t.payload;e.botTurn.active=!0;const o=e.botTurn.segments.find(e=>e.id===r);o?(o.text=n,o.spokenChars=Math.max(o.spokenChars,s)):(e.botTurn.segments.push({id:r,text:n,spokenChars:s}),e.botTurn.segments.sort((e,t)=>e.id-t.id))},clearBotTurn(e){e.botTurn={active:!1,chatMsgId:null,segments:[]}},setMuted(e,t){e.muted=t.payload},setServerMuted(e,t){e.serverMuted=t.payload},setSelectedMicId(e,t){e.selectedMicId=t.payload},setSelectedSpeakerId(e,t){e.selectedSpeakerId=t.payload},setMicrophones(e,t){e.microphones=t.payload},setSpeakers(e,t){e.speakers=t.payload},signalSessionEnded(e){e.sessionEndedSignal+=1},resetVoiceState:()=>d(),resetIndicatorState(e){e.statusText=null,e.isAgentWorking=!1,e.responseStartTime=null,e.interimTranscript=null,e.serverMuted=!1,e.botTurn={active:!1,chatMsgId:null,segments:[]}}}}),{setConnectionStatus:m,setPipelineStatus:v,setListeningEnabled:I,setMuted:S,setServerMuted:E,setStatusText:_,setIsAgentWorking:T,setResponseStartTime:f,setInterimTranscript:C,openBotTurn:b,upsertBotSegment:h,clearBotTurn:k,setSelectedMicId:R,setSelectedSpeakerId:V,setMicrophones:y,setSpeakers:x,signalSessionEnded:M,resetVoiceState:N,resetIndicatorState:w}=p.actions,A=p.reducer,D=e.createContext(null),O=()=>{const t=e.useContext(D);if(!t)throw new Error("useAudioPlatform must be used inside <AudioPlatformProvider>. <VoiceProvider> mounts one by default; if you replaced VoiceProvider, you must wire your own.");return t};function P(){const e={};for(const[t,r]of Object.entries(n.getCustomHeaders()))r&&(e[t]=r);const t=n.readAccessToken();t&&(e[n.AUTH_HEADERS.ACCESS_TOKEN]=t);const r=n.readDeviceToken();return r&&(e[n.DEVICE_TOKEN_HEADER]=r),e}async function L(e){try{const t=await fetch(n.API_ENDPOINTS.WEBRTC_ICE(e),{headers:P()});return t.ok?(await t.json()).iceServers??[]:(n.logger.warn({message:"Voice ICE fetch failed — continuing without TURN",api_name:"voice-webrtc-ice",status:t.status}),[])}catch(e){return n.logger.warn({message:"Voice ICE fetch error — continuing without TURN",api_name:"voice-webrtc-ice",error_message:e instanceof Error?e.message:String(e)}),[]}}const G="voice-user-",U="Could not connect voice. Please try again later.",B=(e,t=`${G}${n.generateUniqueId()}`)=>({id:t,sender:"user",message:e,timestamp:(new Date).toISOString()}),q=(e,t)=>{const r=`${e}${n.generateUniqueId()}`;return{id:r,msg_id:r,sender:"agent",message:"",timestamp:(new Date).toISOString(),...t}};function j(){const e=t.useSelector(e=>e.voice.connectionStatus),r=t.useSelector(e=>e.voice.pipelineStatus),n=t.useSelector(e=>e.voice.listeningEnabled),i=t.useSelector(e=>e.voice.muted);return e===s.CONNECTING?a.CONNECTING:e!==s.CONNECTED?a.OFFLINE:n?i?a.MUTED:r===o.PROCESSING?a.PROCESSING:r===o.SPEAKING?a.SPEAKING:r===o.LISTENING?a.LISTENING:a.IDLE:a.READY}const F=e.createContext(null),K=e.createContext({strategyActive:!1,voiceSlot:null,onEnterVoice:null,onFloatingOrbClick:null,voiceInputHidden:!1,headerCenterSlot:null,floatOrbPositionRef:null}),W=r.createListenerMiddleware();W.startListening({actionCreator:R,effect:async e=>{const t=n.getStorageRef();t&&await t.setItem(l.SELECTED_MIC_ID,e.payload)}}),W.startListening({actionCreator:V,effect:async e=>{const t=n.getStorageRef();t&&await t.setItem(l.SELECTED_SPEAKER_ID,e.payload)}}),W.startListening({matcher:r.isAnyOf(n.clearSession,n.sessionExpired),effect:async(e,t)=>{const r=n.getStorageRef();r&&await Promise.all([r.removeItem(l.SELECTED_MIC_ID),r.removeItem(l.SELECTED_SPEAKER_ID)]),t.dispatch(N())}});const H=e=>{const t=document.createElement("audio");return t.autoplay=!0,t.style.display="none",t.srcObject=new MediaStream([e]),document.body.appendChild(t),()=>{try{t.pause(),t.srcObject=null,t.remove()}catch{}}},$={attachBotAudioTrack:H,requestMicPermission:async function(){try{const e=await(navigator.permissions?.query({name:"microphone"}).catch(()=>null));return n.logger.debug({message:"Voice mic permission probe",api_name:"voice-mic",permission_state:e?.state??"unavailable"}),"granted"===e?.state||(await navigator.mediaDevices.getUserMedia({audio:!0})).getTracks().forEach(e=>e.stop()),{granted:!0}}catch(e){return n.logger.warn({message:"Voice mic permission denied",api_name:"voice-mic",error_message:e instanceof Error?e.message:String(e)}),{granted:!1,error:e}}}};exports.AudioPlatformContext=D,exports.ORB_STATE_CONFIG={offline:{opacity:.15,playbackRate:.3,scale:1},connecting:{opacity:.4,playbackRate:.5,scale:1},ready:{opacity:.4,playbackRate:.4,scale:1},idle:{opacity:.6,playbackRate:.6,scale:1},listening:{opacity:.7,playbackRate:.7,scale:1},muted:{opacity:.7,playbackRate:.5,scale:1},processing:{opacity:1,playbackRate:1,scale:1},speaking:{opacity:1,playbackRate:1,scale:1.3}},exports.VOICE_CONNECTION_STATUSES=s,exports.VOICE_CONNECT_TIMEOUT_MS=15e3,exports.VOICE_DISPLAY_STATES=a,exports.VOICE_PIPELINE_MODES=i,exports.VOICE_PIPELINE_STATUSES=o,exports.VOICE_STORAGE_KEYS=l,exports.VoiceContext=F,exports.VoiceUIContext=K,exports.attachBrowserBotAudioTrack=H,exports.browserAudioPlatform=$,exports.clearBotTurn=k,exports.hydrateVoice=e=>async t=>{const[r,n]=await Promise.all([e.getItem(l.SELECTED_MIC_ID),e.getItem(l.SELECTED_SPEAKER_ID)]);r&&t(p.actions.setSelectedMicId(r)),n&&t(p.actions.setSelectedSpeakerId(n))},exports.openBotTurn=b,exports.resetIndicatorState=w,exports.setConnectionStatus=m,exports.setInterimTranscript=C,exports.setIsAgentWorking=T,exports.setListeningEnabled=I,exports.setMuted=S,exports.setPipelineStatus=v,exports.setResponseStartTime=f,exports.setSelectedMicId=R,exports.setSelectedSpeakerId=V,exports.setServerMuted=E,exports.setStatusText=_,exports.upsertBotSegment=h,exports.useAudioPlatform=O,exports.useMediaDevices=function(){return{microphones:t.useSelector(e=>e.voice.microphones),speakers:t.useSelector(e=>e.voice.speakers),selectedMicId:t.useSelector(e=>e.voice.selectedMicId),selectedSpeakerId:t.useSelector(e=>e.voice.selectedSpeakerId)}},exports.useVoice=()=>{const t=e.useContext(F);if(!t)throw new Error("useVoice must be used within VoiceProvider");return t},exports.useVoiceDisplayState=j,exports.useVoiceProviderValue=({conversationId:r,voiceBridge:a,transportFactory:l,assistantStartsConversation:d})=>{const g=t.useDispatch(),p=O(),N=t.useSelector(e=>e.voice.muted),w=t.useSelector(e=>e.voice.serverMuted),A=t.useSelector(e=>e.voice.pipelineStatus),D=t.useSelector(e=>e.voice.isAgentWorking),F=t.useSelector(e=>e.voice.sessionEndedSignal),{connect:K,disconnect:W,sendText:H,setMicEnabled:$,setInputDevice:Y,setOutputDevice:Z}=function(r,a,l,d){const g=t.useDispatch(),p=t.useStore(),N=O(),w=t.useSelector(e=>e.auth.accessToken),A=e.useRef(a);e.useEffect(()=>{A.current=a});const D=e.useRef(null),j=e.useRef(new Map),F=e.useRef(""),K=e.useRef(!1),W=e.useRef(!1),H=e.useRef(null),$=e.useRef(""),Y=e.useRef(null),Z=e.useRef(null),z=e.useRef(null),J=e.useRef(!0),Q=e.useRef(null),X=e.useRef(!1),ee=e.useRef(""),te=e.useRef(0),re=e.useRef(0),ne=e.useRef(!1),se=e.useRef(0),oe=e.useRef(null),ie=e.useRef(null),ae=e.useRef(0),ce=e.useRef(0),ue=e.useRef(null),le=e.useRef(null),de=e.useCallback(()=>{if(null!=oe.current)return;const e=Date.now();oe.current=e,g(f(e)),g(n.clearLastResponseTime())},[g]),ge=e.useCallback(()=>{const e=oe.current;oe.current=null,g(f(null)),null!=e&&g(n.setLastResponseTime(Date.now()-e))},[g]),pe=e.useCallback(()=>{oe.current=null,g(f(null))},[g]),me=e.useCallback((e=n.INDICATOR_TIMER_ACTIONS.KEEP)=>{g(_(null)),g(T(!1)),e===n.INDICATOR_TIMER_ACTIONS.FINALIZE?ge():e===n.INDICATOR_TIMER_ACTIONS.CANCEL&&pe()},[g,ge,pe]),[ve]=e.useState(()=>({setStatusText:e=>g(_(e||null)),setIsAgentWorking:e=>g(T(e)),appendStreamText:()=>{},pushWidget:e=>{const t=q("voice-widget-",{widget:[e],message_type:["widget"]});A.current.setMessages(e=>[...e,t])},onPayment:e=>A.current.onPaymentInit?.(e),onConversationTitle:e=>A.current.onConversationTitle?.(e),onError:e=>{n.logger.error({message:"Voice RTVI server error",api_name:"voice-rtvi",error_message:e});const t=q("voice-error-",{message:"Something went wrong. Please try again.",message_type:["error"]});A.current.setMessages(e=>[...e,t])},onStatusActive:()=>{de()}})),Ie=e.useCallback(()=>{z.current&&(clearTimeout(z.current),z.current=null)},[]),Se=e.useCallback(()=>[...j.current.entries()].sort((e,t)=>e[0]-t[0]).map(([,e])=>e.text).join(" ").trim(),[]),Ee=e.useCallback(()=>{D.current=null,j.current=new Map,F.current="",g(k())},[g]),_e=e.useCallback(()=>{const e=D.current;if(e){const t=Se();t&&A.current.updateStreamingMessage(e,t),A.current.commitStreamingMessages()}Ee()},[Se,Ee]),Te=e.useCallback(e=>{Ie(),Q.current&&(clearTimeout(Q.current),Q.current=null),X.current=!1,ee.current="",g(y([])),g(x([])),Z.current?.(),Z.current=null,Y.current=null,ne.current=!1,g(I(!1)),g(v(o.IDLE)),g(m(s.DISCONNECTED)),g(C(null)),g(E(!1)),H.current=null,$.current="",K.current=!1,W.current=!1,e?_e():(A.current.discardStreamingMessages(),Ee()),me(n.INDICATOR_TIMER_ACTIONS.CANCEL)},[g,me,Ie,_e,Ee]),fe=e.useCallback(()=>{if(D.current)return;const e=`voice-agent-${n.generateUniqueId()}`;D.current=e,j.current=new Map,F.current="",g(b({chatMsgId:e}))},[g]),Ce=e.useCallback(e=>{const t=e.spoken_progress?(e.spoken_progress?.accumulated_text??"")+(e.spoken_progress?.remaining_text??""):e.text??"";if(!t)return;fe();const r=K.current?function(e,t){const r=(e.get(0)?.text??"")+t;return e.set(0,{text:r,completed:!1}),{id:0,text:r,spokenChars:r.length}}(j.current,t):function(e,t,r,n){const s=t.segment_id??0,o="completed"===t.spoken_status;if(o&&!e.has(s)){const t=[...e.entries()].find(([,e])=>e.text===r&&!e.completed);if(t)return t[1].completed=!0,{id:t[0],text:r,spokenChars:r.length}}const i=e.get(s);return e.set(s,{text:r,completed:o||!0===i?.completed}),{id:s,text:r,spokenChars:n&&t.spoken_progress?(t.spoken_progress.accumulated_text??"").length:r.length}}(j.current,e,t,W.current);g(h(r));const n=Se();n&&D.current&&n!==F.current&&(F.current=n,A.current.updateStreamingMessage(D.current,n))},[g,fe,Se]),be=e.useCallback(e=>{const t=e,s=t?.type,o=t?.data;switch(n.logger.debug({message:"Voice RTVI server-message received",api_name:"voice-rtvi",conversation_id:r??void 0,event_type:s??"unknown"}),s){case"status_update":n.handleSharedEvent({kind:"status_update",statusText:o?.text??""},ve);break;case"started-thinking":g(T(!0)),de();break;case"stopped-thinking":me(n.INDICATOR_TIMER_ACTIONS.FINALIZE);break;case"clear_status":break;case"widget":o&&n.handleSharedEvent({kind:"widget",widget:o},ve);break;case"payment":o&&n.handleSharedEvent({kind:"payment",payment:o},ve);break;case"conversation_title":{const e="string"==typeof o?.text?o.text.trim():"";e&&n.handleSharedEvent({kind:"conversation_title",title:e},ve);break}case"session-ended":le.current={reason:"string"==typeof o?.reason?o.reason:"ended"};break;default:n.logger.debug({message:"Voice RTVI unhandled server-message",api_name:"voice-rtvi",event_type:s??"unknown"})}},[ve,g,de,me,r]),he=e.useRef(()=>{}),ke=e.useRef(async()=>{}),Re=e.useCallback(e=>{const t=()=>e===te.current;return{onConnected:()=>{if(!t())return;n.logger.info({message:"Voice RTVI connected",api_name:"voice-rtvi",conversation_id:r??void 0}),g(m(s.CONNECTED)),g(I(!0)),g(v(o.IDLE)),Y.current&&Y.current.isMicEnabled!==J.current&&(n.logger.debug({message:"Voice re-asserting mic state on reconnect",api_name:"voice-rtvi",conversation_id:r??void 0,enabled:J.current}),Y.current.enableMic(J.current));const{selectedMicId:e,selectedSpeakerId:i}=p.getState().voice;e&&Y.current?.updateMic(e),i&&Y.current?.updateSpeaker(i);const a=e=>t=>n.logger.warn({message:"Voice device enumeration failed",api_name:"voice-mic",conversation_id:r??void 0,device_kind:e,error_message:t instanceof Error?t.message:String(t)});Y.current?.getAllMics().then(e=>{t()&&g(y(e.map(e=>({deviceId:e.deviceId,label:e.label}))))}).catch(a("mics")),Y.current?.getAllSpeakers().then(e=>{t()&&g(x(e.map(e=>({deviceId:e.deviceId,label:e.label}))))}).catch(a("speakers")),X.current=!0},onBotReady:e=>{if(!t())return;const s=e?.about;K.current=s?.pipeline_mode===i.REALTIME,W.current=!0===s?.tts_word_timestamps,ae.current=0,ce.current=0,se.current=0,n.logger.info({message:"Voice RTVI bot-ready",api_name:"voice-rtvi",conversation_id:r??void 0,pipeline_mode:s?.pipeline_mode??"cascade",tts_word_timestamps:W.current,assistant_starts:d}),Ie(),d&&(g(T(!0)),de())},onDisconnected:()=>{if(!t())return;n.logger.info({message:"Voice RTVI disconnected",api_name:"voice-rtvi",conversation_id:r??void 0,drop_outcome:ue.current??"transient",reconnect_attempts:ae.current}),Te(!0);const e=re.current,s=ue.current;ue.current=null;const o=le.current;return le.current=null,o?(n.logger.info({message:"Voice session ended by server — no reconnect",api_name:"voice-rtvi",conversation_id:r??void 0,reason:o.reason}),"idle_timeout"===o.reason&&n.showToast({title:"Voice ended",description:"It got quiet for a while, so we wrapped up. Start again anytime."}),void g(M())):"terminal"===s?(n.logger.warn({message:"Voice RTVI terminal error — no retry",api_name:"voice-rtvi"}),void g(M())):"auth"===s?ce.current>=1||null==r?(n.logger.warn({message:"Voice RTVI auth refresh budget exhausted — giving up",api_name:"voice-rtvi",conversation_id:r??void 0}),void g(M())):(ce.current+=1,n.logger.info({message:"Voice RTVI auth drop — refreshing token then reconnecting",api_name:"voice-rtvi",conversation_id:r??void 0,auth_refresh_count:ce.current}),void(async()=>{let t=!1;try{t=!!await(n.getTokenRefreshHandler()?.())}catch{}e===re.current&&(t?ke.current():(n.logger.warn({message:"Voice RTVI token refresh failed — no reconnect",api_name:"voice-rtvi",conversation_id:r??void 0}),g(M())))})()):ae.current>=3||null==r?(ae.current>=3&&(n.logger.warn({message:"Voice RTVI auto-reconnect cap reached — giving up",api_name:"voice-rtvi",conversation_id:r??void 0,reconnect_attempts:ae.current}),n.showErrorToast("Voice connection lost. Please try again.")),void g(M())):(ae.current+=1,n.logger.info({message:"Voice RTVI scheduling auto-reconnect",api_name:"voice-rtvi",conversation_id:r??void 0,reconnect_attempts:ae.current,delay_ms:1e3}),ie.current&&clearTimeout(ie.current),void(ie.current=setTimeout(()=>{ie.current=null,ke.current()},1e3)))},onError:e=>{if(!t())return;const s=e?.data,i=s?.error??s?.message??"Voice error";n.handleSharedEvent({kind:"error",error:i},ve),g(v(o.IDLE)),me(n.INDICATOR_TIMER_ACTIONS.CANCEL),s?.fatal&&(c.some(e=>e.test(i))?ue.current="auth":u.some(e=>e.test(i))&&(ue.current="terminal"),n.logger.warn({message:"Voice RTVI fatal error — closing transport",api_name:"voice-rtvi",conversation_id:r??void 0,drop_outcome:ue.current??"generic"}),Y.current?.disconnect().catch(()=>{}))},onTrackStarted:(e,s)=>{t()&&(s?.local||"audio"!==e.kind||(n.logger.debug({message:"Voice RTVI bot audio track started",api_name:"voice-rtvi",conversation_id:r??void 0}),Z.current?.(),Z.current=N.attachBotAudioTrack(e)))},onAvailableMicsUpdated:e=>{t()&&(g(y(e.map(e=>({deviceId:e.deviceId,label:e.label})))),X.current&&(Q.current&&clearTimeout(Q.current),Q.current=setTimeout(()=>{if(Q.current=null,!Y.current)return;const{selectedMicId:e,microphones:t}=p.getState().voice;n.logger.info({message:"Voice audio devices changed",api_name:"voice-mic",microphones:t.map(e=>e.label).join(", ")}),n.logger.debug({message:"Voice re-acquiring mic (device change)",api_name:"voice-rtvi",enabled:J.current,device:e||"default"}),Y.current.updateMic(e)},300)))},onAvailableSpeakersUpdated:e=>{if(!t())return;if(g(x(e.map(e=>({deviceId:e.deviceId,label:e.label})))),!X.current)return;const r=e.map(e=>e.label).join(", ");r!==ee.current&&(ee.current=r,n.logger.info({message:"Voice audio output devices changed",api_name:"voice-mic",speakers:r}))},onMicUpdated:e=>{if(!t())return;const s=Y.current;s&&s.isMicEnabled!==J.current&&(n.logger.debug({message:"Voice re-asserting mic state after track swap",api_name:"voice-rtvi",conversation_id:r??void 0,enabled:J.current,device:e?.label}),s.enableMic(J.current))},onUserMuteStarted:()=>{t()&&n.logger.debug({message:"Voice RTVI user-mute-started (ignored)",api_name:"voice-rtvi",conversation_id:r??void 0})},onUserMuteStopped:()=>{t()&&(n.logger.debug({message:"Voice RTVI user-mute-stopped",api_name:"voice-rtvi",conversation_id:r??void 0,honored:d}),d&&g(E(!1)))},onUserStartedSpeaking:()=>{t()&&(n.logger.debug({message:"Voice RTVI user started speaking",api_name:"voice-rtvi",conversation_id:r??void 0}),H.current=null,$.current="",g(C(null)),g(v(o.LISTENING)))},onUserStoppedSpeaking:()=>{if(!t())return;n.logger.debug({message:"Voice RTVI user stopped speaking",api_name:"voice-rtvi",conversation_id:r??void 0,has_interim:!!$.current.trim()}),g(v(o.PROCESSING)),g(T(!0)),g(_(null)),de();const e=$.current.trim();if(e){const t=`${G}${n.generateUniqueId()}`;H.current=t,g(C(null)),A.current.setMessages(r=>[...r,B(e,t)])}},onUserTranscript:e=>{if(!t())return;if(!e.final)return void(e.text&&($.current=e.text,K.current||g(C(e.text))));g(C(null));const s=e.text&&e.text.trim()?e.text:"";n.logger.debug({message:"Voice RTVI user transcript (final)",api_name:"voice-rtvi",conversation_id:r??void 0,has_text:!!s});const o=H.current;H.current=null,o?s&&A.current.setMessages(e=>e.map(e=>e.id===o?{...e,message:s}:e)):s&&A.current.setMessages(e=>[...e,B(s)])},onBotStartedSpeaking:()=>{t()&&(n.logger.debug({message:"Voice RTVI bot started speaking",api_name:"voice-rtvi",conversation_id:r??void 0}),fe(),g(v(o.SPEAKING)),me(n.INDICATOR_TIMER_ACTIONS.FINALIZE))},onBotOutput:e=>{t()&&Ce(e)},onBotStoppedSpeaking:()=>{t()&&(n.logger.debug({message:"Voice RTVI bot stopped speaking",api_name:"voice-rtvi",conversation_id:r??void 0}),_e(),g(v(o.IDLE)),me(n.INDICATOR_TIMER_ACTIONS.FINALIZE),d&&p.getState().voice.serverMuted&&(n.logger.warn({message:"Voice greeting mute-hold released on bot-turn end (server unmute missing)",api_name:"voice-rtvi",conversation_id:r??void 0}),g(E(!1))))},onServerMessage:e=>{t()&&be(e)}}},[g,p,N,Ie,Te,me,de,Ce,fe,_e,be,ve,d,r]),Ve=e.useCallback(async()=>{if(Y.current||ne.current)return;if(!r)return;if(ne.current=!0,n.logger.info({message:"Voice RTVI connecting",api_name:"voice-rtvi",conversation_id:r,assistant_starts:d}),d&&(J.current=!1,g(E(!0)),n.logger.debug({message:"Voice greeting mute-hold armed",api_name:"voice-rtvi",conversation_id:r})),n.isTokenAboutToExpire(n.readTokenExpiresAt())&&se.current<1){se.current+=1,n.logger.debug({message:"Voice pre-connect token refresh (freshness gate)",api_name:"voice-rtvi",conversation_id:r,freshness_refresh_count:se.current});const e=re.current;try{await Promise.resolve(n.getTokenRefreshHandler()?.())}catch{}if(e!==re.current)return}const e=++te.current,t=re.current;le.current=null,ue.current=null,g(m(s.CONNECTING));const o=()=>t!==re.current||e!==te.current;try{const[{PipecatClient:t,LogLevel:s},i]=await Promise.all([Promise.resolve().then(()=>require("./index.module-DCaqEXL5.cjs")),L(r)]);if(o())return;const a=new t({transport:l(),enableMic:!0,enableCam:!1,callbacks:Re(e)});if(a.setLogLevel(s.WARN),o())return void a.disconnect().catch(()=>{});Y.current=a,n.logger.debug({message:"Voice RTVI client constructed",api_name:"voice-rtvi",conversation_id:r,enable_mic:J.current}),Ie(),z.current=setTimeout(()=>{n.logger.warn({message:"Voice RTVI connect timed out (no bot-ready)",api_name:"voice-rtvi"}),n.showErrorToast("Voice connection timed out. Please try again."),g(M()),he.current()},15e3),await a.connect({webrtcRequestParams:{endpoint:n.API_ENDPOINTS.WEBRTC_OFFER(r),headers:new Headers(P())},iceConfig:{iceServers:i}})}catch(t){const r=(e=>{const t="object"==typeof e&&null!==e&&"status"in e?e.status:void 0;return"number"==typeof t?t:void 0})(t);n.logger.error({message:"Voice RTVI connect failed",api_name:"voice-rtvi",status:r,error_message:t instanceof Error?t.message:String(t)});const s=(e=>void 0!==e&&e>=400&&e<500)(r),o=s?await(async(e,t)=>{const r=429===t?"Voice session limit reached. Please try again later.":U,n="object"==typeof e&&null!==e&&"cause"in e?e.cause:void 0;if(!(n instanceof Response))return r;try{const e=await n.json(),t=e?.data?.error_message??e?.data?.message;return"string"==typeof t?t:r}catch{return r}})(t,r):U;e===te.current&&(n.showErrorToast(o),s&&(ie.current&&(clearTimeout(ie.current),ie.current=null),ae.current=0,g(M())),Te(!1))}},[r,g,l,Re,Ie,Te,d]),ye=e.useCallback((e=!0)=>{n.logger.info({message:"Voice RTVI disconnect (intentional teardown)",api_name:"voice-rtvi",conversation_id:r??void 0,commit_turn:e}),re.current+=1,te.current+=1,se.current=0,ie.current&&(clearTimeout(ie.current),ie.current=null),ae.current=0,ce.current=0,ue.current=null,J.current=!0,g(S(!1));const t=Y.current;t?.disconnect().catch(e=>{n.logger.warn({message:"Voice RTVI disconnect threw",api_name:"voice-rtvi",error_message:e instanceof Error?e.message:String(e)})}),Te(e)},[Te,g,r]),xe=e.useCallback(e=>{J.current=e,Y.current&&(n.logger.debug({message:"Voice mic enable toggled",api_name:"voice-rtvi",enabled:e}),Y.current.enableMic(e))},[]),Me=e.useCallback(e=>{g(R(e)),Y.current?.updateMic(e)},[g]),Ne=e.useCallback(e=>{g(V(e)),Y.current?.updateSpeaker(e)},[g]),we=e.useCallback(e=>{const t=Y.current;if(t){n.logger.info({message:"Voice RTVI send-text",api_name:"voice-rtvi",conversation_id:r??void 0,run_immediately:!0});try{t.sendText(e,{run_immediately:!0,audio_response:!0})}catch(e){return void n.logger.error({message:"Voice RTVI send-text failed",api_name:"voice-rtvi",error_message:e instanceof Error?e.message:String(e)})}D.current&&_e(),A.current.setMessages(t=>[...t,B(e)]),g(T(!0)),g(_(null)),de(),g(v(o.PROCESSING))}},[g,de,_e,r]);e.useEffect(()=>{he.current=ye},[ye]),e.useEffect(()=>{ke.current=Ve},[Ve]);const Ae=e.useRef(r),De=e.useRef(!1);e.useEffect(()=>{const e=Ae.current;if(Ae.current=r,e===r)return;const t=ne.current;t&&(De.current=!0),t&&ye(!1),De.current&&null!=r&&(De.current=!1,Ve())},[r,ye,Ve]);const Oe=e.useRef(w);return e.useEffect(()=>{const e=Oe.current;Oe.current=w,e&&!w&&ne.current&&ye(!1)},[w,ye]),e.useEffect(()=>()=>{he.current()},[]),{connect:Ve,disconnect:ye,sendText:we,setMicEnabled:xe,setInputDevice:Me,setOutputDevice:Ne}}(r,a,l,d);e.useEffect(()=>{$(!N&&!w)},[N,w,$]);const z=j();e.useEffect(()=>(a.setVoiceSendText(H),()=>a.setVoiceSendText(null)),[a,H]);const J=e.useRef(!1);return{displayState:z,pipelineStatus:A,isAgentWorking:D,enterVoiceMode:e.useCallback(async()=>{if(J.current)return n.showErrorToast("Microphone permission changed — please reload the page to apply the new setting."),!1;const e=await p.requestMicPermission();if(!e.granted){const t=e.error;return n.logger.error({message:"Mic permission denied",api_name:"voice-mic",error_message:t instanceof Error?t.message:t?String(t):void 0}),J.current=!0,n.showErrorToast("Microphone access is required for voice mode. Please allow it in your browser's site settings and reload the page."),!1}return K(),!0},[p,K]),exitVoiceMode:e.useCallback(()=>{W()},[W]),toggleMute:e.useCallback(()=>{g(S(!N))},[g,N]),sendText:H,muted:N,serverMuted:w,sessionEndedSignal:F,setInputDevice:Y,setOutputDevice:Z}},exports.useVoiceUI=()=>e.useContext(K),exports.voiceListenerMiddleware=W,exports.voiceReducer=A;
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{createContext as e,useContext as t,useRef as n,useEffect as r,useCallback as i,useState as s}from"react";import{useDispatch as c,useStore as a,useSelector as o}from"react-redux";import{createSlice as u,createListenerMiddleware as d,isAnyOf as l}from"@reduxjs/toolkit";import{API_ENDPOINTS as m,logger as p,getCustomHeaders as g,readAccessToken as v,AUTH_HEADERS as _,readDeviceToken as I,DEVICE_TOKEN_HEADER as S,clearLastResponseTime as E,setLastResponseTime as T,INDICATOR_TIMER_ACTIONS as f,generateUniqueId as b,handleSharedEvent as h,showToast as k,getTokenRefreshHandler as y,showErrorToast as V,isTokenAboutToExpire as C,readTokenExpiresAt as M,getStorageRef as w,clearSession as R,sessionExpired as N}from"@1interface/shared-core";const D={DISCONNECTED:"disconnected",CONNECTING:"connecting",CONNECTED:"connected"},x={IDLE:"idle",LISTENING:"listening",PROCESSING:"processing",SPEAKING:"speaking"},A={CASCADE:"cascade",REALTIME:"realtime"},L={OFFLINE:"offline",CONNECTING:"connecting",READY:"ready",IDLE:"idle",LISTENING:"listening",MUTED:"muted",PROCESSING:"processing",SPEAKING:"speaking"},P=15e3,O=[/rejected/i,/unauthori[sz]ed/i,/forbidden/i,/\b40[13]\b/],G=[/\b402\b/,/\b429\b/,/rate[\s-]?limit/i],B={SELECTED_MIC_ID:"voice_selectedMicId",SELECTED_SPEAKER_ID:"voice_selectedSpeakerId"},U={offline:{opacity:.15,playbackRate:.3,scale:1},connecting:{opacity:.4,playbackRate:.5,scale:1},ready:{opacity:.4,playbackRate:.4,scale:1},idle:{opacity:.6,playbackRate:.6,scale:1},listening:{opacity:.7,playbackRate:.7,scale:1},muted:{opacity:.7,playbackRate:.5,scale:1},processing:{opacity:1,playbackRate:1,scale:1},speaking:{opacity:1,playbackRate:1,scale:1.3}};function F(){return{connectionStatus:D.DISCONNECTED,pipelineStatus:x.IDLE,listeningEnabled:!1,muted:!1,serverMuted:!1,statusText:null,isAgentWorking:!1,responseStartTime:null,interimTranscript:null,botTurn:{active:!1,chatMsgId:null,segments:[]},sessionEndedSignal:0,selectedMicId:"",selectedSpeakerId:"",microphones:[],speakers:[]}}const W=e=>async t=>{const[n,r]=await Promise.all([e.getItem(B.SELECTED_MIC_ID),e.getItem(B.SELECTED_SPEAKER_ID)]);n&&t(K.actions.setSelectedMicId(n)),r&&t(K.actions.setSelectedSpeakerId(r))},K=u({name:"voice",initialState:F(),reducers:{setConnectionStatus(e,t){e.connectionStatus=t.payload},setPipelineStatus(e,t){e.pipelineStatus=t.payload},setListeningEnabled(e,t){e.listeningEnabled=t.payload},setStatusText(e,t){e.statusText=t.payload},setIsAgentWorking(e,t){e.isAgentWorking=t.payload},setResponseStartTime(e,t){e.responseStartTime=t.payload},setInterimTranscript(e,t){e.interimTranscript=t.payload},openBotTurn(e,t){e.botTurn={active:!0,chatMsgId:t.payload?.chatMsgId??null,segments:[]}},upsertBotSegment(e,t){const{id:n,text:r,spokenChars:i}=t.payload;e.botTurn.active=!0;const s=e.botTurn.segments.find(e=>e.id===n);s?(s.text=r,s.spokenChars=Math.max(s.spokenChars,i)):(e.botTurn.segments.push({id:n,text:r,spokenChars:i}),e.botTurn.segments.sort((e,t)=>e.id-t.id))},clearBotTurn(e){e.botTurn={active:!1,chatMsgId:null,segments:[]}},setMuted(e,t){e.muted=t.payload},setServerMuted(e,t){e.serverMuted=t.payload},setSelectedMicId(e,t){e.selectedMicId=t.payload},setSelectedSpeakerId(e,t){e.selectedSpeakerId=t.payload},setMicrophones(e,t){e.microphones=t.payload},setSpeakers(e,t){e.speakers=t.payload},signalSessionEnded(e){e.sessionEndedSignal+=1},resetVoiceState:()=>F(),resetIndicatorState(e){e.statusText=null,e.isAgentWorking=!1,e.responseStartTime=null,e.interimTranscript=null,e.serverMuted=!1,e.botTurn={active:!1,chatMsgId:null,segments:[]}}}}),{setConnectionStatus:j,setPipelineStatus:q,setListeningEnabled:$,setMuted:Z,setServerMuted:H,setStatusText:Y,setIsAgentWorking:z,setResponseStartTime:J,setInterimTranscript:Q,openBotTurn:X,upsertBotSegment:ee,clearBotTurn:te,setSelectedMicId:ne,setSelectedSpeakerId:re,setMicrophones:ie,setSpeakers:se,signalSessionEnded:ce,resetVoiceState:ae,resetIndicatorState:oe}=K.actions,ue=K.reducer,de=e(null),le=()=>{const e=t(de);if(!e)throw new Error("useAudioPlatform must be used inside <AudioPlatformProvider>. <VoiceProvider> mounts one by default; if you replaced VoiceProvider, you must wire your own.");return e};function me(){const e={};for(const[t,n]of Object.entries(g()))n&&(e[t]=n);const t=v();t&&(e[_.ACCESS_TOKEN]=t);const n=I();return n&&(e[S]=n),e}async function pe(e){try{const t=await fetch(m.WEBRTC_ICE(e),{headers:me()});return t.ok?(await t.json()).iceServers??[]:(p.warn({message:"Voice ICE fetch failed — continuing without TURN",api_name:"voice-webrtc-ice",status:t.status}),[])}catch(e){return p.warn({message:"Voice ICE fetch error — continuing without TURN",api_name:"voice-webrtc-ice",error_message:e instanceof Error?e.message:String(e)}),[]}}const ge="voice-user-",ve=(e,t=`${ge}${b()}`)=>({id:t,sender:"user",message:e,timestamp:(new Date).toISOString()}),_e=(e,t)=>{const n=`${e}${b()}`;return{id:n,msg_id:n,sender:"agent",message:"",timestamp:(new Date).toISOString(),...t}};function Ie(){const e=o(e=>e.voice.connectionStatus),t=o(e=>e.voice.pipelineStatus),n=o(e=>e.voice.listeningEnabled),r=o(e=>e.voice.muted);return e===D.CONNECTING?L.CONNECTING:e!==D.CONNECTED?L.OFFLINE:n?r?L.MUTED:t===x.PROCESSING?L.PROCESSING:t===x.SPEAKING?L.SPEAKING:t===x.LISTENING?L.LISTENING:L.IDLE:L.READY}const Se=e(null),Ee=({conversationId:e,voiceBridge:t,transportFactory:u,assistantStartsConversation:d})=>{const l=c(),g=le(),v=o(e=>e.voice.muted),_=o(e=>e.voice.serverMuted),I=o(e=>e.voice.pipelineStatus),S=o(e=>e.voice.isAgentWorking),w=o(e=>e.voice.sessionEndedSignal),{connect:R,disconnect:N,sendText:L,setMicEnabled:P,setInputDevice:B,setOutputDevice:U}=function(e,t,u,d){const l=c(),g=a(),v=le(),_=o(e=>e.auth.accessToken),I=n(t);r(()=>{I.current=t});const S=n(null),w=n(new Map),R=n(""),N=n(!1),L=n(!1),P=n(null),B=n(""),U=n(null),F=n(null),W=n(null),K=n(!0),ae=n(null),oe=n(!1),ue=n(""),de=n(0),Ie=n(0),Se=n(!1),Ee=n(0),Te=n(null),fe=n(null),be=n(0),he=n(0),ke=n(null),ye=n(null),Ve=i(()=>{if(null!=Te.current)return;const e=Date.now();Te.current=e,l(J(e)),l(E())},[l]),Ce=i(()=>{const e=Te.current;Te.current=null,l(J(null)),null!=e&&l(T(Date.now()-e))},[l]),Me=i(()=>{Te.current=null,l(J(null))},[l]),we=i((e=f.KEEP)=>{l(Y(null)),l(z(!1)),e===f.FINALIZE?Ce():e===f.CANCEL&&Me()},[l,Ce,Me]),[Re]=s(()=>({setStatusText:e=>l(Y(e||null)),setIsAgentWorking:e=>l(z(e)),appendStreamText:()=>{},pushWidget:e=>{const t=_e("voice-widget-",{widget:[e],message_type:["widget"]});I.current.setMessages(e=>[...e,t])},onPayment:e=>I.current.onPaymentInit?.(e),onConversationTitle:e=>I.current.onConversationTitle?.(e),onError:e=>{p.error({message:"Voice RTVI server error",api_name:"voice-rtvi",error_message:e});const t=_e("voice-error-",{message:"Something went wrong. Please try again.",message_type:["error"]});I.current.setMessages(e=>[...e,t])},onStatusActive:()=>{Ve()}})),Ne=i(()=>{W.current&&(clearTimeout(W.current),W.current=null)},[]),De=i(()=>[...w.current.entries()].sort((e,t)=>e[0]-t[0]).map(([,e])=>e.text).join(" ").trim(),[]),xe=i(()=>{S.current=null,w.current=new Map,R.current="",l(te())},[l]),Ae=i(()=>{const e=S.current;if(e){const t=De();t&&I.current.updateStreamingMessage(e,t),I.current.commitStreamingMessages()}xe()},[De,xe]),Le=i(e=>{Ne(),ae.current&&(clearTimeout(ae.current),ae.current=null),oe.current=!1,ue.current="",l(ie([])),l(se([])),F.current?.(),F.current=null,U.current=null,Se.current=!1,l($(!1)),l(q(x.IDLE)),l(j(D.DISCONNECTED)),l(Q(null)),l(H(!1)),P.current=null,B.current="",N.current=!1,L.current=!1,e?Ae():(I.current.discardStreamingMessages(),xe()),we(f.CANCEL)},[l,we,Ne,Ae,xe]),Pe=i(()=>{if(S.current)return;const e=`voice-agent-${b()}`;S.current=e,w.current=new Map,R.current="",l(X({chatMsgId:e}))},[l]),Oe=i(e=>{const t=e.spoken_progress?(e.spoken_progress?.accumulated_text??"")+(e.spoken_progress?.remaining_text??""):e.text??"";if(!t)return;Pe();const n=N.current?function(e,t){const n=(e.get(0)?.text??"")+t;return e.set(0,{text:n,completed:!1}),{id:0,text:n,spokenChars:n.length}}(w.current,t):function(e,t,n,r){const i=t.segment_id??0,s="completed"===t.spoken_status;if(s&&!e.has(i)){const t=[...e.entries()].find(([,e])=>e.text===n&&!e.completed);if(t)return t[1].completed=!0,{id:t[0],text:n,spokenChars:n.length}}const c=e.get(i);return e.set(i,{text:n,completed:s||!0===c?.completed}),{id:i,text:n,spokenChars:r&&t.spoken_progress?(t.spoken_progress.accumulated_text??"").length:n.length}}(w.current,e,t,L.current);l(ee(n));const r=De();r&&S.current&&r!==R.current&&(R.current=r,I.current.updateStreamingMessage(S.current,r))},[l,Pe,De]),Ge=i(t=>{const n=t,r=n?.type,i=n?.data;switch(p.debug({message:"Voice RTVI server-message received",api_name:"voice-rtvi",conversation_id:e??void 0,event_type:r??"unknown"}),r){case"status_update":h({kind:"status_update",statusText:i?.text??""},Re);break;case"started-thinking":l(z(!0)),Ve();break;case"stopped-thinking":we(f.FINALIZE);break;case"clear_status":break;case"widget":i&&h({kind:"widget",widget:i},Re);break;case"payment":i&&h({kind:"payment",payment:i},Re);break;case"conversation_title":{const e="string"==typeof i?.text?i.text.trim():"";e&&h({kind:"conversation_title",title:e},Re);break}case"session-ended":ye.current={reason:"string"==typeof i?.reason?i.reason:"ended"};break;default:p.debug({message:"Voice RTVI unhandled server-message",api_name:"voice-rtvi",event_type:r??"unknown"})}},[Re,l,Ve,we,e]),Be=n(()=>{}),Ue=n(async()=>{}),Fe=i(t=>{const n=()=>t===de.current;return{onConnected:()=>{if(!n())return;p.info({message:"Voice RTVI connected",api_name:"voice-rtvi",conversation_id:e??void 0}),l(j(D.CONNECTED)),l($(!0)),l(q(x.IDLE)),U.current&&U.current.isMicEnabled!==K.current&&(p.debug({message:"Voice re-asserting mic state on reconnect",api_name:"voice-rtvi",conversation_id:e??void 0,enabled:K.current}),U.current.enableMic(K.current));const{selectedMicId:t,selectedSpeakerId:r}=g.getState().voice;t&&U.current?.updateMic(t),r&&U.current?.updateSpeaker(r);const i=t=>n=>p.warn({message:"Voice device enumeration failed",api_name:"voice-mic",conversation_id:e??void 0,device_kind:t,error_message:n instanceof Error?n.message:String(n)});U.current?.getAllMics().then(e=>{n()&&l(ie(e.map(e=>({deviceId:e.deviceId,label:e.label}))))}).catch(i("mics")),U.current?.getAllSpeakers().then(e=>{n()&&l(se(e.map(e=>({deviceId:e.deviceId,label:e.label}))))}).catch(i("speakers")),oe.current=!0},onBotReady:t=>{if(!n())return;const r=t?.about;N.current=r?.pipeline_mode===A.REALTIME,L.current=!0===r?.tts_word_timestamps,be.current=0,he.current=0,Ee.current=0,p.info({message:"Voice RTVI bot-ready",api_name:"voice-rtvi",conversation_id:e??void 0,pipeline_mode:r?.pipeline_mode??"cascade",tts_word_timestamps:L.current,assistant_starts:d}),Ne(),d&&(l(z(!0)),Ve())},onDisconnected:()=>{if(!n())return;p.info({message:"Voice RTVI disconnected",api_name:"voice-rtvi",conversation_id:e??void 0,drop_outcome:ke.current??"transient",reconnect_attempts:be.current}),Le(!0);const t=Ie.current,r=ke.current;ke.current=null;const i=ye.current;return ye.current=null,i?(p.info({message:"Voice session ended by server — no reconnect",api_name:"voice-rtvi",conversation_id:e??void 0,reason:i.reason}),"idle_timeout"===i.reason&&k({title:"Voice ended",description:"It got quiet for a while, so we wrapped up. Start again anytime."}),void l(ce())):"terminal"===r?(p.warn({message:"Voice RTVI terminal error — no retry",api_name:"voice-rtvi"}),void l(ce())):"auth"===r?he.current>=1||null==e?(p.warn({message:"Voice RTVI auth refresh budget exhausted — giving up",api_name:"voice-rtvi",conversation_id:e??void 0}),void l(ce())):(he.current+=1,p.info({message:"Voice RTVI auth drop — refreshing token then reconnecting",api_name:"voice-rtvi",conversation_id:e??void 0,auth_refresh_count:he.current}),void(async()=>{let n=!1;try{n=!!await(y()?.())}catch{}t===Ie.current&&(n?Ue.current():(p.warn({message:"Voice RTVI token refresh failed — no reconnect",api_name:"voice-rtvi",conversation_id:e??void 0}),l(ce())))})()):be.current>=3||null==e?(be.current>=3&&(p.warn({message:"Voice RTVI auto-reconnect cap reached — giving up",api_name:"voice-rtvi",conversation_id:e??void 0,reconnect_attempts:be.current}),V("Voice connection lost. Please try again.")),void l(ce())):(be.current+=1,p.info({message:"Voice RTVI scheduling auto-reconnect",api_name:"voice-rtvi",conversation_id:e??void 0,reconnect_attempts:be.current,delay_ms:1e3}),fe.current&&clearTimeout(fe.current),void(fe.current=setTimeout(()=>{fe.current=null,Ue.current()},1e3)))},onError:t=>{if(!n())return;const r=t?.data,i=r?.error??r?.message??"Voice error";h({kind:"error",error:i},Re),l(q(x.IDLE)),we(f.CANCEL),r?.fatal&&(O.some(e=>e.test(i))?ke.current="auth":G.some(e=>e.test(i))&&(ke.current="terminal"),p.warn({message:"Voice RTVI fatal error — closing transport",api_name:"voice-rtvi",conversation_id:e??void 0,drop_outcome:ke.current??"generic"}),U.current?.disconnect().catch(()=>{}))},onTrackStarted:(t,r)=>{n()&&(r?.local||"audio"!==t.kind||(p.debug({message:"Voice RTVI bot audio track started",api_name:"voice-rtvi",conversation_id:e??void 0}),F.current?.(),F.current=v.attachBotAudioTrack(t)))},onAvailableMicsUpdated:e=>{n()&&(l(ie(e.map(e=>({deviceId:e.deviceId,label:e.label})))),oe.current&&(ae.current&&clearTimeout(ae.current),ae.current=setTimeout(()=>{if(ae.current=null,!U.current)return;const{selectedMicId:e,microphones:t}=g.getState().voice;p.info({message:"Voice audio devices changed",api_name:"voice-mic",microphones:t.map(e=>e.label).join(", ")}),p.debug({message:"Voice re-acquiring mic (device change)",api_name:"voice-rtvi",enabled:K.current,device:e||"default"}),U.current.updateMic(e)},300)))},onAvailableSpeakersUpdated:e=>{if(!n())return;if(l(se(e.map(e=>({deviceId:e.deviceId,label:e.label})))),!oe.current)return;const t=e.map(e=>e.label).join(", ");t!==ue.current&&(ue.current=t,p.info({message:"Voice audio output devices changed",api_name:"voice-mic",speakers:t}))},onMicUpdated:t=>{if(!n())return;const r=U.current;r&&r.isMicEnabled!==K.current&&(p.debug({message:"Voice re-asserting mic state after track swap",api_name:"voice-rtvi",conversation_id:e??void 0,enabled:K.current,device:t?.label}),r.enableMic(K.current))},onUserMuteStarted:()=>{n()&&p.debug({message:"Voice RTVI user-mute-started (ignored)",api_name:"voice-rtvi",conversation_id:e??void 0})},onUserMuteStopped:()=>{n()&&(p.debug({message:"Voice RTVI user-mute-stopped",api_name:"voice-rtvi",conversation_id:e??void 0,honored:d}),d&&l(H(!1)))},onUserStartedSpeaking:()=>{n()&&(p.debug({message:"Voice RTVI user started speaking",api_name:"voice-rtvi",conversation_id:e??void 0}),P.current=null,B.current="",l(Q(null)),l(q(x.LISTENING)))},onUserStoppedSpeaking:()=>{if(!n())return;p.debug({message:"Voice RTVI user stopped speaking",api_name:"voice-rtvi",conversation_id:e??void 0,has_interim:!!B.current.trim()}),l(q(x.PROCESSING)),l(z(!0)),l(Y(null)),Ve();const t=B.current.trim();if(t){const e=`${ge}${b()}`;P.current=e,l(Q(null)),I.current.setMessages(n=>[...n,ve(t,e)])}},onUserTranscript:t=>{if(!n())return;if(!t.final)return void(t.text&&(B.current=t.text,N.current||l(Q(t.text))));l(Q(null));const r=t.text&&t.text.trim()?t.text:"";p.debug({message:"Voice RTVI user transcript (final)",api_name:"voice-rtvi",conversation_id:e??void 0,has_text:!!r});const i=P.current;P.current=null,i?r&&I.current.setMessages(e=>e.map(e=>e.id===i?{...e,message:r}:e)):r&&I.current.setMessages(e=>[...e,ve(r)])},onBotStartedSpeaking:()=>{n()&&(p.debug({message:"Voice RTVI bot started speaking",api_name:"voice-rtvi",conversation_id:e??void 0}),Pe(),l(q(x.SPEAKING)),we(f.FINALIZE))},onBotOutput:e=>{n()&&Oe(e)},onBotStoppedSpeaking:()=>{n()&&(p.debug({message:"Voice RTVI bot stopped speaking",api_name:"voice-rtvi",conversation_id:e??void 0}),Ae(),l(q(x.IDLE)),we(f.FINALIZE),d&&g.getState().voice.serverMuted&&(p.warn({message:"Voice greeting mute-hold released on bot-turn end (server unmute missing)",api_name:"voice-rtvi",conversation_id:e??void 0}),l(H(!1))))},onServerMessage:e=>{n()&&Ge(e)}}},[l,g,v,Ne,Le,we,Ve,Oe,Pe,Ae,Ge,Re,d,e]),We=i(async()=>{if(U.current||Se.current)return;if(!e)return;if(Se.current=!0,p.info({message:"Voice RTVI connecting",api_name:"voice-rtvi",conversation_id:e,assistant_starts:d}),d&&(K.current=!1,l(H(!0)),p.debug({message:"Voice greeting mute-hold armed",api_name:"voice-rtvi",conversation_id:e})),C(M())&&Ee.current<1){Ee.current+=1,p.debug({message:"Voice pre-connect token refresh (freshness gate)",api_name:"voice-rtvi",conversation_id:e,freshness_refresh_count:Ee.current});const t=Ie.current;try{await Promise.resolve(y()?.())}catch{}if(t!==Ie.current)return}const t=++de.current,n=Ie.current;ye.current=null,ke.current=null,l(j(D.CONNECTING));const r=()=>n!==Ie.current||t!==de.current;try{const[{PipecatClient:n,LogLevel:i},s]=await Promise.all([import("./index.module-S2alA3-r.js"),pe(e)]);if(r())return;const c=new n({transport:u(),enableMic:!0,enableCam:!1,callbacks:Fe(t)});if(c.setLogLevel(i.WARN),r())return void c.disconnect().catch(()=>{});U.current=c,p.debug({message:"Voice RTVI client constructed",api_name:"voice-rtvi",conversation_id:e,enable_mic:K.current}),Ne(),W.current=setTimeout(()=>{p.warn({message:"Voice RTVI connect timed out (no bot-ready)",api_name:"voice-rtvi"}),V("Voice connection timed out. Please try again."),l(ce()),Be.current()},15e3),await c.connect({webrtcRequestParams:{endpoint:m.WEBRTC_OFFER(e),headers:new Headers(me())},iceConfig:{iceServers:s}})}catch(e){p.error({message:"Voice RTVI connect failed",api_name:"voice-rtvi",error_message:e instanceof Error?e.message:String(e)}),t===de.current&&(V("Could not connect voice. Please try again."),Le(!1))}},[e,l,u,Fe,Ne,Le,d]),Ke=i((t=!0)=>{p.info({message:"Voice RTVI disconnect (intentional teardown)",api_name:"voice-rtvi",conversation_id:e??void 0,commit_turn:t}),Ie.current+=1,de.current+=1,Ee.current=0,fe.current&&(clearTimeout(fe.current),fe.current=null),be.current=0,he.current=0,ke.current=null,K.current=!0,l(Z(!1));const n=U.current;n?.disconnect().catch(e=>{p.warn({message:"Voice RTVI disconnect threw",api_name:"voice-rtvi",error_message:e instanceof Error?e.message:String(e)})}),Le(t)},[Le,l,e]),je=i(e=>{K.current=e,U.current&&(p.debug({message:"Voice mic enable toggled",api_name:"voice-rtvi",enabled:e}),U.current.enableMic(e))},[]),qe=i(e=>{l(ne(e)),U.current?.updateMic(e)},[l]),$e=i(e=>{l(re(e)),U.current?.updateSpeaker(e)},[l]),Ze=i(t=>{const n=U.current;if(n){p.info({message:"Voice RTVI send-text",api_name:"voice-rtvi",conversation_id:e??void 0,run_immediately:!0});try{n.sendText(t,{run_immediately:!0,audio_response:!0})}catch(e){return void p.error({message:"Voice RTVI send-text failed",api_name:"voice-rtvi",error_message:e instanceof Error?e.message:String(e)})}S.current&&Ae(),I.current.setMessages(e=>[...e,ve(t)]),l(z(!0)),l(Y(null)),Ve(),l(q(x.PROCESSING))}},[l,Ve,Ae,e]);r(()=>{Be.current=Ke},[Ke]),r(()=>{Ue.current=We},[We]);const He=n(e),Ye=n(!1);r(()=>{const t=He.current;if(He.current=e,t===e)return;const n=Se.current;n&&(Ye.current=!0),n&&Ke(!1),Ye.current&&null!=e&&(Ye.current=!1,We())},[e,Ke,We]);const ze=n(_);return r(()=>{const e=ze.current;ze.current=_,e&&!_&&Se.current&&Ke(!1)},[_,Ke]),r(()=>()=>{Be.current()},[]),{connect:We,disconnect:Ke,sendText:Ze,setMicEnabled:je,setInputDevice:qe,setOutputDevice:$e}}(e,t,u,d);r(()=>{P(!v&&!_)},[v,_,P]);const F=Ie();r(()=>(t.setVoiceSendText(L),()=>t.setVoiceSendText(null)),[t,L]);const W=n(!1);return{displayState:F,pipelineStatus:I,isAgentWorking:S,enterVoiceMode:i(async()=>{if(W.current)return V("Microphone permission changed — please reload the page to apply the new setting."),!1;const e=await g.requestMicPermission();if(!e.granted){const t=e.error;return p.error({message:"Mic permission denied",api_name:"voice-mic",error_message:t instanceof Error?t.message:t?String(t):void 0}),W.current=!0,V("Microphone access is required for voice mode. Please allow it in your browser's site settings and reload the page."),!1}return R(),!0},[g,R]),exitVoiceMode:i(()=>{N()},[N]),toggleMute:i(()=>{l(Z(!v))},[l,v]),sendText:L,muted:v,serverMuted:_,sessionEndedSignal:w,setInputDevice:B,setOutputDevice:U}},Te=()=>{const e=t(Se);if(!e)throw new Error("useVoice must be used within VoiceProvider");return e},fe=e({strategyActive:!1,voiceSlot:null,onEnterVoice:null,onFloatingOrbClick:null,voiceInputHidden:!1,headerCenterSlot:null,floatOrbPositionRef:null}),be=()=>t(fe);function he(){return{microphones:o(e=>e.voice.microphones),speakers:o(e=>e.voice.speakers),selectedMicId:o(e=>e.voice.selectedMicId),selectedSpeakerId:o(e=>e.voice.selectedSpeakerId)}}const ke=d();ke.startListening({actionCreator:ne,effect:async e=>{const t=w();t&&await t.setItem(B.SELECTED_MIC_ID,e.payload)}}),ke.startListening({actionCreator:re,effect:async e=>{const t=w();t&&await t.setItem(B.SELECTED_SPEAKER_ID,e.payload)}}),ke.startListening({matcher:l(R,N),effect:async(e,t)=>{const n=w();n&&await Promise.all([n.removeItem(B.SELECTED_MIC_ID),n.removeItem(B.SELECTED_SPEAKER_ID)]),t.dispatch(ae())}});const ye=e=>{const t=document.createElement("audio");return t.autoplay=!0,t.style.display="none",t.srcObject=new MediaStream([e]),document.body.appendChild(t),()=>{try{t.pause(),t.srcObject=null,t.remove()}catch{}}},Ve={attachBotAudioTrack:ye,requestMicPermission:async function(){try{const e=await(navigator.permissions?.query({name:"microphone"}).catch(()=>null));return p.debug({message:"Voice mic permission probe",api_name:"voice-mic",permission_state:e?.state??"unavailable"}),"granted"===e?.state||(await navigator.mediaDevices.getUserMedia({audio:!0})).getTracks().forEach(e=>e.stop()),{granted:!0}}catch(e){return p.warn({message:"Voice mic permission denied",api_name:"voice-mic",error_message:e instanceof Error?e.message:String(e)}),{granted:!1,error:e}}}};export{de as AudioPlatformContext,U as ORB_STATE_CONFIG,D as VOICE_CONNECTION_STATUSES,P as VOICE_CONNECT_TIMEOUT_MS,L as VOICE_DISPLAY_STATES,A as VOICE_PIPELINE_MODES,x as VOICE_PIPELINE_STATUSES,B as VOICE_STORAGE_KEYS,Se as VoiceContext,fe as VoiceUIContext,ye as attachBrowserBotAudioTrack,Ve as browserAudioPlatform,te as clearBotTurn,W as hydrateVoice,X as openBotTurn,oe as resetIndicatorState,j as setConnectionStatus,Q as setInterimTranscript,z as setIsAgentWorking,$ as setListeningEnabled,Z as setMuted,q as setPipelineStatus,J as setResponseStartTime,ne as setSelectedMicId,re as setSelectedSpeakerId,H as setServerMuted,Y as setStatusText,ee as upsertBotSegment,le as useAudioPlatform,he as useMediaDevices,Te as useVoice,Ie as useVoiceDisplayState,Ee as useVoiceProviderValue,be as useVoiceUI,ke as voiceListenerMiddleware,ue as voiceReducer};
1
+ import{createContext as e,useContext as t,useRef as n,useEffect as r,useCallback as i,useState as s}from"react";import{useDispatch as c,useStore as a,useSelector as o}from"react-redux";import{createSlice as u,createListenerMiddleware as d,isAnyOf as l}from"@reduxjs/toolkit";import{API_ENDPOINTS as m,logger as p,getCustomHeaders as g,readAccessToken as v,AUTH_HEADERS as _,readDeviceToken as I,DEVICE_TOKEN_HEADER as S,clearLastResponseTime as E,setLastResponseTime as f,INDICATOR_TIMER_ACTIONS as T,generateUniqueId as b,handleSharedEvent as h,showToast as y,getTokenRefreshHandler as k,showErrorToast as V,isTokenAboutToExpire as C,readTokenExpiresAt as M,getStorageRef as w,clearSession as R,sessionExpired as N}from"@1interface/shared-core";const D={DISCONNECTED:"disconnected",CONNECTING:"connecting",CONNECTED:"connected"},x={IDLE:"idle",LISTENING:"listening",PROCESSING:"processing",SPEAKING:"speaking"},A={CASCADE:"cascade",REALTIME:"realtime"},L={OFFLINE:"offline",CONNECTING:"connecting",READY:"ready",IDLE:"idle",LISTENING:"listening",MUTED:"muted",PROCESSING:"processing",SPEAKING:"speaking"},P=15e3,O=[/rejected/i,/unauthori[sz]ed/i,/forbidden/i,/\b40[13]\b/],G=[/\b402\b/,/\b429\b/,/rate[\s-]?limit/i],B={SELECTED_MIC_ID:"voice_selectedMicId",SELECTED_SPEAKER_ID:"voice_selectedSpeakerId"},j={offline:{opacity:.15,playbackRate:.3,scale:1},connecting:{opacity:.4,playbackRate:.5,scale:1},ready:{opacity:.4,playbackRate:.4,scale:1},idle:{opacity:.6,playbackRate:.6,scale:1},listening:{opacity:.7,playbackRate:.7,scale:1},muted:{opacity:.7,playbackRate:.5,scale:1},processing:{opacity:1,playbackRate:1,scale:1},speaking:{opacity:1,playbackRate:1,scale:1.3}};function U(){return{connectionStatus:D.DISCONNECTED,pipelineStatus:x.IDLE,listeningEnabled:!1,muted:!1,serverMuted:!1,statusText:null,isAgentWorking:!1,responseStartTime:null,interimTranscript:null,botTurn:{active:!1,chatMsgId:null,segments:[]},sessionEndedSignal:0,selectedMicId:"",selectedSpeakerId:"",microphones:[],speakers:[]}}const F=e=>async t=>{const[n,r]=await Promise.all([e.getItem(B.SELECTED_MIC_ID),e.getItem(B.SELECTED_SPEAKER_ID)]);n&&t(W.actions.setSelectedMicId(n)),r&&t(W.actions.setSelectedSpeakerId(r))},W=u({name:"voice",initialState:U(),reducers:{setConnectionStatus(e,t){e.connectionStatus=t.payload},setPipelineStatus(e,t){e.pipelineStatus=t.payload},setListeningEnabled(e,t){e.listeningEnabled=t.payload},setStatusText(e,t){e.statusText=t.payload},setIsAgentWorking(e,t){e.isAgentWorking=t.payload},setResponseStartTime(e,t){e.responseStartTime=t.payload},setInterimTranscript(e,t){e.interimTranscript=t.payload},openBotTurn(e,t){e.botTurn={active:!0,chatMsgId:t.payload?.chatMsgId??null,segments:[]}},upsertBotSegment(e,t){const{id:n,text:r,spokenChars:i}=t.payload;e.botTurn.active=!0;const s=e.botTurn.segments.find(e=>e.id===n);s?(s.text=r,s.spokenChars=Math.max(s.spokenChars,i)):(e.botTurn.segments.push({id:n,text:r,spokenChars:i}),e.botTurn.segments.sort((e,t)=>e.id-t.id))},clearBotTurn(e){e.botTurn={active:!1,chatMsgId:null,segments:[]}},setMuted(e,t){e.muted=t.payload},setServerMuted(e,t){e.serverMuted=t.payload},setSelectedMicId(e,t){e.selectedMicId=t.payload},setSelectedSpeakerId(e,t){e.selectedSpeakerId=t.payload},setMicrophones(e,t){e.microphones=t.payload},setSpeakers(e,t){e.speakers=t.payload},signalSessionEnded(e){e.sessionEndedSignal+=1},resetVoiceState:()=>U(),resetIndicatorState(e){e.statusText=null,e.isAgentWorking=!1,e.responseStartTime=null,e.interimTranscript=null,e.serverMuted=!1,e.botTurn={active:!1,chatMsgId:null,segments:[]}}}}),{setConnectionStatus:K,setPipelineStatus:q,setListeningEnabled:$,setMuted:Z,setServerMuted:H,setStatusText:Y,setIsAgentWorking:z,setResponseStartTime:J,setInterimTranscript:Q,openBotTurn:X,upsertBotSegment:ee,clearBotTurn:te,setSelectedMicId:ne,setSelectedSpeakerId:re,setMicrophones:ie,setSpeakers:se,signalSessionEnded:ce,resetVoiceState:ae,resetIndicatorState:oe}=W.actions,ue=W.reducer,de=e(null),le=()=>{const e=t(de);if(!e)throw new Error("useAudioPlatform must be used inside <AudioPlatformProvider>. <VoiceProvider> mounts one by default; if you replaced VoiceProvider, you must wire your own.");return e};function me(){const e={};for(const[t,n]of Object.entries(g()))n&&(e[t]=n);const t=v();t&&(e[_.ACCESS_TOKEN]=t);const n=I();return n&&(e[S]=n),e}async function pe(e){try{const t=await fetch(m.WEBRTC_ICE(e),{headers:me()});return t.ok?(await t.json()).iceServers??[]:(p.warn({message:"Voice ICE fetch failed — continuing without TURN",api_name:"voice-webrtc-ice",status:t.status}),[])}catch(e){return p.warn({message:"Voice ICE fetch error — continuing without TURN",api_name:"voice-webrtc-ice",error_message:e instanceof Error?e.message:String(e)}),[]}}const ge="voice-user-",ve="Could not connect voice. Please try again later.",_e=(e,t=`${ge}${b()}`)=>({id:t,sender:"user",message:e,timestamp:(new Date).toISOString()}),Ie=(e,t)=>{const n=`${e}${b()}`;return{id:n,msg_id:n,sender:"agent",message:"",timestamp:(new Date).toISOString(),...t}};function Se(){const e=o(e=>e.voice.connectionStatus),t=o(e=>e.voice.pipelineStatus),n=o(e=>e.voice.listeningEnabled),r=o(e=>e.voice.muted);return e===D.CONNECTING?L.CONNECTING:e!==D.CONNECTED?L.OFFLINE:n?r?L.MUTED:t===x.PROCESSING?L.PROCESSING:t===x.SPEAKING?L.SPEAKING:t===x.LISTENING?L.LISTENING:L.IDLE:L.READY}const Ee=e(null),fe=({conversationId:e,voiceBridge:t,transportFactory:u,assistantStartsConversation:d})=>{const l=c(),g=le(),v=o(e=>e.voice.muted),_=o(e=>e.voice.serverMuted),I=o(e=>e.voice.pipelineStatus),S=o(e=>e.voice.isAgentWorking),w=o(e=>e.voice.sessionEndedSignal),{connect:R,disconnect:N,sendText:L,setMicEnabled:P,setInputDevice:B,setOutputDevice:j}=function(e,t,u,d){const l=c(),g=a(),v=le(),_=o(e=>e.auth.accessToken),I=n(t);r(()=>{I.current=t});const S=n(null),w=n(new Map),R=n(""),N=n(!1),L=n(!1),P=n(null),B=n(""),j=n(null),U=n(null),F=n(null),W=n(!0),ae=n(null),oe=n(!1),ue=n(""),de=n(0),Se=n(0),Ee=n(!1),fe=n(0),Te=n(null),be=n(null),he=n(0),ye=n(0),ke=n(null),Ve=n(null),Ce=i(()=>{if(null!=Te.current)return;const e=Date.now();Te.current=e,l(J(e)),l(E())},[l]),Me=i(()=>{const e=Te.current;Te.current=null,l(J(null)),null!=e&&l(f(Date.now()-e))},[l]),we=i(()=>{Te.current=null,l(J(null))},[l]),Re=i((e=T.KEEP)=>{l(Y(null)),l(z(!1)),e===T.FINALIZE?Me():e===T.CANCEL&&we()},[l,Me,we]),[Ne]=s(()=>({setStatusText:e=>l(Y(e||null)),setIsAgentWorking:e=>l(z(e)),appendStreamText:()=>{},pushWidget:e=>{const t=Ie("voice-widget-",{widget:[e],message_type:["widget"]});I.current.setMessages(e=>[...e,t])},onPayment:e=>I.current.onPaymentInit?.(e),onConversationTitle:e=>I.current.onConversationTitle?.(e),onError:e=>{p.error({message:"Voice RTVI server error",api_name:"voice-rtvi",error_message:e});const t=Ie("voice-error-",{message:"Something went wrong. Please try again.",message_type:["error"]});I.current.setMessages(e=>[...e,t])},onStatusActive:()=>{Ce()}})),De=i(()=>{F.current&&(clearTimeout(F.current),F.current=null)},[]),xe=i(()=>[...w.current.entries()].sort((e,t)=>e[0]-t[0]).map(([,e])=>e.text).join(" ").trim(),[]),Ae=i(()=>{S.current=null,w.current=new Map,R.current="",l(te())},[l]),Le=i(()=>{const e=S.current;if(e){const t=xe();t&&I.current.updateStreamingMessage(e,t),I.current.commitStreamingMessages()}Ae()},[xe,Ae]),Pe=i(e=>{De(),ae.current&&(clearTimeout(ae.current),ae.current=null),oe.current=!1,ue.current="",l(ie([])),l(se([])),U.current?.(),U.current=null,j.current=null,Ee.current=!1,l($(!1)),l(q(x.IDLE)),l(K(D.DISCONNECTED)),l(Q(null)),l(H(!1)),P.current=null,B.current="",N.current=!1,L.current=!1,e?Le():(I.current.discardStreamingMessages(),Ae()),Re(T.CANCEL)},[l,Re,De,Le,Ae]),Oe=i(()=>{if(S.current)return;const e=`voice-agent-${b()}`;S.current=e,w.current=new Map,R.current="",l(X({chatMsgId:e}))},[l]),Ge=i(e=>{const t=e.spoken_progress?(e.spoken_progress?.accumulated_text??"")+(e.spoken_progress?.remaining_text??""):e.text??"";if(!t)return;Oe();const n=N.current?function(e,t){const n=(e.get(0)?.text??"")+t;return e.set(0,{text:n,completed:!1}),{id:0,text:n,spokenChars:n.length}}(w.current,t):function(e,t,n,r){const i=t.segment_id??0,s="completed"===t.spoken_status;if(s&&!e.has(i)){const t=[...e.entries()].find(([,e])=>e.text===n&&!e.completed);if(t)return t[1].completed=!0,{id:t[0],text:n,spokenChars:n.length}}const c=e.get(i);return e.set(i,{text:n,completed:s||!0===c?.completed}),{id:i,text:n,spokenChars:r&&t.spoken_progress?(t.spoken_progress.accumulated_text??"").length:n.length}}(w.current,e,t,L.current);l(ee(n));const r=xe();r&&S.current&&r!==R.current&&(R.current=r,I.current.updateStreamingMessage(S.current,r))},[l,Oe,xe]),Be=i(t=>{const n=t,r=n?.type,i=n?.data;switch(p.debug({message:"Voice RTVI server-message received",api_name:"voice-rtvi",conversation_id:e??void 0,event_type:r??"unknown"}),r){case"status_update":h({kind:"status_update",statusText:i?.text??""},Ne);break;case"started-thinking":l(z(!0)),Ce();break;case"stopped-thinking":Re(T.FINALIZE);break;case"clear_status":break;case"widget":i&&h({kind:"widget",widget:i},Ne);break;case"payment":i&&h({kind:"payment",payment:i},Ne);break;case"conversation_title":{const e="string"==typeof i?.text?i.text.trim():"";e&&h({kind:"conversation_title",title:e},Ne);break}case"session-ended":Ve.current={reason:"string"==typeof i?.reason?i.reason:"ended"};break;default:p.debug({message:"Voice RTVI unhandled server-message",api_name:"voice-rtvi",event_type:r??"unknown"})}},[Ne,l,Ce,Re,e]),je=n(()=>{}),Ue=n(async()=>{}),Fe=i(t=>{const n=()=>t===de.current;return{onConnected:()=>{if(!n())return;p.info({message:"Voice RTVI connected",api_name:"voice-rtvi",conversation_id:e??void 0}),l(K(D.CONNECTED)),l($(!0)),l(q(x.IDLE)),j.current&&j.current.isMicEnabled!==W.current&&(p.debug({message:"Voice re-asserting mic state on reconnect",api_name:"voice-rtvi",conversation_id:e??void 0,enabled:W.current}),j.current.enableMic(W.current));const{selectedMicId:t,selectedSpeakerId:r}=g.getState().voice;t&&j.current?.updateMic(t),r&&j.current?.updateSpeaker(r);const i=t=>n=>p.warn({message:"Voice device enumeration failed",api_name:"voice-mic",conversation_id:e??void 0,device_kind:t,error_message:n instanceof Error?n.message:String(n)});j.current?.getAllMics().then(e=>{n()&&l(ie(e.map(e=>({deviceId:e.deviceId,label:e.label}))))}).catch(i("mics")),j.current?.getAllSpeakers().then(e=>{n()&&l(se(e.map(e=>({deviceId:e.deviceId,label:e.label}))))}).catch(i("speakers")),oe.current=!0},onBotReady:t=>{if(!n())return;const r=t?.about;N.current=r?.pipeline_mode===A.REALTIME,L.current=!0===r?.tts_word_timestamps,he.current=0,ye.current=0,fe.current=0,p.info({message:"Voice RTVI bot-ready",api_name:"voice-rtvi",conversation_id:e??void 0,pipeline_mode:r?.pipeline_mode??"cascade",tts_word_timestamps:L.current,assistant_starts:d}),De(),d&&(l(z(!0)),Ce())},onDisconnected:()=>{if(!n())return;p.info({message:"Voice RTVI disconnected",api_name:"voice-rtvi",conversation_id:e??void 0,drop_outcome:ke.current??"transient",reconnect_attempts:he.current}),Pe(!0);const t=Se.current,r=ke.current;ke.current=null;const i=Ve.current;return Ve.current=null,i?(p.info({message:"Voice session ended by server — no reconnect",api_name:"voice-rtvi",conversation_id:e??void 0,reason:i.reason}),"idle_timeout"===i.reason&&y({title:"Voice ended",description:"It got quiet for a while, so we wrapped up. Start again anytime."}),void l(ce())):"terminal"===r?(p.warn({message:"Voice RTVI terminal error — no retry",api_name:"voice-rtvi"}),void l(ce())):"auth"===r?ye.current>=1||null==e?(p.warn({message:"Voice RTVI auth refresh budget exhausted — giving up",api_name:"voice-rtvi",conversation_id:e??void 0}),void l(ce())):(ye.current+=1,p.info({message:"Voice RTVI auth drop — refreshing token then reconnecting",api_name:"voice-rtvi",conversation_id:e??void 0,auth_refresh_count:ye.current}),void(async()=>{let n=!1;try{n=!!await(k()?.())}catch{}t===Se.current&&(n?Ue.current():(p.warn({message:"Voice RTVI token refresh failed — no reconnect",api_name:"voice-rtvi",conversation_id:e??void 0}),l(ce())))})()):he.current>=3||null==e?(he.current>=3&&(p.warn({message:"Voice RTVI auto-reconnect cap reached — giving up",api_name:"voice-rtvi",conversation_id:e??void 0,reconnect_attempts:he.current}),V("Voice connection lost. Please try again.")),void l(ce())):(he.current+=1,p.info({message:"Voice RTVI scheduling auto-reconnect",api_name:"voice-rtvi",conversation_id:e??void 0,reconnect_attempts:he.current,delay_ms:1e3}),be.current&&clearTimeout(be.current),void(be.current=setTimeout(()=>{be.current=null,Ue.current()},1e3)))},onError:t=>{if(!n())return;const r=t?.data,i=r?.error??r?.message??"Voice error";h({kind:"error",error:i},Ne),l(q(x.IDLE)),Re(T.CANCEL),r?.fatal&&(O.some(e=>e.test(i))?ke.current="auth":G.some(e=>e.test(i))&&(ke.current="terminal"),p.warn({message:"Voice RTVI fatal error — closing transport",api_name:"voice-rtvi",conversation_id:e??void 0,drop_outcome:ke.current??"generic"}),j.current?.disconnect().catch(()=>{}))},onTrackStarted:(t,r)=>{n()&&(r?.local||"audio"!==t.kind||(p.debug({message:"Voice RTVI bot audio track started",api_name:"voice-rtvi",conversation_id:e??void 0}),U.current?.(),U.current=v.attachBotAudioTrack(t)))},onAvailableMicsUpdated:e=>{n()&&(l(ie(e.map(e=>({deviceId:e.deviceId,label:e.label})))),oe.current&&(ae.current&&clearTimeout(ae.current),ae.current=setTimeout(()=>{if(ae.current=null,!j.current)return;const{selectedMicId:e,microphones:t}=g.getState().voice;p.info({message:"Voice audio devices changed",api_name:"voice-mic",microphones:t.map(e=>e.label).join(", ")}),p.debug({message:"Voice re-acquiring mic (device change)",api_name:"voice-rtvi",enabled:W.current,device:e||"default"}),j.current.updateMic(e)},300)))},onAvailableSpeakersUpdated:e=>{if(!n())return;if(l(se(e.map(e=>({deviceId:e.deviceId,label:e.label})))),!oe.current)return;const t=e.map(e=>e.label).join(", ");t!==ue.current&&(ue.current=t,p.info({message:"Voice audio output devices changed",api_name:"voice-mic",speakers:t}))},onMicUpdated:t=>{if(!n())return;const r=j.current;r&&r.isMicEnabled!==W.current&&(p.debug({message:"Voice re-asserting mic state after track swap",api_name:"voice-rtvi",conversation_id:e??void 0,enabled:W.current,device:t?.label}),r.enableMic(W.current))},onUserMuteStarted:()=>{n()&&p.debug({message:"Voice RTVI user-mute-started (ignored)",api_name:"voice-rtvi",conversation_id:e??void 0})},onUserMuteStopped:()=>{n()&&(p.debug({message:"Voice RTVI user-mute-stopped",api_name:"voice-rtvi",conversation_id:e??void 0,honored:d}),d&&l(H(!1)))},onUserStartedSpeaking:()=>{n()&&(p.debug({message:"Voice RTVI user started speaking",api_name:"voice-rtvi",conversation_id:e??void 0}),P.current=null,B.current="",l(Q(null)),l(q(x.LISTENING)))},onUserStoppedSpeaking:()=>{if(!n())return;p.debug({message:"Voice RTVI user stopped speaking",api_name:"voice-rtvi",conversation_id:e??void 0,has_interim:!!B.current.trim()}),l(q(x.PROCESSING)),l(z(!0)),l(Y(null)),Ce();const t=B.current.trim();if(t){const e=`${ge}${b()}`;P.current=e,l(Q(null)),I.current.setMessages(n=>[...n,_e(t,e)])}},onUserTranscript:t=>{if(!n())return;if(!t.final)return void(t.text&&(B.current=t.text,N.current||l(Q(t.text))));l(Q(null));const r=t.text&&t.text.trim()?t.text:"";p.debug({message:"Voice RTVI user transcript (final)",api_name:"voice-rtvi",conversation_id:e??void 0,has_text:!!r});const i=P.current;P.current=null,i?r&&I.current.setMessages(e=>e.map(e=>e.id===i?{...e,message:r}:e)):r&&I.current.setMessages(e=>[...e,_e(r)])},onBotStartedSpeaking:()=>{n()&&(p.debug({message:"Voice RTVI bot started speaking",api_name:"voice-rtvi",conversation_id:e??void 0}),Oe(),l(q(x.SPEAKING)),Re(T.FINALIZE))},onBotOutput:e=>{n()&&Ge(e)},onBotStoppedSpeaking:()=>{n()&&(p.debug({message:"Voice RTVI bot stopped speaking",api_name:"voice-rtvi",conversation_id:e??void 0}),Le(),l(q(x.IDLE)),Re(T.FINALIZE),d&&g.getState().voice.serverMuted&&(p.warn({message:"Voice greeting mute-hold released on bot-turn end (server unmute missing)",api_name:"voice-rtvi",conversation_id:e??void 0}),l(H(!1))))},onServerMessage:e=>{n()&&Be(e)}}},[l,g,v,De,Pe,Re,Ce,Ge,Oe,Le,Be,Ne,d,e]),We=i(async()=>{if(j.current||Ee.current)return;if(!e)return;if(Ee.current=!0,p.info({message:"Voice RTVI connecting",api_name:"voice-rtvi",conversation_id:e,assistant_starts:d}),d&&(W.current=!1,l(H(!0)),p.debug({message:"Voice greeting mute-hold armed",api_name:"voice-rtvi",conversation_id:e})),C(M())&&fe.current<1){fe.current+=1,p.debug({message:"Voice pre-connect token refresh (freshness gate)",api_name:"voice-rtvi",conversation_id:e,freshness_refresh_count:fe.current});const t=Se.current;try{await Promise.resolve(k()?.())}catch{}if(t!==Se.current)return}const t=++de.current,n=Se.current;Ve.current=null,ke.current=null,l(K(D.CONNECTING));const r=()=>n!==Se.current||t!==de.current;try{const[{PipecatClient:n,LogLevel:i},s]=await Promise.all([import("./index.module-S2alA3-r.js"),pe(e)]);if(r())return;const c=new n({transport:u(),enableMic:!0,enableCam:!1,callbacks:Fe(t)});if(c.setLogLevel(i.WARN),r())return void c.disconnect().catch(()=>{});j.current=c,p.debug({message:"Voice RTVI client constructed",api_name:"voice-rtvi",conversation_id:e,enable_mic:W.current}),De(),F.current=setTimeout(()=>{p.warn({message:"Voice RTVI connect timed out (no bot-ready)",api_name:"voice-rtvi"}),V("Voice connection timed out. Please try again."),l(ce()),je.current()},15e3),await c.connect({webrtcRequestParams:{endpoint:m.WEBRTC_OFFER(e),headers:new Headers(me())},iceConfig:{iceServers:s}})}catch(e){const n=(e=>{const t="object"==typeof e&&null!==e&&"status"in e?e.status:void 0;return"number"==typeof t?t:void 0})(e);p.error({message:"Voice RTVI connect failed",api_name:"voice-rtvi",status:n,error_message:e instanceof Error?e.message:String(e)});const r=(e=>void 0!==e&&e>=400&&e<500)(n),i=r?await(async(e,t)=>{const n=429===t?"Voice session limit reached. Please try again later.":ve,r="object"==typeof e&&null!==e&&"cause"in e?e.cause:void 0;if(!(r instanceof Response))return n;try{const e=await r.json(),t=e?.data?.error_message??e?.data?.message;return"string"==typeof t?t:n}catch{return n}})(e,n):ve;t===de.current&&(V(i),r&&(be.current&&(clearTimeout(be.current),be.current=null),he.current=0,l(ce())),Pe(!1))}},[e,l,u,Fe,De,Pe,d]),Ke=i((t=!0)=>{p.info({message:"Voice RTVI disconnect (intentional teardown)",api_name:"voice-rtvi",conversation_id:e??void 0,commit_turn:t}),Se.current+=1,de.current+=1,fe.current=0,be.current&&(clearTimeout(be.current),be.current=null),he.current=0,ye.current=0,ke.current=null,W.current=!0,l(Z(!1));const n=j.current;n?.disconnect().catch(e=>{p.warn({message:"Voice RTVI disconnect threw",api_name:"voice-rtvi",error_message:e instanceof Error?e.message:String(e)})}),Pe(t)},[Pe,l,e]),qe=i(e=>{W.current=e,j.current&&(p.debug({message:"Voice mic enable toggled",api_name:"voice-rtvi",enabled:e}),j.current.enableMic(e))},[]),$e=i(e=>{l(ne(e)),j.current?.updateMic(e)},[l]),Ze=i(e=>{l(re(e)),j.current?.updateSpeaker(e)},[l]),He=i(t=>{const n=j.current;if(n){p.info({message:"Voice RTVI send-text",api_name:"voice-rtvi",conversation_id:e??void 0,run_immediately:!0});try{n.sendText(t,{run_immediately:!0,audio_response:!0})}catch(e){return void p.error({message:"Voice RTVI send-text failed",api_name:"voice-rtvi",error_message:e instanceof Error?e.message:String(e)})}S.current&&Le(),I.current.setMessages(e=>[...e,_e(t)]),l(z(!0)),l(Y(null)),Ce(),l(q(x.PROCESSING))}},[l,Ce,Le,e]);r(()=>{je.current=Ke},[Ke]),r(()=>{Ue.current=We},[We]);const Ye=n(e),ze=n(!1);r(()=>{const t=Ye.current;if(Ye.current=e,t===e)return;const n=Ee.current;n&&(ze.current=!0),n&&Ke(!1),ze.current&&null!=e&&(ze.current=!1,We())},[e,Ke,We]);const Je=n(_);return r(()=>{const e=Je.current;Je.current=_,e&&!_&&Ee.current&&Ke(!1)},[_,Ke]),r(()=>()=>{je.current()},[]),{connect:We,disconnect:Ke,sendText:He,setMicEnabled:qe,setInputDevice:$e,setOutputDevice:Ze}}(e,t,u,d);r(()=>{P(!v&&!_)},[v,_,P]);const U=Se();r(()=>(t.setVoiceSendText(L),()=>t.setVoiceSendText(null)),[t,L]);const F=n(!1);return{displayState:U,pipelineStatus:I,isAgentWorking:S,enterVoiceMode:i(async()=>{if(F.current)return V("Microphone permission changed — please reload the page to apply the new setting."),!1;const e=await g.requestMicPermission();if(!e.granted){const t=e.error;return p.error({message:"Mic permission denied",api_name:"voice-mic",error_message:t instanceof Error?t.message:t?String(t):void 0}),F.current=!0,V("Microphone access is required for voice mode. Please allow it in your browser's site settings and reload the page."),!1}return R(),!0},[g,R]),exitVoiceMode:i(()=>{N()},[N]),toggleMute:i(()=>{l(Z(!v))},[l,v]),sendText:L,muted:v,serverMuted:_,sessionEndedSignal:w,setInputDevice:B,setOutputDevice:j}},Te=()=>{const e=t(Ee);if(!e)throw new Error("useVoice must be used within VoiceProvider");return e},be=e({strategyActive:!1,voiceSlot:null,onEnterVoice:null,onFloatingOrbClick:null,voiceInputHidden:!1,headerCenterSlot:null,floatOrbPositionRef:null}),he=()=>t(be);function ye(){return{microphones:o(e=>e.voice.microphones),speakers:o(e=>e.voice.speakers),selectedMicId:o(e=>e.voice.selectedMicId),selectedSpeakerId:o(e=>e.voice.selectedSpeakerId)}}const ke=d();ke.startListening({actionCreator:ne,effect:async e=>{const t=w();t&&await t.setItem(B.SELECTED_MIC_ID,e.payload)}}),ke.startListening({actionCreator:re,effect:async e=>{const t=w();t&&await t.setItem(B.SELECTED_SPEAKER_ID,e.payload)}}),ke.startListening({matcher:l(R,N),effect:async(e,t)=>{const n=w();n&&await Promise.all([n.removeItem(B.SELECTED_MIC_ID),n.removeItem(B.SELECTED_SPEAKER_ID)]),t.dispatch(ae())}});const Ve=e=>{const t=document.createElement("audio");return t.autoplay=!0,t.style.display="none",t.srcObject=new MediaStream([e]),document.body.appendChild(t),()=>{try{t.pause(),t.srcObject=null,t.remove()}catch{}}},Ce={attachBotAudioTrack:Ve,requestMicPermission:async function(){try{const e=await(navigator.permissions?.query({name:"microphone"}).catch(()=>null));return p.debug({message:"Voice mic permission probe",api_name:"voice-mic",permission_state:e?.state??"unavailable"}),"granted"===e?.state||(await navigator.mediaDevices.getUserMedia({audio:!0})).getTracks().forEach(e=>e.stop()),{granted:!0}}catch(e){return p.warn({message:"Voice mic permission denied",api_name:"voice-mic",error_message:e instanceof Error?e.message:String(e)}),{granted:!1,error:e}}}};export{de as AudioPlatformContext,j as ORB_STATE_CONFIG,D as VOICE_CONNECTION_STATUSES,P as VOICE_CONNECT_TIMEOUT_MS,L as VOICE_DISPLAY_STATES,A as VOICE_PIPELINE_MODES,x as VOICE_PIPELINE_STATUSES,B as VOICE_STORAGE_KEYS,Ee as VoiceContext,be as VoiceUIContext,Ve as attachBrowserBotAudioTrack,Ce as browserAudioPlatform,te as clearBotTurn,F as hydrateVoice,X as openBotTurn,oe as resetIndicatorState,K as setConnectionStatus,Q as setInterimTranscript,z as setIsAgentWorking,$ as setListeningEnabled,Z as setMuted,q as setPipelineStatus,J as setResponseStartTime,ne as setSelectedMicId,re as setSelectedSpeakerId,H as setServerMuted,Y as setStatusText,ee as upsertBotSegment,le as useAudioPlatform,ye as useMediaDevices,Te as useVoice,Se as useVoiceDisplayState,fe as useVoiceProviderValue,he as useVoiceUI,ke as voiceListenerMiddleware,ue as voiceReducer};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1interface/voice-core",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "sideEffects": false,