Spaces:
Running
feat: sync robot preferences to HF, thinking movement + wait/announce on plan
Browse filesSettings sync (user noticed prefs didn't sync): the HF dataset only carried
AppData. Now a NON-SECRET settings subset (language, robotName, voice,
activationMode, allowInterrupt, inventorySort) rides along in a separate
cookaiware-settings.json. Secrets (openaiKey, hfToken) and device-local state
(lastSyncedAt, hfDatasetSync) are deliberately never uploaded. pushToCloud sends
them; "Load from cloud" and the auto-sync "keep cloud" branch pull + validate
them (applySyncableSettings keeps the current value for any invalid field).
Silent-thinking fix: the menu designer takes ~10-20 s during which the robot sat
still and mute. Now:
- VoiceController.startThinking/stopThinking pulses a VARIED thinking body
language (mostly "thinking", an occasional glance) every 2.6 s; app wraps both
designAndCommitPlan and designMeal awaits with it (finally-stopped, also
cleared on session stop). Only active during a live voice session.
- Prompt now tells Reachy to ask the user to wait BEFORE calling the slow tool,
and to announce the result out loud when it returns.
No functional change to transcription — still gpt-4o-mini-transcribe.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- assets/{embed-DS9p21GA.js → embed-eV4K8xkB.js} +0 -0
- assets/{index-BAHUO-4O.js → index-6cwrBYEF.js} +2 -2
- index.html +1 -1
- web/src/storage/hfDataset.ts +31 -4
- web/src/storage/store.ts +42 -0
- web/src/ui/app.ts +21 -2
- web/src/voice/controller.ts +22 -0
- web/src/voice/realtime.ts +1 -1
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/auto-CaTD3xKq.js","assets/sub-paths-F6TP7MGR-8fYyfeHC.js","assets/embed-
|
| 2 |
var G=Object.defineProperty;var q=(o,a,e)=>a in o?G(o,a,{enumerable:!0,configurable:!0,writable:!0,value:e}):o[a]=e;var d=(o,a,e)=>q(o,typeof a!="symbol"?a+"":a,e);(function(){const a=document.createElement("link").relList;if(a&&a.supports&&a.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))t(i);new MutationObserver(i=>{for(const s of i)if(s.type==="childList")for(const n of s.addedNodes)n.tagName==="LINK"&&n.rel==="modulepreload"&&t(n)}).observe(document,{childList:!0,subtree:!0});function e(i){const s={};return i.integrity&&(s.integrity=i.integrity),i.referrerPolicy&&(s.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?s.credentials="include":i.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function t(i){if(i.ep)return;i.ep=!0;const s=e(i);fetch(i.href,s)}})();const W="modulepreload",z=function(o){return"/"+o},O={},U=function(a,e,t){let i=Promise.resolve();if(e&&e.length>0){document.getElementsByTagName("link");const n=document.querySelector("meta[property=csp-nonce]"),r=(n==null?void 0:n.nonce)||(n==null?void 0:n.getAttribute("nonce"));i=Promise.allSettled(e.map(c=>{if(c=z(c),c in O)return;O[c]=!0;const l=c.endsWith(".css"),h=l?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${c}"]${h}`))return;const u=document.createElement("link");if(u.rel=l?"stylesheet":W,l||(u.as="script"),u.crossOrigin="",u.href=c,r&&u.setAttribute("nonce",r),document.head.appendChild(u),l)return new Promise((p,g)=>{u.addEventListener("load",p),u.addEventListener("error",()=>g(new Error(`Unable to preload CSS for ${c}`)))})}))}function s(n){const r=new Event("vite:preloadError",{cancelable:!0});if(r.payload=n,window.dispatchEvent(r),!r.defaultPrevented)throw n}return i.then(n=>{for(const r of n||[])r.status==="rejected"&&s(r.reason);return a().catch(s)})};var J=Object.defineProperty,K=(o,a,e)=>a in o?J(o,a,{enumerable:!0,configurable:!0,writable:!0,value:e}):o[a]=e,k=(o,a,e)=>(K(o,typeof a!="symbol"?a+"":a,e),e),B="https://huggingface.co";async function T(o,a){var e,t;const i=new X(o.url,o.status,(e=o.headers.get("X-Request-Id"))!=null?e:void 0);i.message=`Api error with status ${i.statusCode}`;const s=[`URL: ${i.url}`,i.requestId?`Request ID: ${i.requestId}`:void 0].filter(Boolean).join(". ");if((t=o.headers.get("Content-Type"))!=null&&t.startsWith("application/json")){const n=await o.json();i.message=n.error||n.message||i.message,i.data=n}else i.data={message:await o.text()};throw i.message+=`. ${s}`,i}var X=class extends Error{constructor(o,a,e,t){super(t),k(this,"statusCode"),k(this,"url"),k(this,"requestId"),k(this,"data"),this.statusCode=a,this.requestId=e,this.url=o}};new Promise(o=>{});function Y(o){if(globalThis.Buffer)return globalThis.Buffer.from(o).toString("base64");{const a=[];return o.forEach(e=>{a.push(String.fromCharCode(e))}),globalThis.btoa(a.join(""))}}async function $(o){var a,e;if(typeof window>"u")throw new Error("oauthHandleRedirect is only available in the browser");const t=new URLSearchParams(window.location.search),[i,s]=[t.get("error"),t.get("error_description")];if(i)throw new Error(`${i}: ${s}`);const n=t.get("code"),r=localStorage.getItem("huggingface.co:oauth:nonce");if(!n)throw new Error("Missing oauth code from query parameters in redirected URL");if(!r)throw new Error("Missing oauth nonce from localStorage");const c=localStorage.getItem("huggingface.co:oauth:code_verifier");if(!c)throw new Error("Missing oauth code_verifier from localStorage");const l=t.get("state");if(!l)throw new Error("Missing oauth state from query parameters in redirected URL");let h;try{h=JSON.parse(l)}catch{throw new Error("Invalid oauth state in redirected URL, unable to parse JSON: "+l)}if(h.nonce!==r)throw new Error("Invalid oauth state in redirected URL");const u=B,p=`${new URL(u).origin}/.well-known/openid-configuration`,g=await fetch(p,{headers:{Accept:"application/json"}});if(!g.ok)throw await T(g);const w=await g.json(),m=await fetch(w.token_endpoint,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"authorization_code",code:n,redirect_uri:h.redirectUri,code_verifier:c}).toString()});if(localStorage.removeItem("huggingface.co:oauth:code_verifier"),localStorage.removeItem("huggingface.co:oauth:nonce"),!m.ok)throw await T(m);const f=await m.json(),y=new Date(Date.now()+f.expires_in*1e3),v=await fetch(w.userinfo_endpoint,{headers:{Authorization:`Bearer ${f.access_token}`}});if(!v.ok)throw await T(v);const b=await v.json();return{accessToken:f.access_token,accessTokenExpiresAt:y,userInfo:{id:b.sub,name:b.name,fullname:b.preferred_username,email:b.email,emailVerified:b.email_verified,avatarUrl:b.picture,websiteUrl:b.website,isPro:b.isPro,orgs:(e=(a=b.orgs)==null?void 0:a.map(R=>({id:R.sub,name:R.name,fullname:R.name,isEnterprise:R.isEnterprise,canPay:R.canPay,avatarUrl:R.picture,roleInOrg:R.roleInOrg})))!=null?e:[]},state:h.state,scope:f.scope}}async function Q(o){if(typeof window>"u")throw new Error("oauthHandleRedirect is only available in the browser");const a=new URLSearchParams(window.location.search);return a.has("error")?$():a.has("code")?localStorage.getItem("huggingface.co:oauth:nonce")?$():(console.warn("Missing oauth nonce from localStorage. This can happen when the user refreshes the page after logging in, without changing the URL."),!1):!1}async function Z(o){var a,e;if(typeof window>"u")throw new Error("oauthLogin is only available in the browser");const t=(o==null?void 0:o.hubUrl)||B,i=`${new URL(t).origin}/.well-known/openid-configuration`,s=await fetch(i,{headers:{Accept:"application/json"}});if(!s.ok)throw await T(s);const n=await s.json(),r=globalThis.crypto.randomUUID(),c=globalThis.crypto.randomUUID()+globalThis.crypto.randomUUID();localStorage.setItem("huggingface.co:oauth:nonce",r),localStorage.setItem("huggingface.co:oauth:code_verifier",c);const l=(o==null?void 0:o.redirectUrl)||window.location.href,h=JSON.stringify({nonce:r,redirectUri:l,state:o==null?void 0:o.state}),u=(e=(a=window==null?void 0:window.huggingface)==null?void 0:a.variables)!=null?e:null,p=(o==null?void 0:o.clientId)||(u==null?void 0:u.OAUTH_CLIENT_ID);if(!p)throw u?new Error("Missing clientId, please add hf_oauth: true to the README.md's metadata in your static Space"):new Error("Missing clientId");const g=Y(new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256",new TextEncoder().encode(c)))).replace(/[+]/g,"-").replace(/[/]/g,"_").replace(/=/g,"");return`${n.authorization_endpoint}?${new URLSearchParams({client_id:p,scope:(o==null?void 0:o.scopes)||(u==null?void 0:u.OAUTH_SCOPES)||"openid profile",response_type:"code",redirect_uri:l,state:h,code_challenge:g,code_challenge_method:"S256"}).toString()}`}function C(o){return o*Math.PI/180}function _e(o){return o*180/Math.PI}function ee(o,a,e){const t=C(o),i=C(a),s=C(e),n=Math.cos(s),r=Math.sin(s),c=Math.cos(i),l=Math.sin(i),h=Math.cos(t),u=Math.sin(t);return[[n*c,n*l*u-r*h,n*l*h+r*u,0],[r*c,r*l*u+n*h,r*l*h-n*u,0],[-l,c*u,c*h,0],[0,0,0,1]]}function te(){if(typeof window>"u"||!window.location.hash)return;const o=window.location.hash.startsWith("#")?window.location.hash.slice(1):window.location.hash;let a;try{a=new URLSearchParams(o)}catch{return}const e=a.get("hf_token");if(!e)return;const t=a.get("hf_username")||"user",i=a.get("hf_token_expires"),s=i&&!Number.isNaN(new Date(i).getTime())?i:new Date(Date.now()+365*24*60*60*1e3).toISOString();try{sessionStorage.setItem("hf_token",e),sessionStorage.setItem("hf_username",t),sessionStorage.setItem("hf_token_expires",s)}catch(c){console.warn("[reachy-mini] could not persist pre-seeded HF credentials:",c)}a.delete("hf_token"),a.delete("hf_username"),a.delete("hf_token_expires");const n=a.toString(),r=window.location.pathname+window.location.search+(n?"#"+n:"");try{window.history.replaceState(null,"",r)}catch{}}function ie(){if(typeof window>"u")return null;if(window.location.hash){const o=window.location.hash.startsWith("#")?window.location.hash.slice(1):window.location.hash;try{const e=new URLSearchParams(o).get("robot_peer_id");if(e)return e}catch{}}if(window.location.search)try{const a=new URLSearchParams(window.location.search).get("robot_peer_id");if(a)return a}catch{}return null}function se(o){const a=o.split(`\r
|
| 3 |
`);let e=!1;for(const t of a)if(t.startsWith("m=audio")?e=!0:t.startsWith("m=")&&(e=!1),e&&t==="a=sendrecv")return!0;return!1}const S=12*1024,L=1*1024*1024,ne=512*1024;function oe(){return typeof CompressionStream<"u"}function x(){return"u"+Math.random().toString(36).slice(2,11)+Date.now().toString(36)}function P(o){let a="";for(let t=0;t<o.length;t+=32768)a+=String.fromCharCode.apply(null,Array.from(o.subarray(t,t+32768)));return btoa(a)}async function re(o){const a=new TextEncoder().encode(o),e=await new Response(new Blob([a]).stream().pipeThrough(new CompressionStream("gzip"))).arrayBuffer();return P(new Uint8Array(e))}function N(o){const a=Math.round(Number(o)||0);return Math.max(0,Math.min(100,a))}const H=3e3,V=1e3,ae=6e4;class le extends EventTarget{constructor(e={}){super();d(this,"_signalingUrl");d(this,"_clientId");d(this,"_appName");d(this,"_videoJitterBufferTargetMs");d(this,"_autoStartFromUrl");d(this,"_autoStartAttempted");d(this,"_state","disconnected");d(this,"_robots",[]);d(this,"_robotState",{});d(this,"_preselectedRobotId");d(this,"_token",null);d(this,"_username",null);d(this,"_tokenExpires",null);d(this,"_sseAbortController",null);d(this,"_pc",null);d(this,"_dc",null);d(this,"_sessionId",null);d(this,"_selectedRobotId",null);d(this,"_pendingRemoteIce",[]);d(this,"_micStream",null);d(this,"_micMuted",!0);d(this,"_audioMuted",!0);d(this,"_micSupported",!1);d(this,"_latencyMonitorId",null);d(this,"_stateRefreshInterval",null);d(this,"_versionResolve",null);d(this,"_hardwareIdResolve",null);d(this,"_volumeResolve",null);d(this,"_micVolumeResolve",null);d(this,"_applyAudioConfigResolve",null);d(this,"_readAudioParameterResolve",null);d(this,"_logSubscribers",new Set);d(this,"_broadcastWaiters",[]);d(this,"_activeMoveUploadId",null);d(this,"_activeAudioUploadId",null);d(this,"_sessionResolve",null);d(this,"_sessionReject",null);d(this,"_iceConnected",!1);d(this,"_dcOpen",!1);d(this,"_iceGraceTimer",null);d(this,"_iceGraceReason",null);d(this,"_pendingVisibilityHandler",null);d(this,"_onlineHandler",null);d(this,"_offlineHandler",null);d(this,"_connectionChangeHandler",null);d(this,"_pendingMotionCompletions",{wake_up:[],goto_sleep:[]});d(this,"_videoElement",null);this._signalingUrl=e.signalingUrl||"https://pollen-robotics-reachy-mini-central.hf.space",this._clientId=e.clientId||null,this._appName=e.appName||"unknown",this._videoJitterBufferTargetMs=e.videoJitterBufferTargetMs??0,this._autoStartFromUrl=e.autoStartFromUrl===!0,this._autoStartAttempted=!1,this._preselectedRobotId=ie()}get state(){return this._state}get robots(){return this._robots}get robotState(){return this._robotState}get username(){return this._username}get isAuthenticated(){return!!this._token}get micSupported(){return this._micSupported}get micMuted(){return this._micMuted}get audioMuted(){return this._audioMuted}get preselectedRobotId(){return this._preselectedRobotId}get isEmbedded(){return this._preselectedRobotId!==null}_maybeAutoStart(){if(!this._autoStartFromUrl||this._autoStartAttempted||!this._preselectedRobotId||this._state!=="connected"||!this._robots.find(i=>i.id===this._preselectedRobotId))return;this._autoStartAttempted=!0;const t=this._preselectedRobotId;setTimeout(()=>{this._state==="connected"&&this.startSession(t).catch(i=>{console.warn("[reachy-mini] autoStartFromUrl: startSession rejected:",i)})},0)}async authenticate(){try{te();const e=await Q();if(e)return this._username=e.userInfo.preferred_username||e.userInfo.name||null,this._token=e.accessToken,this._tokenExpires=e.accessTokenExpiresAt,sessionStorage.setItem("hf_token",this._token),sessionStorage.setItem("hf_username",this._username??""),sessionStorage.setItem("hf_token_expires",typeof this._tokenExpires=="string"?this._tokenExpires:this._tokenExpires.toISOString()),!0;const t=sessionStorage.getItem("hf_token"),i=sessionStorage.getItem("hf_username"),s=sessionStorage.getItem("hf_token_expires");return t&&i&&s&&new Date(s)>new Date?(this._token=t,this._username=i,this._tokenExpires=s,!0):!1}catch(e){return console.error("Auth error:",e),!1}}async login(){const e={};this._clientId&&(e.clientId=this._clientId),window.location.href=await Z(e)}logout(){sessionStorage.removeItem("hf_token"),sessionStorage.removeItem("hf_username"),sessionStorage.removeItem("hf_token_expires"),this._username=null,this._tokenExpires=null,this.disconnect()}async connect(e){if(this._state!=="disconnected")throw new Error("Already connected");if(e&&(this._token=e),!this._token)throw new Error("No token — call authenticate() first or pass a token");this._sseAbortController=new AbortController;let t;try{t=await fetch(`${this._signalingUrl}/events`,{signal:this._sseAbortController.signal,headers:{Authorization:`Bearer ${this._token}`}})}catch(i){throw this._sseAbortController=null,i}if(!t.ok)throw this._sseAbortController=null,new Error(`HTTP ${t.status}`);return new Promise((i,s)=>{let n=!1;const r=t.body.getReader(),c=new TextDecoder;let l="";(async()=>{try{for(;;){const{done:u,value:p}=await r.read();if(u)break;l+=c.decode(p,{stream:!0});const g=l.split(`
|
| 4 |
-
`);l=g.pop()??"";for(const w of g)if(w.startsWith("data:"))try{const m=JSON.parse(w.slice(5).trim());!n&&m.type==="welcome"&&m.peerId&&(n=!0,this._state="connected",await this._sendToServer({type:"setPeerStatus",roles:["listener"],meta:{name:this._appName}}),this._emit("connected",{peerId:m.peerId}),i()),this._handleSignalingMessage(m)}catch{}}}catch(u){if(u.name!=="AbortError"&&this._emit("error",{source:"signaling",error:u}),!n){s(u);return}}this._state!=="disconnected"&&(this._state="disconnected",this._emit("disconnected",{reason:"SSE closed"})),n||s(new Error("Connection closed before welcome"))})()})}async autoConnect(e={}){var l,h,u,p,g;const{token:t,pickRobot:i,autoPickIfSingle:s=!0,filterBusy:n=!0,wakeOnConnect:r=!0}=e;if(this._state==="streaming"){const w=(l=this._robots)==null?void 0:l.find(m=>m.id===this._selectedRobotId);return{robotId:this._selectedRobotId,robotName:((h=w==null?void 0:w.meta)==null?void 0:h.name)??null,isEmbedded:this.isEmbedded,alreadyStreaming:!0}}const c=this._autoStartFromUrl;this._autoStartFromUrl=!1;try{if(t)this._token=t;else if(!this._token&&!await this.authenticate())throw new Error("Not authenticated — call login() or pass a token");this._state==="disconnected"&&await this.connect();let w,m=null;if(this.isEmbedded){w=this._preselectedRobotId;try{await this._waitForRobotInList(w,5e3)}catch{}const f=(u=this._robots)==null?void 0:u.find(y=>y.id===w);m=((p=f==null?void 0:f.meta)==null?void 0:p.name)??null}else{const f=await this._fetchOwnedRobots({filterBusy:n});if(f.length===0)throw new Error("No reachable robots");if(s&&f.length===1&&!f[0].busy)w=f[0].id,m=f[0].name;else if(i){const y=await i(f);if(!y)throw new Error("Robot selection cancelled");w=y,m=((g=f.find(v=>v.id===y))==null?void 0:g.name)??null}else throw new Error("Multiple robots available — pass a pickRobot callback to autoConnect()")}if(await this.startSession(w),r&&typeof this.ensureAwake=="function")try{await this.ensureAwake()}catch(f){console.warn("[reachy-mini] autoConnect: ensureAwake failed:",f)}return{robotId:w,robotName:m,isEmbedded:this.isEmbedded}}finally{this._autoStartFromUrl=c}}async _fetchOwnedRobots({filterBusy:e=!0}={}){var t,i,s;try{const n=await fetch(`${this._signalingUrl}/api/robot-status`,{headers:{Authorization:`Bearer ${this._token}`}});if(!n.ok)throw new Error(`HTTP ${n.status}`);const r=await n.json(),c=new Map;for(const l of r.robots||[]){if(e&&l.busy)continue;const h=((t=l.meta)==null?void 0:t.install_id)??((i=l.meta)==null?void 0:i.hardware_id)??l.peerId;c.set(h,{id:l.peerId,name:l.robotName??((s=l.meta)==null?void 0:s.name)??null,busy:!!l.busy,activeApp:l.activeApp??null,meta:l.meta??{},lastSeenAgeSeconds:l.last_seen_age_seconds??null})}return Array.from(c.values()).sort((l,h)=>(l.lastSeenAgeSeconds??1/0)-(h.lastSeenAgeSeconds??1/0))}catch(n){return console.warn("[reachy-mini] /api/robot-status unavailable, using SSE list:",n),(this._robots||[]).map(r=>{var c;return{id:r.id,name:((c=r.meta)==null?void 0:c.name)??null,busy:!1,activeApp:null,meta:r.meta??{},lastSeenAgeSeconds:null}})}}_waitForRobotInList(e,t){var i;return(i=this._robots)!=null&&i.find(s=>s.id===e)?Promise.resolve():new Promise((s,n)=>{const r=()=>{var l;(l=this._robots)!=null&&l.find(h=>h.id===e)&&(this.removeEventListener("robotsChanged",r),clearTimeout(c),s())},c=setTimeout(()=>{this.removeEventListener("robotsChanged",r),n(new Error(`Timeout waiting for robot ${e} in list`))},t);this.addEventListener("robotsChanged",r)})}async startSession(e){if(this._state!=="connected")throw new Error("Not connected");this._selectedRobotId=e,this._iceConnected=!1,this._dcOpen=!1,this._micSupported=!1,this._pendingRemoteIce=[];try{const t=window,i=t.AudioContext??t.webkitAudioContext;if(!i)throw new Error("AudioContext not supported");const s=new i,n=s.createMediaStreamDestination(),r=s.createOscillator(),c=s.createGain();c.gain.value=0,r.connect(c).connect(n),r.start();const l=n.stream;l.getAudioTracks().forEach(h=>{h.enabled=!1}),this._micStream=l,this._micMuted=!0}catch(t){console.warn("Audio sender placeholder setup failed:",t),this._micStream=null}return this._pc=new RTCPeerConnection({iceServers:[{urls:"stun:stun.l.google.com:19302"}]}),this._installNetworkListeners(),new Promise((t,i)=>{this._sessionResolve=t,this._sessionReject=i,this._pc.ontrack=s=>{if(s.track.kind==="video"){const n=this._videoJitterBufferTargetMs;try{s.receiver.jitterBufferTarget=n}catch{}try{s.receiver.playoutDelayHint=n/1e3}catch{}this._emit("videoTrack",{track:s.track,stream:s.streams[0]})}},this._pc.onicecandidate=async s=>{s.candidate&&this._sessionId&&await this._sendToServer({type:"peer",sessionId:this._sessionId,ice:{candidate:s.candidate.candidate,sdpMLineIndex:s.candidate.sdpMLineIndex,sdpMid:s.candidate.sdpMid}})},this._pc.oniceconnectionstatechange=()=>{var n;const s=(n=this._pc)==null?void 0:n.iceConnectionState;if(s){if(this._emit("iceStateChange",{state:s}),s==="connected"||s==="completed"){this._clearIceGrace(),this._iceConnected=!0,this._checkSessionReady();return}if(s==="disconnected"){typeof document<"u"&&document.hidden?this._armIceGraceOnVisibility():this._scheduleIceGrace(H,"disconnected");return}if(s==="failed"){this._scheduleIceGrace(V,"failed");return}}},this._pc.ondatachannel=s=>{this._dc=s.channel,this._dc.onopen=()=>{this._dcOpen=!0,this._checkSessionReady()},this._dc.onmessage=n=>this._handleRobotMessage(JSON.parse(n.data))},this._sendToServer({type:"startSession",peerId:e}).then(s=>{if((s==null?void 0:s.type)==="sessionRejected"){this._failSessionRejected(s);return}s!=null&&s.sessionId&&(this._sessionId=s.sessionId)})})}_failSessionRejected(e){const t=new Error(e.reason==="robot_busy"?`Robot is busy: "${e.activeApp||"another app"}" is already connected`:`Session rejected: ${e.reason||"unknown reason"}`);if(t.reason=e.reason??null,t.activeApp=e.activeApp??null,this._clearIceGrace(),this._uninstallNetworkListeners(),this._pc&&(this._pc.close(),this._pc=null),this._micStream&&(this._micStream.getTracks().forEach(i=>i.stop()),this._micStream=null),this._iceConnected=!1,this._dcOpen=!1,this._micMuted=!0,this._micSupported=!1,this._emit("sessionRejected",{reason:e.reason,activeApp:e.activeApp}),this._sessionReject){const i=this._sessionReject;this._sessionResolve=null,this._sessionReject=null,i(t)}}async stopSession(){this._versionResolve&&(this._versionResolve(null),this._versionResolve=null),this._hardwareIdResolve&&(this._hardwareIdResolve(null),this._hardwareIdResolve=null),this._volumeResolve&&(this._volumeResolve(null),this._volumeResolve=null),this._micVolumeResolve&&(this._micVolumeResolve(null),this._micVolumeResolve=null),this._applyAudioConfigResolve&&(this._applyAudioConfigResolve(!1),this._applyAudioConfigResolve=null),this._readAudioParameterResolve&&(this._readAudioParameterResolve(null),this._readAudioParameterResolve=null),this._logSubscribers.clear(),this._rejectPendingMotionCompletions(new Error("Session stopped")),this._clearIceGrace(),this._uninstallNetworkListeners(),this._sessionReject&&(this._sessionReject(new Error("Session stopped")),this._sessionResolve=null,this._sessionReject=null),this._stateRefreshInterval&&(clearInterval(this._stateRefreshInterval),this._stateRefreshInterval=null),this._latencyMonitorId&&(clearInterval(this._latencyMonitorId),this._latencyMonitorId=null),this._sessionId&&await this._sendToServer({type:"endSession",sessionId:this._sessionId}),this._micStream&&(this._micStream.getTracks().forEach(t=>t.stop()),this._micStream=null),this._micMuted=!0,this._micSupported=!1,this._pc&&(this._pc.close(),this._pc=null),this._dc&&(this._dc.close(),this._dc=null),this._sessionId=null,this._iceConnected=!1,this._dcOpen=!1,this._state==="streaming"&&(this._state="connected",this._emit("sessionStopped",{reason:"user"}))}disconnect(){this._sseAbortController&&(this._sseAbortController.abort(),this._sseAbortController=null),this._versionResolve&&(this._versionResolve(null),this._versionResolve=null),this._hardwareIdResolve&&(this._hardwareIdResolve(null),this._hardwareIdResolve=null),this._volumeResolve&&(this._volumeResolve(null),this._volumeResolve=null),this._micVolumeResolve&&(this._micVolumeResolve(null),this._micVolumeResolve=null),this._applyAudioConfigResolve&&(this._applyAudioConfigResolve(!1),this._applyAudioConfigResolve=null),this._readAudioParameterResolve&&(this._readAudioParameterResolve(null),this._readAudioParameterResolve=null),this._logSubscribers.clear(),this._rejectPendingMotionCompletions(new Error("Disconnected")),this._clearIceGrace(),this._uninstallNetworkListeners(),this._sessionReject&&(this._sessionReject(new Error("Disconnected")),this._sessionResolve=null,this._sessionReject=null),this._stateRefreshInterval&&(clearInterval(this._stateRefreshInterval),this._stateRefreshInterval=null),this._latencyMonitorId&&(clearInterval(this._latencyMonitorId),this._latencyMonitorId=null),this._sessionId&&this._token&&this._sendToServer({type:"endSession",sessionId:this._sessionId}),this._micStream&&(this._micStream.getTracks().forEach(e=>e.stop()),this._micStream=null),this._pc&&(this._pc.close(),this._pc=null),this._dc&&(this._dc.close(),this._dc=null),this._sessionId=null,this._micMuted=!0,this._micSupported=!1,this._iceConnected=!1,this._dcOpen=!1,this._robots=[],this._state="disconnected",this._emit("disconnected",{reason:"user"})}_clearIceGrace(){this._iceGraceTimer!==null&&(clearTimeout(this._iceGraceTimer),this._iceGraceTimer=null),this._iceGraceReason=null,this._pendingVisibilityHandler&&typeof document<"u"&&document.removeEventListener("visibilitychange",this._pendingVisibilityHandler),this._pendingVisibilityHandler=null}_scheduleIceGrace(e,t){if(this._iceGraceTimer!==null){if(this._iceGraceReason===t)return;clearTimeout(this._iceGraceTimer)}this._iceGraceReason=t,this._iceGraceTimer=setTimeout(()=>{var n;this._iceGraceTimer=null;const i=this._iceGraceReason;this._iceGraceReason=null;const s=(n=this._pc)==null?void 0:n.iceConnectionState;if(!(s==="connected"||s==="completed")){if(i==="disconnected"&&s==="disconnected"){this._emit("error",{source:"webrtc",error:new Error(`ICE stuck in 'disconnected' for > ${e}ms`)});return}if(i==="failed"||s==="failed"){const r=new Error("ICE connection failed");this._sessionReject&&(this._sessionReject(r),this._sessionResolve=null,this._sessionReject=null),this._emit("error",{source:"webrtc",error:r})}}},e)}_armIceGraceOnVisibility(){if(this._pendingVisibilityHandler)return;const e=Date.now(),t=()=>{if(typeof document<"u"&&document.hidden||(document.removeEventListener("visibilitychange",t),this._pendingVisibilityHandler=null,!this._pc))return;const i=this._pc.iceConnectionState;if(!(i==="connected"||i==="completed")){if(Date.now()-e>ae){const s=new Error("Session expired while tab was backgrounded");this._sessionReject&&(this._sessionReject(s),this._sessionResolve=null,this._sessionReject=null),this._emit("error",{source:"webrtc",error:s});return}if(i==="failed"){this._scheduleIceGrace(V,"failed");return}this._scheduleIceGrace(H,"disconnected")}};document.addEventListener("visibilitychange",t),this._pendingVisibilityHandler=t}_installNetworkListeners(){if(this._onlineHandler||typeof window>"u")return;const e=()=>this._emit("networkOnline",{}),t=()=>this._emit("networkOffline",{});window.addEventListener("online",e),window.addEventListener("offline",t),this._onlineHandler=e,this._offlineHandler=t;const i=navigator.connection;if(i&&typeof i.addEventListener=="function"){const s=()=>this._emit("networkChange",{effectiveType:i.effectiveType,downlink:i.downlink,rtt:i.rtt,saveData:i.saveData});i.addEventListener("change",s),this._connectionChangeHandler=s}}_uninstallNetworkListeners(){typeof window<"u"&&(this._onlineHandler&&window.removeEventListener("online",this._onlineHandler),this._offlineHandler&&window.removeEventListener("offline",this._offlineHandler));const e=navigator.connection;e&&this._connectionChangeHandler&&typeof e.removeEventListener=="function"&&e.removeEventListener("change",this._connectionChangeHandler),this._onlineHandler=null,this._offlineHandler=null,this._connectionChangeHandler=null}setTarget({head:e,antennas:t,body_yaw:i}={}){const s={type:"set_full_target"};if(e!==void 0){if(!Array.isArray(e)||e.length!==16||!e.every(n=>Number.isFinite(n)))throw new TypeError(`setTarget: head must be a 16-element flat row-major 4×4 matrix of finite numbers; got ${Array.isArray(e)?`Array(${e.length})`:typeof e}`);s.head=e}if(t!==void 0){if(!Array.isArray(t)||t.length!==2||!t.every(n=>Number.isFinite(n)))throw new TypeError(`setTarget: antennas must be [rightRad, leftRad] (2 finite numbers); got ${Array.isArray(t)?`Array(${t.length})`:typeof t}`);s.antennas=t}if(i!==void 0){if(!Number.isFinite(i))throw new TypeError(`setTarget: body_yaw must be a finite number (radians); got ${i}`);s.body_yaw=i}return this._sendCommand(s)}gotoTarget({head:e,antennas:t,body_yaw:i,duration:s}){const n={type:"goto_target"};if(e!==void 0){if(!Array.isArray(e)||e.length!==16||!e.every(r=>Number.isFinite(r)))throw new TypeError(`gotoTarget: head must be a 16-element flat row-major 4×4 matrix of finite numbers; got ${Array.isArray(e)?`Array(${e.length})`:typeof e}`);n.head=e}if(t!==void 0){if(!Array.isArray(t)||t.length!==2||!t.every(r=>Number.isFinite(r)))throw new TypeError(`gotoTarget: antennas must be [rightRad, leftRad] (2 finite numbers); got ${Array.isArray(t)?`Array(${t.length})`:typeof t}`);n.antennas=t}if(i!==void 0){if(!Number.isFinite(i))throw new TypeError(`gotoTarget: body_yaw must be a finite number (radians); got ${i}`);n.body_yaw=i}if(!Number.isFinite(s)||s<=0)throw new TypeError(`gotoTarget: duration must be a positive finite number (seconds); got ${s}`);return n.duration=s,this._sendCommand(n)}setHeadRpyDeg(e,t,i){return this.setTarget({head:ee(e,t,i).flat()})}setAntennasDeg(e,t){return this.setTarget({antennas:[C(e),C(t)]})}setBodyYawDeg(e){return this.setTarget({body_yaw:C(e)})}playSound(e){return this._sendCommand({type:"play_sound",file:e})}clearIncomingAudio(){return this._sendCommand({type:"clear_incoming_audio"})}setMotorMode(e){return this._sendCommand({type:"set_motor_mode",mode:e})}setMotorTorque(e,t=null){return this._sendCommand({type:"set_torque",on:e,ids:t})}wakeUp({timeoutMs:e=8e3}={}){return this._sendCommand({type:"set_motor_mode",mode:"enabled"}),this._sendCommandAwaitCompletion("wake_up",e)}gotoSleep({timeoutMs:e=8e3}={}){return this._sendCommandAwaitCompletion("goto_sleep",e)}_sendCommandAwaitCompletion(e,t){return this._sendCommand({type:e})?new Promise((i,s)=>{const n={resolve:i,reject:s,timer:setTimeout(()=>{const r=this._pendingMotionCompletions[e],c=r.indexOf(n);c!==-1&&r.splice(c,1),s(new Error(`${e} timed out after ${t}ms`))},t)};this._pendingMotionCompletions[e].push(n)}):Promise.reject(new Error(`${e}: data channel not open`))}_rejectPendingMotionCompletions(e){for(const t of Object.keys(this._pendingMotionCompletions)){const i=this._pendingMotionCompletions[t];for(;i.length;){const s=i.shift();clearTimeout(s.timer),s.reject(e)}}}isAwake(){var t;const e=(t=this._robotState)==null?void 0:t.motor_mode;return e==="enabled"||e==="gravity_compensation"}async ensureAwake(e=1e3){var t;return((t=this._robotState)==null?void 0:t.motor_mode)===void 0&&await new Promise(i=>{const s=()=>{this.removeEventListener("state",s),clearTimeout(n),i()},n=setTimeout(s,e);this.addEventListener("state",s),this.requestState()}),this.isAwake()||this.wakeUp().catch(()=>{}),!0}getVersion(){return new Promise((e,t)=>{if(!this._dc||this._dc.readyState!=="open"){t(new Error("Data channel not open"));return}this._versionResolve&&this._versionResolve(null),this._versionResolve=e,this._sendCommand({type:"get_version"})})}getHardwareId(){return new Promise((e,t)=>{if(!this._dc||this._dc.readyState!=="open"){t(new Error("Data channel not open"));return}this._hardwareIdResolve&&this._hardwareIdResolve(null),this._hardwareIdResolve=e,this._sendCommand({type:"get_hardware_id"})})}getVolume(){return this._slotRoundtrip(()=>this._volumeResolve,e=>{this._volumeResolve=e},{type:"get_volume"})}setVolume(e){return this._slotRoundtrip(()=>this._volumeResolve,t=>{this._volumeResolve=t},{type:"set_volume",volume:N(e)})}getMicrophoneVolume(){return this._slotRoundtrip(()=>this._micVolumeResolve,e=>{this._micVolumeResolve=e},{type:"get_microphone_volume"})}setMicrophoneVolume(e){return this._slotRoundtrip(()=>this._micVolumeResolve,t=>{this._micVolumeResolve=t},{type:"set_microphone_volume",volume:N(e)})}applyAudioConfig(e,{verify:t=!0}={}){return this._slotRoundtrip(()=>this._applyAudioConfigResolve,i=>{this._applyAudioConfigResolve=i},{type:"apply_audio_config",config:e,verify:t}).then(i=>i===!0)}readAudioParameter(e){return this._slotRoundtrip(()=>this._readAudioParameterResolve,t=>{this._readAudioParameterResolve=t},{type:"read_audio_parameter",name:e})}_slotRoundtrip(e,t,i){return new Promise((s,n)=>{if(!this._dc||this._dc.readyState!=="open"){n(new Error("Data channel not open"));return}const r=e();r&&r(null),t(s),this._sendCommand(i)})}sendRaw(e){return this._sendCommand(e)}subscribeLogs({onLine:e,onError:t}){if(typeof e!="function")throw new TypeError("subscribeLogs: onLine callback is required");const i={onLine:e,onError:t},s=this._logSubscribers.size===0;this._logSubscribers.add(i),s&&this._sendCommand({type:"subscribe_logs"});let n=!1;return()=>{n||(n=!0,this._logSubscribers.delete(i),this._logSubscribers.size===0&&this._sendCommand({type:"unsubscribe_logs"}))}}requestState(){return this._sendCommand({type:"get_state"})}setAudioMuted(e){this._audioMuted=e,this._videoElement&&(this._videoElement.muted=e)}setMicMuted(e){this._micMuted=e,this._micStream&&this._micStream.getAudioTracks().forEach(t=>{t.enabled=!e})}attachVideo(e){this._videoElement=e,e.muted=this._audioMuted;const t=s=>{const n=s;e.srcObject=n.detail.stream,e.playsInline=!0,"requestVideoFrameCallback"in e&&this._startLatencyMonitor(e)},i=()=>{e.srcObject=null};return this.addEventListener("videoTrack",t),this.addEventListener("sessionStopped",i),()=>{this.removeEventListener("videoTrack",t),this.removeEventListener("sessionStopped",i),this._latencyMonitorId&&(clearInterval(this._latencyMonitorId),this._latencyMonitorId=null),e.srcObject=null,this._videoElement=null}}async playMove(e,{audioBlob:t=null,audioLeadMs:i=-100,description:s="move",encoding:n="gzip+base64",playFrequency:r=100,initialGotoDuration:c=0,startTimeoutMs:l=8e3,onProgress:h=()=>{},onStarted:u=()=>{}}={}){var R,j;if(!this._dc||this._dc.readyState!=="open")throw new Error("data channel not open");if(!((R=e==null?void 0:e.time)!=null&&R.length)||!((j=e==null?void 0:e.set_target_data)!=null&&j.length))throw new Error("playMove: motion must have time + set_target_data");const p=x();this._activeMoveUploadId=p;const g={description:s,time:e.time,set_target_data:e.set_target_data},w=JSON.stringify(g);let m,f;n==="gzip+base64"&&oe()?(m=await re(w),f="gzip+base64"):(m=w,f="json");const y=Math.ceil(m.length/S)||1;h({phase:"starting",sent:0,total:y,bytes:m.length,encoding:f}),this._sendCommand({type:"upload_move_start",upload_id:p,total_chunks:y,description:s,encoding:f});for(let _=0;_<y;_++){this._dc.bufferedAmount>L&&await this._awaitDataChannelDrain();const I=_*S;this._sendCommand({type:"upload_move_chunk",upload_id:p,chunk_index:_,chunk:m.slice(I,I+S)}),h({phase:"upload",sent:_+1,total:y})}if(this._sendCommand({type:"upload_move_finish",upload_id:p}),h({phase:"uploaded",sent:y,total:y}),t){const _=new Uint8Array(await t.arrayBuffer()),I=P(_),E=Math.ceil(I.length/S)||1;h({phase:"audio-starting",sent:0,total:E,bytes:I.length}),this._sendCommand({type:"upload_audio_start",upload_id:p,total_chunks:E,encoding:"wav-base64",description:s});for(let A=0;A<E;A++){this._dc.bufferedAmount>L&&await this._awaitDataChannelDrain();const D=A*S;this._sendCommand({type:"upload_audio_chunk",upload_id:p,chunk_index:A,chunk:I.slice(D,D+S)}),h({phase:"audio-upload",sent:A+1,total:E})}this._sendCommand({type:"upload_audio_finish",upload_id:p}),h({phase:"audio-uploaded",sent:E,total:E})}this._sendCommand({type:"play_uploaded_move",upload_id:p,play_frequency:r,initial_goto_duration:c,audio_lead_ms:i});let v;try{v=await this._waitForBroadcast(_=>(_==null?void 0:_.type)==="play_uploaded_move"&&(_==null?void 0:_.upload_id)===p&&(_.started===!0||typeof _.error=="string"),{timeoutMs:l,debugLabel:"play_uploaded_move started"})}catch(_){throw new Error(`Daemon did not respond to play_uploaded_move (requires the reachy_mini daemon with feature/daemon-side-move-upload). Underlying: ${_.message}`)}if(typeof v.error=="string")throw new Error(`play_uploaded_move: ${v.error}`);try{u({duration_s:v.duration_s,has_audio:v.has_audio===!0})}catch(_){console.warn("playMove.onStarted threw:",_)}h({phase:"playing",duration_s:v.duration_s});const b=await this._waitForBroadcast(_=>(_==null?void 0:_.type)==="play_uploaded_move"&&(_==null?void 0:_.upload_id)===p&&(_.finished===!0||_.cancelled===!0||typeof _.error=="string"),{timeoutMs:(v.duration_s+30)*1e3,debugLabel:"play_uploaded_move final"});return this._activeMoveUploadId===p&&(this._activeMoveUploadId=null),b}cancelMove(e=null){const t=e??this._activeMoveUploadId;return t?this._sendCommand({type:"cancel_move",upload_id:t}):!1}async uploadAudio(e,{description:t="audio",onProgress:i=()=>{}}={}){if(!this._dc||this._dc.readyState!=="open")throw new Error("data channel not open");if(!(e instanceof Blob))throw new TypeError("uploadAudio: expected a Blob");const s=x(),n=new Uint8Array(await e.arrayBuffer()),r=P(n),c=Math.ceil(r.length/S)||1;i({phase:"audio-starting",sent:0,total:c,bytes:r.length}),this._sendCommand({type:"upload_audio_start",upload_id:s,total_chunks:c,encoding:"wav-base64",description:t});for(let l=0;l<c;l++){this._dc.bufferedAmount>L&&await this._awaitDataChannelDrain();const h=l*S;this._sendCommand({type:"upload_audio_chunk",upload_id:s,chunk_index:l,chunk:r.slice(h,h+S)}),i({phase:"audio-upload",sent:l+1,total:c})}return this._sendCommand({type:"upload_audio_finish",upload_id:s}),i({phase:"audio-uploaded",sent:c,total:c}),s}async playUploadedAudio(e,{timeoutMs:t=8e3}={}){if(!this._dc||this._dc.readyState!=="open")throw new Error("data channel not open");const i=this._waitForBroadcast(n=>(n==null?void 0:n.type)==="play_uploaded_audio"&&(n==null?void 0:n.upload_id)===e&&(n.started===!0||typeof n.error=="string"),{timeoutMs:t,debugLabel:"play_uploaded_audio started"});this._sendCommand({type:"play_uploaded_audio",upload_id:e});const s=await i;if(typeof s.error=="string")throw new Error(s.error);return this._activeAudioUploadId=e,s}cancelAudio(e=null){const t=e??this._activeAudioUploadId;return t?(this._activeAudioUploadId===t&&(this._activeAudioUploadId=null),this._sendCommand({type:"cancel_audio",upload_id:t})):!1}_emit(e,t){this.dispatchEvent(new CustomEvent(e,{detail:t}))}_waitForBroadcast(e,{timeoutMs:t=5e3,debugLabel:i=""}={}){return new Promise((s,n)=>{const r={predicate:e,resolve:s,timer:setTimeout(()=>{const c=this._broadcastWaiters.indexOf(r);c!==-1&&this._broadcastWaiters.splice(c,1),n(new Error(`broadcast timeout (${t} ms): ${i}`))},t)};this._broadcastWaiters.push(r)})}async _awaitDataChannelDrain(){for(;this._dc&&this._dc.bufferedAmount>ne;)if(await new Promise(e=>setTimeout(e,30)),!this._dc||this._dc.readyState!=="open")throw new Error("data channel closed mid-upload")}async _sendToServer(e){if(!this._token)throw new Error("No token — authenticate() first");try{const t=await fetch(`${this._signalingUrl}/send`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this._token}`},body:JSON.stringify(e)});if(!t.ok){let i="";try{i=await t.text()}catch{}return console.warn(`[reachy-mini] /send rejected (${t.status}) for type=${e==null?void 0:e.type}; body=${i||"<empty>"}`),null}return await t.json()}catch(t){return console.error("Send error:",t),null}}_sendCommand(e){return!this._dc||this._dc.readyState!=="open"?!1:(this._dc.send(JSON.stringify(e)),!0)}_checkSessionReady(){this._iceConnected&&this._dcOpen&&this._sessionResolve&&(this._state="streaming",this.requestState(),this._stateRefreshInterval=setInterval(()=>this.requestState(),500),this._emit("streaming",{sessionId:this._sessionId,robotId:this._selectedRobotId}),this._sessionResolve(),this._sessionResolve=null,this._sessionReject=null)}async _handleSignalingMessage(e){switch(e.type){case"welcome":break;case"list":this._robots=e.producers||[],this._emit("robotsChanged",{robots:this._robots}),this._maybeAutoStart();break;case"peerStatusChanged":{const t=await this._sendToServer({type:"list"});t!=null&&t.producers&&(this._robots=t.producers,this._emit("robotsChanged",{robots:this._robots}),this._maybeAutoStart());break}case"sessionStarted":this._sessionId=e.sessionId??null;break;case"sessionRejected":this._failSessionRejected(e);break;case"endSession":this._handleEndSession(e);break;case"peer":this._handlePeerMessage(e);break}}_handleEndSession(e){const t=e.reason,i=t==="robot_busy_local_app"?"Robot is busy: a local Python app is running":t==="local_app_started"?"Disconnected: a local Python app started on the robot":t==="robot_busy_local"?"Robot is busy: another session is already active":null;if(this._sessionReject){const s=new Error(i||`Session ended before it could start: ${t||"unknown reason"}`);s.reason=t??null,this._emit("sessionRejected",{reason:t,activeApp:null}),this._clearIceGrace(),this._uninstallNetworkListeners(),this._pc&&(this._pc.close(),this._pc=null),this._micStream&&(this._micStream.getTracks().forEach(r=>r.stop()),this._micStream=null),this._iceConnected=!1,this._dcOpen=!1,this._micMuted=!0,this._micSupported=!1;const n=this._sessionReject;this._sessionResolve=null,this._sessionReject=null,n(s);return}this._state==="streaming"&&(this._emit("sessionStopped",{reason:t||"remote_end",message:i}),this.stopSession().catch(()=>{}))}async _handlePeerMessage(e){if(this._pc)try{if(e.sdp){const t=e.sdp;if(t.type==="offer"){const s=se(t.sdp);if(this._micSupported=s,this._emit("micSupported",{supported:s}),s&&this._micStream)for(const r of this._micStream.getAudioTracks())this._pc.addTrack(r,this._micStream);await this._pc.setRemoteDescription(new RTCSessionDescription(t));const n=await this._pc.createAnswer();await this._pc.setLocalDescription(n),await this._sendToServer({type:"peer",sessionId:this._sessionId,sdp:{type:"answer",sdp:n.sdp}})}else await this._pc.setRemoteDescription(new RTCSessionDescription(t));const i=this._pendingRemoteIce;if(i&&i.length){this._pendingRemoteIce=[];for(const s of i)try{await this._pc.addIceCandidate(new RTCIceCandidate(s))}catch(n){console.warn("[reachy-mini] buffered ICE candidate rejected:",n)}}}if(e.ice){if(!e.ice.candidate)return;this._pc.remoteDescription?await this._pc.addIceCandidate(new RTCIceCandidate(e.ice)):(this._pendingRemoteIce||(this._pendingRemoteIce=[]),this._pendingRemoteIce.push(e.ice))}}catch(t){console.error("WebRTC error:",t),this._emit("error",{source:"webrtc",error:t})}}_handleRobotMessage(e){if("version"in e&&this._versionResolve){this._versionResolve(e.version),this._versionResolve=null;return}if("hardware_id"in e&&this._hardwareIdResolve){this._hardwareIdResolve(e.hardware_id),this._hardwareIdResolve=null;return}if(e.command==="get_volume"||e.command==="set_volume"){this._volumeResolve&&(this._volumeResolve(e.status==="error"?null:e.volume),this._volumeResolve=null);return}if(e.command==="get_microphone_volume"||e.command==="set_microphone_volume"){this._micVolumeResolve&&(this._micVolumeResolve(e.status==="error"?null:e.volume),this._micVolumeResolve=null);return}if(e.command==="apply_audio_config"){this._applyAudioConfigResolve&&(this._applyAudioConfigResolve(e.error?!1:!!e.applied),this._applyAudioConfigResolve=null);return}if(e.command==="read_audio_parameter"){this._readAudioParameterResolve&&(this._readAudioParameterResolve(e.error?null:e.values??null),this._readAudioParameterResolve=null);return}if((e.command==="wake_up"||e.command==="goto_sleep")&&this._pendingMotionCompletions&&this._pendingMotionCompletions[e.command]){const t=this._pendingMotionCompletions[e.command];if(e.completed===!0&&t.length>0){const i=t.shift();clearTimeout(i.timer),i.resolve();return}if(e.error&&t.length>0){const i=t.shift();clearTimeout(i.timer),i.reject(new Error(`${e.command}: ${e.error}`));return}}if(e.type==="log_line"){for(const t of this._logSubscribers)try{t.onLine({timestamp:e.timestamp,line:e.line})}catch(i){console.error("subscribeLogs onLine threw:",i)}return}if(e.type==="log_stream_error"){for(const t of this._logSubscribers)if(typeof t.onError=="function")try{t.onError(e.error)}catch(i){console.error("subscribeLogs onError threw:",i)}return}if(e.state){const t=e.state;t.head_pose&&(this._robotState.head=t.head_pose.flat()),t.antennas&&(this._robotState.antennas=[t.antennas[0],t.antennas[1]]),typeof t.body_yaw=="number"&&(this._robotState.body_yaw=t.body_yaw),t.motor_mode&&(this._robotState.motor_mode=t.motor_mode),typeof t.is_move_running=="boolean"&&(this._robotState.is_move_running=t.is_move_running),this._emit("state",{...this._robotState})}if(e.error&&this._emit("error",{source:"robot",error:e.error}),this._broadcastWaiters.length>0)for(let t=this._broadcastWaiters.length-1;t>=0;t--){const i=this._broadcastWaiters[t];if(i.predicate(e)){this._broadcastWaiters.splice(t,1),clearTimeout(i.timer),i.resolve(e);return}}}_startLatencyMonitor(e){this._latencyMonitorId&&clearInterval(this._latencyMonitorId),this._latencyMonitorId=setInterval(()=>{if(!e.srcObject||e.paused)return;const t=e.buffered;if(t.length>0){const i=t.end(t.length-1),s=i-e.currentTime;s>.5&&(console.log(`Latency correction: was ${s.toFixed(2)}s behind`),e.currentTime=i-.1)}},2e3)}}window.ReachyMini=le;window.dispatchEvent(new Event("reachymini:ready"));const M=new URLSearchParams(window.location.search),ce=M.get("embedded")==="1"||M.get("embed")==="1",de=M.has("code")||M.has("error"),he=sessionStorage.getItem("cookaiware:launched")==="1";function F(){sessionStorage.setItem("cookaiware:launched","1");const o=document.getElementById("root");return o&&(o.innerHTML=""),U(async()=>{const{mountHost:a}=await import("./auto-CaTD3xKq.js");return{mountHost:a}},__vite__mapDeps([0,1])).then(({mountHost:a})=>{a({appName:"CookAIware",appIconUrl:"/icon.png",appEmoji:"🍲",enableMicrophone:!0,devToken:void 0,clientId:void 0})})}ce?U(()=>import("./embed-DS9p21GA.js"),__vite__mapDeps([2,1,3])):de||he?F():U(async()=>{const{renderLanding:o}=await import("./landing-DkIk3-oJ.js");return{renderLanding:o}},__vite__mapDeps([4,5])).then(({renderLanding:o})=>o(()=>void F()));export{U as _,_e as r};
|
|
|
|
| 1 |
+
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/auto-CaTD3xKq.js","assets/sub-paths-F6TP7MGR-8fYyfeHC.js","assets/embed-eV4K8xkB.js","assets/embed-CEaxTZ7I.css","assets/landing-DkIk3-oJ.js","assets/landing-BJE7fNCb.css"])))=>i.map(i=>d[i]);
|
| 2 |
var G=Object.defineProperty;var q=(o,a,e)=>a in o?G(o,a,{enumerable:!0,configurable:!0,writable:!0,value:e}):o[a]=e;var d=(o,a,e)=>q(o,typeof a!="symbol"?a+"":a,e);(function(){const a=document.createElement("link").relList;if(a&&a.supports&&a.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))t(i);new MutationObserver(i=>{for(const s of i)if(s.type==="childList")for(const n of s.addedNodes)n.tagName==="LINK"&&n.rel==="modulepreload"&&t(n)}).observe(document,{childList:!0,subtree:!0});function e(i){const s={};return i.integrity&&(s.integrity=i.integrity),i.referrerPolicy&&(s.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?s.credentials="include":i.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function t(i){if(i.ep)return;i.ep=!0;const s=e(i);fetch(i.href,s)}})();const W="modulepreload",z=function(o){return"/"+o},O={},U=function(a,e,t){let i=Promise.resolve();if(e&&e.length>0){document.getElementsByTagName("link");const n=document.querySelector("meta[property=csp-nonce]"),r=(n==null?void 0:n.nonce)||(n==null?void 0:n.getAttribute("nonce"));i=Promise.allSettled(e.map(c=>{if(c=z(c),c in O)return;O[c]=!0;const l=c.endsWith(".css"),h=l?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${c}"]${h}`))return;const u=document.createElement("link");if(u.rel=l?"stylesheet":W,l||(u.as="script"),u.crossOrigin="",u.href=c,r&&u.setAttribute("nonce",r),document.head.appendChild(u),l)return new Promise((p,g)=>{u.addEventListener("load",p),u.addEventListener("error",()=>g(new Error(`Unable to preload CSS for ${c}`)))})}))}function s(n){const r=new Event("vite:preloadError",{cancelable:!0});if(r.payload=n,window.dispatchEvent(r),!r.defaultPrevented)throw n}return i.then(n=>{for(const r of n||[])r.status==="rejected"&&s(r.reason);return a().catch(s)})};var J=Object.defineProperty,K=(o,a,e)=>a in o?J(o,a,{enumerable:!0,configurable:!0,writable:!0,value:e}):o[a]=e,k=(o,a,e)=>(K(o,typeof a!="symbol"?a+"":a,e),e),B="https://huggingface.co";async function T(o,a){var e,t;const i=new X(o.url,o.status,(e=o.headers.get("X-Request-Id"))!=null?e:void 0);i.message=`Api error with status ${i.statusCode}`;const s=[`URL: ${i.url}`,i.requestId?`Request ID: ${i.requestId}`:void 0].filter(Boolean).join(". ");if((t=o.headers.get("Content-Type"))!=null&&t.startsWith("application/json")){const n=await o.json();i.message=n.error||n.message||i.message,i.data=n}else i.data={message:await o.text()};throw i.message+=`. ${s}`,i}var X=class extends Error{constructor(o,a,e,t){super(t),k(this,"statusCode"),k(this,"url"),k(this,"requestId"),k(this,"data"),this.statusCode=a,this.requestId=e,this.url=o}};new Promise(o=>{});function Y(o){if(globalThis.Buffer)return globalThis.Buffer.from(o).toString("base64");{const a=[];return o.forEach(e=>{a.push(String.fromCharCode(e))}),globalThis.btoa(a.join(""))}}async function $(o){var a,e;if(typeof window>"u")throw new Error("oauthHandleRedirect is only available in the browser");const t=new URLSearchParams(window.location.search),[i,s]=[t.get("error"),t.get("error_description")];if(i)throw new Error(`${i}: ${s}`);const n=t.get("code"),r=localStorage.getItem("huggingface.co:oauth:nonce");if(!n)throw new Error("Missing oauth code from query parameters in redirected URL");if(!r)throw new Error("Missing oauth nonce from localStorage");const c=localStorage.getItem("huggingface.co:oauth:code_verifier");if(!c)throw new Error("Missing oauth code_verifier from localStorage");const l=t.get("state");if(!l)throw new Error("Missing oauth state from query parameters in redirected URL");let h;try{h=JSON.parse(l)}catch{throw new Error("Invalid oauth state in redirected URL, unable to parse JSON: "+l)}if(h.nonce!==r)throw new Error("Invalid oauth state in redirected URL");const u=B,p=`${new URL(u).origin}/.well-known/openid-configuration`,g=await fetch(p,{headers:{Accept:"application/json"}});if(!g.ok)throw await T(g);const w=await g.json(),m=await fetch(w.token_endpoint,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"authorization_code",code:n,redirect_uri:h.redirectUri,code_verifier:c}).toString()});if(localStorage.removeItem("huggingface.co:oauth:code_verifier"),localStorage.removeItem("huggingface.co:oauth:nonce"),!m.ok)throw await T(m);const f=await m.json(),y=new Date(Date.now()+f.expires_in*1e3),v=await fetch(w.userinfo_endpoint,{headers:{Authorization:`Bearer ${f.access_token}`}});if(!v.ok)throw await T(v);const b=await v.json();return{accessToken:f.access_token,accessTokenExpiresAt:y,userInfo:{id:b.sub,name:b.name,fullname:b.preferred_username,email:b.email,emailVerified:b.email_verified,avatarUrl:b.picture,websiteUrl:b.website,isPro:b.isPro,orgs:(e=(a=b.orgs)==null?void 0:a.map(R=>({id:R.sub,name:R.name,fullname:R.name,isEnterprise:R.isEnterprise,canPay:R.canPay,avatarUrl:R.picture,roleInOrg:R.roleInOrg})))!=null?e:[]},state:h.state,scope:f.scope}}async function Q(o){if(typeof window>"u")throw new Error("oauthHandleRedirect is only available in the browser");const a=new URLSearchParams(window.location.search);return a.has("error")?$():a.has("code")?localStorage.getItem("huggingface.co:oauth:nonce")?$():(console.warn("Missing oauth nonce from localStorage. This can happen when the user refreshes the page after logging in, without changing the URL."),!1):!1}async function Z(o){var a,e;if(typeof window>"u")throw new Error("oauthLogin is only available in the browser");const t=(o==null?void 0:o.hubUrl)||B,i=`${new URL(t).origin}/.well-known/openid-configuration`,s=await fetch(i,{headers:{Accept:"application/json"}});if(!s.ok)throw await T(s);const n=await s.json(),r=globalThis.crypto.randomUUID(),c=globalThis.crypto.randomUUID()+globalThis.crypto.randomUUID();localStorage.setItem("huggingface.co:oauth:nonce",r),localStorage.setItem("huggingface.co:oauth:code_verifier",c);const l=(o==null?void 0:o.redirectUrl)||window.location.href,h=JSON.stringify({nonce:r,redirectUri:l,state:o==null?void 0:o.state}),u=(e=(a=window==null?void 0:window.huggingface)==null?void 0:a.variables)!=null?e:null,p=(o==null?void 0:o.clientId)||(u==null?void 0:u.OAUTH_CLIENT_ID);if(!p)throw u?new Error("Missing clientId, please add hf_oauth: true to the README.md's metadata in your static Space"):new Error("Missing clientId");const g=Y(new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256",new TextEncoder().encode(c)))).replace(/[+]/g,"-").replace(/[/]/g,"_").replace(/=/g,"");return`${n.authorization_endpoint}?${new URLSearchParams({client_id:p,scope:(o==null?void 0:o.scopes)||(u==null?void 0:u.OAUTH_SCOPES)||"openid profile",response_type:"code",redirect_uri:l,state:h,code_challenge:g,code_challenge_method:"S256"}).toString()}`}function C(o){return o*Math.PI/180}function _e(o){return o*180/Math.PI}function ee(o,a,e){const t=C(o),i=C(a),s=C(e),n=Math.cos(s),r=Math.sin(s),c=Math.cos(i),l=Math.sin(i),h=Math.cos(t),u=Math.sin(t);return[[n*c,n*l*u-r*h,n*l*h+r*u,0],[r*c,r*l*u+n*h,r*l*h-n*u,0],[-l,c*u,c*h,0],[0,0,0,1]]}function te(){if(typeof window>"u"||!window.location.hash)return;const o=window.location.hash.startsWith("#")?window.location.hash.slice(1):window.location.hash;let a;try{a=new URLSearchParams(o)}catch{return}const e=a.get("hf_token");if(!e)return;const t=a.get("hf_username")||"user",i=a.get("hf_token_expires"),s=i&&!Number.isNaN(new Date(i).getTime())?i:new Date(Date.now()+365*24*60*60*1e3).toISOString();try{sessionStorage.setItem("hf_token",e),sessionStorage.setItem("hf_username",t),sessionStorage.setItem("hf_token_expires",s)}catch(c){console.warn("[reachy-mini] could not persist pre-seeded HF credentials:",c)}a.delete("hf_token"),a.delete("hf_username"),a.delete("hf_token_expires");const n=a.toString(),r=window.location.pathname+window.location.search+(n?"#"+n:"");try{window.history.replaceState(null,"",r)}catch{}}function ie(){if(typeof window>"u")return null;if(window.location.hash){const o=window.location.hash.startsWith("#")?window.location.hash.slice(1):window.location.hash;try{const e=new URLSearchParams(o).get("robot_peer_id");if(e)return e}catch{}}if(window.location.search)try{const a=new URLSearchParams(window.location.search).get("robot_peer_id");if(a)return a}catch{}return null}function se(o){const a=o.split(`\r
|
| 3 |
`);let e=!1;for(const t of a)if(t.startsWith("m=audio")?e=!0:t.startsWith("m=")&&(e=!1),e&&t==="a=sendrecv")return!0;return!1}const S=12*1024,L=1*1024*1024,ne=512*1024;function oe(){return typeof CompressionStream<"u"}function x(){return"u"+Math.random().toString(36).slice(2,11)+Date.now().toString(36)}function P(o){let a="";for(let t=0;t<o.length;t+=32768)a+=String.fromCharCode.apply(null,Array.from(o.subarray(t,t+32768)));return btoa(a)}async function re(o){const a=new TextEncoder().encode(o),e=await new Response(new Blob([a]).stream().pipeThrough(new CompressionStream("gzip"))).arrayBuffer();return P(new Uint8Array(e))}function N(o){const a=Math.round(Number(o)||0);return Math.max(0,Math.min(100,a))}const H=3e3,V=1e3,ae=6e4;class le extends EventTarget{constructor(e={}){super();d(this,"_signalingUrl");d(this,"_clientId");d(this,"_appName");d(this,"_videoJitterBufferTargetMs");d(this,"_autoStartFromUrl");d(this,"_autoStartAttempted");d(this,"_state","disconnected");d(this,"_robots",[]);d(this,"_robotState",{});d(this,"_preselectedRobotId");d(this,"_token",null);d(this,"_username",null);d(this,"_tokenExpires",null);d(this,"_sseAbortController",null);d(this,"_pc",null);d(this,"_dc",null);d(this,"_sessionId",null);d(this,"_selectedRobotId",null);d(this,"_pendingRemoteIce",[]);d(this,"_micStream",null);d(this,"_micMuted",!0);d(this,"_audioMuted",!0);d(this,"_micSupported",!1);d(this,"_latencyMonitorId",null);d(this,"_stateRefreshInterval",null);d(this,"_versionResolve",null);d(this,"_hardwareIdResolve",null);d(this,"_volumeResolve",null);d(this,"_micVolumeResolve",null);d(this,"_applyAudioConfigResolve",null);d(this,"_readAudioParameterResolve",null);d(this,"_logSubscribers",new Set);d(this,"_broadcastWaiters",[]);d(this,"_activeMoveUploadId",null);d(this,"_activeAudioUploadId",null);d(this,"_sessionResolve",null);d(this,"_sessionReject",null);d(this,"_iceConnected",!1);d(this,"_dcOpen",!1);d(this,"_iceGraceTimer",null);d(this,"_iceGraceReason",null);d(this,"_pendingVisibilityHandler",null);d(this,"_onlineHandler",null);d(this,"_offlineHandler",null);d(this,"_connectionChangeHandler",null);d(this,"_pendingMotionCompletions",{wake_up:[],goto_sleep:[]});d(this,"_videoElement",null);this._signalingUrl=e.signalingUrl||"https://pollen-robotics-reachy-mini-central.hf.space",this._clientId=e.clientId||null,this._appName=e.appName||"unknown",this._videoJitterBufferTargetMs=e.videoJitterBufferTargetMs??0,this._autoStartFromUrl=e.autoStartFromUrl===!0,this._autoStartAttempted=!1,this._preselectedRobotId=ie()}get state(){return this._state}get robots(){return this._robots}get robotState(){return this._robotState}get username(){return this._username}get isAuthenticated(){return!!this._token}get micSupported(){return this._micSupported}get micMuted(){return this._micMuted}get audioMuted(){return this._audioMuted}get preselectedRobotId(){return this._preselectedRobotId}get isEmbedded(){return this._preselectedRobotId!==null}_maybeAutoStart(){if(!this._autoStartFromUrl||this._autoStartAttempted||!this._preselectedRobotId||this._state!=="connected"||!this._robots.find(i=>i.id===this._preselectedRobotId))return;this._autoStartAttempted=!0;const t=this._preselectedRobotId;setTimeout(()=>{this._state==="connected"&&this.startSession(t).catch(i=>{console.warn("[reachy-mini] autoStartFromUrl: startSession rejected:",i)})},0)}async authenticate(){try{te();const e=await Q();if(e)return this._username=e.userInfo.preferred_username||e.userInfo.name||null,this._token=e.accessToken,this._tokenExpires=e.accessTokenExpiresAt,sessionStorage.setItem("hf_token",this._token),sessionStorage.setItem("hf_username",this._username??""),sessionStorage.setItem("hf_token_expires",typeof this._tokenExpires=="string"?this._tokenExpires:this._tokenExpires.toISOString()),!0;const t=sessionStorage.getItem("hf_token"),i=sessionStorage.getItem("hf_username"),s=sessionStorage.getItem("hf_token_expires");return t&&i&&s&&new Date(s)>new Date?(this._token=t,this._username=i,this._tokenExpires=s,!0):!1}catch(e){return console.error("Auth error:",e),!1}}async login(){const e={};this._clientId&&(e.clientId=this._clientId),window.location.href=await Z(e)}logout(){sessionStorage.removeItem("hf_token"),sessionStorage.removeItem("hf_username"),sessionStorage.removeItem("hf_token_expires"),this._username=null,this._tokenExpires=null,this.disconnect()}async connect(e){if(this._state!=="disconnected")throw new Error("Already connected");if(e&&(this._token=e),!this._token)throw new Error("No token — call authenticate() first or pass a token");this._sseAbortController=new AbortController;let t;try{t=await fetch(`${this._signalingUrl}/events`,{signal:this._sseAbortController.signal,headers:{Authorization:`Bearer ${this._token}`}})}catch(i){throw this._sseAbortController=null,i}if(!t.ok)throw this._sseAbortController=null,new Error(`HTTP ${t.status}`);return new Promise((i,s)=>{let n=!1;const r=t.body.getReader(),c=new TextDecoder;let l="";(async()=>{try{for(;;){const{done:u,value:p}=await r.read();if(u)break;l+=c.decode(p,{stream:!0});const g=l.split(`
|
| 4 |
+
`);l=g.pop()??"";for(const w of g)if(w.startsWith("data:"))try{const m=JSON.parse(w.slice(5).trim());!n&&m.type==="welcome"&&m.peerId&&(n=!0,this._state="connected",await this._sendToServer({type:"setPeerStatus",roles:["listener"],meta:{name:this._appName}}),this._emit("connected",{peerId:m.peerId}),i()),this._handleSignalingMessage(m)}catch{}}}catch(u){if(u.name!=="AbortError"&&this._emit("error",{source:"signaling",error:u}),!n){s(u);return}}this._state!=="disconnected"&&(this._state="disconnected",this._emit("disconnected",{reason:"SSE closed"})),n||s(new Error("Connection closed before welcome"))})()})}async autoConnect(e={}){var l,h,u,p,g;const{token:t,pickRobot:i,autoPickIfSingle:s=!0,filterBusy:n=!0,wakeOnConnect:r=!0}=e;if(this._state==="streaming"){const w=(l=this._robots)==null?void 0:l.find(m=>m.id===this._selectedRobotId);return{robotId:this._selectedRobotId,robotName:((h=w==null?void 0:w.meta)==null?void 0:h.name)??null,isEmbedded:this.isEmbedded,alreadyStreaming:!0}}const c=this._autoStartFromUrl;this._autoStartFromUrl=!1;try{if(t)this._token=t;else if(!this._token&&!await this.authenticate())throw new Error("Not authenticated — call login() or pass a token");this._state==="disconnected"&&await this.connect();let w,m=null;if(this.isEmbedded){w=this._preselectedRobotId;try{await this._waitForRobotInList(w,5e3)}catch{}const f=(u=this._robots)==null?void 0:u.find(y=>y.id===w);m=((p=f==null?void 0:f.meta)==null?void 0:p.name)??null}else{const f=await this._fetchOwnedRobots({filterBusy:n});if(f.length===0)throw new Error("No reachable robots");if(s&&f.length===1&&!f[0].busy)w=f[0].id,m=f[0].name;else if(i){const y=await i(f);if(!y)throw new Error("Robot selection cancelled");w=y,m=((g=f.find(v=>v.id===y))==null?void 0:g.name)??null}else throw new Error("Multiple robots available — pass a pickRobot callback to autoConnect()")}if(await this.startSession(w),r&&typeof this.ensureAwake=="function")try{await this.ensureAwake()}catch(f){console.warn("[reachy-mini] autoConnect: ensureAwake failed:",f)}return{robotId:w,robotName:m,isEmbedded:this.isEmbedded}}finally{this._autoStartFromUrl=c}}async _fetchOwnedRobots({filterBusy:e=!0}={}){var t,i,s;try{const n=await fetch(`${this._signalingUrl}/api/robot-status`,{headers:{Authorization:`Bearer ${this._token}`}});if(!n.ok)throw new Error(`HTTP ${n.status}`);const r=await n.json(),c=new Map;for(const l of r.robots||[]){if(e&&l.busy)continue;const h=((t=l.meta)==null?void 0:t.install_id)??((i=l.meta)==null?void 0:i.hardware_id)??l.peerId;c.set(h,{id:l.peerId,name:l.robotName??((s=l.meta)==null?void 0:s.name)??null,busy:!!l.busy,activeApp:l.activeApp??null,meta:l.meta??{},lastSeenAgeSeconds:l.last_seen_age_seconds??null})}return Array.from(c.values()).sort((l,h)=>(l.lastSeenAgeSeconds??1/0)-(h.lastSeenAgeSeconds??1/0))}catch(n){return console.warn("[reachy-mini] /api/robot-status unavailable, using SSE list:",n),(this._robots||[]).map(r=>{var c;return{id:r.id,name:((c=r.meta)==null?void 0:c.name)??null,busy:!1,activeApp:null,meta:r.meta??{},lastSeenAgeSeconds:null}})}}_waitForRobotInList(e,t){var i;return(i=this._robots)!=null&&i.find(s=>s.id===e)?Promise.resolve():new Promise((s,n)=>{const r=()=>{var l;(l=this._robots)!=null&&l.find(h=>h.id===e)&&(this.removeEventListener("robotsChanged",r),clearTimeout(c),s())},c=setTimeout(()=>{this.removeEventListener("robotsChanged",r),n(new Error(`Timeout waiting for robot ${e} in list`))},t);this.addEventListener("robotsChanged",r)})}async startSession(e){if(this._state!=="connected")throw new Error("Not connected");this._selectedRobotId=e,this._iceConnected=!1,this._dcOpen=!1,this._micSupported=!1,this._pendingRemoteIce=[];try{const t=window,i=t.AudioContext??t.webkitAudioContext;if(!i)throw new Error("AudioContext not supported");const s=new i,n=s.createMediaStreamDestination(),r=s.createOscillator(),c=s.createGain();c.gain.value=0,r.connect(c).connect(n),r.start();const l=n.stream;l.getAudioTracks().forEach(h=>{h.enabled=!1}),this._micStream=l,this._micMuted=!0}catch(t){console.warn("Audio sender placeholder setup failed:",t),this._micStream=null}return this._pc=new RTCPeerConnection({iceServers:[{urls:"stun:stun.l.google.com:19302"}]}),this._installNetworkListeners(),new Promise((t,i)=>{this._sessionResolve=t,this._sessionReject=i,this._pc.ontrack=s=>{if(s.track.kind==="video"){const n=this._videoJitterBufferTargetMs;try{s.receiver.jitterBufferTarget=n}catch{}try{s.receiver.playoutDelayHint=n/1e3}catch{}this._emit("videoTrack",{track:s.track,stream:s.streams[0]})}},this._pc.onicecandidate=async s=>{s.candidate&&this._sessionId&&await this._sendToServer({type:"peer",sessionId:this._sessionId,ice:{candidate:s.candidate.candidate,sdpMLineIndex:s.candidate.sdpMLineIndex,sdpMid:s.candidate.sdpMid}})},this._pc.oniceconnectionstatechange=()=>{var n;const s=(n=this._pc)==null?void 0:n.iceConnectionState;if(s){if(this._emit("iceStateChange",{state:s}),s==="connected"||s==="completed"){this._clearIceGrace(),this._iceConnected=!0,this._checkSessionReady();return}if(s==="disconnected"){typeof document<"u"&&document.hidden?this._armIceGraceOnVisibility():this._scheduleIceGrace(H,"disconnected");return}if(s==="failed"){this._scheduleIceGrace(V,"failed");return}}},this._pc.ondatachannel=s=>{this._dc=s.channel,this._dc.onopen=()=>{this._dcOpen=!0,this._checkSessionReady()},this._dc.onmessage=n=>this._handleRobotMessage(JSON.parse(n.data))},this._sendToServer({type:"startSession",peerId:e}).then(s=>{if((s==null?void 0:s.type)==="sessionRejected"){this._failSessionRejected(s);return}s!=null&&s.sessionId&&(this._sessionId=s.sessionId)})})}_failSessionRejected(e){const t=new Error(e.reason==="robot_busy"?`Robot is busy: "${e.activeApp||"another app"}" is already connected`:`Session rejected: ${e.reason||"unknown reason"}`);if(t.reason=e.reason??null,t.activeApp=e.activeApp??null,this._clearIceGrace(),this._uninstallNetworkListeners(),this._pc&&(this._pc.close(),this._pc=null),this._micStream&&(this._micStream.getTracks().forEach(i=>i.stop()),this._micStream=null),this._iceConnected=!1,this._dcOpen=!1,this._micMuted=!0,this._micSupported=!1,this._emit("sessionRejected",{reason:e.reason,activeApp:e.activeApp}),this._sessionReject){const i=this._sessionReject;this._sessionResolve=null,this._sessionReject=null,i(t)}}async stopSession(){this._versionResolve&&(this._versionResolve(null),this._versionResolve=null),this._hardwareIdResolve&&(this._hardwareIdResolve(null),this._hardwareIdResolve=null),this._volumeResolve&&(this._volumeResolve(null),this._volumeResolve=null),this._micVolumeResolve&&(this._micVolumeResolve(null),this._micVolumeResolve=null),this._applyAudioConfigResolve&&(this._applyAudioConfigResolve(!1),this._applyAudioConfigResolve=null),this._readAudioParameterResolve&&(this._readAudioParameterResolve(null),this._readAudioParameterResolve=null),this._logSubscribers.clear(),this._rejectPendingMotionCompletions(new Error("Session stopped")),this._clearIceGrace(),this._uninstallNetworkListeners(),this._sessionReject&&(this._sessionReject(new Error("Session stopped")),this._sessionResolve=null,this._sessionReject=null),this._stateRefreshInterval&&(clearInterval(this._stateRefreshInterval),this._stateRefreshInterval=null),this._latencyMonitorId&&(clearInterval(this._latencyMonitorId),this._latencyMonitorId=null),this._sessionId&&await this._sendToServer({type:"endSession",sessionId:this._sessionId}),this._micStream&&(this._micStream.getTracks().forEach(t=>t.stop()),this._micStream=null),this._micMuted=!0,this._micSupported=!1,this._pc&&(this._pc.close(),this._pc=null),this._dc&&(this._dc.close(),this._dc=null),this._sessionId=null,this._iceConnected=!1,this._dcOpen=!1,this._state==="streaming"&&(this._state="connected",this._emit("sessionStopped",{reason:"user"}))}disconnect(){this._sseAbortController&&(this._sseAbortController.abort(),this._sseAbortController=null),this._versionResolve&&(this._versionResolve(null),this._versionResolve=null),this._hardwareIdResolve&&(this._hardwareIdResolve(null),this._hardwareIdResolve=null),this._volumeResolve&&(this._volumeResolve(null),this._volumeResolve=null),this._micVolumeResolve&&(this._micVolumeResolve(null),this._micVolumeResolve=null),this._applyAudioConfigResolve&&(this._applyAudioConfigResolve(!1),this._applyAudioConfigResolve=null),this._readAudioParameterResolve&&(this._readAudioParameterResolve(null),this._readAudioParameterResolve=null),this._logSubscribers.clear(),this._rejectPendingMotionCompletions(new Error("Disconnected")),this._clearIceGrace(),this._uninstallNetworkListeners(),this._sessionReject&&(this._sessionReject(new Error("Disconnected")),this._sessionResolve=null,this._sessionReject=null),this._stateRefreshInterval&&(clearInterval(this._stateRefreshInterval),this._stateRefreshInterval=null),this._latencyMonitorId&&(clearInterval(this._latencyMonitorId),this._latencyMonitorId=null),this._sessionId&&this._token&&this._sendToServer({type:"endSession",sessionId:this._sessionId}),this._micStream&&(this._micStream.getTracks().forEach(e=>e.stop()),this._micStream=null),this._pc&&(this._pc.close(),this._pc=null),this._dc&&(this._dc.close(),this._dc=null),this._sessionId=null,this._micMuted=!0,this._micSupported=!1,this._iceConnected=!1,this._dcOpen=!1,this._robots=[],this._state="disconnected",this._emit("disconnected",{reason:"user"})}_clearIceGrace(){this._iceGraceTimer!==null&&(clearTimeout(this._iceGraceTimer),this._iceGraceTimer=null),this._iceGraceReason=null,this._pendingVisibilityHandler&&typeof document<"u"&&document.removeEventListener("visibilitychange",this._pendingVisibilityHandler),this._pendingVisibilityHandler=null}_scheduleIceGrace(e,t){if(this._iceGraceTimer!==null){if(this._iceGraceReason===t)return;clearTimeout(this._iceGraceTimer)}this._iceGraceReason=t,this._iceGraceTimer=setTimeout(()=>{var n;this._iceGraceTimer=null;const i=this._iceGraceReason;this._iceGraceReason=null;const s=(n=this._pc)==null?void 0:n.iceConnectionState;if(!(s==="connected"||s==="completed")){if(i==="disconnected"&&s==="disconnected"){this._emit("error",{source:"webrtc",error:new Error(`ICE stuck in 'disconnected' for > ${e}ms`)});return}if(i==="failed"||s==="failed"){const r=new Error("ICE connection failed");this._sessionReject&&(this._sessionReject(r),this._sessionResolve=null,this._sessionReject=null),this._emit("error",{source:"webrtc",error:r})}}},e)}_armIceGraceOnVisibility(){if(this._pendingVisibilityHandler)return;const e=Date.now(),t=()=>{if(typeof document<"u"&&document.hidden||(document.removeEventListener("visibilitychange",t),this._pendingVisibilityHandler=null,!this._pc))return;const i=this._pc.iceConnectionState;if(!(i==="connected"||i==="completed")){if(Date.now()-e>ae){const s=new Error("Session expired while tab was backgrounded");this._sessionReject&&(this._sessionReject(s),this._sessionResolve=null,this._sessionReject=null),this._emit("error",{source:"webrtc",error:s});return}if(i==="failed"){this._scheduleIceGrace(V,"failed");return}this._scheduleIceGrace(H,"disconnected")}};document.addEventListener("visibilitychange",t),this._pendingVisibilityHandler=t}_installNetworkListeners(){if(this._onlineHandler||typeof window>"u")return;const e=()=>this._emit("networkOnline",{}),t=()=>this._emit("networkOffline",{});window.addEventListener("online",e),window.addEventListener("offline",t),this._onlineHandler=e,this._offlineHandler=t;const i=navigator.connection;if(i&&typeof i.addEventListener=="function"){const s=()=>this._emit("networkChange",{effectiveType:i.effectiveType,downlink:i.downlink,rtt:i.rtt,saveData:i.saveData});i.addEventListener("change",s),this._connectionChangeHandler=s}}_uninstallNetworkListeners(){typeof window<"u"&&(this._onlineHandler&&window.removeEventListener("online",this._onlineHandler),this._offlineHandler&&window.removeEventListener("offline",this._offlineHandler));const e=navigator.connection;e&&this._connectionChangeHandler&&typeof e.removeEventListener=="function"&&e.removeEventListener("change",this._connectionChangeHandler),this._onlineHandler=null,this._offlineHandler=null,this._connectionChangeHandler=null}setTarget({head:e,antennas:t,body_yaw:i}={}){const s={type:"set_full_target"};if(e!==void 0){if(!Array.isArray(e)||e.length!==16||!e.every(n=>Number.isFinite(n)))throw new TypeError(`setTarget: head must be a 16-element flat row-major 4×4 matrix of finite numbers; got ${Array.isArray(e)?`Array(${e.length})`:typeof e}`);s.head=e}if(t!==void 0){if(!Array.isArray(t)||t.length!==2||!t.every(n=>Number.isFinite(n)))throw new TypeError(`setTarget: antennas must be [rightRad, leftRad] (2 finite numbers); got ${Array.isArray(t)?`Array(${t.length})`:typeof t}`);s.antennas=t}if(i!==void 0){if(!Number.isFinite(i))throw new TypeError(`setTarget: body_yaw must be a finite number (radians); got ${i}`);s.body_yaw=i}return this._sendCommand(s)}gotoTarget({head:e,antennas:t,body_yaw:i,duration:s}){const n={type:"goto_target"};if(e!==void 0){if(!Array.isArray(e)||e.length!==16||!e.every(r=>Number.isFinite(r)))throw new TypeError(`gotoTarget: head must be a 16-element flat row-major 4×4 matrix of finite numbers; got ${Array.isArray(e)?`Array(${e.length})`:typeof e}`);n.head=e}if(t!==void 0){if(!Array.isArray(t)||t.length!==2||!t.every(r=>Number.isFinite(r)))throw new TypeError(`gotoTarget: antennas must be [rightRad, leftRad] (2 finite numbers); got ${Array.isArray(t)?`Array(${t.length})`:typeof t}`);n.antennas=t}if(i!==void 0){if(!Number.isFinite(i))throw new TypeError(`gotoTarget: body_yaw must be a finite number (radians); got ${i}`);n.body_yaw=i}if(!Number.isFinite(s)||s<=0)throw new TypeError(`gotoTarget: duration must be a positive finite number (seconds); got ${s}`);return n.duration=s,this._sendCommand(n)}setHeadRpyDeg(e,t,i){return this.setTarget({head:ee(e,t,i).flat()})}setAntennasDeg(e,t){return this.setTarget({antennas:[C(e),C(t)]})}setBodyYawDeg(e){return this.setTarget({body_yaw:C(e)})}playSound(e){return this._sendCommand({type:"play_sound",file:e})}clearIncomingAudio(){return this._sendCommand({type:"clear_incoming_audio"})}setMotorMode(e){return this._sendCommand({type:"set_motor_mode",mode:e})}setMotorTorque(e,t=null){return this._sendCommand({type:"set_torque",on:e,ids:t})}wakeUp({timeoutMs:e=8e3}={}){return this._sendCommand({type:"set_motor_mode",mode:"enabled"}),this._sendCommandAwaitCompletion("wake_up",e)}gotoSleep({timeoutMs:e=8e3}={}){return this._sendCommandAwaitCompletion("goto_sleep",e)}_sendCommandAwaitCompletion(e,t){return this._sendCommand({type:e})?new Promise((i,s)=>{const n={resolve:i,reject:s,timer:setTimeout(()=>{const r=this._pendingMotionCompletions[e],c=r.indexOf(n);c!==-1&&r.splice(c,1),s(new Error(`${e} timed out after ${t}ms`))},t)};this._pendingMotionCompletions[e].push(n)}):Promise.reject(new Error(`${e}: data channel not open`))}_rejectPendingMotionCompletions(e){for(const t of Object.keys(this._pendingMotionCompletions)){const i=this._pendingMotionCompletions[t];for(;i.length;){const s=i.shift();clearTimeout(s.timer),s.reject(e)}}}isAwake(){var t;const e=(t=this._robotState)==null?void 0:t.motor_mode;return e==="enabled"||e==="gravity_compensation"}async ensureAwake(e=1e3){var t;return((t=this._robotState)==null?void 0:t.motor_mode)===void 0&&await new Promise(i=>{const s=()=>{this.removeEventListener("state",s),clearTimeout(n),i()},n=setTimeout(s,e);this.addEventListener("state",s),this.requestState()}),this.isAwake()||this.wakeUp().catch(()=>{}),!0}getVersion(){return new Promise((e,t)=>{if(!this._dc||this._dc.readyState!=="open"){t(new Error("Data channel not open"));return}this._versionResolve&&this._versionResolve(null),this._versionResolve=e,this._sendCommand({type:"get_version"})})}getHardwareId(){return new Promise((e,t)=>{if(!this._dc||this._dc.readyState!=="open"){t(new Error("Data channel not open"));return}this._hardwareIdResolve&&this._hardwareIdResolve(null),this._hardwareIdResolve=e,this._sendCommand({type:"get_hardware_id"})})}getVolume(){return this._slotRoundtrip(()=>this._volumeResolve,e=>{this._volumeResolve=e},{type:"get_volume"})}setVolume(e){return this._slotRoundtrip(()=>this._volumeResolve,t=>{this._volumeResolve=t},{type:"set_volume",volume:N(e)})}getMicrophoneVolume(){return this._slotRoundtrip(()=>this._micVolumeResolve,e=>{this._micVolumeResolve=e},{type:"get_microphone_volume"})}setMicrophoneVolume(e){return this._slotRoundtrip(()=>this._micVolumeResolve,t=>{this._micVolumeResolve=t},{type:"set_microphone_volume",volume:N(e)})}applyAudioConfig(e,{verify:t=!0}={}){return this._slotRoundtrip(()=>this._applyAudioConfigResolve,i=>{this._applyAudioConfigResolve=i},{type:"apply_audio_config",config:e,verify:t}).then(i=>i===!0)}readAudioParameter(e){return this._slotRoundtrip(()=>this._readAudioParameterResolve,t=>{this._readAudioParameterResolve=t},{type:"read_audio_parameter",name:e})}_slotRoundtrip(e,t,i){return new Promise((s,n)=>{if(!this._dc||this._dc.readyState!=="open"){n(new Error("Data channel not open"));return}const r=e();r&&r(null),t(s),this._sendCommand(i)})}sendRaw(e){return this._sendCommand(e)}subscribeLogs({onLine:e,onError:t}){if(typeof e!="function")throw new TypeError("subscribeLogs: onLine callback is required");const i={onLine:e,onError:t},s=this._logSubscribers.size===0;this._logSubscribers.add(i),s&&this._sendCommand({type:"subscribe_logs"});let n=!1;return()=>{n||(n=!0,this._logSubscribers.delete(i),this._logSubscribers.size===0&&this._sendCommand({type:"unsubscribe_logs"}))}}requestState(){return this._sendCommand({type:"get_state"})}setAudioMuted(e){this._audioMuted=e,this._videoElement&&(this._videoElement.muted=e)}setMicMuted(e){this._micMuted=e,this._micStream&&this._micStream.getAudioTracks().forEach(t=>{t.enabled=!e})}attachVideo(e){this._videoElement=e,e.muted=this._audioMuted;const t=s=>{const n=s;e.srcObject=n.detail.stream,e.playsInline=!0,"requestVideoFrameCallback"in e&&this._startLatencyMonitor(e)},i=()=>{e.srcObject=null};return this.addEventListener("videoTrack",t),this.addEventListener("sessionStopped",i),()=>{this.removeEventListener("videoTrack",t),this.removeEventListener("sessionStopped",i),this._latencyMonitorId&&(clearInterval(this._latencyMonitorId),this._latencyMonitorId=null),e.srcObject=null,this._videoElement=null}}async playMove(e,{audioBlob:t=null,audioLeadMs:i=-100,description:s="move",encoding:n="gzip+base64",playFrequency:r=100,initialGotoDuration:c=0,startTimeoutMs:l=8e3,onProgress:h=()=>{},onStarted:u=()=>{}}={}){var R,j;if(!this._dc||this._dc.readyState!=="open")throw new Error("data channel not open");if(!((R=e==null?void 0:e.time)!=null&&R.length)||!((j=e==null?void 0:e.set_target_data)!=null&&j.length))throw new Error("playMove: motion must have time + set_target_data");const p=x();this._activeMoveUploadId=p;const g={description:s,time:e.time,set_target_data:e.set_target_data},w=JSON.stringify(g);let m,f;n==="gzip+base64"&&oe()?(m=await re(w),f="gzip+base64"):(m=w,f="json");const y=Math.ceil(m.length/S)||1;h({phase:"starting",sent:0,total:y,bytes:m.length,encoding:f}),this._sendCommand({type:"upload_move_start",upload_id:p,total_chunks:y,description:s,encoding:f});for(let _=0;_<y;_++){this._dc.bufferedAmount>L&&await this._awaitDataChannelDrain();const I=_*S;this._sendCommand({type:"upload_move_chunk",upload_id:p,chunk_index:_,chunk:m.slice(I,I+S)}),h({phase:"upload",sent:_+1,total:y})}if(this._sendCommand({type:"upload_move_finish",upload_id:p}),h({phase:"uploaded",sent:y,total:y}),t){const _=new Uint8Array(await t.arrayBuffer()),I=P(_),E=Math.ceil(I.length/S)||1;h({phase:"audio-starting",sent:0,total:E,bytes:I.length}),this._sendCommand({type:"upload_audio_start",upload_id:p,total_chunks:E,encoding:"wav-base64",description:s});for(let A=0;A<E;A++){this._dc.bufferedAmount>L&&await this._awaitDataChannelDrain();const D=A*S;this._sendCommand({type:"upload_audio_chunk",upload_id:p,chunk_index:A,chunk:I.slice(D,D+S)}),h({phase:"audio-upload",sent:A+1,total:E})}this._sendCommand({type:"upload_audio_finish",upload_id:p}),h({phase:"audio-uploaded",sent:E,total:E})}this._sendCommand({type:"play_uploaded_move",upload_id:p,play_frequency:r,initial_goto_duration:c,audio_lead_ms:i});let v;try{v=await this._waitForBroadcast(_=>(_==null?void 0:_.type)==="play_uploaded_move"&&(_==null?void 0:_.upload_id)===p&&(_.started===!0||typeof _.error=="string"),{timeoutMs:l,debugLabel:"play_uploaded_move started"})}catch(_){throw new Error(`Daemon did not respond to play_uploaded_move (requires the reachy_mini daemon with feature/daemon-side-move-upload). Underlying: ${_.message}`)}if(typeof v.error=="string")throw new Error(`play_uploaded_move: ${v.error}`);try{u({duration_s:v.duration_s,has_audio:v.has_audio===!0})}catch(_){console.warn("playMove.onStarted threw:",_)}h({phase:"playing",duration_s:v.duration_s});const b=await this._waitForBroadcast(_=>(_==null?void 0:_.type)==="play_uploaded_move"&&(_==null?void 0:_.upload_id)===p&&(_.finished===!0||_.cancelled===!0||typeof _.error=="string"),{timeoutMs:(v.duration_s+30)*1e3,debugLabel:"play_uploaded_move final"});return this._activeMoveUploadId===p&&(this._activeMoveUploadId=null),b}cancelMove(e=null){const t=e??this._activeMoveUploadId;return t?this._sendCommand({type:"cancel_move",upload_id:t}):!1}async uploadAudio(e,{description:t="audio",onProgress:i=()=>{}}={}){if(!this._dc||this._dc.readyState!=="open")throw new Error("data channel not open");if(!(e instanceof Blob))throw new TypeError("uploadAudio: expected a Blob");const s=x(),n=new Uint8Array(await e.arrayBuffer()),r=P(n),c=Math.ceil(r.length/S)||1;i({phase:"audio-starting",sent:0,total:c,bytes:r.length}),this._sendCommand({type:"upload_audio_start",upload_id:s,total_chunks:c,encoding:"wav-base64",description:t});for(let l=0;l<c;l++){this._dc.bufferedAmount>L&&await this._awaitDataChannelDrain();const h=l*S;this._sendCommand({type:"upload_audio_chunk",upload_id:s,chunk_index:l,chunk:r.slice(h,h+S)}),i({phase:"audio-upload",sent:l+1,total:c})}return this._sendCommand({type:"upload_audio_finish",upload_id:s}),i({phase:"audio-uploaded",sent:c,total:c}),s}async playUploadedAudio(e,{timeoutMs:t=8e3}={}){if(!this._dc||this._dc.readyState!=="open")throw new Error("data channel not open");const i=this._waitForBroadcast(n=>(n==null?void 0:n.type)==="play_uploaded_audio"&&(n==null?void 0:n.upload_id)===e&&(n.started===!0||typeof n.error=="string"),{timeoutMs:t,debugLabel:"play_uploaded_audio started"});this._sendCommand({type:"play_uploaded_audio",upload_id:e});const s=await i;if(typeof s.error=="string")throw new Error(s.error);return this._activeAudioUploadId=e,s}cancelAudio(e=null){const t=e??this._activeAudioUploadId;return t?(this._activeAudioUploadId===t&&(this._activeAudioUploadId=null),this._sendCommand({type:"cancel_audio",upload_id:t})):!1}_emit(e,t){this.dispatchEvent(new CustomEvent(e,{detail:t}))}_waitForBroadcast(e,{timeoutMs:t=5e3,debugLabel:i=""}={}){return new Promise((s,n)=>{const r={predicate:e,resolve:s,timer:setTimeout(()=>{const c=this._broadcastWaiters.indexOf(r);c!==-1&&this._broadcastWaiters.splice(c,1),n(new Error(`broadcast timeout (${t} ms): ${i}`))},t)};this._broadcastWaiters.push(r)})}async _awaitDataChannelDrain(){for(;this._dc&&this._dc.bufferedAmount>ne;)if(await new Promise(e=>setTimeout(e,30)),!this._dc||this._dc.readyState!=="open")throw new Error("data channel closed mid-upload")}async _sendToServer(e){if(!this._token)throw new Error("No token — authenticate() first");try{const t=await fetch(`${this._signalingUrl}/send`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this._token}`},body:JSON.stringify(e)});if(!t.ok){let i="";try{i=await t.text()}catch{}return console.warn(`[reachy-mini] /send rejected (${t.status}) for type=${e==null?void 0:e.type}; body=${i||"<empty>"}`),null}return await t.json()}catch(t){return console.error("Send error:",t),null}}_sendCommand(e){return!this._dc||this._dc.readyState!=="open"?!1:(this._dc.send(JSON.stringify(e)),!0)}_checkSessionReady(){this._iceConnected&&this._dcOpen&&this._sessionResolve&&(this._state="streaming",this.requestState(),this._stateRefreshInterval=setInterval(()=>this.requestState(),500),this._emit("streaming",{sessionId:this._sessionId,robotId:this._selectedRobotId}),this._sessionResolve(),this._sessionResolve=null,this._sessionReject=null)}async _handleSignalingMessage(e){switch(e.type){case"welcome":break;case"list":this._robots=e.producers||[],this._emit("robotsChanged",{robots:this._robots}),this._maybeAutoStart();break;case"peerStatusChanged":{const t=await this._sendToServer({type:"list"});t!=null&&t.producers&&(this._robots=t.producers,this._emit("robotsChanged",{robots:this._robots}),this._maybeAutoStart());break}case"sessionStarted":this._sessionId=e.sessionId??null;break;case"sessionRejected":this._failSessionRejected(e);break;case"endSession":this._handleEndSession(e);break;case"peer":this._handlePeerMessage(e);break}}_handleEndSession(e){const t=e.reason,i=t==="robot_busy_local_app"?"Robot is busy: a local Python app is running":t==="local_app_started"?"Disconnected: a local Python app started on the robot":t==="robot_busy_local"?"Robot is busy: another session is already active":null;if(this._sessionReject){const s=new Error(i||`Session ended before it could start: ${t||"unknown reason"}`);s.reason=t??null,this._emit("sessionRejected",{reason:t,activeApp:null}),this._clearIceGrace(),this._uninstallNetworkListeners(),this._pc&&(this._pc.close(),this._pc=null),this._micStream&&(this._micStream.getTracks().forEach(r=>r.stop()),this._micStream=null),this._iceConnected=!1,this._dcOpen=!1,this._micMuted=!0,this._micSupported=!1;const n=this._sessionReject;this._sessionResolve=null,this._sessionReject=null,n(s);return}this._state==="streaming"&&(this._emit("sessionStopped",{reason:t||"remote_end",message:i}),this.stopSession().catch(()=>{}))}async _handlePeerMessage(e){if(this._pc)try{if(e.sdp){const t=e.sdp;if(t.type==="offer"){const s=se(t.sdp);if(this._micSupported=s,this._emit("micSupported",{supported:s}),s&&this._micStream)for(const r of this._micStream.getAudioTracks())this._pc.addTrack(r,this._micStream);await this._pc.setRemoteDescription(new RTCSessionDescription(t));const n=await this._pc.createAnswer();await this._pc.setLocalDescription(n),await this._sendToServer({type:"peer",sessionId:this._sessionId,sdp:{type:"answer",sdp:n.sdp}})}else await this._pc.setRemoteDescription(new RTCSessionDescription(t));const i=this._pendingRemoteIce;if(i&&i.length){this._pendingRemoteIce=[];for(const s of i)try{await this._pc.addIceCandidate(new RTCIceCandidate(s))}catch(n){console.warn("[reachy-mini] buffered ICE candidate rejected:",n)}}}if(e.ice){if(!e.ice.candidate)return;this._pc.remoteDescription?await this._pc.addIceCandidate(new RTCIceCandidate(e.ice)):(this._pendingRemoteIce||(this._pendingRemoteIce=[]),this._pendingRemoteIce.push(e.ice))}}catch(t){console.error("WebRTC error:",t),this._emit("error",{source:"webrtc",error:t})}}_handleRobotMessage(e){if("version"in e&&this._versionResolve){this._versionResolve(e.version),this._versionResolve=null;return}if("hardware_id"in e&&this._hardwareIdResolve){this._hardwareIdResolve(e.hardware_id),this._hardwareIdResolve=null;return}if(e.command==="get_volume"||e.command==="set_volume"){this._volumeResolve&&(this._volumeResolve(e.status==="error"?null:e.volume),this._volumeResolve=null);return}if(e.command==="get_microphone_volume"||e.command==="set_microphone_volume"){this._micVolumeResolve&&(this._micVolumeResolve(e.status==="error"?null:e.volume),this._micVolumeResolve=null);return}if(e.command==="apply_audio_config"){this._applyAudioConfigResolve&&(this._applyAudioConfigResolve(e.error?!1:!!e.applied),this._applyAudioConfigResolve=null);return}if(e.command==="read_audio_parameter"){this._readAudioParameterResolve&&(this._readAudioParameterResolve(e.error?null:e.values??null),this._readAudioParameterResolve=null);return}if((e.command==="wake_up"||e.command==="goto_sleep")&&this._pendingMotionCompletions&&this._pendingMotionCompletions[e.command]){const t=this._pendingMotionCompletions[e.command];if(e.completed===!0&&t.length>0){const i=t.shift();clearTimeout(i.timer),i.resolve();return}if(e.error&&t.length>0){const i=t.shift();clearTimeout(i.timer),i.reject(new Error(`${e.command}: ${e.error}`));return}}if(e.type==="log_line"){for(const t of this._logSubscribers)try{t.onLine({timestamp:e.timestamp,line:e.line})}catch(i){console.error("subscribeLogs onLine threw:",i)}return}if(e.type==="log_stream_error"){for(const t of this._logSubscribers)if(typeof t.onError=="function")try{t.onError(e.error)}catch(i){console.error("subscribeLogs onError threw:",i)}return}if(e.state){const t=e.state;t.head_pose&&(this._robotState.head=t.head_pose.flat()),t.antennas&&(this._robotState.antennas=[t.antennas[0],t.antennas[1]]),typeof t.body_yaw=="number"&&(this._robotState.body_yaw=t.body_yaw),t.motor_mode&&(this._robotState.motor_mode=t.motor_mode),typeof t.is_move_running=="boolean"&&(this._robotState.is_move_running=t.is_move_running),this._emit("state",{...this._robotState})}if(e.error&&this._emit("error",{source:"robot",error:e.error}),this._broadcastWaiters.length>0)for(let t=this._broadcastWaiters.length-1;t>=0;t--){const i=this._broadcastWaiters[t];if(i.predicate(e)){this._broadcastWaiters.splice(t,1),clearTimeout(i.timer),i.resolve(e);return}}}_startLatencyMonitor(e){this._latencyMonitorId&&clearInterval(this._latencyMonitorId),this._latencyMonitorId=setInterval(()=>{if(!e.srcObject||e.paused)return;const t=e.buffered;if(t.length>0){const i=t.end(t.length-1),s=i-e.currentTime;s>.5&&(console.log(`Latency correction: was ${s.toFixed(2)}s behind`),e.currentTime=i-.1)}},2e3)}}window.ReachyMini=le;window.dispatchEvent(new Event("reachymini:ready"));const M=new URLSearchParams(window.location.search),ce=M.get("embedded")==="1"||M.get("embed")==="1",de=M.has("code")||M.has("error"),he=sessionStorage.getItem("cookaiware:launched")==="1";function F(){sessionStorage.setItem("cookaiware:launched","1");const o=document.getElementById("root");return o&&(o.innerHTML=""),U(async()=>{const{mountHost:a}=await import("./auto-CaTD3xKq.js");return{mountHost:a}},__vite__mapDeps([0,1])).then(({mountHost:a})=>{a({appName:"CookAIware",appIconUrl:"/icon.png",appEmoji:"🍲",enableMicrophone:!0,devToken:void 0,clientId:void 0})})}ce?U(()=>import("./embed-eV4K8xkB.js"),__vite__mapDeps([2,1,3])):de||he?F():U(async()=>{const{renderLanding:o}=await import("./landing-DkIk3-oJ.js");return{renderLanding:o}},__vite__mapDeps([4,5])).then(({renderLanding:o})=>o(()=>void F()));export{U as _,_e as r};
|
|
@@ -62,7 +62,7 @@
|
|
| 62 |
}
|
| 63 |
})();
|
| 64 |
</script>
|
| 65 |
-
<script type="module" crossorigin src="/assets/index-
|
| 66 |
</head>
|
| 67 |
<body>
|
| 68 |
<!-- Capture the robot's inbound audio track BEFORE the Reachy SDK module
|
|
|
|
| 62 |
}
|
| 63 |
})();
|
| 64 |
</script>
|
| 65 |
+
<script type="module" crossorigin src="/assets/index-6cwrBYEF.js"></script>
|
| 66 |
</head>
|
| 67 |
<body>
|
| 68 |
<!-- Capture the robot's inbound audio track BEFORE the Reachy SDK module
|
|
@@ -1,5 +1,5 @@
|
|
| 1 |
import { createRepo, uploadFiles, downloadFile, whoAmI } from "@huggingface/hub";
|
| 2 |
-
import type { AppData } from "./store";
|
| 3 |
import { validateData } from "./store";
|
| 4 |
|
| 5 |
/** Optional sync of the app data to a private HF dataset owned by the user.
|
|
@@ -9,6 +9,7 @@ import { validateData } from "./store";
|
|
| 9 |
|
| 10 |
const DATASET_NAME = "cookaiware-data";
|
| 11 |
const DATA_FILE = "cookaiware-data.json";
|
|
|
|
| 12 |
|
| 13 |
export interface HfSyncIdentity {
|
| 14 |
userName: string;
|
|
@@ -32,14 +33,26 @@ async function ensureRepo(token: string, repoId: string): Promise<void> {
|
|
| 32 |
}
|
| 33 |
}
|
| 34 |
|
| 35 |
-
export async function uploadData(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
const identity = await resolveIdentity(token);
|
| 37 |
await ensureRepo(token, identity.repoId);
|
| 38 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
await uploadFiles({
|
| 40 |
repo: identity.repoId,
|
| 41 |
accessToken: token,
|
| 42 |
-
files
|
| 43 |
commitTitle: "Update CookAIware data",
|
| 44 |
});
|
| 45 |
return identity;
|
|
@@ -51,3 +64,17 @@ export async function downloadData(token: string): Promise<AppData | null> {
|
|
| 51 |
if (!res) return null;
|
| 52 |
return validateData(JSON.parse(await res.text()));
|
| 53 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import { createRepo, uploadFiles, downloadFile, whoAmI } from "@huggingface/hub";
|
| 2 |
+
import type { AppData, SyncableSettings } from "./store";
|
| 3 |
import { validateData } from "./store";
|
| 4 |
|
| 5 |
/** Optional sync of the app data to a private HF dataset owned by the user.
|
|
|
|
| 9 |
|
| 10 |
const DATASET_NAME = "cookaiware-data";
|
| 11 |
const DATA_FILE = "cookaiware-data.json";
|
| 12 |
+
const SETTINGS_FILE = "cookaiware-settings.json";
|
| 13 |
|
| 14 |
export interface HfSyncIdentity {
|
| 15 |
userName: string;
|
|
|
|
| 33 |
}
|
| 34 |
}
|
| 35 |
|
| 36 |
+
export async function uploadData(
|
| 37 |
+
token: string,
|
| 38 |
+
data: AppData,
|
| 39 |
+
settings?: SyncableSettings,
|
| 40 |
+
): Promise<HfSyncIdentity> {
|
| 41 |
const identity = await resolveIdentity(token);
|
| 42 |
await ensureRepo(token, identity.repoId);
|
| 43 |
+
const files = [
|
| 44 |
+
{ path: DATA_FILE, content: new Blob([JSON.stringify(data, null, 2)], { type: "application/json" }) },
|
| 45 |
+
];
|
| 46 |
+
if (settings) {
|
| 47 |
+
files.push({
|
| 48 |
+
path: SETTINGS_FILE,
|
| 49 |
+
content: new Blob([JSON.stringify(settings, null, 2)], { type: "application/json" }),
|
| 50 |
+
});
|
| 51 |
+
}
|
| 52 |
await uploadFiles({
|
| 53 |
repo: identity.repoId,
|
| 54 |
accessToken: token,
|
| 55 |
+
files,
|
| 56 |
commitTitle: "Update CookAIware data",
|
| 57 |
});
|
| 58 |
return identity;
|
|
|
|
| 64 |
if (!res) return null;
|
| 65 |
return validateData(JSON.parse(await res.text()));
|
| 66 |
}
|
| 67 |
+
|
| 68 |
+
/** Fetch the synced settings subset, or null if none has been uploaded yet
|
| 69 |
+
* (older datasets, or a device that never enabled sync). */
|
| 70 |
+
export async function downloadSettings(token: string): Promise<unknown | null> {
|
| 71 |
+
try {
|
| 72 |
+
const identity = await resolveIdentity(token);
|
| 73 |
+
const res = await downloadFile({ repo: identity.repoId, path: SETTINGS_FILE, accessToken: token });
|
| 74 |
+
if (!res) return null;
|
| 75 |
+
return JSON.parse(await res.text());
|
| 76 |
+
} catch (err) {
|
| 77 |
+
console.debug("[hf-sync] no settings file:", err instanceof Error ? err.message : err);
|
| 78 |
+
return null;
|
| 79 |
+
}
|
| 80 |
+
}
|
|
@@ -44,6 +44,48 @@ export const REALTIME_VOICES = [
|
|
| 44 |
const DATA_KEY = "cookaiware:data";
|
| 45 |
const SETTINGS_KEY = "cookaiware:settings";
|
| 46 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
export function emptyData(): AppData {
|
| 48 |
return {
|
| 49 |
inventory: [],
|
|
|
|
| 44 |
const DATA_KEY = "cookaiware:data";
|
| 45 |
const SETTINGS_KEY = "cookaiware:settings";
|
| 46 |
|
| 47 |
+
/** The settings that are safe to sync across devices via the HF dataset —
|
| 48 |
+
* preferences only. Secrets (openaiKey, hfToken) and device-local state
|
| 49 |
+
* (lastSyncedAt, hfDatasetSync) are DELIBERATELY excluded and never leave
|
| 50 |
+
* this device. */
|
| 51 |
+
export type SyncableSettings = Pick<
|
| 52 |
+
AppSettings,
|
| 53 |
+
"language" | "robotName" | "voice" | "activationMode" | "allowInterrupt" | "inventorySort"
|
| 54 |
+
>;
|
| 55 |
+
|
| 56 |
+
export function pickSyncableSettings(s: AppSettings): SyncableSettings {
|
| 57 |
+
return {
|
| 58 |
+
language: s.language,
|
| 59 |
+
robotName: s.robotName,
|
| 60 |
+
voice: s.voice,
|
| 61 |
+
activationMode: s.activationMode,
|
| 62 |
+
allowInterrupt: s.allowInterrupt,
|
| 63 |
+
inventorySort: s.inventorySort,
|
| 64 |
+
};
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
/** Merge a downloaded settings subset onto the current settings, validating
|
| 68 |
+
* every field (unknown/invalid values keep the current one). */
|
| 69 |
+
export function applySyncableSettings(current: AppSettings, incoming: unknown): AppSettings {
|
| 70 |
+
const p = (incoming ?? {}) as Partial<SyncableSettings>;
|
| 71 |
+
return {
|
| 72 |
+
...current,
|
| 73 |
+
language: p.language === "es" || p.language === "en" ? p.language : current.language,
|
| 74 |
+
robotName: typeof p.robotName === "string" && p.robotName.trim() ? p.robotName.trim() : current.robotName,
|
| 75 |
+
voice:
|
| 76 |
+
typeof p.voice === "string" && (REALTIME_VOICES as readonly string[]).includes(p.voice) ? p.voice : current.voice,
|
| 77 |
+
activationMode: p.activationMode === "name" || p.activationMode === "always" ? p.activationMode : current.activationMode,
|
| 78 |
+
allowInterrupt: typeof p.allowInterrupt === "boolean" ? p.allowInterrupt : current.allowInterrupt,
|
| 79 |
+
inventorySort:
|
| 80 |
+
p.inventorySort === "name" ||
|
| 81 |
+
p.inventorySort === "custom" ||
|
| 82 |
+
p.inventorySort === "location" ||
|
| 83 |
+
p.inventorySort === "expiration"
|
| 84 |
+
? p.inventorySort
|
| 85 |
+
: current.inventorySort,
|
| 86 |
+
};
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
export function emptyData(): AppData {
|
| 90 |
return {
|
| 91 |
inventory: [],
|
|
@@ -13,8 +13,10 @@ import {
|
|
| 13 |
importFromFile,
|
| 14 |
summarizeData,
|
| 15 |
REALTIME_VOICES,
|
|
|
|
|
|
|
| 16 |
} from "../storage/store";
|
| 17 |
-
import { uploadData, downloadData } from "../storage/hfDataset";
|
| 18 |
import { VoiceController, type VoiceStatus } from "../voice/controller";
|
| 19 |
import { captureFrame, getRobotVideoTrack } from "../voice/camera";
|
| 20 |
import * as inv from "../domain/inventory";
|
|
@@ -282,7 +284,7 @@ export class App {
|
|
| 282 |
const token = this.settings.hfToken;
|
| 283 |
if (!token) return;
|
| 284 |
try {
|
| 285 |
-
const identity = await uploadData(token, this.data);
|
| 286 |
this.settings.lastSyncedAt = new Date().toISOString();
|
| 287 |
saveSettings(this.settings);
|
| 288 |
if (!silent) this.toast(this.s.hf_sync_ok(identity.repoId));
|
|
@@ -1120,6 +1122,7 @@ export class App {
|
|
| 1120 |
const notes =
|
| 1121 |
[this.data.family_profile.plan_notes, guidance].filter((n): n is string => !!n && !!n.trim()).join(". ") || null;
|
| 1122 |
let generated: MealPlan;
|
|
|
|
| 1123 |
try {
|
| 1124 |
const key = this.settings.openaiKey;
|
| 1125 |
if (!key) throw new Error("no OpenAI key");
|
|
@@ -1128,6 +1131,8 @@ export class App {
|
|
| 1128 |
console.warn("[plan] LLM generation failed, using algorithmic fallback:", err);
|
| 1129 |
this.toast(this.s.plan_llm_fallback);
|
| 1130 |
generated = planner.generatePlanFromMatrix(this.data.inventory, matrix, profile.schedule);
|
|
|
|
|
|
|
| 1131 |
}
|
| 1132 |
this.data.meal_plan = this.mergeWithLocked(generated);
|
| 1133 |
this.commit();
|
|
@@ -1199,6 +1204,7 @@ export class App {
|
|
| 1199 |
.join(". ") || null;
|
| 1200 |
|
| 1201 |
let generated: MealPlan;
|
|
|
|
| 1202 |
try {
|
| 1203 |
const key = this.settings.openaiKey;
|
| 1204 |
if (!key) throw new Error("no OpenAI key");
|
|
@@ -1214,6 +1220,8 @@ export class App {
|
|
| 1214 |
} catch (err) {
|
| 1215 |
console.warn("[plan] single-meal LLM design failed, using fallback:", err);
|
| 1216 |
generated = planner.generatePlanFromMatrix(this.data.inventory, slot, this.data.family_profile.schedule);
|
|
|
|
|
|
|
| 1217 |
}
|
| 1218 |
const newMeal = generated.days.find((d) => d.day === day)?.meals.find((m) => m.meal === kind);
|
| 1219 |
if (!newMeal) return { ok: false, error: "could not design that meal" };
|
|
@@ -1523,6 +1531,12 @@ export class App {
|
|
| 1523 |
const summary = s.data_summary(sum.inventory, sum.hasPlan, sum.shopping);
|
| 1524 |
if (!(await this.confirm(s.hf_load_confirm(summary)))) return;
|
| 1525 |
this.data = remote;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1526 |
this.commit();
|
| 1527 |
this.toast(s.import_ok);
|
| 1528 |
} catch (err) {
|
|
@@ -1577,6 +1591,11 @@ export class App {
|
|
| 1577 |
if (pick === "a") {
|
| 1578 |
this.data = remote;
|
| 1579 |
saveData(this.data);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1580 |
}
|
| 1581 |
}
|
| 1582 |
}
|
|
|
|
| 13 |
importFromFile,
|
| 14 |
summarizeData,
|
| 15 |
REALTIME_VOICES,
|
| 16 |
+
pickSyncableSettings,
|
| 17 |
+
applySyncableSettings,
|
| 18 |
} from "../storage/store";
|
| 19 |
+
import { uploadData, downloadData, downloadSettings } from "../storage/hfDataset";
|
| 20 |
import { VoiceController, type VoiceStatus } from "../voice/controller";
|
| 21 |
import { captureFrame, getRobotVideoTrack } from "../voice/camera";
|
| 22 |
import * as inv from "../domain/inventory";
|
|
|
|
| 284 |
const token = this.settings.hfToken;
|
| 285 |
if (!token) return;
|
| 286 |
try {
|
| 287 |
+
const identity = await uploadData(token, this.data, pickSyncableSettings(this.settings));
|
| 288 |
this.settings.lastSyncedAt = new Date().toISOString();
|
| 289 |
saveSettings(this.settings);
|
| 290 |
if (!silent) this.toast(this.s.hf_sync_ok(identity.repoId));
|
|
|
|
| 1122 |
const notes =
|
| 1123 |
[this.data.family_profile.plan_notes, guidance].filter((n): n is string => !!n && !!n.trim()).join(". ") || null;
|
| 1124 |
let generated: MealPlan;
|
| 1125 |
+
this.voice.startThinking();
|
| 1126 |
try {
|
| 1127 |
const key = this.settings.openaiKey;
|
| 1128 |
if (!key) throw new Error("no OpenAI key");
|
|
|
|
| 1131 |
console.warn("[plan] LLM generation failed, using algorithmic fallback:", err);
|
| 1132 |
this.toast(this.s.plan_llm_fallback);
|
| 1133 |
generated = planner.generatePlanFromMatrix(this.data.inventory, matrix, profile.schedule);
|
| 1134 |
+
} finally {
|
| 1135 |
+
this.voice.stopThinking();
|
| 1136 |
}
|
| 1137 |
this.data.meal_plan = this.mergeWithLocked(generated);
|
| 1138 |
this.commit();
|
|
|
|
| 1204 |
.join(". ") || null;
|
| 1205 |
|
| 1206 |
let generated: MealPlan;
|
| 1207 |
+
this.voice.startThinking();
|
| 1208 |
try {
|
| 1209 |
const key = this.settings.openaiKey;
|
| 1210 |
if (!key) throw new Error("no OpenAI key");
|
|
|
|
| 1220 |
} catch (err) {
|
| 1221 |
console.warn("[plan] single-meal LLM design failed, using fallback:", err);
|
| 1222 |
generated = planner.generatePlanFromMatrix(this.data.inventory, slot, this.data.family_profile.schedule);
|
| 1223 |
+
} finally {
|
| 1224 |
+
this.voice.stopThinking();
|
| 1225 |
}
|
| 1226 |
const newMeal = generated.days.find((d) => d.day === day)?.meals.find((m) => m.meal === kind);
|
| 1227 |
if (!newMeal) return { ok: false, error: "could not design that meal" };
|
|
|
|
| 1531 |
const summary = s.data_summary(sum.inventory, sum.hasPlan, sum.shopping);
|
| 1532 |
if (!(await this.confirm(s.hf_load_confirm(summary)))) return;
|
| 1533 |
this.data = remote;
|
| 1534 |
+
// Bring over synced preferences too (robot name, voice, activation…).
|
| 1535 |
+
const rs = await downloadSettings(token);
|
| 1536 |
+
if (rs) {
|
| 1537 |
+
this.settings = applySyncableSettings(this.settings, rs);
|
| 1538 |
+
saveSettings(this.settings);
|
| 1539 |
+
}
|
| 1540 |
this.commit();
|
| 1541 |
this.toast(s.import_ok);
|
| 1542 |
} catch (err) {
|
|
|
|
| 1591 |
if (pick === "a") {
|
| 1592 |
this.data = remote;
|
| 1593 |
saveData(this.data);
|
| 1594 |
+
const rs = await downloadSettings(token);
|
| 1595 |
+
if (rs) {
|
| 1596 |
+
this.settings = applySyncableSettings(this.settings, rs);
|
| 1597 |
+
saveSettings(this.settings);
|
| 1598 |
+
}
|
| 1599 |
}
|
| 1600 |
}
|
| 1601 |
}
|
|
@@ -211,6 +211,7 @@ export class VoiceController {
|
|
| 211 |
this.session?.close();
|
| 212 |
this.session = null;
|
| 213 |
this.speaking = false;
|
|
|
|
| 214 |
this.stopMotion();
|
| 215 |
const reachy = this.opts.getReachy();
|
| 216 |
try {
|
|
@@ -274,6 +275,27 @@ export class VoiceController {
|
|
| 274 |
this.lastActivity = this.emoteStart;
|
| 275 |
}
|
| 276 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
private emoteOffset(now: number): EmoteOffset {
|
| 278 |
if (!this.emoteSpec) return { roll: 0, pitch: 0, yaw: 0, ant: 0 };
|
| 279 |
const elapsed = (now - this.emoteStart) / 1000;
|
|
|
|
| 211 |
this.session?.close();
|
| 212 |
this.session = null;
|
| 213 |
this.speaking = false;
|
| 214 |
+
this.stopThinking();
|
| 215 |
this.stopMotion();
|
| 216 |
const reachy = this.opts.getReachy();
|
| 217 |
try {
|
|
|
|
| 275 |
this.lastActivity = this.emoteStart;
|
| 276 |
}
|
| 277 |
|
| 278 |
+
// Deliberate "I'm thinking" body language while a slow tool (menu design)
|
| 279 |
+
// runs — otherwise the robot sits silent and still for 10-20 s. Varied so it
|
| 280 |
+
// reads as pondering, not a loop: mostly thinking, an occasional glance.
|
| 281 |
+
private thinkingTimer: ReturnType<typeof setInterval> | null = null;
|
| 282 |
+
private thinkingBeats = ["thinking", "attentive", "thinking", "confused", "thinking"];
|
| 283 |
+
startThinking(): void {
|
| 284 |
+
if (!this.active || this.thinkingTimer) return;
|
| 285 |
+
let i = 0;
|
| 286 |
+
this.emote("thinking");
|
| 287 |
+
this.thinkingTimer = setInterval(() => {
|
| 288 |
+
i = (i + 1) % this.thinkingBeats.length;
|
| 289 |
+
this.emote(this.thinkingBeats[i]);
|
| 290 |
+
}, 2600);
|
| 291 |
+
}
|
| 292 |
+
stopThinking(): void {
|
| 293 |
+
if (this.thinkingTimer) {
|
| 294 |
+
clearInterval(this.thinkingTimer);
|
| 295 |
+
this.thinkingTimer = null;
|
| 296 |
+
}
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
private emoteOffset(now: number): EmoteOffset {
|
| 300 |
if (!this.emoteSpec) return { roll: 0, pitch: 0, yaw: 0, ant: 0 };
|
| 301 |
const elapsed = (now - this.emoteStart) / 1000;
|
|
@@ -50,7 +50,7 @@ function systemPrompt(lang: Lang, robotName: string, activationMode: "always" |
|
|
| 50 |
]
|
| 51 |
: []),
|
| 52 |
// Intelligent meal planning (in-session).
|
| 53 |
-
"MEAL PLANNING — pick the RIGHT scope: if the user mentions ONE meal or ONE day (e.g. 'a dinner for tonight with the yogurt that just expired', 'change Friday lunch'), call set_day_meal with that day, meal and their exact words as `request` — it edits only that slot. ONLY call design_meal_plan when they clearly want a whole new week; pass any overall request as `guidance` (e.g. 'vegetarian this week'). NEVER regenerate the whole week when the user only talked about one meal or day. Both run the same careful designer (prioritizes expiring pantry, real dishes, correct servings, kept meals and wishes) and take
|
| 54 |
"LOCKED MEALS: use set_meal_lock to keep ('keep Friday's dinner') or free a meal. If set_day_meal returns a 'locked' error, tell the user that meal is kept and ASK before changing it — only unlock (set_meal_lock locked=false) and replace it if they confirm. To take back a change, call undo_plan_change.",
|
| 55 |
// Tools.
|
| 56 |
"Use the provided tools to read or change data — never invent inventory you have not read.",
|
|
|
|
| 50 |
]
|
| 51 |
: []),
|
| 52 |
// Intelligent meal planning (in-session).
|
| 53 |
+
"MEAL PLANNING — pick the RIGHT scope: if the user mentions ONE meal or ONE day (e.g. 'a dinner for tonight with the yogurt that just expired', 'change Friday lunch'), call set_day_meal with that day, meal and their exact words as `request` — it edits only that slot. ONLY call design_meal_plan when they clearly want a whole new week; pass any overall request as `guidance` (e.g. 'vegetarian this week'). NEVER regenerate the whole week when the user only talked about one meal or day. Both run the same careful designer (prioritizes expiring pantry, real dishes, correct servings, kept meals and wishes) and take about ten seconds. So ALWAYS, before calling: say a short line asking them to wait (e.g. 'Let me think about that for a moment…') and play the thinking emotion — THEN call the tool. When it returns, announce the result out loud: for one meal say the dish, for a week say it's planned and name a highlight. Never hand-write a menu yourself.",
|
| 54 |
"LOCKED MEALS: use set_meal_lock to keep ('keep Friday's dinner') or free a meal. If set_day_meal returns a 'locked' error, tell the user that meal is kept and ASK before changing it — only unlock (set_meal_lock locked=false) and replace it if they confirm. To take back a change, call undo_plan_change.",
|
| 55 |
// Tools.
|
| 56 |
"Use the provided tools to read or change data — never invent inventory you have not read.",
|