@1interface/voice-core 0.2.0 → 0.2.1

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"},a={IDLE:"idle",LISTENING:"listening",PROCESSING:"processing",SPEAKING:"speaking"},o={CASCADE:"cascade",REALTIME:"realtime"},c={OFFLINE:"offline",CONNECTING:"connecting",READY:"ready",IDLE:"idle",LISTENING:"listening",MUTED:"muted",PROCESSING:"processing",SPEAKING:"speaking"},i=[/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:a.IDLE,listeningEnabled:!1,muted:!1,serverMuted:!1,statusText:null,isAgentWorking:!1,responseStartTime:null,interimTranscript:null,botTurn:{active:!1,chatMsgId:null,segments:[]},selectedMicId:"",selectedSpeakerId:""}}const p=d(),g=r.createSlice({name:"voice",initialState:p,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 a=e.botTurn.segments.find(e=>e.id===r);a?(a.text=n,a.spokenChars=Math.max(a.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},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:E,setPipelineStatus:S,setListeningEnabled:m,setMuted:I,setServerMuted:T,setStatusText:C,setIsAgentWorking:f,setResponseStartTime:v,setInterimTranscript:k,openBotTurn:h,upsertBotSegment:x,clearBotTurn:_,setSelectedMicId:R,setSelectedSpeakerId:N,resetVoiceState:b,resetIndicatorState:y}=g.actions,M=g.reducer,A=e.createContext(null),D=()=>{const t=e.useContext(A);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 w(){const e={};for(const[t,r]of Object.entries(n.getCustomHeaders()))r&&(e[t]=r);const t=n.readAccessToken();return t&&(e[n.AUTH_HEADERS.ACCESS_TOKEN]=t),e}async function O(e){try{const t=await fetch(n.API_ENDPOINTS.WEBRTC_ICE(e),{headers:w()});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 P="voice-user-",L=(e,t=`${P}${n.generateUniqueId()}`)=>({id:t,sender:"user",message:e,timestamp:(new Date).toISOString()}),V=(e,t)=>{const r=`${e}${n.generateUniqueId()}`;return{id:r,msg_id:r,sender:"agent",message:"",timestamp:(new Date).toISOString(),...t}};function G(){const e=t.useSelector(e=>e.voice.connectionStatus),r=t.useSelector(e=>e.voice.pipelineStatus),n=t.useSelector(e=>e.voice.listeningEnabled),o=t.useSelector(e=>e.voice.muted);return e===s.CONNECTING?c.CONNECTING:e!==s.CONNECTED?c.OFFLINE:n?o?c.MUTED:r===a.PROCESSING?c.PROCESSING:r===a.SPEAKING?c.SPEAKING:r===a.LISTENING?c.LISTENING:c.IDLE:c.READY}const B=e.createContext(null),U=e.createContext({strategyActive:!1,voiceSlot:null,onEnterVoice:null,onFloatingOrbClick:null,voiceInputHidden:!1,headerCenterSlot:null,floatOrbPositionRef:null}),q=r.createListenerMiddleware();q.startListening({actionCreator:R,effect:async e=>{const t=n.getStorageRef();t&&await t.setItem(l.SELECTED_MIC_ID,e.payload)}}),q.startListening({actionCreator:N,effect:async e=>{const t=n.getStorageRef();t&&await t.setItem(l.SELECTED_SPEAKER_ID,e.payload)}}),q.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(b())}});class F{async enumerate(){const e={microphones:[],speakers:[]};if(!navigator.mediaDevices?.enumerateDevices)return e;try{const e=await navigator.mediaDevices.enumerateDevices();let t=0,r=0;return{microphones:e.filter(e=>"audioinput"===e.kind).map(e=>({deviceId:e.deviceId,label:e.label||"Microphone "+ ++t})),speakers:e.filter(e=>"audiooutput"===e.kind).map(e=>({deviceId:e.deviceId,label:e.label||"Speaker "+ ++r}))}}catch{return e}}onChange(e){const t=navigator.mediaDevices;return t?.addEventListener?(t.addEventListener("devicechange",e),()=>t.removeEventListener("devicechange",e)):()=>{}}}const W=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{}}},K={attachBotAudioTrack:W,devices:new F,requestMicPermission:async function(){try{return(await navigator.mediaDevices.getUserMedia({audio:!0})).getTracks().forEach(e=>e.stop()),{granted:!0}}catch(e){return{granted:!1,error:e}}}};exports.AudioPlatformContext=A,exports.BrowserAudioDevices=F,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=c,exports.VOICE_PIPELINE_MODES=o,exports.VOICE_PIPELINE_STATUSES=a,exports.VOICE_STORAGE_KEYS=l,exports.VoiceContext=B,exports.VoiceUIContext=U,exports.attachBrowserBotAudioTrack=W,exports.browserAudioPlatform=K,exports.clearBotTurn=_,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(g.actions.setSelectedMicId(r)),n&&t(g.actions.setSelectedSpeakerId(n))},exports.openBotTurn=h,exports.resetIndicatorState=y,exports.setConnectionStatus=E,exports.setInterimTranscript=k,exports.setIsAgentWorking=f,exports.setListeningEnabled=m,exports.setMuted=I,exports.setPipelineStatus=S,exports.setResponseStartTime=v,exports.setSelectedMicId=R,exports.setSelectedSpeakerId=N,exports.setServerMuted=T,exports.setStatusText=C,exports.upsertBotSegment=x,exports.useAudioPlatform=D,exports.useMediaDevices=function(){const r=D(),[n,s]=e.useState([]),[a,o]=e.useState([]),c=t.useSelector(e=>e.voice.listeningEnabled),i=e.useCallback(async()=>{const e=await r.devices.enumerate();s(e.microphones),o(e.speakers)},[r]);return e.useEffect(()=>(i(),r.devices.onChange(i)),[r,i]),e.useEffect(()=>{c&&i()},[c,i]),{microphones:n,speakers:a,refresh:i}},exports.useVoice=()=>{const t=e.useContext(B);if(!t)throw new Error("useVoice must be used within VoiceProvider");return t},exports.useVoiceDisplayState=G,exports.useVoiceProviderValue=({conversationId:r,voiceBridge:c,transportFactory:l,assistantStartsConversation:d})=>{const p=t.useDispatch(),g=D(),R=t.useSelector(e=>e.voice.muted),N=t.useSelector(e=>e.voice.serverMuted),b=t.useSelector(e=>e.voice.pipelineStatus),y=t.useSelector(e=>e.voice.isAgentWorking),{connect:M,disconnect:A,sendText:B,setMicEnabled:U}=function(r,c,l,d){const p=t.useDispatch(),g=D(),I=t.useSelector(e=>e.auth.accessToken),R=e.useRef(c);e.useEffect(()=>{R.current=c});const N=e.useRef(null),b=e.useRef(new Map),y=e.useRef(""),M=e.useRef(!1),A=e.useRef(!1),G=e.useRef(null),B=e.useRef(""),U=e.useRef(null),q=e.useRef(null),F=e.useRef(null),W=e.useRef(0),K=e.useRef(0),j=e.useRef(!1),H=e.useRef(0),$=e.useRef(null),Y=e.useRef(null),Z=e.useRef(0),z=e.useRef(0),J=e.useRef(null),Q=e.useCallback(()=>{if(null!=$.current)return;const e=Date.now();$.current=e,p(v(e)),p(n.clearLastResponseTime())},[p]),X=e.useCallback(()=>{const e=$.current;$.current=null,p(v(null)),null!=e&&p(n.setLastResponseTime(Date.now()-e))},[p]),ee=e.useCallback(()=>{$.current=null,p(v(null))},[p]),te=e.useCallback((e=n.INDICATOR_TIMER_ACTIONS.KEEP)=>{p(C(null)),p(f(!1)),e===n.INDICATOR_TIMER_ACTIONS.FINALIZE?X():e===n.INDICATOR_TIMER_ACTIONS.CANCEL&&ee()},[p,X,ee]),[re]=e.useState(()=>({setStatusText:e=>p(C(e||null)),setIsAgentWorking:e=>p(f(e)),appendStreamText:()=>{},pushWidget:e=>{const t=V("voice-widget-",{widget:[e],message_type:["widget"]});R.current.setMessages(e=>[...e,t])},onPayment:e=>R.current.onPaymentInit?.(e),onConversationTitle:e=>R.current.onConversationTitle?.(e),onError:e=>{n.logger.error({message:"Voice RTVI server error",api_name:"voice-rtvi",error_message:e});const t=V("voice-error-",{message:e,message_type:["error"]});R.current.setMessages(e=>[...e,t])},onStatusActive:()=>{Q()}})),ne=e.useCallback(()=>{F.current&&(clearTimeout(F.current),F.current=null)},[]),se=e.useCallback(()=>[...b.current.entries()].sort((e,t)=>e[0]-t[0]).map(([,e])=>e.text).join(" ").trim(),[]),ae=e.useCallback(()=>{N.current=null,b.current=new Map,y.current="",p(_())},[p]),oe=e.useCallback(()=>{const e=N.current;if(e){const t=se();t&&R.current.updateStreamingMessage(e,t),R.current.commitStreamingMessages()}ae()},[se,ae]),ce=e.useCallback(e=>{ne(),q.current?.(),q.current=null,U.current=null,j.current=!1,p(m(!1)),p(S(a.IDLE)),p(E(s.DISCONNECTED)),p(k(null)),p(T(!1)),G.current=null,B.current="",M.current=!1,A.current=!1,e?oe():(R.current.discardStreamingMessages(),ae()),te(n.INDICATOR_TIMER_ACTIONS.CANCEL)},[p,te,ne,oe,ae]),ie=e.useCallback(()=>{if(N.current)return;const e=`voice-agent-${n.generateUniqueId()}`;N.current=e,b.current=new Map,y.current="",p(h({chatMsgId:e}))},[p]),ue=e.useCallback(e=>{const t=e.spoken_progress?(e.spoken_progress?.accumulated_text??"")+(e.spoken_progress?.remaining_text??""):e.text??"";if(!t)return;ie();const r=M.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}}(b.current,t):function(e,t,r,n){const s=t.segment_id??0,a="completed"===t.spoken_status;if(a&&!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 o=e.get(s);return e.set(s,{text:r,completed:a||!0===o?.completed}),{id:s,text:r,spokenChars:n&&t.spoken_progress?(t.spoken_progress.accumulated_text??"").length:r.length}}(b.current,e,t,A.current);p(x(r));const n=se();n&&N.current&&n!==y.current&&(y.current=n,R.current.updateStreamingMessage(N.current,n))},[p,ie,se]),le=e.useCallback(e=>{const t=e,r=t?.type,s=t?.data;switch(r){case"status_update":n.handleSharedEvent({kind:"status_update",statusText:s?.text??""},re);break;case"started-thinking":p(f(!0)),Q();break;case"stopped-thinking":te(n.INDICATOR_TIMER_ACTIONS.FINALIZE);break;case"clear_status":break;case"widget":s&&n.handleSharedEvent({kind:"widget",widget:s},re);break;case"payment":s&&n.handleSharedEvent({kind:"payment",payment:s},re);break;case"conversation_title":{const e="string"==typeof s?.text?s.text.trim():"";e&&n.handleSharedEvent({kind:"conversation_title",title:e},re);break}default:n.logger.debug({message:"Voice RTVI unhandled server-message",api_name:"voice-rtvi",event_type:r??"unknown"})}},[re,p,Q,te]),de=e.useRef(()=>{}),pe=e.useRef(async()=>{}),ge=e.useCallback(e=>{const t=()=>e===W.current;return{onConnected:()=>{t()&&(Z.current=0,z.current=0,H.current=0,p(E(s.CONNECTED)),p(m(!0)),p(S(a.IDLE)))},onBotReady:e=>{if(!t())return;const r=e?.about;M.current=r?.pipeline_mode===o.REALTIME,A.current=!0===r?.tts_word_timestamps,ne(),d&&(p(f(!0)),Q())},onDisconnected:()=>{if(!t())return;ce(!0);const e=K.current,s=J.current;if(J.current=null,"terminal"!==s){if("auth"===s){if(z.current>=1||null==r)return;return z.current+=1,void(async()=>{let t=!1;try{t=!!await(n.getTokenRefreshHandler()?.())}catch{}e===K.current&&t&&pe.current()})()}Z.current>=3||null==r?Z.current>=3&&n.showErrorToast("Voice connection lost. Please try again."):(Z.current+=1,Y.current&&clearTimeout(Y.current),Y.current=setTimeout(()=>{Y.current=null,pe.current()},1e3))}else n.logger.warn({message:"Voice RTVI terminal error — no retry",api_name:"voice-rtvi"})},onError:e=>{if(!t())return;const r=e?.data,s=r?.error??r?.message??"Voice error";n.handleSharedEvent({kind:"error",error:s},re),p(S(a.IDLE)),te(n.INDICATOR_TIMER_ACTIONS.CANCEL),r?.fatal&&(i.some(e=>e.test(s))?J.current="auth":u.some(e=>e.test(s))&&(J.current="terminal"),U.current?.disconnect().catch(()=>{}))},onTrackStarted:(e,r)=>{t()&&(r?.local||"audio"!==e.kind||(q.current?.(),q.current=g.attachBotAudioTrack(e)))},onUserMuteStarted:()=>{t()&&p(T(!0))},onUserMuteStopped:()=>{t()&&p(T(!1))},onUserStartedSpeaking:()=>{t()&&(G.current=null,B.current="",p(k(null)),p(S(a.LISTENING)))},onUserStoppedSpeaking:()=>{if(!t())return;p(S(a.PROCESSING)),p(f(!0)),p(C(null)),Q();const e=B.current.trim();if(e){const t=`${P}${n.generateUniqueId()}`;G.current=t,p(k(null)),R.current.setMessages(r=>[...r,L(e,t)])}},onUserTranscript:e=>{if(!t())return;if(!e.final)return void(e.text&&(B.current=e.text,M.current||p(k(e.text))));p(k(null));const r=e.text&&e.text.trim()?e.text:"",n=G.current;G.current=null,n?r&&R.current.setMessages(e=>e.map(e=>e.id===n?{...e,message:r}:e)):r&&R.current.setMessages(e=>[...e,L(r)])},onBotStartedSpeaking:()=>{t()&&(ie(),p(S(a.SPEAKING)),te(n.INDICATOR_TIMER_ACTIONS.FINALIZE))},onBotOutput:e=>{t()&&ue(e)},onBotStoppedSpeaking:()=>{t()&&(oe(),p(S(a.IDLE)),te(n.INDICATOR_TIMER_ACTIONS.FINALIZE))},onServerMessage:e=>{t()&&le(e)}}},[p,g,ne,ce,te,Q,ue,ie,oe,le,re,d,r]),Ee=e.useCallback(async()=>{if(U.current||j.current)return;if(!r)return;if(j.current=!0,n.isTokenAboutToExpire(n.readTokenExpiresAt())&&H.current<1){H.current+=1;const e=K.current;try{await Promise.resolve(n.getTokenRefreshHandler()?.())}catch{}if(e!==K.current)return}const e=++W.current,t=K.current;p(E(s.CONNECTING));const a=()=>t!==K.current||e!==W.current;try{const[{PipecatClient:t,LogLevel:s},o]=await Promise.all([Promise.resolve().then(()=>require("./index.module-DCaqEXL5.cjs")),O(r)]);if(a())return;const c=new t({transport:l(),enableMic:!0,enableCam:!1,callbacks:ge(e)});if(c.setLogLevel(s.WARN),a())return void c.disconnect().catch(()=>{});U.current=c,ne(),F.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."),de.current()},15e3),await c.connect({webrtcRequestParams:{endpoint:n.API_ENDPOINTS.WEBRTC_OFFER(r),headers:new Headers(w())},iceConfig:{iceServers:o}})}catch(t){n.logger.error({message:"Voice RTVI connect failed",api_name:"voice-rtvi",error_message:t instanceof Error?t.message:String(t)}),e===W.current&&(n.showErrorToast("Could not connect voice. Please try again."),ce(!1))}},[r,p,l,ge,ne,ce]),Se=e.useCallback((e=!0)=>{K.current+=1,W.current+=1,H.current=0,Y.current&&(clearTimeout(Y.current),Y.current=null),Z.current=0,z.current=0,J.current=null;const t=U.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)})}),ce(e)},[ce]),me=e.useCallback(e=>{U.current?.enableMic(e)},[]),Ie=e.useCallback(e=>{const t=U.current;if(t){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)})}N.current&&oe(),R.current.setMessages(t=>[...t,L(e)]),p(f(!0)),p(C(null)),Q(),p(S(a.PROCESSING))}},[p,Q,oe]);e.useEffect(()=>{de.current=Se},[Se]),e.useEffect(()=>{pe.current=Ee},[Ee]);const Te=e.useRef(r),Ce=e.useRef(!1);e.useEffect(()=>{const e=Te.current;if(Te.current=r,e===r)return;const t=j.current;t&&(Ce.current=!0),t&&Se(!1),Ce.current&&null!=r&&(Ce.current=!1,Ee())},[r,Se,Ee]);const fe=e.useRef(I);return e.useEffect(()=>{const e=fe.current;fe.current=I,e&&!I&&j.current&&Se(!1)},[I,Se]),e.useEffect(()=>()=>{de.current()},[]),{connect:Ee,disconnect:Se,sendText:Ie,setMicEnabled:me}}(r,c,l,d);e.useEffect(()=>{U(!R&&!N)},[R,N,U]);const q=G();e.useEffect(()=>(c.setVoiceSendText(B),()=>c.setVoiceSendText(null)),[c,B]);const F=e.useRef(!1);return{displayState:q,pipelineStatus:b,isAgentWorking:y,enterVoiceMode:e.useCallback(async()=>{if(F.current)return n.showErrorToast("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 n.logger.error({message:"Mic permission denied",api_name:"voice-mic",error_message:t instanceof Error?t.message:t?String(t):void 0}),F.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 M(),!0},[g,M]),exitVoiceMode:e.useCallback(()=>{A()},[A]),toggleMute:e.useCallback(()=>{p(I(!R))},[p,R]),sendText:B,muted:R,serverMuted:N}},exports.useVoiceUI=()=>e.useContext(U),exports.voiceListenerMiddleware=q,exports.voiceReducer=M;
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"},a={IDLE:"idle",LISTENING:"listening",PROCESSING:"processing",SPEAKING:"speaking"},o={CASCADE:"cascade",REALTIME:"realtime"},c={OFFLINE:"offline",CONNECTING:"connecting",READY:"ready",IDLE:"idle",LISTENING:"listening",MUTED:"muted",PROCESSING:"processing",SPEAKING:"speaking"},i=[/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:a.IDLE,listeningEnabled:!1,muted:!1,serverMuted:!1,statusText:null,isAgentWorking:!1,responseStartTime:null,interimTranscript:null,botTurn:{active:!1,chatMsgId:null,segments:[]},selectedMicId:"",selectedSpeakerId:""}}const p=d(),g=r.createSlice({name:"voice",initialState:p,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 a=e.botTurn.segments.find(e=>e.id===r);a?(a.text=n,a.spokenChars=Math.max(a.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},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:E,setPipelineStatus:S,setListeningEnabled:m,setMuted:I,setServerMuted:T,setStatusText:C,setIsAgentWorking:f,setResponseStartTime:v,setInterimTranscript:k,openBotTurn:h,upsertBotSegment:_,clearBotTurn:x,setSelectedMicId:R,setSelectedSpeakerId:N,resetVoiceState:b,resetIndicatorState:y}=g.actions,M=g.reducer,A=e.createContext(null),D=()=>{const t=e.useContext(A);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 w(){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 O(e){try{const t=await fetch(n.API_ENDPOINTS.WEBRTC_ICE(e),{headers:w()});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 P="voice-user-",L=(e,t=`${P}${n.generateUniqueId()}`)=>({id:t,sender:"user",message:e,timestamp:(new Date).toISOString()}),V=(e,t)=>{const r=`${e}${n.generateUniqueId()}`;return{id:r,msg_id:r,sender:"agent",message:"",timestamp:(new Date).toISOString(),...t}};function G(){const e=t.useSelector(e=>e.voice.connectionStatus),r=t.useSelector(e=>e.voice.pipelineStatus),n=t.useSelector(e=>e.voice.listeningEnabled),o=t.useSelector(e=>e.voice.muted);return e===s.CONNECTING?c.CONNECTING:e!==s.CONNECTED?c.OFFLINE:n?o?c.MUTED:r===a.PROCESSING?c.PROCESSING:r===a.SPEAKING?c.SPEAKING:r===a.LISTENING?c.LISTENING:c.IDLE:c.READY}const B=e.createContext(null),U=e.createContext({strategyActive:!1,voiceSlot:null,onEnterVoice:null,onFloatingOrbClick:null,voiceInputHidden:!1,headerCenterSlot:null,floatOrbPositionRef:null}),q=r.createListenerMiddleware();q.startListening({actionCreator:R,effect:async e=>{const t=n.getStorageRef();t&&await t.setItem(l.SELECTED_MIC_ID,e.payload)}}),q.startListening({actionCreator:N,effect:async e=>{const t=n.getStorageRef();t&&await t.setItem(l.SELECTED_SPEAKER_ID,e.payload)}}),q.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(b())}});class F{async enumerate(){const e={microphones:[],speakers:[]};if(!navigator.mediaDevices?.enumerateDevices)return e;try{const e=await navigator.mediaDevices.enumerateDevices();let t=0,r=0;return{microphones:e.filter(e=>"audioinput"===e.kind).map(e=>({deviceId:e.deviceId,label:e.label||"Microphone "+ ++t})),speakers:e.filter(e=>"audiooutput"===e.kind).map(e=>({deviceId:e.deviceId,label:e.label||"Speaker "+ ++r}))}}catch{return e}}onChange(e){const t=navigator.mediaDevices;return t?.addEventListener?(t.addEventListener("devicechange",e),()=>t.removeEventListener("devicechange",e)):()=>{}}}const K=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{}}},W={attachBotAudioTrack:K,devices:new F,requestMicPermission:async function(){try{return(await navigator.mediaDevices.getUserMedia({audio:!0})).getTracks().forEach(e=>e.stop()),{granted:!0}}catch(e){return{granted:!1,error:e}}}};exports.AudioPlatformContext=A,exports.BrowserAudioDevices=F,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=c,exports.VOICE_PIPELINE_MODES=o,exports.VOICE_PIPELINE_STATUSES=a,exports.VOICE_STORAGE_KEYS=l,exports.VoiceContext=B,exports.VoiceUIContext=U,exports.attachBrowserBotAudioTrack=K,exports.browserAudioPlatform=W,exports.clearBotTurn=x,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(g.actions.setSelectedMicId(r)),n&&t(g.actions.setSelectedSpeakerId(n))},exports.openBotTurn=h,exports.resetIndicatorState=y,exports.setConnectionStatus=E,exports.setInterimTranscript=k,exports.setIsAgentWorking=f,exports.setListeningEnabled=m,exports.setMuted=I,exports.setPipelineStatus=S,exports.setResponseStartTime=v,exports.setSelectedMicId=R,exports.setSelectedSpeakerId=N,exports.setServerMuted=T,exports.setStatusText=C,exports.upsertBotSegment=_,exports.useAudioPlatform=D,exports.useMediaDevices=function(){const r=D(),[n,s]=e.useState([]),[a,o]=e.useState([]),c=t.useSelector(e=>e.voice.listeningEnabled),i=e.useCallback(async()=>{const e=await r.devices.enumerate();s(e.microphones),o(e.speakers)},[r]);return e.useEffect(()=>(i(),r.devices.onChange(i)),[r,i]),e.useEffect(()=>{c&&i()},[c,i]),{microphones:n,speakers:a,refresh:i}},exports.useVoice=()=>{const t=e.useContext(B);if(!t)throw new Error("useVoice must be used within VoiceProvider");return t},exports.useVoiceDisplayState=G,exports.useVoiceProviderValue=({conversationId:r,voiceBridge:c,transportFactory:l,assistantStartsConversation:d})=>{const p=t.useDispatch(),g=D(),R=t.useSelector(e=>e.voice.muted),N=t.useSelector(e=>e.voice.serverMuted),b=t.useSelector(e=>e.voice.pipelineStatus),y=t.useSelector(e=>e.voice.isAgentWorking),{connect:M,disconnect:A,sendText:B,setMicEnabled:U}=function(r,c,l,d){const p=t.useDispatch(),g=D(),I=t.useSelector(e=>e.auth.accessToken),R=e.useRef(c);e.useEffect(()=>{R.current=c});const N=e.useRef(null),b=e.useRef(new Map),y=e.useRef(""),M=e.useRef(!1),A=e.useRef(!1),G=e.useRef(null),B=e.useRef(""),U=e.useRef(null),q=e.useRef(null),F=e.useRef(null),K=e.useRef(0),W=e.useRef(0),j=e.useRef(!1),H=e.useRef(0),$=e.useRef(null),Y=e.useRef(null),Z=e.useRef(0),z=e.useRef(0),J=e.useRef(null),Q=e.useCallback(()=>{if(null!=$.current)return;const e=Date.now();$.current=e,p(v(e)),p(n.clearLastResponseTime())},[p]),X=e.useCallback(()=>{const e=$.current;$.current=null,p(v(null)),null!=e&&p(n.setLastResponseTime(Date.now()-e))},[p]),ee=e.useCallback(()=>{$.current=null,p(v(null))},[p]),te=e.useCallback((e=n.INDICATOR_TIMER_ACTIONS.KEEP)=>{p(C(null)),p(f(!1)),e===n.INDICATOR_TIMER_ACTIONS.FINALIZE?X():e===n.INDICATOR_TIMER_ACTIONS.CANCEL&&ee()},[p,X,ee]),[re]=e.useState(()=>({setStatusText:e=>p(C(e||null)),setIsAgentWorking:e=>p(f(e)),appendStreamText:()=>{},pushWidget:e=>{const t=V("voice-widget-",{widget:[e],message_type:["widget"]});R.current.setMessages(e=>[...e,t])},onPayment:e=>R.current.onPaymentInit?.(e),onConversationTitle:e=>R.current.onConversationTitle?.(e),onError:e=>{n.logger.error({message:"Voice RTVI server error",api_name:"voice-rtvi",error_message:e});const t=V("voice-error-",{message:e,message_type:["error"]});R.current.setMessages(e=>[...e,t])},onStatusActive:()=>{Q()}})),ne=e.useCallback(()=>{F.current&&(clearTimeout(F.current),F.current=null)},[]),se=e.useCallback(()=>[...b.current.entries()].sort((e,t)=>e[0]-t[0]).map(([,e])=>e.text).join(" ").trim(),[]),ae=e.useCallback(()=>{N.current=null,b.current=new Map,y.current="",p(x())},[p]),oe=e.useCallback(()=>{const e=N.current;if(e){const t=se();t&&R.current.updateStreamingMessage(e,t),R.current.commitStreamingMessages()}ae()},[se,ae]),ce=e.useCallback(e=>{ne(),q.current?.(),q.current=null,U.current=null,j.current=!1,p(m(!1)),p(S(a.IDLE)),p(E(s.DISCONNECTED)),p(k(null)),p(T(!1)),G.current=null,B.current="",M.current=!1,A.current=!1,e?oe():(R.current.discardStreamingMessages(),ae()),te(n.INDICATOR_TIMER_ACTIONS.CANCEL)},[p,te,ne,oe,ae]),ie=e.useCallback(()=>{if(N.current)return;const e=`voice-agent-${n.generateUniqueId()}`;N.current=e,b.current=new Map,y.current="",p(h({chatMsgId:e}))},[p]),ue=e.useCallback(e=>{const t=e.spoken_progress?(e.spoken_progress?.accumulated_text??"")+(e.spoken_progress?.remaining_text??""):e.text??"";if(!t)return;ie();const r=M.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}}(b.current,t):function(e,t,r,n){const s=t.segment_id??0,a="completed"===t.spoken_status;if(a&&!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 o=e.get(s);return e.set(s,{text:r,completed:a||!0===o?.completed}),{id:s,text:r,spokenChars:n&&t.spoken_progress?(t.spoken_progress.accumulated_text??"").length:r.length}}(b.current,e,t,A.current);p(_(r));const n=se();n&&N.current&&n!==y.current&&(y.current=n,R.current.updateStreamingMessage(N.current,n))},[p,ie,se]),le=e.useCallback(e=>{const t=e,r=t?.type,s=t?.data;switch(r){case"status_update":n.handleSharedEvent({kind:"status_update",statusText:s?.text??""},re);break;case"started-thinking":p(f(!0)),Q();break;case"stopped-thinking":te(n.INDICATOR_TIMER_ACTIONS.FINALIZE);break;case"clear_status":break;case"widget":s&&n.handleSharedEvent({kind:"widget",widget:s},re);break;case"payment":s&&n.handleSharedEvent({kind:"payment",payment:s},re);break;case"conversation_title":{const e="string"==typeof s?.text?s.text.trim():"";e&&n.handleSharedEvent({kind:"conversation_title",title:e},re);break}default:n.logger.debug({message:"Voice RTVI unhandled server-message",api_name:"voice-rtvi",event_type:r??"unknown"})}},[re,p,Q,te]),de=e.useRef(()=>{}),pe=e.useRef(async()=>{}),ge=e.useCallback(e=>{const t=()=>e===K.current;return{onConnected:()=>{t()&&(Z.current=0,z.current=0,H.current=0,p(E(s.CONNECTED)),p(m(!0)),p(S(a.IDLE)))},onBotReady:e=>{if(!t())return;const r=e?.about;M.current=r?.pipeline_mode===o.REALTIME,A.current=!0===r?.tts_word_timestamps,ne(),d&&(p(f(!0)),Q())},onDisconnected:()=>{if(!t())return;ce(!0);const e=W.current,s=J.current;if(J.current=null,"terminal"!==s){if("auth"===s){if(z.current>=1||null==r)return;return z.current+=1,void(async()=>{let t=!1;try{t=!!await(n.getTokenRefreshHandler()?.())}catch{}e===W.current&&t&&pe.current()})()}Z.current>=3||null==r?Z.current>=3&&n.showErrorToast("Voice connection lost. Please try again."):(Z.current+=1,Y.current&&clearTimeout(Y.current),Y.current=setTimeout(()=>{Y.current=null,pe.current()},1e3))}else n.logger.warn({message:"Voice RTVI terminal error — no retry",api_name:"voice-rtvi"})},onError:e=>{if(!t())return;const r=e?.data,s=r?.error??r?.message??"Voice error";n.handleSharedEvent({kind:"error",error:s},re),p(S(a.IDLE)),te(n.INDICATOR_TIMER_ACTIONS.CANCEL),r?.fatal&&(i.some(e=>e.test(s))?J.current="auth":u.some(e=>e.test(s))&&(J.current="terminal"),U.current?.disconnect().catch(()=>{}))},onTrackStarted:(e,r)=>{t()&&(r?.local||"audio"!==e.kind||(q.current?.(),q.current=g.attachBotAudioTrack(e)))},onUserMuteStarted:()=>{t()&&p(T(!0))},onUserMuteStopped:()=>{t()&&p(T(!1))},onUserStartedSpeaking:()=>{t()&&(G.current=null,B.current="",p(k(null)),p(S(a.LISTENING)))},onUserStoppedSpeaking:()=>{if(!t())return;p(S(a.PROCESSING)),p(f(!0)),p(C(null)),Q();const e=B.current.trim();if(e){const t=`${P}${n.generateUniqueId()}`;G.current=t,p(k(null)),R.current.setMessages(r=>[...r,L(e,t)])}},onUserTranscript:e=>{if(!t())return;if(!e.final)return void(e.text&&(B.current=e.text,M.current||p(k(e.text))));p(k(null));const r=e.text&&e.text.trim()?e.text:"",n=G.current;G.current=null,n?r&&R.current.setMessages(e=>e.map(e=>e.id===n?{...e,message:r}:e)):r&&R.current.setMessages(e=>[...e,L(r)])},onBotStartedSpeaking:()=>{t()&&(ie(),p(S(a.SPEAKING)),te(n.INDICATOR_TIMER_ACTIONS.FINALIZE))},onBotOutput:e=>{t()&&ue(e)},onBotStoppedSpeaking:()=>{t()&&(oe(),p(S(a.IDLE)),te(n.INDICATOR_TIMER_ACTIONS.FINALIZE))},onServerMessage:e=>{t()&&le(e)}}},[p,g,ne,ce,te,Q,ue,ie,oe,le,re,d,r]),Ee=e.useCallback(async()=>{if(U.current||j.current)return;if(!r)return;if(j.current=!0,n.isTokenAboutToExpire(n.readTokenExpiresAt())&&H.current<1){H.current+=1;const e=W.current;try{await Promise.resolve(n.getTokenRefreshHandler()?.())}catch{}if(e!==W.current)return}const e=++K.current,t=W.current;p(E(s.CONNECTING));const a=()=>t!==W.current||e!==K.current;try{const[{PipecatClient:t,LogLevel:s},o]=await Promise.all([Promise.resolve().then(()=>require("./index.module-DCaqEXL5.cjs")),O(r)]);if(a())return;const c=new t({transport:l(),enableMic:!0,enableCam:!1,callbacks:ge(e)});if(c.setLogLevel(s.WARN),a())return void c.disconnect().catch(()=>{});U.current=c,ne(),F.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."),de.current()},15e3),await c.connect({webrtcRequestParams:{endpoint:n.API_ENDPOINTS.WEBRTC_OFFER(r),headers:new Headers(w())},iceConfig:{iceServers:o}})}catch(t){n.logger.error({message:"Voice RTVI connect failed",api_name:"voice-rtvi",error_message:t instanceof Error?t.message:String(t)}),e===K.current&&(n.showErrorToast("Could not connect voice. Please try again."),ce(!1))}},[r,p,l,ge,ne,ce]),Se=e.useCallback((e=!0)=>{W.current+=1,K.current+=1,H.current=0,Y.current&&(clearTimeout(Y.current),Y.current=null),Z.current=0,z.current=0,J.current=null;const t=U.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)})}),ce(e)},[ce]),me=e.useCallback(e=>{U.current?.enableMic(e)},[]),Ie=e.useCallback(e=>{const t=U.current;if(t){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)})}N.current&&oe(),R.current.setMessages(t=>[...t,L(e)]),p(f(!0)),p(C(null)),Q(),p(S(a.PROCESSING))}},[p,Q,oe]);e.useEffect(()=>{de.current=Se},[Se]),e.useEffect(()=>{pe.current=Ee},[Ee]);const Te=e.useRef(r),Ce=e.useRef(!1);e.useEffect(()=>{const e=Te.current;if(Te.current=r,e===r)return;const t=j.current;t&&(Ce.current=!0),t&&Se(!1),Ce.current&&null!=r&&(Ce.current=!1,Ee())},[r,Se,Ee]);const fe=e.useRef(I);return e.useEffect(()=>{const e=fe.current;fe.current=I,e&&!I&&j.current&&Se(!1)},[I,Se]),e.useEffect(()=>()=>{de.current()},[]),{connect:Ee,disconnect:Se,sendText:Ie,setMicEnabled:me}}(r,c,l,d);e.useEffect(()=>{U(!R&&!N)},[R,N,U]);const q=G();e.useEffect(()=>(c.setVoiceSendText(B),()=>c.setVoiceSendText(null)),[c,B]);const F=e.useRef(!1);return{displayState:q,pipelineStatus:b,isAgentWorking:y,enterVoiceMode:e.useCallback(async()=>{if(F.current)return n.showErrorToast("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 n.logger.error({message:"Mic permission denied",api_name:"voice-mic",error_message:t instanceof Error?t.message:t?String(t):void 0}),F.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 M(),!0},[g,M]),exitVoiceMode:e.useCallback(()=>{A()},[A]),toggleMute:e.useCallback(()=>{p(I(!R))},[p,R]),sendText:B,muted:R,serverMuted:N}},exports.useVoiceUI=()=>e.useContext(U),exports.voiceListenerMiddleware=q,exports.voiceReducer=M;
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{createContext as e,useContext as t,useRef as r,useEffect as n,useCallback as s,useState as c}from"react";import{useDispatch as a,useSelector as i}from"react-redux";import{createSlice as o,createListenerMiddleware as u,isAnyOf as l}from"@reduxjs/toolkit";import{API_ENDPOINTS as d,logger as p,getCustomHeaders as m,readAccessToken as g,AUTH_HEADERS as E,clearLastResponseTime as S,setLastResponseTime as I,INDICATOR_TIMER_ACTIONS as v,generateUniqueId as T,handleSharedEvent as f,getTokenRefreshHandler as y,showErrorToast as C,isTokenAboutToExpire as h,readTokenExpiresAt as k,getStorageRef as b,clearSession as M,sessionExpired as N}from"@1interface/shared-core";const _={DISCONNECTED:"disconnected",CONNECTING:"connecting",CONNECTED:"connected"},w={IDLE:"idle",LISTENING:"listening",PROCESSING:"processing",SPEAKING:"speaking"},D={CASCADE:"cascade",REALTIME:"realtime"},x={OFFLINE:"offline",CONNECTING:"connecting",READY:"ready",IDLE:"idle",LISTENING:"listening",MUTED:"muted",PROCESSING:"processing",SPEAKING:"speaking"},L=15e3,R=[/rejected/i,/unauthori[sz]ed/i,/forbidden/i,/\b40[13]\b/],P=[/\b402\b/,/\b429\b/,/rate[\s-]?limit/i],A={SELECTED_MIC_ID:"voice_selectedMicId",SELECTED_SPEAKER_ID:"voice_selectedSpeakerId"},V={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 O(){return{connectionStatus:_.DISCONNECTED,pipelineStatus:w.IDLE,listeningEnabled:!1,muted:!1,serverMuted:!1,statusText:null,isAgentWorking:!1,responseStartTime:null,interimTranscript:null,botTurn:{active:!1,chatMsgId:null,segments:[]},selectedMicId:"",selectedSpeakerId:""}}const G=e=>async t=>{const[r,n]=await Promise.all([e.getItem(A.SELECTED_MIC_ID),e.getItem(A.SELECTED_SPEAKER_ID)]);r&&t(B.actions.setSelectedMicId(r)),n&&t(B.actions.setSelectedSpeakerId(n))},B=o({name:"voice",initialState:O(),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 c=e.botTurn.segments.find(e=>e.id===r);c?(c.text=n,c.spokenChars=Math.max(c.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},resetVoiceState:()=>O(),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:F,setPipelineStatus:W,setListeningEnabled:K,setMuted:U,setServerMuted:j,setStatusText:$,setIsAgentWorking:q,setResponseStartTime:Z,setInterimTranscript:H,openBotTurn:Y,upsertBotSegment:z,clearBotTurn:J,setSelectedMicId:Q,setSelectedSpeakerId:X,resetVoiceState:ee,resetIndicatorState:te}=B.actions,re=B.reducer,ne=e(null),se=()=>{const e=t(ne);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 ce(){const e={};for(const[t,r]of Object.entries(m()))r&&(e[t]=r);const t=g();return t&&(e[E.ACCESS_TOKEN]=t),e}async function ae(e){try{const t=await fetch(d.WEBRTC_ICE(e),{headers:ce()});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 ie="voice-user-",oe=(e,t=`${ie}${T()}`)=>({id:t,sender:"user",message:e,timestamp:(new Date).toISOString()}),ue=(e,t)=>{const r=`${e}${T()}`;return{id:r,msg_id:r,sender:"agent",message:"",timestamp:(new Date).toISOString(),...t}};function le(){const e=i(e=>e.voice.connectionStatus),t=i(e=>e.voice.pipelineStatus),r=i(e=>e.voice.listeningEnabled),n=i(e=>e.voice.muted);return e===_.CONNECTING?x.CONNECTING:e!==_.CONNECTED?x.OFFLINE:r?n?x.MUTED:t===w.PROCESSING?x.PROCESSING:t===w.SPEAKING?x.SPEAKING:t===w.LISTENING?x.LISTENING:x.IDLE:x.READY}const de=e(null),pe=({conversationId:e,voiceBridge:t,transportFactory:o,assistantStartsConversation:u})=>{const l=a(),m=se(),g=i(e=>e.voice.muted),E=i(e=>e.voice.serverMuted),b=i(e=>e.voice.pipelineStatus),M=i(e=>e.voice.isAgentWorking),{connect:N,disconnect:x,sendText:L,setMicEnabled:A}=function(e,t,o,u){const l=a(),m=se(),g=i(e=>e.auth.accessToken),E=r(t);n(()=>{E.current=t});const b=r(null),M=r(new Map),N=r(""),x=r(!1),L=r(!1),A=r(null),V=r(""),O=r(null),G=r(null),B=r(null),U=r(0),Q=r(0),X=r(!1),ee=r(0),te=r(null),re=r(null),ne=r(0),le=r(0),de=r(null),pe=s(()=>{if(null!=te.current)return;const e=Date.now();te.current=e,l(Z(e)),l(S())},[l]),me=s(()=>{const e=te.current;te.current=null,l(Z(null)),null!=e&&l(I(Date.now()-e))},[l]),ge=s(()=>{te.current=null,l(Z(null))},[l]),Ee=s((e=v.KEEP)=>{l($(null)),l(q(!1)),e===v.FINALIZE?me():e===v.CANCEL&&ge()},[l,me,ge]),[Se]=c(()=>({setStatusText:e=>l($(e||null)),setIsAgentWorking:e=>l(q(e)),appendStreamText:()=>{},pushWidget:e=>{const t=ue("voice-widget-",{widget:[e],message_type:["widget"]});E.current.setMessages(e=>[...e,t])},onPayment:e=>E.current.onPaymentInit?.(e),onConversationTitle:e=>E.current.onConversationTitle?.(e),onError:e=>{p.error({message:"Voice RTVI server error",api_name:"voice-rtvi",error_message:e});const t=ue("voice-error-",{message:e,message_type:["error"]});E.current.setMessages(e=>[...e,t])},onStatusActive:()=>{pe()}})),Ie=s(()=>{B.current&&(clearTimeout(B.current),B.current=null)},[]),ve=s(()=>[...M.current.entries()].sort((e,t)=>e[0]-t[0]).map(([,e])=>e.text).join(" ").trim(),[]),Te=s(()=>{b.current=null,M.current=new Map,N.current="",l(J())},[l]),fe=s(()=>{const e=b.current;if(e){const t=ve();t&&E.current.updateStreamingMessage(e,t),E.current.commitStreamingMessages()}Te()},[ve,Te]),ye=s(e=>{Ie(),G.current?.(),G.current=null,O.current=null,X.current=!1,l(K(!1)),l(W(w.IDLE)),l(F(_.DISCONNECTED)),l(H(null)),l(j(!1)),A.current=null,V.current="",x.current=!1,L.current=!1,e?fe():(E.current.discardStreamingMessages(),Te()),Ee(v.CANCEL)},[l,Ee,Ie,fe,Te]),Ce=s(()=>{if(b.current)return;const e=`voice-agent-${T()}`;b.current=e,M.current=new Map,N.current="",l(Y({chatMsgId:e}))},[l]),he=s(e=>{const t=e.spoken_progress?(e.spoken_progress?.accumulated_text??"")+(e.spoken_progress?.remaining_text??""):e.text??"";if(!t)return;Ce();const r=x.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}}(M.current,t):function(e,t,r,n){const s=t.segment_id??0,c="completed"===t.spoken_status;if(c&&!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 a=e.get(s);return e.set(s,{text:r,completed:c||!0===a?.completed}),{id:s,text:r,spokenChars:n&&t.spoken_progress?(t.spoken_progress.accumulated_text??"").length:r.length}}(M.current,e,t,L.current);l(z(r));const n=ve();n&&b.current&&n!==N.current&&(N.current=n,E.current.updateStreamingMessage(b.current,n))},[l,Ce,ve]),ke=s(e=>{const t=e,r=t?.type,n=t?.data;switch(r){case"status_update":f({kind:"status_update",statusText:n?.text??""},Se);break;case"started-thinking":l(q(!0)),pe();break;case"stopped-thinking":Ee(v.FINALIZE);break;case"clear_status":break;case"widget":n&&f({kind:"widget",widget:n},Se);break;case"payment":n&&f({kind:"payment",payment:n},Se);break;case"conversation_title":{const e="string"==typeof n?.text?n.text.trim():"";e&&f({kind:"conversation_title",title:e},Se);break}default:p.debug({message:"Voice RTVI unhandled server-message",api_name:"voice-rtvi",event_type:r??"unknown"})}},[Se,l,pe,Ee]),be=r(()=>{}),Me=r(async()=>{}),Ne=s(t=>{const r=()=>t===U.current;return{onConnected:()=>{r()&&(ne.current=0,le.current=0,ee.current=0,l(F(_.CONNECTED)),l(K(!0)),l(W(w.IDLE)))},onBotReady:e=>{if(!r())return;const t=e?.about;x.current=t?.pipeline_mode===D.REALTIME,L.current=!0===t?.tts_word_timestamps,Ie(),u&&(l(q(!0)),pe())},onDisconnected:()=>{if(!r())return;ye(!0);const t=Q.current,n=de.current;if(de.current=null,"terminal"!==n){if("auth"===n){if(le.current>=1||null==e)return;return le.current+=1,void(async()=>{let e=!1;try{e=!!await(y()?.())}catch{}t===Q.current&&e&&Me.current()})()}ne.current>=3||null==e?ne.current>=3&&C("Voice connection lost. Please try again."):(ne.current+=1,re.current&&clearTimeout(re.current),re.current=setTimeout(()=>{re.current=null,Me.current()},1e3))}else p.warn({message:"Voice RTVI terminal error — no retry",api_name:"voice-rtvi"})},onError:e=>{if(!r())return;const t=e?.data,n=t?.error??t?.message??"Voice error";f({kind:"error",error:n},Se),l(W(w.IDLE)),Ee(v.CANCEL),t?.fatal&&(R.some(e=>e.test(n))?de.current="auth":P.some(e=>e.test(n))&&(de.current="terminal"),O.current?.disconnect().catch(()=>{}))},onTrackStarted:(e,t)=>{r()&&(t?.local||"audio"!==e.kind||(G.current?.(),G.current=m.attachBotAudioTrack(e)))},onUserMuteStarted:()=>{r()&&l(j(!0))},onUserMuteStopped:()=>{r()&&l(j(!1))},onUserStartedSpeaking:()=>{r()&&(A.current=null,V.current="",l(H(null)),l(W(w.LISTENING)))},onUserStoppedSpeaking:()=>{if(!r())return;l(W(w.PROCESSING)),l(q(!0)),l($(null)),pe();const e=V.current.trim();if(e){const t=`${ie}${T()}`;A.current=t,l(H(null)),E.current.setMessages(r=>[...r,oe(e,t)])}},onUserTranscript:e=>{if(!r())return;if(!e.final)return void(e.text&&(V.current=e.text,x.current||l(H(e.text))));l(H(null));const t=e.text&&e.text.trim()?e.text:"",n=A.current;A.current=null,n?t&&E.current.setMessages(e=>e.map(e=>e.id===n?{...e,message:t}:e)):t&&E.current.setMessages(e=>[...e,oe(t)])},onBotStartedSpeaking:()=>{r()&&(Ce(),l(W(w.SPEAKING)),Ee(v.FINALIZE))},onBotOutput:e=>{r()&&he(e)},onBotStoppedSpeaking:()=>{r()&&(fe(),l(W(w.IDLE)),Ee(v.FINALIZE))},onServerMessage:e=>{r()&&ke(e)}}},[l,m,Ie,ye,Ee,pe,he,Ce,fe,ke,Se,u,e]),_e=s(async()=>{if(O.current||X.current)return;if(!e)return;if(X.current=!0,h(k())&&ee.current<1){ee.current+=1;const e=Q.current;try{await Promise.resolve(y()?.())}catch{}if(e!==Q.current)return}const t=++U.current,r=Q.current;l(F(_.CONNECTING));const n=()=>r!==Q.current||t!==U.current;try{const[{PipecatClient:r,LogLevel:s},c]=await Promise.all([import("./index.module-S2alA3-r.js"),ae(e)]);if(n())return;const a=new r({transport:o(),enableMic:!0,enableCam:!1,callbacks:Ne(t)});if(a.setLogLevel(s.WARN),n())return void a.disconnect().catch(()=>{});O.current=a,Ie(),B.current=setTimeout(()=>{p.warn({message:"Voice RTVI connect timed out (no bot-ready)",api_name:"voice-rtvi"}),C("Voice connection timed out. Please try again."),be.current()},15e3),await a.connect({webrtcRequestParams:{endpoint:d.WEBRTC_OFFER(e),headers:new Headers(ce())},iceConfig:{iceServers:c}})}catch(e){p.error({message:"Voice RTVI connect failed",api_name:"voice-rtvi",error_message:e instanceof Error?e.message:String(e)}),t===U.current&&(C("Could not connect voice. Please try again."),ye(!1))}},[e,l,o,Ne,Ie,ye]),we=s((e=!0)=>{Q.current+=1,U.current+=1,ee.current=0,re.current&&(clearTimeout(re.current),re.current=null),ne.current=0,le.current=0,de.current=null;const t=O.current;t?.disconnect().catch(e=>{p.warn({message:"Voice RTVI disconnect threw",api_name:"voice-rtvi",error_message:e instanceof Error?e.message:String(e)})}),ye(e)},[ye]),De=s(e=>{O.current?.enableMic(e)},[]),xe=s(e=>{const t=O.current;if(t){try{t.sendText(e,{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)})}b.current&&fe(),E.current.setMessages(t=>[...t,oe(e)]),l(q(!0)),l($(null)),pe(),l(W(w.PROCESSING))}},[l,pe,fe]);n(()=>{be.current=we},[we]),n(()=>{Me.current=_e},[_e]);const Le=r(e),Re=r(!1);n(()=>{const t=Le.current;if(Le.current=e,t===e)return;const r=X.current;r&&(Re.current=!0),r&&we(!1),Re.current&&null!=e&&(Re.current=!1,_e())},[e,we,_e]);const Pe=r(g);return n(()=>{const e=Pe.current;Pe.current=g,e&&!g&&X.current&&we(!1)},[g,we]),n(()=>()=>{be.current()},[]),{connect:_e,disconnect:we,sendText:xe,setMicEnabled:De}}(e,t,o,u);n(()=>{A(!g&&!E)},[g,E,A]);const V=le();n(()=>(t.setVoiceSendText(L),()=>t.setVoiceSendText(null)),[t,L]);const O=r(!1);return{displayState:V,pipelineStatus:b,isAgentWorking:M,enterVoiceMode:s(async()=>{if(O.current)return C("Microphone permission changed — please reload the page to apply the new setting."),!1;const e=await m.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}),O.current=!0,C("Microphone access is required for voice mode. Please allow it in your browser's site settings and reload the page."),!1}return N(),!0},[m,N]),exitVoiceMode:s(()=>{x()},[x]),toggleMute:s(()=>{l(U(!g))},[l,g]),sendText:L,muted:g,serverMuted:E}},me=()=>{const e=t(de);if(!e)throw new Error("useVoice must be used within VoiceProvider");return e},ge=e({strategyActive:!1,voiceSlot:null,onEnterVoice:null,onFloatingOrbClick:null,voiceInputHidden:!1,headerCenterSlot:null,floatOrbPositionRef:null}),Ee=()=>t(ge);function Se(){const e=se(),[t,r]=c([]),[a,o]=c([]),u=i(e=>e.voice.listeningEnabled),l=s(async()=>{const t=await e.devices.enumerate();r(t.microphones),o(t.speakers)},[e]);return n(()=>(l(),e.devices.onChange(l)),[e,l]),n(()=>{u&&l()},[u,l]),{microphones:t,speakers:a,refresh:l}}const Ie=u();Ie.startListening({actionCreator:Q,effect:async e=>{const t=b();t&&await t.setItem(A.SELECTED_MIC_ID,e.payload)}}),Ie.startListening({actionCreator:X,effect:async e=>{const t=b();t&&await t.setItem(A.SELECTED_SPEAKER_ID,e.payload)}}),Ie.startListening({matcher:l(M,N),effect:async(e,t)=>{const r=b();r&&await Promise.all([r.removeItem(A.SELECTED_MIC_ID),r.removeItem(A.SELECTED_SPEAKER_ID)]),t.dispatch(ee())}});class ve{async enumerate(){const e={microphones:[],speakers:[]};if(!navigator.mediaDevices?.enumerateDevices)return e;try{const e=await navigator.mediaDevices.enumerateDevices();let t=0,r=0;return{microphones:e.filter(e=>"audioinput"===e.kind).map(e=>({deviceId:e.deviceId,label:e.label||"Microphone "+ ++t})),speakers:e.filter(e=>"audiooutput"===e.kind).map(e=>({deviceId:e.deviceId,label:e.label||"Speaker "+ ++r}))}}catch{return e}}onChange(e){const t=navigator.mediaDevices;return t?.addEventListener?(t.addEventListener("devicechange",e),()=>t.removeEventListener("devicechange",e)):()=>{}}}const Te=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{}}},fe={attachBotAudioTrack:Te,devices:new ve,requestMicPermission:async function(){try{return(await navigator.mediaDevices.getUserMedia({audio:!0})).getTracks().forEach(e=>e.stop()),{granted:!0}}catch(e){return{granted:!1,error:e}}}};export{ne as AudioPlatformContext,ve as BrowserAudioDevices,V as ORB_STATE_CONFIG,_ as VOICE_CONNECTION_STATUSES,L as VOICE_CONNECT_TIMEOUT_MS,x as VOICE_DISPLAY_STATES,D as VOICE_PIPELINE_MODES,w as VOICE_PIPELINE_STATUSES,A as VOICE_STORAGE_KEYS,de as VoiceContext,ge as VoiceUIContext,Te as attachBrowserBotAudioTrack,fe as browserAudioPlatform,J as clearBotTurn,G as hydrateVoice,Y as openBotTurn,te as resetIndicatorState,F as setConnectionStatus,H as setInterimTranscript,q as setIsAgentWorking,K as setListeningEnabled,U as setMuted,W as setPipelineStatus,Z as setResponseStartTime,Q as setSelectedMicId,X as setSelectedSpeakerId,j as setServerMuted,$ as setStatusText,z as upsertBotSegment,se as useAudioPlatform,Se as useMediaDevices,me as useVoice,le as useVoiceDisplayState,pe as useVoiceProviderValue,Ee as useVoiceUI,Ie as voiceListenerMiddleware,re as voiceReducer};
1
+ import{createContext as e,useContext as t,useRef as r,useEffect as n,useCallback as s,useState as c}from"react";import{useDispatch as a,useSelector as i}from"react-redux";import{createSlice as o,createListenerMiddleware as u,isAnyOf as l}from"@reduxjs/toolkit";import{API_ENDPOINTS as d,logger as p,getCustomHeaders as m,readAccessToken as g,AUTH_HEADERS as E,readDeviceToken as S,DEVICE_TOKEN_HEADER as I,clearLastResponseTime as v,setLastResponseTime as T,INDICATOR_TIMER_ACTIONS as f,generateUniqueId as y,handleSharedEvent as C,getTokenRefreshHandler as h,showErrorToast as k,isTokenAboutToExpire as b,readTokenExpiresAt as M,getStorageRef as N,clearSession as _,sessionExpired as w}from"@1interface/shared-core";const D={DISCONNECTED:"disconnected",CONNECTING:"connecting",CONNECTED:"connected"},x={IDLE:"idle",LISTENING:"listening",PROCESSING:"processing",SPEAKING:"speaking"},L={CASCADE:"cascade",REALTIME:"realtime"},R={OFFLINE:"offline",CONNECTING:"connecting",READY:"ready",IDLE:"idle",LISTENING:"listening",MUTED:"muted",PROCESSING:"processing",SPEAKING:"speaking"},P=15e3,A=[/rejected/i,/unauthori[sz]ed/i,/forbidden/i,/\b40[13]\b/],V=[/\b402\b/,/\b429\b/,/rate[\s-]?limit/i],O={SELECTED_MIC_ID:"voice_selectedMicId",SELECTED_SPEAKER_ID:"voice_selectedSpeakerId"},G={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 B(){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:[]},selectedMicId:"",selectedSpeakerId:""}}const F=e=>async t=>{const[r,n]=await Promise.all([e.getItem(O.SELECTED_MIC_ID),e.getItem(O.SELECTED_SPEAKER_ID)]);r&&t(W.actions.setSelectedMicId(r)),n&&t(W.actions.setSelectedSpeakerId(n))},W=o({name:"voice",initialState:B(),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 c=e.botTurn.segments.find(e=>e.id===r);c?(c.text=n,c.spokenChars=Math.max(c.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},resetVoiceState:()=>B(),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:U,setListeningEnabled:j,setMuted:$,setServerMuted:q,setStatusText:Z,setIsAgentWorking:H,setResponseStartTime:Y,setInterimTranscript:z,openBotTurn:J,upsertBotSegment:Q,clearBotTurn:X,setSelectedMicId:ee,setSelectedSpeakerId:te,resetVoiceState:re,resetIndicatorState:ne}=W.actions,se=W.reducer,ce=e(null),ae=()=>{const e=t(ce);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 ie(){const e={};for(const[t,r]of Object.entries(m()))r&&(e[t]=r);const t=g();t&&(e[E.ACCESS_TOKEN]=t);const r=S();return r&&(e[I]=r),e}async function oe(e){try{const t=await fetch(d.WEBRTC_ICE(e),{headers:ie()});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 ue="voice-user-",le=(e,t=`${ue}${y()}`)=>({id:t,sender:"user",message:e,timestamp:(new Date).toISOString()}),de=(e,t)=>{const r=`${e}${y()}`;return{id:r,msg_id:r,sender:"agent",message:"",timestamp:(new Date).toISOString(),...t}};function pe(){const e=i(e=>e.voice.connectionStatus),t=i(e=>e.voice.pipelineStatus),r=i(e=>e.voice.listeningEnabled),n=i(e=>e.voice.muted);return e===D.CONNECTING?R.CONNECTING:e!==D.CONNECTED?R.OFFLINE:r?n?R.MUTED:t===x.PROCESSING?R.PROCESSING:t===x.SPEAKING?R.SPEAKING:t===x.LISTENING?R.LISTENING:R.IDLE:R.READY}const me=e(null),ge=({conversationId:e,voiceBridge:t,transportFactory:o,assistantStartsConversation:u})=>{const l=a(),m=ae(),g=i(e=>e.voice.muted),E=i(e=>e.voice.serverMuted),S=i(e=>e.voice.pipelineStatus),I=i(e=>e.voice.isAgentWorking),{connect:N,disconnect:_,sendText:w,setMicEnabled:R}=function(e,t,o,u){const l=a(),m=ae(),g=i(e=>e.auth.accessToken),E=r(t);n(()=>{E.current=t});const S=r(null),I=r(new Map),N=r(""),_=r(!1),w=r(!1),R=r(null),P=r(""),O=r(null),G=r(null),B=r(null),F=r(0),W=r(0),$=r(!1),ee=r(0),te=r(null),re=r(null),ne=r(0),se=r(0),ce=r(null),pe=s(()=>{if(null!=te.current)return;const e=Date.now();te.current=e,l(Y(e)),l(v())},[l]),me=s(()=>{const e=te.current;te.current=null,l(Y(null)),null!=e&&l(T(Date.now()-e))},[l]),ge=s(()=>{te.current=null,l(Y(null))},[l]),Ee=s((e=f.KEEP)=>{l(Z(null)),l(H(!1)),e===f.FINALIZE?me():e===f.CANCEL&&ge()},[l,me,ge]),[Se]=c(()=>({setStatusText:e=>l(Z(e||null)),setIsAgentWorking:e=>l(H(e)),appendStreamText:()=>{},pushWidget:e=>{const t=de("voice-widget-",{widget:[e],message_type:["widget"]});E.current.setMessages(e=>[...e,t])},onPayment:e=>E.current.onPaymentInit?.(e),onConversationTitle:e=>E.current.onConversationTitle?.(e),onError:e=>{p.error({message:"Voice RTVI server error",api_name:"voice-rtvi",error_message:e});const t=de("voice-error-",{message:e,message_type:["error"]});E.current.setMessages(e=>[...e,t])},onStatusActive:()=>{pe()}})),Ie=s(()=>{B.current&&(clearTimeout(B.current),B.current=null)},[]),ve=s(()=>[...I.current.entries()].sort((e,t)=>e[0]-t[0]).map(([,e])=>e.text).join(" ").trim(),[]),Te=s(()=>{S.current=null,I.current=new Map,N.current="",l(X())},[l]),fe=s(()=>{const e=S.current;if(e){const t=ve();t&&E.current.updateStreamingMessage(e,t),E.current.commitStreamingMessages()}Te()},[ve,Te]),ye=s(e=>{Ie(),G.current?.(),G.current=null,O.current=null,$.current=!1,l(j(!1)),l(U(x.IDLE)),l(K(D.DISCONNECTED)),l(z(null)),l(q(!1)),R.current=null,P.current="",_.current=!1,w.current=!1,e?fe():(E.current.discardStreamingMessages(),Te()),Ee(f.CANCEL)},[l,Ee,Ie,fe,Te]),Ce=s(()=>{if(S.current)return;const e=`voice-agent-${y()}`;S.current=e,I.current=new Map,N.current="",l(J({chatMsgId:e}))},[l]),he=s(e=>{const t=e.spoken_progress?(e.spoken_progress?.accumulated_text??"")+(e.spoken_progress?.remaining_text??""):e.text??"";if(!t)return;Ce();const r=_.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}}(I.current,t):function(e,t,r,n){const s=t.segment_id??0,c="completed"===t.spoken_status;if(c&&!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 a=e.get(s);return e.set(s,{text:r,completed:c||!0===a?.completed}),{id:s,text:r,spokenChars:n&&t.spoken_progress?(t.spoken_progress.accumulated_text??"").length:r.length}}(I.current,e,t,w.current);l(Q(r));const n=ve();n&&S.current&&n!==N.current&&(N.current=n,E.current.updateStreamingMessage(S.current,n))},[l,Ce,ve]),ke=s(e=>{const t=e,r=t?.type,n=t?.data;switch(r){case"status_update":C({kind:"status_update",statusText:n?.text??""},Se);break;case"started-thinking":l(H(!0)),pe();break;case"stopped-thinking":Ee(f.FINALIZE);break;case"clear_status":break;case"widget":n&&C({kind:"widget",widget:n},Se);break;case"payment":n&&C({kind:"payment",payment:n},Se);break;case"conversation_title":{const e="string"==typeof n?.text?n.text.trim():"";e&&C({kind:"conversation_title",title:e},Se);break}default:p.debug({message:"Voice RTVI unhandled server-message",api_name:"voice-rtvi",event_type:r??"unknown"})}},[Se,l,pe,Ee]),be=r(()=>{}),Me=r(async()=>{}),Ne=s(t=>{const r=()=>t===F.current;return{onConnected:()=>{r()&&(ne.current=0,se.current=0,ee.current=0,l(K(D.CONNECTED)),l(j(!0)),l(U(x.IDLE)))},onBotReady:e=>{if(!r())return;const t=e?.about;_.current=t?.pipeline_mode===L.REALTIME,w.current=!0===t?.tts_word_timestamps,Ie(),u&&(l(H(!0)),pe())},onDisconnected:()=>{if(!r())return;ye(!0);const t=W.current,n=ce.current;if(ce.current=null,"terminal"!==n){if("auth"===n){if(se.current>=1||null==e)return;return se.current+=1,void(async()=>{let e=!1;try{e=!!await(h()?.())}catch{}t===W.current&&e&&Me.current()})()}ne.current>=3||null==e?ne.current>=3&&k("Voice connection lost. Please try again."):(ne.current+=1,re.current&&clearTimeout(re.current),re.current=setTimeout(()=>{re.current=null,Me.current()},1e3))}else p.warn({message:"Voice RTVI terminal error — no retry",api_name:"voice-rtvi"})},onError:e=>{if(!r())return;const t=e?.data,n=t?.error??t?.message??"Voice error";C({kind:"error",error:n},Se),l(U(x.IDLE)),Ee(f.CANCEL),t?.fatal&&(A.some(e=>e.test(n))?ce.current="auth":V.some(e=>e.test(n))&&(ce.current="terminal"),O.current?.disconnect().catch(()=>{}))},onTrackStarted:(e,t)=>{r()&&(t?.local||"audio"!==e.kind||(G.current?.(),G.current=m.attachBotAudioTrack(e)))},onUserMuteStarted:()=>{r()&&l(q(!0))},onUserMuteStopped:()=>{r()&&l(q(!1))},onUserStartedSpeaking:()=>{r()&&(R.current=null,P.current="",l(z(null)),l(U(x.LISTENING)))},onUserStoppedSpeaking:()=>{if(!r())return;l(U(x.PROCESSING)),l(H(!0)),l(Z(null)),pe();const e=P.current.trim();if(e){const t=`${ue}${y()}`;R.current=t,l(z(null)),E.current.setMessages(r=>[...r,le(e,t)])}},onUserTranscript:e=>{if(!r())return;if(!e.final)return void(e.text&&(P.current=e.text,_.current||l(z(e.text))));l(z(null));const t=e.text&&e.text.trim()?e.text:"",n=R.current;R.current=null,n?t&&E.current.setMessages(e=>e.map(e=>e.id===n?{...e,message:t}:e)):t&&E.current.setMessages(e=>[...e,le(t)])},onBotStartedSpeaking:()=>{r()&&(Ce(),l(U(x.SPEAKING)),Ee(f.FINALIZE))},onBotOutput:e=>{r()&&he(e)},onBotStoppedSpeaking:()=>{r()&&(fe(),l(U(x.IDLE)),Ee(f.FINALIZE))},onServerMessage:e=>{r()&&ke(e)}}},[l,m,Ie,ye,Ee,pe,he,Ce,fe,ke,Se,u,e]),_e=s(async()=>{if(O.current||$.current)return;if(!e)return;if($.current=!0,b(M())&&ee.current<1){ee.current+=1;const e=W.current;try{await Promise.resolve(h()?.())}catch{}if(e!==W.current)return}const t=++F.current,r=W.current;l(K(D.CONNECTING));const n=()=>r!==W.current||t!==F.current;try{const[{PipecatClient:r,LogLevel:s},c]=await Promise.all([import("./index.module-S2alA3-r.js"),oe(e)]);if(n())return;const a=new r({transport:o(),enableMic:!0,enableCam:!1,callbacks:Ne(t)});if(a.setLogLevel(s.WARN),n())return void a.disconnect().catch(()=>{});O.current=a,Ie(),B.current=setTimeout(()=>{p.warn({message:"Voice RTVI connect timed out (no bot-ready)",api_name:"voice-rtvi"}),k("Voice connection timed out. Please try again."),be.current()},15e3),await a.connect({webrtcRequestParams:{endpoint:d.WEBRTC_OFFER(e),headers:new Headers(ie())},iceConfig:{iceServers:c}})}catch(e){p.error({message:"Voice RTVI connect failed",api_name:"voice-rtvi",error_message:e instanceof Error?e.message:String(e)}),t===F.current&&(k("Could not connect voice. Please try again."),ye(!1))}},[e,l,o,Ne,Ie,ye]),we=s((e=!0)=>{W.current+=1,F.current+=1,ee.current=0,re.current&&(clearTimeout(re.current),re.current=null),ne.current=0,se.current=0,ce.current=null;const t=O.current;t?.disconnect().catch(e=>{p.warn({message:"Voice RTVI disconnect threw",api_name:"voice-rtvi",error_message:e instanceof Error?e.message:String(e)})}),ye(e)},[ye]),De=s(e=>{O.current?.enableMic(e)},[]),xe=s(e=>{const t=O.current;if(t){try{t.sendText(e,{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&&fe(),E.current.setMessages(t=>[...t,le(e)]),l(H(!0)),l(Z(null)),pe(),l(U(x.PROCESSING))}},[l,pe,fe]);n(()=>{be.current=we},[we]),n(()=>{Me.current=_e},[_e]);const Le=r(e),Re=r(!1);n(()=>{const t=Le.current;if(Le.current=e,t===e)return;const r=$.current;r&&(Re.current=!0),r&&we(!1),Re.current&&null!=e&&(Re.current=!1,_e())},[e,we,_e]);const Pe=r(g);return n(()=>{const e=Pe.current;Pe.current=g,e&&!g&&$.current&&we(!1)},[g,we]),n(()=>()=>{be.current()},[]),{connect:_e,disconnect:we,sendText:xe,setMicEnabled:De}}(e,t,o,u);n(()=>{R(!g&&!E)},[g,E,R]);const P=pe();n(()=>(t.setVoiceSendText(w),()=>t.setVoiceSendText(null)),[t,w]);const O=r(!1);return{displayState:P,pipelineStatus:S,isAgentWorking:I,enterVoiceMode:s(async()=>{if(O.current)return k("Microphone permission changed — please reload the page to apply the new setting."),!1;const e=await m.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}),O.current=!0,k("Microphone access is required for voice mode. Please allow it in your browser's site settings and reload the page."),!1}return N(),!0},[m,N]),exitVoiceMode:s(()=>{_()},[_]),toggleMute:s(()=>{l($(!g))},[l,g]),sendText:w,muted:g,serverMuted:E}},Ee=()=>{const e=t(me);if(!e)throw new Error("useVoice must be used within VoiceProvider");return e},Se=e({strategyActive:!1,voiceSlot:null,onEnterVoice:null,onFloatingOrbClick:null,voiceInputHidden:!1,headerCenterSlot:null,floatOrbPositionRef:null}),Ie=()=>t(Se);function ve(){const e=ae(),[t,r]=c([]),[a,o]=c([]),u=i(e=>e.voice.listeningEnabled),l=s(async()=>{const t=await e.devices.enumerate();r(t.microphones),o(t.speakers)},[e]);return n(()=>(l(),e.devices.onChange(l)),[e,l]),n(()=>{u&&l()},[u,l]),{microphones:t,speakers:a,refresh:l}}const Te=u();Te.startListening({actionCreator:ee,effect:async e=>{const t=N();t&&await t.setItem(O.SELECTED_MIC_ID,e.payload)}}),Te.startListening({actionCreator:te,effect:async e=>{const t=N();t&&await t.setItem(O.SELECTED_SPEAKER_ID,e.payload)}}),Te.startListening({matcher:l(_,w),effect:async(e,t)=>{const r=N();r&&await Promise.all([r.removeItem(O.SELECTED_MIC_ID),r.removeItem(O.SELECTED_SPEAKER_ID)]),t.dispatch(re())}});class fe{async enumerate(){const e={microphones:[],speakers:[]};if(!navigator.mediaDevices?.enumerateDevices)return e;try{const e=await navigator.mediaDevices.enumerateDevices();let t=0,r=0;return{microphones:e.filter(e=>"audioinput"===e.kind).map(e=>({deviceId:e.deviceId,label:e.label||"Microphone "+ ++t})),speakers:e.filter(e=>"audiooutput"===e.kind).map(e=>({deviceId:e.deviceId,label:e.label||"Speaker "+ ++r}))}}catch{return e}}onChange(e){const t=navigator.mediaDevices;return t?.addEventListener?(t.addEventListener("devicechange",e),()=>t.removeEventListener("devicechange",e)):()=>{}}}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{}}},Ce={attachBotAudioTrack:ye,devices:new fe,requestMicPermission:async function(){try{return(await navigator.mediaDevices.getUserMedia({audio:!0})).getTracks().forEach(e=>e.stop()),{granted:!0}}catch(e){return{granted:!1,error:e}}}};export{ce as AudioPlatformContext,fe as BrowserAudioDevices,G as ORB_STATE_CONFIG,D as VOICE_CONNECTION_STATUSES,P as VOICE_CONNECT_TIMEOUT_MS,R as VOICE_DISPLAY_STATES,L as VOICE_PIPELINE_MODES,x as VOICE_PIPELINE_STATUSES,O as VOICE_STORAGE_KEYS,me as VoiceContext,Se as VoiceUIContext,ye as attachBrowserBotAudioTrack,Ce as browserAudioPlatform,X as clearBotTurn,F as hydrateVoice,J as openBotTurn,ne as resetIndicatorState,K as setConnectionStatus,z as setInterimTranscript,H as setIsAgentWorking,j as setListeningEnabled,$ as setMuted,U as setPipelineStatus,Y as setResponseStartTime,ee as setSelectedMicId,te as setSelectedSpeakerId,q as setServerMuted,Z as setStatusText,Q as upsertBotSegment,ae as useAudioPlatform,ve as useMediaDevices,Ee as useVoice,pe as useVoiceDisplayState,ge as useVoiceProviderValue,Ie as useVoiceUI,Te as voiceListenerMiddleware,se as voiceReducer};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1interface/voice-core",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "@pipecat-ai/client-js": "^1.11.0",
30
- "@1interface/shared-core": "^0.2.0"
30
+ "@1interface/shared-core": "^0.2.1"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "@reduxjs/toolkit": "^2.0.0",