Juan Jimenez Carrero Claude Opus 4.8 commited on
Commit
ff39cb2
·
1 Parent(s): a03aea0

fix: garbled voice on tool calls, sync safety, edit-not-duplicate, calmer persona

Browse files

- Garbled/'buzzing' speech before plan creation: we requested a new response
while the previous one was still streaming, so two responses' audio chunks
interleaved on one playback timeline. Responses are now gated: track
response.created/done and defer response.create until the active one ends.
- Auto-sync: enabling the toggle now downloads the cloud copy first and, if it
differs, asks which side to keep (cloud vs this device) before the first
auto-save — it can no longer silently overwrite. Switch styling fixed (the
global input rules were deforming the checkbox).
- New update_inventory_item tool: edits an existing item in place (location,
expiration, quantity, rename); prompt now forbids add_inventory for changes,
which was creating duplicates.
- Sort by Location added (groups by location, then name; no-location last).
- Robot name setting (default Reachy) fed into the voice persona; persona
rewritten to be REACTIVE: do only what was asked, one-two short sentences,
no unprompted suggestions; greeting shortened to a few words.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

assets/embed-BPTJET-M.js ADDED
The diff for this file is too large to render. See raw diff
 
assets/{embed-DdITxSHM.css → embed-CsvkHiDH.css} RENAMED
@@ -1 +1 @@
1
- :root{--cream: #fff4e6;--cream-strong: #ffe8c9;--peach: #f8b48a;--peach-deep: #e5885c;--teal: #2c8b88;--teal-dark: #0f5453;--ink: #1c2428;--muted: #4e5c61;--white: #ffffff;--card: #fffaf3;--danger: #b3402a;--danger-bg: #ffe1d6;--warn: #8a6210;--warn-bg: #ffedc2;--shadow-sm: 0 8px 18px rgba(24, 38, 38, .08);--shadow-lg: 0 18px 40px rgba(24, 38, 38, .15)}*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}html,body{margin:0;padding:0;height:100%}body{font-family:Nunito,-apple-system,BlinkMacSystemFont,sans-serif;background:linear-gradient(180deg,#fff7ed,#fef0e1,#f5ece6);color:var(--ink);-webkit-font-smoothing:antialiased;overscroll-behavior:none}#root{display:flex;flex-direction:column;height:100dvh}.app-main{flex:1;overflow-y:auto;-webkit-overflow-scrolling:touch;padding:1rem 1rem calc(7.5rem + env(safe-area-inset-bottom,0px));max-width:640px;width:100%;margin:0 auto}.app-tabs{display:flex;background:var(--white);border-top:1px solid rgba(44,139,136,.12);box-shadow:0 -6px 18px #1826260f;padding-bottom:env(safe-area-inset-bottom,0px)}.app-tabs button{flex:1;min-height:58px;border:none;background:none;color:var(--muted);font-family:inherit;font-size:.72rem;font-weight:700;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.2rem;cursor:pointer;padding:.4rem .2rem;touch-action:manipulation;transition:color .2s ease,transform .15s ease}.app-tabs button:active{transform:scale(.94)}.app-tabs button .tab-icon{line-height:1;display:flex}.app-tabs button .tab-icon svg{width:23px;height:23px}.app-tabs button.active{color:var(--teal)}h1{font-family:Fraunces,serif;font-size:1.55rem;margin:.3rem 0 1rem;color:var(--ink)}h2{font-family:inherit;font-size:.82rem;font-weight:800;margin:1.5rem 0 .6rem;color:var(--teal-dark);text-transform:uppercase;letter-spacing:.06em}.card{background:var(--white);border:1px solid rgba(0,0,0,.04);border-radius:20px;padding:.95rem 1.05rem;margin-bottom:.7rem;box-shadow:var(--shadow-sm);transition:transform .2s ease,box-shadow .2s ease}.row{display:flex;align-items:center;gap:.6rem}.row .grow{flex:1;min-width:0}.item-title{font-weight:800;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.item-sub{color:var(--muted);font-size:.85rem;margin-top:.15rem}.chip{display:inline-block;font-size:.72rem;font-weight:700;border-radius:999px;padding:.16rem .6rem;margin-left:.4rem;vertical-align:middle}.chip.warn{background:var(--warn-bg);color:var(--warn)}.chip.danger{background:var(--danger-bg);color:var(--danger)}button.btn{min-height:46px;min-width:46px;border-radius:14px;border:1px solid rgba(44,139,136,.2);background:var(--card);color:var(--teal-dark);font-family:inherit;font-size:.95rem;font-weight:700;padding:0 1rem;cursor:pointer;touch-action:manipulation;transition:transform .15s ease,box-shadow .2s ease,background .2s ease}button.btn:active{transform:scale(.97)}button.btn.primary{background:var(--teal);border-color:var(--teal);color:var(--white);box-shadow:0 10px 24px #2c8b8840}button.btn.small{min-height:44px;font-size:.85rem;padding:0 .75rem}button.btn.block{width:100%;margin:.45rem 0}input,select{min-height:46px;width:100%;min-width:0;max-width:100%;border-radius:14px;border:1.5px solid var(--cream-strong);background:var(--white);color:var(--ink);font-family:inherit;font-size:1rem;padding:0 .8rem;transition:border-color .2s ease,box-shadow .2s ease}input[type=date]{-webkit-appearance:none;-moz-appearance:none;appearance:none;display:block;min-width:0}.form-grid>div{min-width:0}input:focus,select:focus{outline:none;border-color:var(--teal);box-shadow:0 0 0 3px #2c8b8826}label{display:block;font-size:.8rem;font-weight:700;color:var(--muted);margin:.65rem 0 .28rem}.form-grid{display:grid;grid-template-columns:2fr 1fr;gap:0 .6rem}.empty{color:var(--muted);text-align:center;padding:2rem 1rem}.day-card .day-name{font-family:Fraunces,serif;font-weight:700;font-size:1.05rem;margin-bottom:.4rem}.meal{border-top:1px solid var(--cream-strong);padding:.55rem 0}.meal:first-of-type{border-top:none}.meal .meal-kind{font-size:.72rem;font-weight:800;color:var(--teal);text-transform:uppercase;letter-spacing:.06em}.meal .meal-name{font-weight:800;margin:.12rem 0}.meal .meal-ing{color:var(--muted);font-size:.85rem}.toast{position:fixed;left:50%;bottom:calc(84px + env(safe-area-inset-bottom,0px));transform:translate(-50%);background:var(--teal-dark);color:var(--white);border-radius:14px;padding:.65rem 1.05rem;font-size:.9rem;font-weight:600;box-shadow:var(--shadow-lg);z-index:10;max-width:90vw;animation:toast-in .35s cubic-bezier(.175,.885,.32,1.275)}@keyframes toast-in{0%{opacity:0;transform:translate(-50%,12px)}to{opacity:1;transform:translate(-50%)}}.hint{font-size:.82rem;color:var(--muted);line-height:1.45}.lang-toggle{display:flex;gap:.5rem}.lang-toggle button{flex:1}.lang-toggle button.active{background:var(--teal);color:var(--white);border-color:var(--teal)}.voice-fab{position:fixed;left:50%;transform:translate(-50%);bottom:calc(74px + env(safe-area-inset-bottom,0px));z-index:9;display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:50px;padding:0 1.4rem;border-radius:999px;border:none;background:var(--teal);color:var(--white);font-family:inherit;font-size:.98rem;font-weight:800;box-shadow:0 12px 30px #2c8b8859;cursor:pointer;max-width:90vw;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;touch-action:manipulation;transition:transform .15s ease,background .25s ease,box-shadow .25s ease}.voice-fab:active{transform:translate(-50%) scale(.96)}.voice-fab.active{background:var(--peach-deep);box-shadow:0 12px 30px #e5885c66}.voice-fab.speaking{background:var(--peach-deep);animation:voice-pulse 1.2s ease-in-out infinite}@keyframes voice-pulse{0%,to{box-shadow:0 12px 30px #e5885c59}50%{box-shadow:0 12px 40px #e5885ca6}}.voice-fab svg{width:19px;height:19px;flex:none}.voice-fab .spin-path{transform-origin:12px 12px;animation:fab-spin .9s linear infinite}@keyframes fab-spin{to{transform:rotate(360deg)}}.voice-transcript{position:fixed;left:50%;transform:translate(-50%);bottom:calc(136px + env(safe-area-inset-bottom,0px));width:min(92vw,560px);max-height:38vh;display:none;background:#f8f9fa;border:1px solid #e9ecef;border-radius:24px;box-shadow:var(--shadow-lg);z-index:8;overflow:hidden}.voice-transcript .vt-head{display:flex;align-items:center;justify-content:space-between;padding:.5rem 1rem;border-bottom:1px solid #e9ecef;font-size:.75rem;font-weight:800;color:var(--teal-dark);text-transform:uppercase;letter-spacing:.06em;background:var(--white)}.voice-transcript .vt-clear{border:none;background:none;color:var(--peach-deep);font-family:inherit;font-size:.78rem;font-weight:700;cursor:pointer;min-height:32px;padding:0 .4rem}.voice-transcript .vt-body{overflow-y:auto;-webkit-overflow-scrolling:touch;max-height:calc(38vh - 44px);padding:.8rem .9rem .9rem;display:flex;flex-direction:column;gap:.7rem}.voice-transcript .vt-msg{display:flex;align-items:flex-end;gap:8px;animation:slide-in .4s cubic-bezier(.175,.885,.32,1.275)}.voice-transcript .vt-msg.user{flex-direction:row-reverse}@keyframes slide-in{0%{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}.voice-transcript .avatar{width:34px;height:34px;flex:none}.voice-transcript .bubble{padding:9px 13px;border-radius:16px;max-width:80%;font-size:.9rem;line-height:1.45;color:var(--white)}.voice-transcript .vt-msg.assistant .bubble{background:var(--peach-deep);border-bottom-left-radius:4px;box-shadow:0 4px 15px #2c8b8826}.voice-transcript .vt-msg.user .bubble{background:var(--teal);border-bottom-right-radius:4px;box-shadow:0 4px 15px #2c8b8833}.voice-transcript .vt-empty{color:var(--muted);font-size:.85rem;text-align:center;padding:.6rem 0}.voice-transcript .bubble.photo-bubble{padding:5px;background:var(--white);box-shadow:var(--shadow-sm)}.voice-transcript .bubble .photo{display:block;max-width:100%;max-height:180px;border-radius:12px}.sort-bar{display:flex;align-items:center;gap:.6rem;margin:.2rem 0 .8rem}.sort-bar .sort-label{font-size:.8rem;font-weight:700;color:var(--muted)}.segmented{display:inline-flex;background:var(--cream-strong);border-radius:12px;padding:3px;gap:2px}.segmented button{border:none;background:none;font-family:inherit;font-size:.82rem;font-weight:700;color:var(--teal-dark);padding:.35rem .7rem;border-radius:9px;cursor:pointer;min-height:36px;touch-action:manipulation}.segmented button.active{background:var(--white);color:var(--teal);box-shadow:var(--shadow-sm)}.drag-hint{display:flex;align-items:center;gap:.35rem;margin:0 0 .5rem}.drag-hint svg{width:16px;height:16px}.filter-banner{display:flex;align-items:center;gap:.4rem;width:100%;border:none;background:var(--cream-strong);color:var(--teal-dark);font-family:inherit;font-weight:700;font-size:.85rem;border-radius:12px;padding:.5rem .8rem;margin-bottom:.6rem;cursor:pointer}.filter-banner svg{width:16px;height:16px;flex:none}.inv-list{display:flex;flex-direction:column;gap:.6rem}.inv-card{display:flex;align-items:center;gap:.5rem;margin-bottom:0;touch-action:pan-y}.inv-card.dragging{opacity:.85;box-shadow:var(--shadow-lg);transform:scale(1.02)}.drag-handle{display:flex;align-items:center;color:var(--muted);cursor:grab;padding:.2rem;margin:-.2rem 0 -.2rem -.3rem}.drag-handle svg{width:18px;height:18px}.item-meta{display:flex;flex-wrap:wrap;align-items:center;gap:.35rem .7rem;margin-top:.25rem;font-size:.82rem;color:var(--muted)}.item-meta .qty{font-weight:700;color:var(--ink)}.item-meta .exp{display:inline-flex;align-items:center;gap:.2rem}.item-meta .exp.over{color:var(--danger);font-weight:700}.item-meta .exp svg{width:14px;height:14px}.item-meta .loc-chip{display:inline-flex;align-items:center;gap:.15rem;border:none;background:var(--cream-strong);color:var(--teal-dark);font-family:inherit;font-size:.78rem;font-weight:700;border-radius:999px;padding:.12rem .5rem .12rem .35rem;cursor:pointer;touch-action:manipulation}.item-meta .loc-chip svg{width:13px;height:13px}.inv-actions{display:flex;gap:.15rem;flex:none}.icon-btn{display:inline-flex;align-items:center;justify-content:center;width:40px;height:40px;border:none;background:none;color:var(--teal-dark);border-radius:10px;cursor:pointer;touch-action:manipulation;transition:background .15s ease,transform .15s ease}.icon-btn:active{transform:scale(.9)}.icon-btn:hover{background:var(--cream)}.icon-btn svg{width:20px;height:20px}.icon-btn.danger{color:var(--danger)}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:30;background:#18262673;display:flex;align-items:flex-end;justify-content:center;padding:1rem;padding-bottom:calc(1rem + env(safe-area-inset-bottom,0px));animation:fade-in .2s ease}@media (min-width: 540px){.modal-backdrop{align-items:center}}@keyframes fade-in{0%{opacity:0}to{opacity:1}}.modal-card{background:var(--white);border-radius:22px;width:100%;max-width:440px;box-shadow:var(--shadow-lg);padding:1.1rem 1.1rem 1rem;animation:modal-up .28s cubic-bezier(.175,.885,.32,1.1)}@keyframes modal-up{0%{transform:translateY(20px);opacity:.6}to{transform:translateY(0);opacity:1}}.modal-title{font-family:Fraunces,serif;font-size:1.2rem;font-weight:700;margin-bottom:.6rem}.modal-body.confirm-msg{color:var(--muted);line-height:1.5;padding:.2rem 0 .4rem}.modal-actions{display:flex;gap:.6rem;margin-top:1rem}.modal-actions .btn{flex:1}.sync-row{display:flex;gap:.6rem;margin-top:.6rem}.sync-row .btn{flex:1}.switch-row{display:flex;align-items:center;gap:.7rem;margin-top:.9rem}.switch-row>span:first-child{flex:1}.switch{-moz-appearance:none;appearance:none;-webkit-appearance:none;width:46px;height:28px;border-radius:999px;background:var(--cream-strong);position:relative;flex:none;cursor:pointer;transition:background .2s ease}.switch:after{content:"";position:absolute;top:3px;left:3px;width:22px;height:22px;border-radius:50%;background:var(--white);box-shadow:0 1px 3px #00000040;transition:transform .2s ease}.switch:checked{background:var(--teal)}.switch:checked:after{transform:translate(18px)}.sync-status{margin-top:.7rem;text-align:center}.vt-head-actions{display:flex;align-items:center;gap:.2rem}.vt-min{width:32px;height:32px;color:var(--teal-dark)}.vt-min svg{width:20px;height:20px}.voice-transcript.minimized{max-height:none}.voice-transcript.minimized .vt-body{display:none}.voice-transcript.minimized .vt-head{cursor:pointer}
 
1
+ :root{--cream: #fff4e6;--cream-strong: #ffe8c9;--peach: #f8b48a;--peach-deep: #e5885c;--teal: #2c8b88;--teal-dark: #0f5453;--ink: #1c2428;--muted: #4e5c61;--white: #ffffff;--card: #fffaf3;--danger: #b3402a;--danger-bg: #ffe1d6;--warn: #8a6210;--warn-bg: #ffedc2;--shadow-sm: 0 8px 18px rgba(24, 38, 38, .08);--shadow-lg: 0 18px 40px rgba(24, 38, 38, .15)}*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}html,body{margin:0;padding:0;height:100%}body{font-family:Nunito,-apple-system,BlinkMacSystemFont,sans-serif;background:linear-gradient(180deg,#fff7ed,#fef0e1,#f5ece6);color:var(--ink);-webkit-font-smoothing:antialiased;overscroll-behavior:none}#root{display:flex;flex-direction:column;height:100dvh}.app-main{flex:1;overflow-y:auto;-webkit-overflow-scrolling:touch;padding:1rem 1rem calc(7.5rem + env(safe-area-inset-bottom,0px));max-width:640px;width:100%;margin:0 auto}.app-tabs{display:flex;background:var(--white);border-top:1px solid rgba(44,139,136,.12);box-shadow:0 -6px 18px #1826260f;padding-bottom:env(safe-area-inset-bottom,0px)}.app-tabs button{flex:1;min-height:58px;border:none;background:none;color:var(--muted);font-family:inherit;font-size:.72rem;font-weight:700;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.2rem;cursor:pointer;padding:.4rem .2rem;touch-action:manipulation;transition:color .2s ease,transform .15s ease}.app-tabs button:active{transform:scale(.94)}.app-tabs button .tab-icon{line-height:1;display:flex}.app-tabs button .tab-icon svg{width:23px;height:23px}.app-tabs button.active{color:var(--teal)}h1{font-family:Fraunces,serif;font-size:1.55rem;margin:.3rem 0 1rem;color:var(--ink)}h2{font-family:inherit;font-size:.82rem;font-weight:800;margin:1.5rem 0 .6rem;color:var(--teal-dark);text-transform:uppercase;letter-spacing:.06em}.card{background:var(--white);border:1px solid rgba(0,0,0,.04);border-radius:20px;padding:.95rem 1.05rem;margin-bottom:.7rem;box-shadow:var(--shadow-sm);transition:transform .2s ease,box-shadow .2s ease}.row{display:flex;align-items:center;gap:.6rem}.row .grow{flex:1;min-width:0}.item-title{font-weight:800;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.item-sub{color:var(--muted);font-size:.85rem;margin-top:.15rem}.chip{display:inline-block;font-size:.72rem;font-weight:700;border-radius:999px;padding:.16rem .6rem;margin-left:.4rem;vertical-align:middle}.chip.warn{background:var(--warn-bg);color:var(--warn)}.chip.danger{background:var(--danger-bg);color:var(--danger)}button.btn{min-height:46px;min-width:46px;border-radius:14px;border:1px solid rgba(44,139,136,.2);background:var(--card);color:var(--teal-dark);font-family:inherit;font-size:.95rem;font-weight:700;padding:0 1rem;cursor:pointer;touch-action:manipulation;transition:transform .15s ease,box-shadow .2s ease,background .2s ease}button.btn:active{transform:scale(.97)}button.btn.primary{background:var(--teal);border-color:var(--teal);color:var(--white);box-shadow:0 10px 24px #2c8b8840}button.btn.small{min-height:44px;font-size:.85rem;padding:0 .75rem}button.btn.block{width:100%;margin:.45rem 0}input,select{min-height:46px;width:100%;min-width:0;max-width:100%;border-radius:14px;border:1.5px solid var(--cream-strong);background:var(--white);color:var(--ink);font-family:inherit;font-size:1rem;padding:0 .8rem;transition:border-color .2s ease,box-shadow .2s ease}input[type=date]{-webkit-appearance:none;-moz-appearance:none;appearance:none;display:block;min-width:0}.form-grid>div{min-width:0}input:focus,select:focus{outline:none;border-color:var(--teal);box-shadow:0 0 0 3px #2c8b8826}label{display:block;font-size:.8rem;font-weight:700;color:var(--muted);margin:.65rem 0 .28rem}.form-grid{display:grid;grid-template-columns:2fr 1fr;gap:0 .6rem}.empty{color:var(--muted);text-align:center;padding:2rem 1rem}.day-card .day-name{font-family:Fraunces,serif;font-weight:700;font-size:1.05rem;margin-bottom:.4rem}.meal{border-top:1px solid var(--cream-strong);padding:.55rem 0}.meal:first-of-type{border-top:none}.meal .meal-kind{font-size:.72rem;font-weight:800;color:var(--teal);text-transform:uppercase;letter-spacing:.06em}.meal .meal-name{font-weight:800;margin:.12rem 0}.meal .meal-ing{color:var(--muted);font-size:.85rem}.toast{position:fixed;left:50%;bottom:calc(84px + env(safe-area-inset-bottom,0px));transform:translate(-50%);background:var(--teal-dark);color:var(--white);border-radius:14px;padding:.65rem 1.05rem;font-size:.9rem;font-weight:600;box-shadow:var(--shadow-lg);z-index:10;max-width:90vw;animation:toast-in .35s cubic-bezier(.175,.885,.32,1.275)}@keyframes toast-in{0%{opacity:0;transform:translate(-50%,12px)}to{opacity:1;transform:translate(-50%)}}.hint{font-size:.82rem;color:var(--muted);line-height:1.45}.lang-toggle{display:flex;gap:.5rem}.lang-toggle button{flex:1}.lang-toggle button.active{background:var(--teal);color:var(--white);border-color:var(--teal)}.voice-fab{position:fixed;left:50%;transform:translate(-50%);bottom:calc(74px + env(safe-area-inset-bottom,0px));z-index:9;display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:50px;padding:0 1.4rem;border-radius:999px;border:none;background:var(--teal);color:var(--white);font-family:inherit;font-size:.98rem;font-weight:800;box-shadow:0 12px 30px #2c8b8859;cursor:pointer;max-width:90vw;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;touch-action:manipulation;transition:transform .15s ease,background .25s ease,box-shadow .25s ease}.voice-fab:active{transform:translate(-50%) scale(.96)}.voice-fab.active{background:var(--peach-deep);box-shadow:0 12px 30px #e5885c66}.voice-fab.speaking{background:var(--peach-deep);animation:voice-pulse 1.2s ease-in-out infinite}@keyframes voice-pulse{0%,to{box-shadow:0 12px 30px #e5885c59}50%{box-shadow:0 12px 40px #e5885ca6}}.voice-fab svg{width:19px;height:19px;flex:none}.voice-fab .spin-path{transform-origin:12px 12px;animation:fab-spin .9s linear infinite}@keyframes fab-spin{to{transform:rotate(360deg)}}.voice-transcript{position:fixed;left:50%;transform:translate(-50%);bottom:calc(136px + env(safe-area-inset-bottom,0px));width:min(92vw,560px);max-height:38vh;display:none;background:#f8f9fa;border:1px solid #e9ecef;border-radius:24px;box-shadow:var(--shadow-lg);z-index:8;overflow:hidden}.voice-transcript .vt-head{display:flex;align-items:center;justify-content:space-between;padding:.5rem 1rem;border-bottom:1px solid #e9ecef;font-size:.75rem;font-weight:800;color:var(--teal-dark);text-transform:uppercase;letter-spacing:.06em;background:var(--white)}.voice-transcript .vt-clear{border:none;background:none;color:var(--peach-deep);font-family:inherit;font-size:.78rem;font-weight:700;cursor:pointer;min-height:32px;padding:0 .4rem}.voice-transcript .vt-body{overflow-y:auto;-webkit-overflow-scrolling:touch;max-height:calc(38vh - 44px);padding:.8rem .9rem .9rem;display:flex;flex-direction:column;gap:.7rem}.voice-transcript .vt-msg{display:flex;align-items:flex-end;gap:8px;animation:slide-in .4s cubic-bezier(.175,.885,.32,1.275)}.voice-transcript .vt-msg.user{flex-direction:row-reverse}@keyframes slide-in{0%{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}.voice-transcript .avatar{width:34px;height:34px;flex:none}.voice-transcript .bubble{padding:9px 13px;border-radius:16px;max-width:80%;font-size:.9rem;line-height:1.45;color:var(--white)}.voice-transcript .vt-msg.assistant .bubble{background:var(--peach-deep);border-bottom-left-radius:4px;box-shadow:0 4px 15px #2c8b8826}.voice-transcript .vt-msg.user .bubble{background:var(--teal);border-bottom-right-radius:4px;box-shadow:0 4px 15px #2c8b8833}.voice-transcript .vt-empty{color:var(--muted);font-size:.85rem;text-align:center;padding:.6rem 0}.voice-transcript .bubble.photo-bubble{padding:5px;background:var(--white);box-shadow:var(--shadow-sm)}.voice-transcript .bubble .photo{display:block;max-width:100%;max-height:180px;border-radius:12px}.sort-bar{display:flex;align-items:center;gap:.6rem;margin:.2rem 0 .8rem}.sort-bar .sort-label{font-size:.8rem;font-weight:700;color:var(--muted)}.segmented{display:inline-flex;background:var(--cream-strong);border-radius:12px;padding:3px;gap:2px}.segmented button{border:none;background:none;font-family:inherit;font-size:.82rem;font-weight:700;color:var(--teal-dark);padding:.35rem .7rem;border-radius:9px;cursor:pointer;min-height:36px;touch-action:manipulation}.segmented button.active{background:var(--white);color:var(--teal);box-shadow:var(--shadow-sm)}.drag-hint{display:flex;align-items:center;gap:.35rem;margin:0 0 .5rem}.drag-hint svg{width:16px;height:16px}.filter-banner{display:flex;align-items:center;gap:.4rem;width:100%;border:none;background:var(--cream-strong);color:var(--teal-dark);font-family:inherit;font-weight:700;font-size:.85rem;border-radius:12px;padding:.5rem .8rem;margin-bottom:.6rem;cursor:pointer}.filter-banner svg{width:16px;height:16px;flex:none}.inv-list{display:flex;flex-direction:column;gap:.6rem}.inv-card{display:flex;align-items:center;gap:.5rem;margin-bottom:0;touch-action:pan-y}.inv-card.dragging{opacity:.85;box-shadow:var(--shadow-lg);transform:scale(1.02)}.drag-handle{display:flex;align-items:center;color:var(--muted);cursor:grab;padding:.2rem;margin:-.2rem 0 -.2rem -.3rem}.drag-handle svg{width:18px;height:18px}.item-meta{display:flex;flex-wrap:wrap;align-items:center;gap:.35rem .7rem;margin-top:.25rem;font-size:.82rem;color:var(--muted)}.item-meta .qty{font-weight:700;color:var(--ink)}.item-meta .exp{display:inline-flex;align-items:center;gap:.2rem}.item-meta .exp.over{color:var(--danger);font-weight:700}.item-meta .exp svg{width:14px;height:14px}.item-meta .loc-chip{display:inline-flex;align-items:center;gap:.15rem;border:none;background:var(--cream-strong);color:var(--teal-dark);font-family:inherit;font-size:.78rem;font-weight:700;border-radius:999px;padding:.12rem .5rem .12rem .35rem;cursor:pointer;touch-action:manipulation}.item-meta .loc-chip svg{width:13px;height:13px}.inv-actions{display:flex;gap:.15rem;flex:none}.icon-btn{display:inline-flex;align-items:center;justify-content:center;width:40px;height:40px;border:none;background:none;color:var(--teal-dark);border-radius:10px;cursor:pointer;touch-action:manipulation;transition:background .15s ease,transform .15s ease}.icon-btn:active{transform:scale(.9)}.icon-btn:hover{background:var(--cream)}.icon-btn svg{width:20px;height:20px}.icon-btn.danger{color:var(--danger)}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:30;background:#18262673;display:flex;align-items:flex-end;justify-content:center;padding:1rem;padding-bottom:calc(1rem + env(safe-area-inset-bottom,0px));animation:fade-in .2s ease}@media (min-width: 540px){.modal-backdrop{align-items:center}}@keyframes fade-in{0%{opacity:0}to{opacity:1}}.modal-card{background:var(--white);border-radius:22px;width:100%;max-width:440px;box-shadow:var(--shadow-lg);padding:1.1rem 1.1rem 1rem;animation:modal-up .28s cubic-bezier(.175,.885,.32,1.1)}@keyframes modal-up{0%{transform:translateY(20px);opacity:.6}to{transform:translateY(0);opacity:1}}.modal-title{font-family:Fraunces,serif;font-size:1.2rem;font-weight:700;margin-bottom:.6rem}.modal-body.confirm-msg{color:var(--muted);line-height:1.5;padding:.2rem 0 .4rem}.modal-actions{display:flex;gap:.6rem;margin-top:1rem}.modal-actions .btn{flex:1}.sync-row{display:flex;gap:.6rem;margin-top:.6rem}.sync-row .btn{flex:1}.switch-row{display:flex;align-items:center;gap:.7rem;margin-top:.9rem}.switch-row>span:first-child{flex:1}input.switch{-moz-appearance:none;appearance:none;-webkit-appearance:none;width:46px;min-width:46px;max-width:46px;height:28px;min-height:28px;padding:0;border:none;border-radius:999px;background:var(--cream-strong);position:relative;flex:none;cursor:pointer;transition:background .2s ease}.switch:after{content:"";position:absolute;top:3px;left:3px;width:22px;height:22px;border-radius:50%;background:var(--white);box-shadow:0 1px 3px #00000040;transition:transform .2s ease}.switch:checked{background:var(--teal)}.switch:checked:after{transform:translate(18px)}.sync-status{margin-top:.7rem;text-align:center}.vt-head-actions{display:flex;align-items:center;gap:.2rem}.vt-min{width:32px;height:32px;color:var(--teal-dark)}.vt-min svg{width:20px;height:20px}.voice-transcript.minimized{max-height:none}.voice-transcript.minimized .vt-body{display:none}.voice-transcript.minimized .vt-head{cursor:pointer}
assets/embed-CtskQx_e.js DELETED
The diff for this file is too large to render. See raw diff
 
assets/{index-BkQO3aLW.js → index-BNLguQgW.js} RENAMED
@@ -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-CtskQx_e.js","assets/embed-DdITxSHM.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-CtskQx_e.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-BPTJET-M.js","assets/embed-CsvkHiDH.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-BPTJET-M.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};
index.html CHANGED
@@ -59,7 +59,7 @@
59
  }
60
  })();
61
  </script>
62
- <script type="module" crossorigin src="/assets/index-BkQO3aLW.js"></script>
63
  </head>
64
  <body>
65
  <!-- Capture the robot's inbound audio track BEFORE the Reachy SDK module
 
59
  }
60
  })();
61
  </script>
62
+ <script type="module" crossorigin src="/assets/index-BNLguQgW.js"></script>
63
  </head>
64
  <body>
65
  <!-- Capture the robot's inbound audio track BEFORE the Reachy SDK module
web/src/i18n/index.ts CHANGED
@@ -50,6 +50,7 @@ const STRINGS = {
50
  sort_label: "Sort",
51
  sort_expiration: "Expiration",
52
  sort_name: "Name",
 
53
  sort_custom: "Custom",
54
  drag_hint: "Drag items to reorder",
55
  filter_location: (loc: string) => `Showing “${loc}” — tap to clear`,
@@ -93,6 +94,11 @@ const STRINGS = {
93
  hf_sync_err: "Sync failed. Check the token and try again.",
94
  hf_autosync: "Auto-sync",
95
  hf_autosync_hint: "Save to your HF account automatically a few seconds after each change.",
 
 
 
 
 
96
  hf_last_synced: (when: string) => `Last synced ${when}`,
97
  hf_never_synced: "Not synced yet",
98
  hf_save_confirm: (summary: string) =>
@@ -103,6 +109,8 @@ const STRINGS = {
103
  `${inv} item${inv === 1 ? "" : "s"}, ${plan ? "a meal plan" : "no meal plan"}, ${shop} to buy`,
104
  confirm_yes: "Continue",
105
  voice_title: "Talk to Reachy",
 
 
106
  voice_key_label: "OpenAI API key",
107
  voice_key_hint: "Used for voice (gpt-realtime) and for Reachy's camera vision (gpt-4o-mini). The key needs permissions for BOTH the Realtime API and Model capabilities — a key with 'All' permissions is simplest. Stored only on this device, sent only to OpenAI, uses your own OpenAI credits.",
108
  voice_start: "Talk to Reachy",
@@ -178,6 +186,7 @@ const STRINGS = {
178
  sort_label: "Ordenar",
179
  sort_expiration: "Caducidad",
180
  sort_name: "Nombre",
 
181
  sort_custom: "Manual",
182
  drag_hint: "Arrastra para reordenar",
183
  filter_location: (loc: string) => `Mostrando “${loc}” — toca para quitar`,
@@ -222,6 +231,11 @@ const STRINGS = {
222
  hf_sync_err: "Error de sincronización. Revisa el token e inténtalo de nuevo.",
223
  hf_autosync: "Sincronización automática",
224
  hf_autosync_hint: "Guarda en tu cuenta de HF automáticamente unos segundos después de cada cambio.",
 
 
 
 
 
225
  hf_last_synced: (when: string) => `Última sincronización ${when}`,
226
  hf_never_synced: "Aún sin sincronizar",
227
  hf_save_confirm: (summary: string) =>
@@ -232,6 +246,8 @@ const STRINGS = {
232
  `${inv} alimento${inv === 1 ? "" : "s"}, ${plan ? "un menú" : "sin menú"}, ${shop} por comprar`,
233
  confirm_yes: "Continuar",
234
  voice_title: "Habla con Reachy",
 
 
235
  voice_key_label: "Clave de API de OpenAI",
236
  voice_key_hint: "Se usa para la voz (gpt-realtime) y para la visión de la cámara de Reachy (gpt-4o-mini). La clave necesita permisos para la API Realtime Y para Model capabilities — lo más sencillo es una clave con permisos 'All'. Se guarda solo en este dispositivo, se envía solo a OpenAI y consume tus créditos de OpenAI.",
237
  voice_start: "Hablar con Reachy",
 
50
  sort_label: "Sort",
51
  sort_expiration: "Expiration",
52
  sort_name: "Name",
53
+ sort_location: "Location",
54
  sort_custom: "Custom",
55
  drag_hint: "Drag items to reorder",
56
  filter_location: (loc: string) => `Showing “${loc}” — tap to clear`,
 
94
  hf_sync_err: "Sync failed. Check the token and try again.",
95
  hf_autosync: "Auto-sync",
96
  hf_autosync_hint: "Save to your HF account automatically a few seconds after each change.",
97
+ hf_choose_title: "Your HF account already has data",
98
+ hf_choose_body: (cloud: string, local: string) =>
99
+ `Cloud: ${cloud}. This device: ${local}. Which one do you want to keep? The other will be overwritten.`,
100
+ hf_keep_cloud: "Keep cloud",
101
+ hf_keep_device: "Keep this device",
102
  hf_last_synced: (when: string) => `Last synced ${when}`,
103
  hf_never_synced: "Not synced yet",
104
  hf_save_confirm: (summary: string) =>
 
109
  `${inv} item${inv === 1 ? "" : "s"}, ${plan ? "a meal plan" : "no meal plan"}, ${shop} to buy`,
110
  confirm_yes: "Continue",
111
  voice_title: "Talk to Reachy",
112
+ voice_name_label: "Robot name",
113
+ voice_name_hint: "What your robot is called in conversation.",
114
  voice_key_label: "OpenAI API key",
115
  voice_key_hint: "Used for voice (gpt-realtime) and for Reachy's camera vision (gpt-4o-mini). The key needs permissions for BOTH the Realtime API and Model capabilities — a key with 'All' permissions is simplest. Stored only on this device, sent only to OpenAI, uses your own OpenAI credits.",
116
  voice_start: "Talk to Reachy",
 
186
  sort_label: "Ordenar",
187
  sort_expiration: "Caducidad",
188
  sort_name: "Nombre",
189
+ sort_location: "Ubicación",
190
  sort_custom: "Manual",
191
  drag_hint: "Arrastra para reordenar",
192
  filter_location: (loc: string) => `Mostrando “${loc}” — toca para quitar`,
 
231
  hf_sync_err: "Error de sincronización. Revisa el token e inténtalo de nuevo.",
232
  hf_autosync: "Sincronización automática",
233
  hf_autosync_hint: "Guarda en tu cuenta de HF automáticamente unos segundos después de cada cambio.",
234
+ hf_choose_title: "Tu cuenta de HF ya tiene datos",
235
+ hf_choose_body: (cloud: string, local: string) =>
236
+ `Nube: ${cloud}. Este dispositivo: ${local}. ¿Cuál quieres conservar? El otro se sobrescribirá.`,
237
+ hf_keep_cloud: "Conservar la nube",
238
+ hf_keep_device: "Conservar este dispositivo",
239
  hf_last_synced: (when: string) => `Última sincronización ${when}`,
240
  hf_never_synced: "Aún sin sincronizar",
241
  hf_save_confirm: (summary: string) =>
 
246
  `${inv} alimento${inv === 1 ? "" : "s"}, ${plan ? "un menú" : "sin menú"}, ${shop} por comprar`,
247
  confirm_yes: "Continuar",
248
  voice_title: "Habla con Reachy",
249
+ voice_name_label: "Nombre del robot",
250
+ voice_name_hint: "Cómo se llama tu robot en la conversación.",
251
  voice_key_label: "Clave de API de OpenAI",
252
  voice_key_hint: "Se usa para la voz (gpt-realtime) y para la visión de la cámara de Reachy (gpt-4o-mini). La clave necesita permisos para la API Realtime Y para Model capabilities — lo más sencillo es una clave con permisos 'All'. Se guarda solo en este dispositivo, se envía solo a OpenAI y consume tus créditos de OpenAI.",
253
  voice_start: "Hablar con Reachy",
web/src/storage/store.ts CHANGED
@@ -16,7 +16,8 @@ export interface AppSettings {
16
  hfDatasetSync: boolean; // auto-sync to HF after each change
17
  lastSyncedAt: string | null; // ISO timestamp of last successful upload
18
  openaiKey: string | null;
19
- inventorySort: "expiration" | "name" | "custom";
 
20
  }
21
 
22
  const DATA_KEY = "cookaiware:data";
@@ -53,6 +54,7 @@ export function loadSettings(): AppSettings {
53
  lastSyncedAt: null,
54
  openaiKey: null,
55
  inventorySort: "expiration",
 
56
  };
57
  try {
58
  const raw = localStorage.getItem(SETTINGS_KEY);
@@ -65,7 +67,10 @@ export function loadSettings(): AppSettings {
65
  lastSyncedAt: typeof parsed.lastSyncedAt === "string" ? parsed.lastSyncedAt : null,
66
  openaiKey: typeof parsed.openaiKey === "string" && parsed.openaiKey ? parsed.openaiKey : null,
67
  inventorySort:
68
- parsed.inventorySort === "name" || parsed.inventorySort === "custom" ? parsed.inventorySort : "expiration",
 
 
 
69
  };
70
  } catch {
71
  return fallback;
 
16
  hfDatasetSync: boolean; // auto-sync to HF after each change
17
  lastSyncedAt: string | null; // ISO timestamp of last successful upload
18
  openaiKey: string | null;
19
+ inventorySort: "expiration" | "name" | "location" | "custom";
20
+ robotName: string;
21
  }
22
 
23
  const DATA_KEY = "cookaiware:data";
 
54
  lastSyncedAt: null,
55
  openaiKey: null,
56
  inventorySort: "expiration",
57
+ robotName: "Reachy",
58
  };
59
  try {
60
  const raw = localStorage.getItem(SETTINGS_KEY);
 
67
  lastSyncedAt: typeof parsed.lastSyncedAt === "string" ? parsed.lastSyncedAt : null,
68
  openaiKey: typeof parsed.openaiKey === "string" && parsed.openaiKey ? parsed.openaiKey : null,
69
  inventorySort:
70
+ parsed.inventorySort === "name" || parsed.inventorySort === "custom" || parsed.inventorySort === "location"
71
+ ? parsed.inventorySort
72
+ : "expiration",
73
+ robotName: typeof parsed.robotName === "string" && parsed.robotName.trim() ? parsed.robotName.trim() : "Reachy",
74
  };
75
  } catch {
76
  return fallback;
web/src/ui/app.ts CHANGED
@@ -83,6 +83,7 @@ export class App {
83
  gestures,
84
  getApiKey: () => this.settings.openaiKey,
85
  getLang: () => this.settings.language,
 
86
  toolCtx: {
87
  getData: () => this.data,
88
  commit: (d) => {
@@ -198,6 +199,33 @@ export class App {
198
  bodyWrap.querySelector<HTMLElement>("input, select")?.focus();
199
  }
200
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  private confirm(message: string): Promise<boolean> {
202
  const s = this.s;
203
  return new Promise((resolve) => {
@@ -455,6 +483,7 @@ export class App {
455
  for (const [mode, label] of [
456
  ["expiration", s.sort_expiration],
457
  ["name", s.sort_name],
 
458
  ["custom", s.sort_custom],
459
  ] as const) {
460
  const b = document.createElement("button");
@@ -538,6 +567,14 @@ export class App {
538
  const mode = this.settings.inventorySort;
539
  if (mode === "name") items = [...items].sort((a, b) => a.display_name.localeCompare(b.display_name));
540
  else if (mode === "expiration") items = inv.sortInventory(items);
 
 
 
 
 
 
 
 
541
  // "custom" keeps the underlying array order.
542
  if (this.locationFilter) items = items.filter((i) => (i.storage_location ?? "") === this.locationFilter);
543
  return items;
@@ -983,10 +1020,52 @@ export class App {
983
  autoInput.type = "checkbox";
984
  autoInput.className = "switch";
985
  autoInput.checked = this.settings.hfDatasetSync;
986
- autoInput.onchange = () => {
987
- this.settings.hfDatasetSync = autoInput.checked;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
988
  saveSettings(this.settings);
989
- if (autoInput.checked) void this.pushToCloud(false);
 
990
  };
991
  autoRow.appendChild(autoInput);
992
 
@@ -1019,6 +1098,19 @@ export class App {
1019
  };
1020
  voiceCard.append(keyLabel, keyInput);
1021
  voiceCard.insertAdjacentHTML("beforeend", `<p class="hint">${s.voice_key_hint}</p>`);
 
 
 
 
 
 
 
 
 
 
 
 
 
1022
  main.appendChild(voiceCard);
1023
  }
1024
  }
 
83
  gestures,
84
  getApiKey: () => this.settings.openaiKey,
85
  getLang: () => this.settings.language,
86
+ getName: () => this.settings.robotName,
87
  toolCtx: {
88
  getData: () => this.data,
89
  commit: (d) => {
 
199
  bodyWrap.querySelector<HTMLElement>("input, select")?.focus();
200
  }
201
 
202
+ /** Two-way choice dialog; resolves "a", "b", or null when dismissed. */
203
+ private choose(title: string, message: string, labelA: string, labelB: string): Promise<"a" | "b" | null> {
204
+ return new Promise((resolve) => {
205
+ const backdrop = document.createElement("div");
206
+ backdrop.className = "modal-backdrop";
207
+ backdrop.innerHTML = `
208
+ <div class="modal-card">
209
+ <div class="modal-title">${escapeHtml(title)}</div>
210
+ <div class="modal-body confirm-msg">${escapeHtml(message)}</div>
211
+ <div class="modal-actions">
212
+ <button class="btn" data-a>${escapeHtml(labelA)}</button>
213
+ <button class="btn primary" data-b>${escapeHtml(labelB)}</button>
214
+ </div>
215
+ </div>`;
216
+ document.body.appendChild(backdrop);
217
+ const done = (v: "a" | "b" | null) => {
218
+ backdrop.remove();
219
+ resolve(v);
220
+ };
221
+ backdrop.querySelector<HTMLButtonElement>("[data-a]")!.onclick = () => done("a");
222
+ backdrop.querySelector<HTMLButtonElement>("[data-b]")!.onclick = () => done("b");
223
+ backdrop.onclick = (e) => {
224
+ if (e.target === backdrop) done(null);
225
+ };
226
+ });
227
+ }
228
+
229
  private confirm(message: string): Promise<boolean> {
230
  const s = this.s;
231
  return new Promise((resolve) => {
 
483
  for (const [mode, label] of [
484
  ["expiration", s.sort_expiration],
485
  ["name", s.sort_name],
486
+ ["location", s.sort_location],
487
  ["custom", s.sort_custom],
488
  ] as const) {
489
  const b = document.createElement("button");
 
567
  const mode = this.settings.inventorySort;
568
  if (mode === "name") items = [...items].sort((a, b) => a.display_name.localeCompare(b.display_name));
569
  else if (mode === "expiration") items = inv.sortInventory(items);
570
+ else if (mode === "location")
571
+ items = [...items].sort((a, b) => {
572
+ // Items without a location go last; within a location, sort by name.
573
+ const la = a.storage_location ?? "￿";
574
+ const lb = b.storage_location ?? "￿";
575
+ const cmp = la.localeCompare(lb);
576
+ return cmp !== 0 ? cmp : a.display_name.localeCompare(b.display_name);
577
+ });
578
  // "custom" keeps the underlying array order.
579
  if (this.locationFilter) items = items.filter((i) => (i.storage_location ?? "") === this.locationFilter);
580
  return items;
 
1020
  autoInput.type = "checkbox";
1021
  autoInput.className = "switch";
1022
  autoInput.checked = this.settings.hfDatasetSync;
1023
+ autoInput.onchange = async () => {
1024
+ if (!autoInput.checked) {
1025
+ this.settings.hfDatasetSync = false;
1026
+ saveSettings(this.settings);
1027
+ return;
1028
+ }
1029
+ const token = this.settings.hfToken;
1030
+ if (!token) {
1031
+ autoInput.checked = false;
1032
+ return this.toast(s.hf_sync_err);
1033
+ }
1034
+ // Before the first auto-save, reconcile with whatever is already in the
1035
+ // cloud so enabling the toggle can never silently overwrite it.
1036
+ try {
1037
+ const remote = await downloadData(token);
1038
+ if (remote) {
1039
+ const rc = summarizeData(remote);
1040
+ const lc = summarizeData(this.data);
1041
+ const differs = JSON.stringify(remote) !== JSON.stringify(this.data);
1042
+ if (differs && (rc.inventory > 0 || rc.hasPlan || rc.shopping > 0)) {
1043
+ const pick = await this.choose(
1044
+ s.hf_choose_title,
1045
+ s.hf_choose_body(
1046
+ s.data_summary(rc.inventory, rc.hasPlan, rc.shopping),
1047
+ s.data_summary(lc.inventory, lc.hasPlan, lc.shopping),
1048
+ ),
1049
+ s.hf_keep_cloud,
1050
+ s.hf_keep_device,
1051
+ );
1052
+ if (pick === null) {
1053
+ autoInput.checked = false;
1054
+ return;
1055
+ }
1056
+ if (pick === "a") {
1057
+ this.data = remote;
1058
+ saveData(this.data);
1059
+ }
1060
+ }
1061
+ }
1062
+ } catch (err) {
1063
+ console.warn("[cookaiware] auto-sync pre-check failed", err);
1064
+ }
1065
+ this.settings.hfDatasetSync = true;
1066
  saveSettings(this.settings);
1067
+ await this.pushToCloud(false);
1068
+ this.render();
1069
  };
1070
  autoRow.appendChild(autoInput);
1071
 
 
1098
  };
1099
  voiceCard.append(keyLabel, keyInput);
1100
  voiceCard.insertAdjacentHTML("beforeend", `<p class="hint">${s.voice_key_hint}</p>`);
1101
+
1102
+ const nameLabel = document.createElement("label");
1103
+ nameLabel.textContent = s.voice_name_label;
1104
+ const nameInput = document.createElement("input");
1105
+ nameInput.type = "text";
1106
+ nameInput.autocomplete = "off";
1107
+ nameInput.value = this.settings.robotName;
1108
+ nameInput.oninput = () => {
1109
+ this.settings.robotName = nameInput.value.trim() || "Reachy";
1110
+ saveSettings(this.settings);
1111
+ };
1112
+ voiceCard.append(nameLabel, nameInput);
1113
+ voiceCard.insertAdjacentHTML("beforeend", `<p class="hint">${s.voice_name_hint}</p>`);
1114
  main.appendChild(voiceCard);
1115
  }
1116
  }
web/src/ui/styles.css CHANGED
@@ -829,11 +829,18 @@ label {
829
  flex: 1;
830
  }
831
 
832
- .switch {
 
 
833
  appearance: none;
834
  -webkit-appearance: none;
835
  width: 46px;
 
 
836
  height: 28px;
 
 
 
837
  border-radius: 999px;
838
  background: var(--cream-strong);
839
  position: relative;
 
829
  flex: 1;
830
  }
831
 
832
+ /* Override the global input rules (min-height/width/border/padding) that were
833
+ deforming the toggle. */
834
+ input.switch {
835
  appearance: none;
836
  -webkit-appearance: none;
837
  width: 46px;
838
+ min-width: 46px;
839
+ max-width: 46px;
840
  height: 28px;
841
+ min-height: 28px;
842
+ padding: 0;
843
+ border: none;
844
  border-radius: 999px;
845
  background: var(--cream-strong);
846
  position: relative;
web/src/voice/controller.ts CHANGED
@@ -21,6 +21,7 @@ export class VoiceController {
21
  gestures: RobotGestures | null;
22
  getApiKey: () => string | null;
23
  getLang: () => Lang;
 
24
  toolCtx: ToolContext;
25
  onStatus: (s: VoiceStatus) => void;
26
  onError: (message: string) => void;
@@ -93,6 +94,7 @@ export class VoiceController {
93
  this.session = new RealtimeSession({
94
  apiKey,
95
  lang: this.opts.getLang(),
 
96
  micTrack,
97
  toolCtx: this.opts.toolCtx,
98
  callbacks: {
 
21
  gestures: RobotGestures | null;
22
  getApiKey: () => string | null;
23
  getLang: () => Lang;
24
+ getName: () => string;
25
  toolCtx: ToolContext;
26
  onStatus: (s: VoiceStatus) => void;
27
  onError: (message: string) => void;
 
94
  this.session = new RealtimeSession({
95
  apiKey,
96
  lang: this.opts.getLang(),
97
+ robotName: this.opts.getName(),
98
  micTrack,
99
  toolCtx: this.opts.toolCtx,
100
  callbacks: {
web/src/voice/realtime.ts CHANGED
@@ -11,24 +11,32 @@ import type { Lang } from "../i18n";
11
 
12
  const REALTIME_URL = "wss://api.openai.com/v1/realtime?model=gpt-realtime";
13
 
14
- function systemPrompt(lang: Lang): string {
15
  const langLine =
16
  lang === "es"
17
  ? "Habla SIEMPRE en español, de forma breve y familiar."
18
  : "Always speak in English, briefly and in a family-friendly way.";
19
  return [
20
- "You are CookAIware, a warm kitchen assistant living in a Reachy Mini robot.",
21
- "You help a family track their food inventory, plan weekly meals, and build shopping lists.",
22
  langLine,
 
 
 
 
 
 
 
23
  "Use the provided tools to read or change data — never invent inventory you have not read.",
 
 
 
 
 
 
24
  "You have NO continuous vision. Your ONLY way to see is the take_picture tool.",
25
  "NEVER describe or claim to see the user's surroundings unless it comes from a photo you just took.",
26
  "When the user asks what you see or shows you food, say you'll take a look and call take_picture. The photo then arrives as the next user message (an image).",
27
  "Describe ONLY what that photo shows. If food items are visible, name them with estimated quantity and unit, and offer to add them with add_inventory after confirming with the user.",
28
- "Use metric units only (g, kg, ml, l, pcs). Keep spoken replies short and natural.",
29
- "When the user says what they bought, call add_inventory. When they cooked something, call cook_inventory.",
30
- "When asked to plan, call generate_meal_plan; for a specific day, get_day_meals; for shopping, generate_shopping_list.",
31
- "After a tool runs, briefly tell the user what you did or found. Ask a short clarifying question only if a quantity or unit is missing.",
32
  ].join(" ");
33
  }
34
 
@@ -49,6 +57,7 @@ export class RealtimeSession {
49
  private cb: RealtimeCallbacks;
50
 
51
  assistantSpeaking = false;
 
52
  private micBytes = new Uint8Array(0);
53
  private micWorklet: AudioWorkletNode | null = null;
54
  private playbackEndsAt = 0;
@@ -56,21 +65,38 @@ export class RealtimeSession {
56
  private fnArgs = new Map<string, string>();
57
  private openWatchdog: ReturnType<typeof setTimeout> | null = null;
58
  private speakingWatchdog: ReturnType<typeof setTimeout> | null = null;
 
 
 
 
 
59
 
60
  constructor(opts: {
61
  apiKey: string;
62
  lang: Lang;
 
63
  micTrack: MediaStreamTrack;
64
  toolCtx: ToolContext;
65
  callbacks: RealtimeCallbacks;
66
  }) {
67
  this.apiKey = opts.apiKey;
68
  this.lang = opts.lang;
 
69
  this.micTrack = opts.micTrack;
70
  this.toolCtx = opts.toolCtx;
71
  this.cb = opts.callbacks;
72
  }
73
 
 
 
 
 
 
 
 
 
 
 
74
  start(): void {
75
  this.ws = new WebSocket(REALTIME_URL, ["realtime", `openai-insecure-api-key.${this.apiKey}`]);
76
  this.openWatchdog = setTimeout(() => {
@@ -104,7 +130,7 @@ export class RealtimeSession {
104
  type: "session.update",
105
  session: {
106
  type: "realtime",
107
- instructions: systemPrompt(this.lang),
108
  audio: {
109
  input: {
110
  format: { type: "audio/pcm", rate: 24000 },
@@ -130,7 +156,11 @@ export class RealtimeSession {
130
  });
131
  void this.startMicPump();
132
  // Short greeting so the user knows it's listening.
133
- const hello = this.lang === "es" ? "Saluda en una frase y di que estás listo para ayudar en la cocina." : "Greet in one short sentence and say you're ready to help in the kitchen.";
 
 
 
 
134
  this.send({ type: "response.create", response: { output_modalities: ["audio"], instructions: hello } });
135
  }
136
 
@@ -290,11 +320,21 @@ export class RealtimeSession {
290
  item: { type: "function_call_output", call_id: id, output: JSON.stringify(result) },
291
  });
292
  }
293
- this.send({ type: "response.create", response: { output_modalities: ["audio"] } });
 
 
294
  })();
295
  break;
296
  }
 
 
 
297
  case "response.done": {
 
 
 
 
 
298
  const drainMs = Math.max(0, (this.playbackEndsAt - audioContext().currentTime) * 1000);
299
  setTimeout(() => {
300
  this.setSpeaking(false);
 
11
 
12
  const REALTIME_URL = "wss://api.openai.com/v1/realtime?model=gpt-realtime";
13
 
14
+ function systemPrompt(lang: Lang, robotName: string): string {
15
  const langLine =
16
  lang === "es"
17
  ? "Habla SIEMPRE en español, de forma breve y familiar."
18
  : "Always speak in English, briefly and in a family-friendly way.";
19
  return [
20
+ `You are ${robotName}, a Reachy Mini robot that helps a family with their kitchen: food inventory, weekly meal plans, and shopping lists.`,
 
21
  langLine,
22
+ // Reactive, not proactive.
23
+ "STYLE: warm but VERY concise — one or two short sentences per reply.",
24
+ "Be REACTIVE, not proactive: do exactly what the user asked and nothing more.",
25
+ "Never chain extra actions, suggestions, tips or questions the user didn't ask for.",
26
+ "Only offer ideas when the user explicitly asks for suggestions or help deciding.",
27
+ "If required data is missing (a quantity or unit), ask ONE short question; otherwise don't ask anything.",
28
+ // Tools.
29
  "Use the provided tools to read or change data — never invent inventory you have not read.",
30
+ "When the user says what they bought, call add_inventory. When they cooked or used something, call cook_inventory.",
31
+ "To CHANGE an existing item (its storage location, expiration date, quantity, or name), call update_inventory_item — NEVER add_inventory, which would create a duplicate.",
32
+ "When asked to plan, call generate_meal_plan; for a specific day, get_day_meals; for shopping, generate_shopping_list.",
33
+ "After a tool runs, state the result in one short sentence.",
34
+ "Use metric units only (g, kg, ml, l, pcs).",
35
+ // Vision.
36
  "You have NO continuous vision. Your ONLY way to see is the take_picture tool.",
37
  "NEVER describe or claim to see the user's surroundings unless it comes from a photo you just took.",
38
  "When the user asks what you see or shows you food, say you'll take a look and call take_picture. The photo then arrives as the next user message (an image).",
39
  "Describe ONLY what that photo shows. If food items are visible, name them with estimated quantity and unit, and offer to add them with add_inventory after confirming with the user.",
 
 
 
 
40
  ].join(" ");
41
  }
42
 
 
57
  private cb: RealtimeCallbacks;
58
 
59
  assistantSpeaking = false;
60
+ private robotName: string;
61
  private micBytes = new Uint8Array(0);
62
  private micWorklet: AudioWorkletNode | null = null;
63
  private playbackEndsAt = 0;
 
65
  private fnArgs = new Map<string, string>();
66
  private openWatchdog: ReturnType<typeof setTimeout> | null = null;
67
  private speakingWatchdog: ReturnType<typeof setTimeout> | null = null;
68
+ // Response gating: requesting a new response while one is still streaming
69
+ // makes their audio chunks interleave on the playback timeline → garbled
70
+ // "buzzing" voice. Track the active response and defer response.create.
71
+ private responseActive = false;
72
+ private responseQueued = false;
73
 
74
  constructor(opts: {
75
  apiKey: string;
76
  lang: Lang;
77
+ robotName: string;
78
  micTrack: MediaStreamTrack;
79
  toolCtx: ToolContext;
80
  callbacks: RealtimeCallbacks;
81
  }) {
82
  this.apiKey = opts.apiKey;
83
  this.lang = opts.lang;
84
+ this.robotName = opts.robotName;
85
  this.micTrack = opts.micTrack;
86
  this.toolCtx = opts.toolCtx;
87
  this.cb = opts.callbacks;
88
  }
89
 
90
+ /** Request an assistant response now, or as soon as the active one ends. */
91
+ private requestResponse(): void {
92
+ if (this.responseActive) {
93
+ this.responseQueued = true;
94
+ } else {
95
+ this.responseActive = true;
96
+ this.send({ type: "response.create", response: { output_modalities: ["audio"] } });
97
+ }
98
+ }
99
+
100
  start(): void {
101
  this.ws = new WebSocket(REALTIME_URL, ["realtime", `openai-insecure-api-key.${this.apiKey}`]);
102
  this.openWatchdog = setTimeout(() => {
 
130
  type: "session.update",
131
  session: {
132
  type: "realtime",
133
+ instructions: systemPrompt(this.lang, this.robotName),
134
  audio: {
135
  input: {
136
  format: { type: "audio/pcm", rate: 24000 },
 
156
  });
157
  void this.startMicPump();
158
  // Short greeting so the user knows it's listening.
159
+ const hello =
160
+ this.lang === "es"
161
+ ? "Saluda en 4-6 palabras, nada más."
162
+ : "Greet in 4-6 words, nothing more.";
163
+ this.responseActive = true;
164
  this.send({ type: "response.create", response: { output_modalities: ["audio"], instructions: hello } });
165
  }
166
 
 
320
  item: { type: "function_call_output", call_id: id, output: JSON.stringify(result) },
321
  });
322
  }
323
+ // Deferred if a response is still streaming — overlapping responses
324
+ // interleave audio chunks and sound like garbled buzzing.
325
+ this.requestResponse();
326
  })();
327
  break;
328
  }
329
+ case "response.created":
330
+ this.responseActive = true;
331
+ break;
332
  case "response.done": {
333
+ this.responseActive = false;
334
+ if (this.responseQueued) {
335
+ this.responseQueued = false;
336
+ this.requestResponse();
337
+ }
338
  const drainMs = Math.max(0, (this.playbackEndsAt - audioContext().currentTime) * 1000);
339
  setTimeout(() => {
340
  this.setSpeaking(false);
web/src/voice/tools.ts CHANGED
@@ -71,6 +71,51 @@ export const TOOLS: ToolDef[] = [
71
  return { ok: errors.length === 0, errors, count: (args.items as unknown[])?.length ?? 0 };
72
  },
73
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  {
75
  name: "cook_inventory",
76
  description: "Reduce inventory quantities because the user cooked or used these items.",
 
71
  return { ok: errors.length === 0, errors, count: (args.items as unknown[])?.length ?? 0 };
72
  },
73
  },
74
+ {
75
+ name: "update_inventory_item",
76
+ description:
77
+ "Update ONE existing inventory item in place — change its storage location, expiration date, quantity, unit, or rename it. " +
78
+ "Use this (NOT add_inventory) whenever the item already exists; add_inventory would create a duplicate.",
79
+ parameters: {
80
+ type: "object",
81
+ properties: {
82
+ name: { type: "string", description: "Current name of the item to update" },
83
+ new_name: { type: "string", description: "New name, only if renaming" },
84
+ quantity: { type: "number", description: "New total quantity, only if changing it" },
85
+ unit: { type: "string", enum: ["g", "kg", "ml", "l", "pcs"] },
86
+ expiration_date: { type: "string", description: "New ISO date YYYY-MM-DD, or empty string to clear" },
87
+ storage_location: { type: "string", description: "New storage location, or empty string to clear" },
88
+ },
89
+ required: ["name"],
90
+ },
91
+ run: (args, ctx) => {
92
+ const data = ctx.getData();
93
+ const target = inv.normalizeName(String(args.name ?? ""));
94
+ const idx = data.inventory.findIndex((i) => i.name === target && i.quantity > 0);
95
+ if (idx < 0) return { error: `not_found: ${target}` };
96
+ const existing = data.inventory[idx];
97
+ const updated = inv.normalizeItemInput({
98
+ name: typeof args.new_name === "string" && args.new_name.trim() ? args.new_name : existing.display_name,
99
+ quantity: typeof args.quantity === "number" ? args.quantity : existing.quantity,
100
+ unit: typeof args.unit === "string" && args.unit ? args.unit : existing.unit,
101
+ expiration_date:
102
+ args.expiration_date === undefined ? existing.expiration_date : String(args.expiration_date) || null,
103
+ storage_location:
104
+ args.storage_location === undefined ? existing.storage_location : String(args.storage_location) || null,
105
+ });
106
+ if (!updated) return { error: "invalid_quantity_or_unit" };
107
+ ctx.commit({ ...data, inventory: data.inventory.map((e, i) => (i === idx ? updated : e)) });
108
+ return {
109
+ ok: true,
110
+ item: {
111
+ name: updated.display_name,
112
+ quantity: formatQuantity(updated.quantity, updated.unit),
113
+ expiration_date: updated.expiration_date,
114
+ storage_location: updated.storage_location,
115
+ },
116
+ };
117
+ },
118
+ },
119
  {
120
  name: "cook_inventory",
121
  description: "Reduce inventory quantities because the user cooked or used these items.",