Spaces:
Running
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 +0 -0
- assets/{embed-DdITxSHM.css → embed-CsvkHiDH.css} +1 -1
- assets/embed-CtskQx_e.js +0 -0
- assets/{index-BkQO3aLW.js → index-BNLguQgW.js} +2 -2
- index.html +1 -1
- web/src/i18n/index.ts +16 -0
- web/src/storage/store.ts +7 -2
- web/src/ui/app.ts +95 -3
- web/src/ui/styles.css +8 -1
- web/src/voice/controller.ts +2 -0
- web/src/voice/realtime.ts +50 -10
- web/src/voice/tools.ts +45 -0
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
@@ -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}
|
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/auto-CaTD3xKq.js","assets/sub-paths-F6TP7MGR-8fYyfeHC.js","assets/embed-
|
| 2 |
var G=Object.defineProperty;var q=(o,a,e)=>a in o?G(o,a,{enumerable:!0,configurable:!0,writable:!0,value:e}):o[a]=e;var d=(o,a,e)=>q(o,typeof a!="symbol"?a+"":a,e);(function(){const a=document.createElement("link").relList;if(a&&a.supports&&a.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))t(i);new MutationObserver(i=>{for(const s of i)if(s.type==="childList")for(const n of s.addedNodes)n.tagName==="LINK"&&n.rel==="modulepreload"&&t(n)}).observe(document,{childList:!0,subtree:!0});function e(i){const s={};return i.integrity&&(s.integrity=i.integrity),i.referrerPolicy&&(s.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?s.credentials="include":i.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function t(i){if(i.ep)return;i.ep=!0;const s=e(i);fetch(i.href,s)}})();const W="modulepreload",z=function(o){return"/"+o},O={},U=function(a,e,t){let i=Promise.resolve();if(e&&e.length>0){document.getElementsByTagName("link");const n=document.querySelector("meta[property=csp-nonce]"),r=(n==null?void 0:n.nonce)||(n==null?void 0:n.getAttribute("nonce"));i=Promise.allSettled(e.map(c=>{if(c=z(c),c in O)return;O[c]=!0;const l=c.endsWith(".css"),h=l?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${c}"]${h}`))return;const u=document.createElement("link");if(u.rel=l?"stylesheet":W,l||(u.as="script"),u.crossOrigin="",u.href=c,r&&u.setAttribute("nonce",r),document.head.appendChild(u),l)return new Promise((p,g)=>{u.addEventListener("load",p),u.addEventListener("error",()=>g(new Error(`Unable to preload CSS for ${c}`)))})}))}function s(n){const r=new Event("vite:preloadError",{cancelable:!0});if(r.payload=n,window.dispatchEvent(r),!r.defaultPrevented)throw n}return i.then(n=>{for(const r of n||[])r.status==="rejected"&&s(r.reason);return a().catch(s)})};var J=Object.defineProperty,K=(o,a,e)=>a in o?J(o,a,{enumerable:!0,configurable:!0,writable:!0,value:e}):o[a]=e,k=(o,a,e)=>(K(o,typeof a!="symbol"?a+"":a,e),e),B="https://huggingface.co";async function T(o,a){var e,t;const i=new X(o.url,o.status,(e=o.headers.get("X-Request-Id"))!=null?e:void 0);i.message=`Api error with status ${i.statusCode}`;const s=[`URL: ${i.url}`,i.requestId?`Request ID: ${i.requestId}`:void 0].filter(Boolean).join(". ");if((t=o.headers.get("Content-Type"))!=null&&t.startsWith("application/json")){const n=await o.json();i.message=n.error||n.message||i.message,i.data=n}else i.data={message:await o.text()};throw i.message+=`. ${s}`,i}var X=class extends Error{constructor(o,a,e,t){super(t),k(this,"statusCode"),k(this,"url"),k(this,"requestId"),k(this,"data"),this.statusCode=a,this.requestId=e,this.url=o}};new Promise(o=>{});function Y(o){if(globalThis.Buffer)return globalThis.Buffer.from(o).toString("base64");{const a=[];return o.forEach(e=>{a.push(String.fromCharCode(e))}),globalThis.btoa(a.join(""))}}async function $(o){var a,e;if(typeof window>"u")throw new Error("oauthHandleRedirect is only available in the browser");const t=new URLSearchParams(window.location.search),[i,s]=[t.get("error"),t.get("error_description")];if(i)throw new Error(`${i}: ${s}`);const n=t.get("code"),r=localStorage.getItem("huggingface.co:oauth:nonce");if(!n)throw new Error("Missing oauth code from query parameters in redirected URL");if(!r)throw new Error("Missing oauth nonce from localStorage");const c=localStorage.getItem("huggingface.co:oauth:code_verifier");if(!c)throw new Error("Missing oauth code_verifier from localStorage");const l=t.get("state");if(!l)throw new Error("Missing oauth state from query parameters in redirected URL");let h;try{h=JSON.parse(l)}catch{throw new Error("Invalid oauth state in redirected URL, unable to parse JSON: "+l)}if(h.nonce!==r)throw new Error("Invalid oauth state in redirected URL");const u=B,p=`${new URL(u).origin}/.well-known/openid-configuration`,g=await fetch(p,{headers:{Accept:"application/json"}});if(!g.ok)throw await T(g);const w=await g.json(),m=await fetch(w.token_endpoint,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"authorization_code",code:n,redirect_uri:h.redirectUri,code_verifier:c}).toString()});if(localStorage.removeItem("huggingface.co:oauth:code_verifier"),localStorage.removeItem("huggingface.co:oauth:nonce"),!m.ok)throw await T(m);const f=await m.json(),y=new Date(Date.now()+f.expires_in*1e3),v=await fetch(w.userinfo_endpoint,{headers:{Authorization:`Bearer ${f.access_token}`}});if(!v.ok)throw await T(v);const b=await v.json();return{accessToken:f.access_token,accessTokenExpiresAt:y,userInfo:{id:b.sub,name:b.name,fullname:b.preferred_username,email:b.email,emailVerified:b.email_verified,avatarUrl:b.picture,websiteUrl:b.website,isPro:b.isPro,orgs:(e=(a=b.orgs)==null?void 0:a.map(R=>({id:R.sub,name:R.name,fullname:R.name,isEnterprise:R.isEnterprise,canPay:R.canPay,avatarUrl:R.picture,roleInOrg:R.roleInOrg})))!=null?e:[]},state:h.state,scope:f.scope}}async function Q(o){if(typeof window>"u")throw new Error("oauthHandleRedirect is only available in the browser");const a=new URLSearchParams(window.location.search);return a.has("error")?$():a.has("code")?localStorage.getItem("huggingface.co:oauth:nonce")?$():(console.warn("Missing oauth nonce from localStorage. This can happen when the user refreshes the page after logging in, without changing the URL."),!1):!1}async function Z(o){var a,e;if(typeof window>"u")throw new Error("oauthLogin is only available in the browser");const t=(o==null?void 0:o.hubUrl)||B,i=`${new URL(t).origin}/.well-known/openid-configuration`,s=await fetch(i,{headers:{Accept:"application/json"}});if(!s.ok)throw await T(s);const n=await s.json(),r=globalThis.crypto.randomUUID(),c=globalThis.crypto.randomUUID()+globalThis.crypto.randomUUID();localStorage.setItem("huggingface.co:oauth:nonce",r),localStorage.setItem("huggingface.co:oauth:code_verifier",c);const l=(o==null?void 0:o.redirectUrl)||window.location.href,h=JSON.stringify({nonce:r,redirectUri:l,state:o==null?void 0:o.state}),u=(e=(a=window==null?void 0:window.huggingface)==null?void 0:a.variables)!=null?e:null,p=(o==null?void 0:o.clientId)||(u==null?void 0:u.OAUTH_CLIENT_ID);if(!p)throw u?new Error("Missing clientId, please add hf_oauth: true to the README.md's metadata in your static Space"):new Error("Missing clientId");const g=Y(new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256",new TextEncoder().encode(c)))).replace(/[+]/g,"-").replace(/[/]/g,"_").replace(/=/g,"");return`${n.authorization_endpoint}?${new URLSearchParams({client_id:p,scope:(o==null?void 0:o.scopes)||(u==null?void 0:u.OAUTH_SCOPES)||"openid profile",response_type:"code",redirect_uri:l,state:h,code_challenge:g,code_challenge_method:"S256"}).toString()}`}function C(o){return o*Math.PI/180}function _e(o){return o*180/Math.PI}function ee(o,a,e){const t=C(o),i=C(a),s=C(e),n=Math.cos(s),r=Math.sin(s),c=Math.cos(i),l=Math.sin(i),h=Math.cos(t),u=Math.sin(t);return[[n*c,n*l*u-r*h,n*l*h+r*u,0],[r*c,r*l*u+n*h,r*l*h-n*u,0],[-l,c*u,c*h,0],[0,0,0,1]]}function te(){if(typeof window>"u"||!window.location.hash)return;const o=window.location.hash.startsWith("#")?window.location.hash.slice(1):window.location.hash;let a;try{a=new URLSearchParams(o)}catch{return}const e=a.get("hf_token");if(!e)return;const t=a.get("hf_username")||"user",i=a.get("hf_token_expires"),s=i&&!Number.isNaN(new Date(i).getTime())?i:new Date(Date.now()+365*24*60*60*1e3).toISOString();try{sessionStorage.setItem("hf_token",e),sessionStorage.setItem("hf_username",t),sessionStorage.setItem("hf_token_expires",s)}catch(c){console.warn("[reachy-mini] could not persist pre-seeded HF credentials:",c)}a.delete("hf_token"),a.delete("hf_username"),a.delete("hf_token_expires");const n=a.toString(),r=window.location.pathname+window.location.search+(n?"#"+n:"");try{window.history.replaceState(null,"",r)}catch{}}function ie(){if(typeof window>"u")return null;if(window.location.hash){const o=window.location.hash.startsWith("#")?window.location.hash.slice(1):window.location.hash;try{const e=new URLSearchParams(o).get("robot_peer_id");if(e)return e}catch{}}if(window.location.search)try{const a=new URLSearchParams(window.location.search).get("robot_peer_id");if(a)return a}catch{}return null}function se(o){const a=o.split(`\r
|
| 3 |
`);let e=!1;for(const t of a)if(t.startsWith("m=audio")?e=!0:t.startsWith("m=")&&(e=!1),e&&t==="a=sendrecv")return!0;return!1}const S=12*1024,L=1*1024*1024,ne=512*1024;function oe(){return typeof CompressionStream<"u"}function x(){return"u"+Math.random().toString(36).slice(2,11)+Date.now().toString(36)}function P(o){let a="";for(let t=0;t<o.length;t+=32768)a+=String.fromCharCode.apply(null,Array.from(o.subarray(t,t+32768)));return btoa(a)}async function re(o){const a=new TextEncoder().encode(o),e=await new Response(new Blob([a]).stream().pipeThrough(new CompressionStream("gzip"))).arrayBuffer();return P(new Uint8Array(e))}function N(o){const a=Math.round(Number(o)||0);return Math.max(0,Math.min(100,a))}const H=3e3,V=1e3,ae=6e4;class le extends EventTarget{constructor(e={}){super();d(this,"_signalingUrl");d(this,"_clientId");d(this,"_appName");d(this,"_videoJitterBufferTargetMs");d(this,"_autoStartFromUrl");d(this,"_autoStartAttempted");d(this,"_state","disconnected");d(this,"_robots",[]);d(this,"_robotState",{});d(this,"_preselectedRobotId");d(this,"_token",null);d(this,"_username",null);d(this,"_tokenExpires",null);d(this,"_sseAbortController",null);d(this,"_pc",null);d(this,"_dc",null);d(this,"_sessionId",null);d(this,"_selectedRobotId",null);d(this,"_pendingRemoteIce",[]);d(this,"_micStream",null);d(this,"_micMuted",!0);d(this,"_audioMuted",!0);d(this,"_micSupported",!1);d(this,"_latencyMonitorId",null);d(this,"_stateRefreshInterval",null);d(this,"_versionResolve",null);d(this,"_hardwareIdResolve",null);d(this,"_volumeResolve",null);d(this,"_micVolumeResolve",null);d(this,"_applyAudioConfigResolve",null);d(this,"_readAudioParameterResolve",null);d(this,"_logSubscribers",new Set);d(this,"_broadcastWaiters",[]);d(this,"_activeMoveUploadId",null);d(this,"_activeAudioUploadId",null);d(this,"_sessionResolve",null);d(this,"_sessionReject",null);d(this,"_iceConnected",!1);d(this,"_dcOpen",!1);d(this,"_iceGraceTimer",null);d(this,"_iceGraceReason",null);d(this,"_pendingVisibilityHandler",null);d(this,"_onlineHandler",null);d(this,"_offlineHandler",null);d(this,"_connectionChangeHandler",null);d(this,"_pendingMotionCompletions",{wake_up:[],goto_sleep:[]});d(this,"_videoElement",null);this._signalingUrl=e.signalingUrl||"https://pollen-robotics-reachy-mini-central.hf.space",this._clientId=e.clientId||null,this._appName=e.appName||"unknown",this._videoJitterBufferTargetMs=e.videoJitterBufferTargetMs??0,this._autoStartFromUrl=e.autoStartFromUrl===!0,this._autoStartAttempted=!1,this._preselectedRobotId=ie()}get state(){return this._state}get robots(){return this._robots}get robotState(){return this._robotState}get username(){return this._username}get isAuthenticated(){return!!this._token}get micSupported(){return this._micSupported}get micMuted(){return this._micMuted}get audioMuted(){return this._audioMuted}get preselectedRobotId(){return this._preselectedRobotId}get isEmbedded(){return this._preselectedRobotId!==null}_maybeAutoStart(){if(!this._autoStartFromUrl||this._autoStartAttempted||!this._preselectedRobotId||this._state!=="connected"||!this._robots.find(i=>i.id===this._preselectedRobotId))return;this._autoStartAttempted=!0;const t=this._preselectedRobotId;setTimeout(()=>{this._state==="connected"&&this.startSession(t).catch(i=>{console.warn("[reachy-mini] autoStartFromUrl: startSession rejected:",i)})},0)}async authenticate(){try{te();const e=await Q();if(e)return this._username=e.userInfo.preferred_username||e.userInfo.name||null,this._token=e.accessToken,this._tokenExpires=e.accessTokenExpiresAt,sessionStorage.setItem("hf_token",this._token),sessionStorage.setItem("hf_username",this._username??""),sessionStorage.setItem("hf_token_expires",typeof this._tokenExpires=="string"?this._tokenExpires:this._tokenExpires.toISOString()),!0;const t=sessionStorage.getItem("hf_token"),i=sessionStorage.getItem("hf_username"),s=sessionStorage.getItem("hf_token_expires");return t&&i&&s&&new Date(s)>new Date?(this._token=t,this._username=i,this._tokenExpires=s,!0):!1}catch(e){return console.error("Auth error:",e),!1}}async login(){const e={};this._clientId&&(e.clientId=this._clientId),window.location.href=await Z(e)}logout(){sessionStorage.removeItem("hf_token"),sessionStorage.removeItem("hf_username"),sessionStorage.removeItem("hf_token_expires"),this._username=null,this._tokenExpires=null,this.disconnect()}async connect(e){if(this._state!=="disconnected")throw new Error("Already connected");if(e&&(this._token=e),!this._token)throw new Error("No token — call authenticate() first or pass a token");this._sseAbortController=new AbortController;let t;try{t=await fetch(`${this._signalingUrl}/events`,{signal:this._sseAbortController.signal,headers:{Authorization:`Bearer ${this._token}`}})}catch(i){throw this._sseAbortController=null,i}if(!t.ok)throw this._sseAbortController=null,new Error(`HTTP ${t.status}`);return new Promise((i,s)=>{let n=!1;const r=t.body.getReader(),c=new TextDecoder;let l="";(async()=>{try{for(;;){const{done:u,value:p}=await r.read();if(u)break;l+=c.decode(p,{stream:!0});const g=l.split(`
|
| 4 |
-
`);l=g.pop()??"";for(const w of g)if(w.startsWith("data:"))try{const m=JSON.parse(w.slice(5).trim());!n&&m.type==="welcome"&&m.peerId&&(n=!0,this._state="connected",await this._sendToServer({type:"setPeerStatus",roles:["listener"],meta:{name:this._appName}}),this._emit("connected",{peerId:m.peerId}),i()),this._handleSignalingMessage(m)}catch{}}}catch(u){if(u.name!=="AbortError"&&this._emit("error",{source:"signaling",error:u}),!n){s(u);return}}this._state!=="disconnected"&&(this._state="disconnected",this._emit("disconnected",{reason:"SSE closed"})),n||s(new Error("Connection closed before welcome"))})()})}async autoConnect(e={}){var l,h,u,p,g;const{token:t,pickRobot:i,autoPickIfSingle:s=!0,filterBusy:n=!0,wakeOnConnect:r=!0}=e;if(this._state==="streaming"){const w=(l=this._robots)==null?void 0:l.find(m=>m.id===this._selectedRobotId);return{robotId:this._selectedRobotId,robotName:((h=w==null?void 0:w.meta)==null?void 0:h.name)??null,isEmbedded:this.isEmbedded,alreadyStreaming:!0}}const c=this._autoStartFromUrl;this._autoStartFromUrl=!1;try{if(t)this._token=t;else if(!this._token&&!await this.authenticate())throw new Error("Not authenticated — call login() or pass a token");this._state==="disconnected"&&await this.connect();let w,m=null;if(this.isEmbedded){w=this._preselectedRobotId;try{await this._waitForRobotInList(w,5e3)}catch{}const f=(u=this._robots)==null?void 0:u.find(y=>y.id===w);m=((p=f==null?void 0:f.meta)==null?void 0:p.name)??null}else{const f=await this._fetchOwnedRobots({filterBusy:n});if(f.length===0)throw new Error("No reachable robots");if(s&&f.length===1&&!f[0].busy)w=f[0].id,m=f[0].name;else if(i){const y=await i(f);if(!y)throw new Error("Robot selection cancelled");w=y,m=((g=f.find(v=>v.id===y))==null?void 0:g.name)??null}else throw new Error("Multiple robots available — pass a pickRobot callback to autoConnect()")}if(await this.startSession(w),r&&typeof this.ensureAwake=="function")try{await this.ensureAwake()}catch(f){console.warn("[reachy-mini] autoConnect: ensureAwake failed:",f)}return{robotId:w,robotName:m,isEmbedded:this.isEmbedded}}finally{this._autoStartFromUrl=c}}async _fetchOwnedRobots({filterBusy:e=!0}={}){var t,i,s;try{const n=await fetch(`${this._signalingUrl}/api/robot-status`,{headers:{Authorization:`Bearer ${this._token}`}});if(!n.ok)throw new Error(`HTTP ${n.status}`);const r=await n.json(),c=new Map;for(const l of r.robots||[]){if(e&&l.busy)continue;const h=((t=l.meta)==null?void 0:t.install_id)??((i=l.meta)==null?void 0:i.hardware_id)??l.peerId;c.set(h,{id:l.peerId,name:l.robotName??((s=l.meta)==null?void 0:s.name)??null,busy:!!l.busy,activeApp:l.activeApp??null,meta:l.meta??{},lastSeenAgeSeconds:l.last_seen_age_seconds??null})}return Array.from(c.values()).sort((l,h)=>(l.lastSeenAgeSeconds??1/0)-(h.lastSeenAgeSeconds??1/0))}catch(n){return console.warn("[reachy-mini] /api/robot-status unavailable, using SSE list:",n),(this._robots||[]).map(r=>{var c;return{id:r.id,name:((c=r.meta)==null?void 0:c.name)??null,busy:!1,activeApp:null,meta:r.meta??{},lastSeenAgeSeconds:null}})}}_waitForRobotInList(e,t){var i;return(i=this._robots)!=null&&i.find(s=>s.id===e)?Promise.resolve():new Promise((s,n)=>{const r=()=>{var l;(l=this._robots)!=null&&l.find(h=>h.id===e)&&(this.removeEventListener("robotsChanged",r),clearTimeout(c),s())},c=setTimeout(()=>{this.removeEventListener("robotsChanged",r),n(new Error(`Timeout waiting for robot ${e} in list`))},t);this.addEventListener("robotsChanged",r)})}async startSession(e){if(this._state!=="connected")throw new Error("Not connected");this._selectedRobotId=e,this._iceConnected=!1,this._dcOpen=!1,this._micSupported=!1,this._pendingRemoteIce=[];try{const t=window,i=t.AudioContext??t.webkitAudioContext;if(!i)throw new Error("AudioContext not supported");const s=new i,n=s.createMediaStreamDestination(),r=s.createOscillator(),c=s.createGain();c.gain.value=0,r.connect(c).connect(n),r.start();const l=n.stream;l.getAudioTracks().forEach(h=>{h.enabled=!1}),this._micStream=l,this._micMuted=!0}catch(t){console.warn("Audio sender placeholder setup failed:",t),this._micStream=null}return this._pc=new RTCPeerConnection({iceServers:[{urls:"stun:stun.l.google.com:19302"}]}),this._installNetworkListeners(),new Promise((t,i)=>{this._sessionResolve=t,this._sessionReject=i,this._pc.ontrack=s=>{if(s.track.kind==="video"){const n=this._videoJitterBufferTargetMs;try{s.receiver.jitterBufferTarget=n}catch{}try{s.receiver.playoutDelayHint=n/1e3}catch{}this._emit("videoTrack",{track:s.track,stream:s.streams[0]})}},this._pc.onicecandidate=async s=>{s.candidate&&this._sessionId&&await this._sendToServer({type:"peer",sessionId:this._sessionId,ice:{candidate:s.candidate.candidate,sdpMLineIndex:s.candidate.sdpMLineIndex,sdpMid:s.candidate.sdpMid}})},this._pc.oniceconnectionstatechange=()=>{var n;const s=(n=this._pc)==null?void 0:n.iceConnectionState;if(s){if(this._emit("iceStateChange",{state:s}),s==="connected"||s==="completed"){this._clearIceGrace(),this._iceConnected=!0,this._checkSessionReady();return}if(s==="disconnected"){typeof document<"u"&&document.hidden?this._armIceGraceOnVisibility():this._scheduleIceGrace(H,"disconnected");return}if(s==="failed"){this._scheduleIceGrace(V,"failed");return}}},this._pc.ondatachannel=s=>{this._dc=s.channel,this._dc.onopen=()=>{this._dcOpen=!0,this._checkSessionReady()},this._dc.onmessage=n=>this._handleRobotMessage(JSON.parse(n.data))},this._sendToServer({type:"startSession",peerId:e}).then(s=>{if((s==null?void 0:s.type)==="sessionRejected"){this._failSessionRejected(s);return}s!=null&&s.sessionId&&(this._sessionId=s.sessionId)})})}_failSessionRejected(e){const t=new Error(e.reason==="robot_busy"?`Robot is busy: "${e.activeApp||"another app"}" is already connected`:`Session rejected: ${e.reason||"unknown reason"}`);if(t.reason=e.reason??null,t.activeApp=e.activeApp??null,this._clearIceGrace(),this._uninstallNetworkListeners(),this._pc&&(this._pc.close(),this._pc=null),this._micStream&&(this._micStream.getTracks().forEach(i=>i.stop()),this._micStream=null),this._iceConnected=!1,this._dcOpen=!1,this._micMuted=!0,this._micSupported=!1,this._emit("sessionRejected",{reason:e.reason,activeApp:e.activeApp}),this._sessionReject){const i=this._sessionReject;this._sessionResolve=null,this._sessionReject=null,i(t)}}async stopSession(){this._versionResolve&&(this._versionResolve(null),this._versionResolve=null),this._hardwareIdResolve&&(this._hardwareIdResolve(null),this._hardwareIdResolve=null),this._volumeResolve&&(this._volumeResolve(null),this._volumeResolve=null),this._micVolumeResolve&&(this._micVolumeResolve(null),this._micVolumeResolve=null),this._applyAudioConfigResolve&&(this._applyAudioConfigResolve(!1),this._applyAudioConfigResolve=null),this._readAudioParameterResolve&&(this._readAudioParameterResolve(null),this._readAudioParameterResolve=null),this._logSubscribers.clear(),this._rejectPendingMotionCompletions(new Error("Session stopped")),this._clearIceGrace(),this._uninstallNetworkListeners(),this._sessionReject&&(this._sessionReject(new Error("Session stopped")),this._sessionResolve=null,this._sessionReject=null),this._stateRefreshInterval&&(clearInterval(this._stateRefreshInterval),this._stateRefreshInterval=null),this._latencyMonitorId&&(clearInterval(this._latencyMonitorId),this._latencyMonitorId=null),this._sessionId&&await this._sendToServer({type:"endSession",sessionId:this._sessionId}),this._micStream&&(this._micStream.getTracks().forEach(t=>t.stop()),this._micStream=null),this._micMuted=!0,this._micSupported=!1,this._pc&&(this._pc.close(),this._pc=null),this._dc&&(this._dc.close(),this._dc=null),this._sessionId=null,this._iceConnected=!1,this._dcOpen=!1,this._state==="streaming"&&(this._state="connected",this._emit("sessionStopped",{reason:"user"}))}disconnect(){this._sseAbortController&&(this._sseAbortController.abort(),this._sseAbortController=null),this._versionResolve&&(this._versionResolve(null),this._versionResolve=null),this._hardwareIdResolve&&(this._hardwareIdResolve(null),this._hardwareIdResolve=null),this._volumeResolve&&(this._volumeResolve(null),this._volumeResolve=null),this._micVolumeResolve&&(this._micVolumeResolve(null),this._micVolumeResolve=null),this._applyAudioConfigResolve&&(this._applyAudioConfigResolve(!1),this._applyAudioConfigResolve=null),this._readAudioParameterResolve&&(this._readAudioParameterResolve(null),this._readAudioParameterResolve=null),this._logSubscribers.clear(),this._rejectPendingMotionCompletions(new Error("Disconnected")),this._clearIceGrace(),this._uninstallNetworkListeners(),this._sessionReject&&(this._sessionReject(new Error("Disconnected")),this._sessionResolve=null,this._sessionReject=null),this._stateRefreshInterval&&(clearInterval(this._stateRefreshInterval),this._stateRefreshInterval=null),this._latencyMonitorId&&(clearInterval(this._latencyMonitorId),this._latencyMonitorId=null),this._sessionId&&this._token&&this._sendToServer({type:"endSession",sessionId:this._sessionId}),this._micStream&&(this._micStream.getTracks().forEach(e=>e.stop()),this._micStream=null),this._pc&&(this._pc.close(),this._pc=null),this._dc&&(this._dc.close(),this._dc=null),this._sessionId=null,this._micMuted=!0,this._micSupported=!1,this._iceConnected=!1,this._dcOpen=!1,this._robots=[],this._state="disconnected",this._emit("disconnected",{reason:"user"})}_clearIceGrace(){this._iceGraceTimer!==null&&(clearTimeout(this._iceGraceTimer),this._iceGraceTimer=null),this._iceGraceReason=null,this._pendingVisibilityHandler&&typeof document<"u"&&document.removeEventListener("visibilitychange",this._pendingVisibilityHandler),this._pendingVisibilityHandler=null}_scheduleIceGrace(e,t){if(this._iceGraceTimer!==null){if(this._iceGraceReason===t)return;clearTimeout(this._iceGraceTimer)}this._iceGraceReason=t,this._iceGraceTimer=setTimeout(()=>{var n;this._iceGraceTimer=null;const i=this._iceGraceReason;this._iceGraceReason=null;const s=(n=this._pc)==null?void 0:n.iceConnectionState;if(!(s==="connected"||s==="completed")){if(i==="disconnected"&&s==="disconnected"){this._emit("error",{source:"webrtc",error:new Error(`ICE stuck in 'disconnected' for > ${e}ms`)});return}if(i==="failed"||s==="failed"){const r=new Error("ICE connection failed");this._sessionReject&&(this._sessionReject(r),this._sessionResolve=null,this._sessionReject=null),this._emit("error",{source:"webrtc",error:r})}}},e)}_armIceGraceOnVisibility(){if(this._pendingVisibilityHandler)return;const e=Date.now(),t=()=>{if(typeof document<"u"&&document.hidden||(document.removeEventListener("visibilitychange",t),this._pendingVisibilityHandler=null,!this._pc))return;const i=this._pc.iceConnectionState;if(!(i==="connected"||i==="completed")){if(Date.now()-e>ae){const s=new Error("Session expired while tab was backgrounded");this._sessionReject&&(this._sessionReject(s),this._sessionResolve=null,this._sessionReject=null),this._emit("error",{source:"webrtc",error:s});return}if(i==="failed"){this._scheduleIceGrace(V,"failed");return}this._scheduleIceGrace(H,"disconnected")}};document.addEventListener("visibilitychange",t),this._pendingVisibilityHandler=t}_installNetworkListeners(){if(this._onlineHandler||typeof window>"u")return;const e=()=>this._emit("networkOnline",{}),t=()=>this._emit("networkOffline",{});window.addEventListener("online",e),window.addEventListener("offline",t),this._onlineHandler=e,this._offlineHandler=t;const i=navigator.connection;if(i&&typeof i.addEventListener=="function"){const s=()=>this._emit("networkChange",{effectiveType:i.effectiveType,downlink:i.downlink,rtt:i.rtt,saveData:i.saveData});i.addEventListener("change",s),this._connectionChangeHandler=s}}_uninstallNetworkListeners(){typeof window<"u"&&(this._onlineHandler&&window.removeEventListener("online",this._onlineHandler),this._offlineHandler&&window.removeEventListener("offline",this._offlineHandler));const e=navigator.connection;e&&this._connectionChangeHandler&&typeof e.removeEventListener=="function"&&e.removeEventListener("change",this._connectionChangeHandler),this._onlineHandler=null,this._offlineHandler=null,this._connectionChangeHandler=null}setTarget({head:e,antennas:t,body_yaw:i}={}){const s={type:"set_full_target"};if(e!==void 0){if(!Array.isArray(e)||e.length!==16||!e.every(n=>Number.isFinite(n)))throw new TypeError(`setTarget: head must be a 16-element flat row-major 4×4 matrix of finite numbers; got ${Array.isArray(e)?`Array(${e.length})`:typeof e}`);s.head=e}if(t!==void 0){if(!Array.isArray(t)||t.length!==2||!t.every(n=>Number.isFinite(n)))throw new TypeError(`setTarget: antennas must be [rightRad, leftRad] (2 finite numbers); got ${Array.isArray(t)?`Array(${t.length})`:typeof t}`);s.antennas=t}if(i!==void 0){if(!Number.isFinite(i))throw new TypeError(`setTarget: body_yaw must be a finite number (radians); got ${i}`);s.body_yaw=i}return this._sendCommand(s)}gotoTarget({head:e,antennas:t,body_yaw:i,duration:s}){const n={type:"goto_target"};if(e!==void 0){if(!Array.isArray(e)||e.length!==16||!e.every(r=>Number.isFinite(r)))throw new TypeError(`gotoTarget: head must be a 16-element flat row-major 4×4 matrix of finite numbers; got ${Array.isArray(e)?`Array(${e.length})`:typeof e}`);n.head=e}if(t!==void 0){if(!Array.isArray(t)||t.length!==2||!t.every(r=>Number.isFinite(r)))throw new TypeError(`gotoTarget: antennas must be [rightRad, leftRad] (2 finite numbers); got ${Array.isArray(t)?`Array(${t.length})`:typeof t}`);n.antennas=t}if(i!==void 0){if(!Number.isFinite(i))throw new TypeError(`gotoTarget: body_yaw must be a finite number (radians); got ${i}`);n.body_yaw=i}if(!Number.isFinite(s)||s<=0)throw new TypeError(`gotoTarget: duration must be a positive finite number (seconds); got ${s}`);return n.duration=s,this._sendCommand(n)}setHeadRpyDeg(e,t,i){return this.setTarget({head:ee(e,t,i).flat()})}setAntennasDeg(e,t){return this.setTarget({antennas:[C(e),C(t)]})}setBodyYawDeg(e){return this.setTarget({body_yaw:C(e)})}playSound(e){return this._sendCommand({type:"play_sound",file:e})}clearIncomingAudio(){return this._sendCommand({type:"clear_incoming_audio"})}setMotorMode(e){return this._sendCommand({type:"set_motor_mode",mode:e})}setMotorTorque(e,t=null){return this._sendCommand({type:"set_torque",on:e,ids:t})}wakeUp({timeoutMs:e=8e3}={}){return this._sendCommand({type:"set_motor_mode",mode:"enabled"}),this._sendCommandAwaitCompletion("wake_up",e)}gotoSleep({timeoutMs:e=8e3}={}){return this._sendCommandAwaitCompletion("goto_sleep",e)}_sendCommandAwaitCompletion(e,t){return this._sendCommand({type:e})?new Promise((i,s)=>{const n={resolve:i,reject:s,timer:setTimeout(()=>{const r=this._pendingMotionCompletions[e],c=r.indexOf(n);c!==-1&&r.splice(c,1),s(new Error(`${e} timed out after ${t}ms`))},t)};this._pendingMotionCompletions[e].push(n)}):Promise.reject(new Error(`${e}: data channel not open`))}_rejectPendingMotionCompletions(e){for(const t of Object.keys(this._pendingMotionCompletions)){const i=this._pendingMotionCompletions[t];for(;i.length;){const s=i.shift();clearTimeout(s.timer),s.reject(e)}}}isAwake(){var t;const e=(t=this._robotState)==null?void 0:t.motor_mode;return e==="enabled"||e==="gravity_compensation"}async ensureAwake(e=1e3){var t;return((t=this._robotState)==null?void 0:t.motor_mode)===void 0&&await new Promise(i=>{const s=()=>{this.removeEventListener("state",s),clearTimeout(n),i()},n=setTimeout(s,e);this.addEventListener("state",s),this.requestState()}),this.isAwake()||this.wakeUp().catch(()=>{}),!0}getVersion(){return new Promise((e,t)=>{if(!this._dc||this._dc.readyState!=="open"){t(new Error("Data channel not open"));return}this._versionResolve&&this._versionResolve(null),this._versionResolve=e,this._sendCommand({type:"get_version"})})}getHardwareId(){return new Promise((e,t)=>{if(!this._dc||this._dc.readyState!=="open"){t(new Error("Data channel not open"));return}this._hardwareIdResolve&&this._hardwareIdResolve(null),this._hardwareIdResolve=e,this._sendCommand({type:"get_hardware_id"})})}getVolume(){return this._slotRoundtrip(()=>this._volumeResolve,e=>{this._volumeResolve=e},{type:"get_volume"})}setVolume(e){return this._slotRoundtrip(()=>this._volumeResolve,t=>{this._volumeResolve=t},{type:"set_volume",volume:N(e)})}getMicrophoneVolume(){return this._slotRoundtrip(()=>this._micVolumeResolve,e=>{this._micVolumeResolve=e},{type:"get_microphone_volume"})}setMicrophoneVolume(e){return this._slotRoundtrip(()=>this._micVolumeResolve,t=>{this._micVolumeResolve=t},{type:"set_microphone_volume",volume:N(e)})}applyAudioConfig(e,{verify:t=!0}={}){return this._slotRoundtrip(()=>this._applyAudioConfigResolve,i=>{this._applyAudioConfigResolve=i},{type:"apply_audio_config",config:e,verify:t}).then(i=>i===!0)}readAudioParameter(e){return this._slotRoundtrip(()=>this._readAudioParameterResolve,t=>{this._readAudioParameterResolve=t},{type:"read_audio_parameter",name:e})}_slotRoundtrip(e,t,i){return new Promise((s,n)=>{if(!this._dc||this._dc.readyState!=="open"){n(new Error("Data channel not open"));return}const r=e();r&&r(null),t(s),this._sendCommand(i)})}sendRaw(e){return this._sendCommand(e)}subscribeLogs({onLine:e,onError:t}){if(typeof e!="function")throw new TypeError("subscribeLogs: onLine callback is required");const i={onLine:e,onError:t},s=this._logSubscribers.size===0;this._logSubscribers.add(i),s&&this._sendCommand({type:"subscribe_logs"});let n=!1;return()=>{n||(n=!0,this._logSubscribers.delete(i),this._logSubscribers.size===0&&this._sendCommand({type:"unsubscribe_logs"}))}}requestState(){return this._sendCommand({type:"get_state"})}setAudioMuted(e){this._audioMuted=e,this._videoElement&&(this._videoElement.muted=e)}setMicMuted(e){this._micMuted=e,this._micStream&&this._micStream.getAudioTracks().forEach(t=>{t.enabled=!e})}attachVideo(e){this._videoElement=e,e.muted=this._audioMuted;const t=s=>{const n=s;e.srcObject=n.detail.stream,e.playsInline=!0,"requestVideoFrameCallback"in e&&this._startLatencyMonitor(e)},i=()=>{e.srcObject=null};return this.addEventListener("videoTrack",t),this.addEventListener("sessionStopped",i),()=>{this.removeEventListener("videoTrack",t),this.removeEventListener("sessionStopped",i),this._latencyMonitorId&&(clearInterval(this._latencyMonitorId),this._latencyMonitorId=null),e.srcObject=null,this._videoElement=null}}async playMove(e,{audioBlob:t=null,audioLeadMs:i=-100,description:s="move",encoding:n="gzip+base64",playFrequency:r=100,initialGotoDuration:c=0,startTimeoutMs:l=8e3,onProgress:h=()=>{},onStarted:u=()=>{}}={}){var R,j;if(!this._dc||this._dc.readyState!=="open")throw new Error("data channel not open");if(!((R=e==null?void 0:e.time)!=null&&R.length)||!((j=e==null?void 0:e.set_target_data)!=null&&j.length))throw new Error("playMove: motion must have time + set_target_data");const p=x();this._activeMoveUploadId=p;const g={description:s,time:e.time,set_target_data:e.set_target_data},w=JSON.stringify(g);let m,f;n==="gzip+base64"&&oe()?(m=await re(w),f="gzip+base64"):(m=w,f="json");const y=Math.ceil(m.length/S)||1;h({phase:"starting",sent:0,total:y,bytes:m.length,encoding:f}),this._sendCommand({type:"upload_move_start",upload_id:p,total_chunks:y,description:s,encoding:f});for(let _=0;_<y;_++){this._dc.bufferedAmount>L&&await this._awaitDataChannelDrain();const I=_*S;this._sendCommand({type:"upload_move_chunk",upload_id:p,chunk_index:_,chunk:m.slice(I,I+S)}),h({phase:"upload",sent:_+1,total:y})}if(this._sendCommand({type:"upload_move_finish",upload_id:p}),h({phase:"uploaded",sent:y,total:y}),t){const _=new Uint8Array(await t.arrayBuffer()),I=P(_),E=Math.ceil(I.length/S)||1;h({phase:"audio-starting",sent:0,total:E,bytes:I.length}),this._sendCommand({type:"upload_audio_start",upload_id:p,total_chunks:E,encoding:"wav-base64",description:s});for(let A=0;A<E;A++){this._dc.bufferedAmount>L&&await this._awaitDataChannelDrain();const D=A*S;this._sendCommand({type:"upload_audio_chunk",upload_id:p,chunk_index:A,chunk:I.slice(D,D+S)}),h({phase:"audio-upload",sent:A+1,total:E})}this._sendCommand({type:"upload_audio_finish",upload_id:p}),h({phase:"audio-uploaded",sent:E,total:E})}this._sendCommand({type:"play_uploaded_move",upload_id:p,play_frequency:r,initial_goto_duration:c,audio_lead_ms:i});let v;try{v=await this._waitForBroadcast(_=>(_==null?void 0:_.type)==="play_uploaded_move"&&(_==null?void 0:_.upload_id)===p&&(_.started===!0||typeof _.error=="string"),{timeoutMs:l,debugLabel:"play_uploaded_move started"})}catch(_){throw new Error(`Daemon did not respond to play_uploaded_move (requires the reachy_mini daemon with feature/daemon-side-move-upload). Underlying: ${_.message}`)}if(typeof v.error=="string")throw new Error(`play_uploaded_move: ${v.error}`);try{u({duration_s:v.duration_s,has_audio:v.has_audio===!0})}catch(_){console.warn("playMove.onStarted threw:",_)}h({phase:"playing",duration_s:v.duration_s});const b=await this._waitForBroadcast(_=>(_==null?void 0:_.type)==="play_uploaded_move"&&(_==null?void 0:_.upload_id)===p&&(_.finished===!0||_.cancelled===!0||typeof _.error=="string"),{timeoutMs:(v.duration_s+30)*1e3,debugLabel:"play_uploaded_move final"});return this._activeMoveUploadId===p&&(this._activeMoveUploadId=null),b}cancelMove(e=null){const t=e??this._activeMoveUploadId;return t?this._sendCommand({type:"cancel_move",upload_id:t}):!1}async uploadAudio(e,{description:t="audio",onProgress:i=()=>{}}={}){if(!this._dc||this._dc.readyState!=="open")throw new Error("data channel not open");if(!(e instanceof Blob))throw new TypeError("uploadAudio: expected a Blob");const s=x(),n=new Uint8Array(await e.arrayBuffer()),r=P(n),c=Math.ceil(r.length/S)||1;i({phase:"audio-starting",sent:0,total:c,bytes:r.length}),this._sendCommand({type:"upload_audio_start",upload_id:s,total_chunks:c,encoding:"wav-base64",description:t});for(let l=0;l<c;l++){this._dc.bufferedAmount>L&&await this._awaitDataChannelDrain();const h=l*S;this._sendCommand({type:"upload_audio_chunk",upload_id:s,chunk_index:l,chunk:r.slice(h,h+S)}),i({phase:"audio-upload",sent:l+1,total:c})}return this._sendCommand({type:"upload_audio_finish",upload_id:s}),i({phase:"audio-uploaded",sent:c,total:c}),s}async playUploadedAudio(e,{timeoutMs:t=8e3}={}){if(!this._dc||this._dc.readyState!=="open")throw new Error("data channel not open");const i=this._waitForBroadcast(n=>(n==null?void 0:n.type)==="play_uploaded_audio"&&(n==null?void 0:n.upload_id)===e&&(n.started===!0||typeof n.error=="string"),{timeoutMs:t,debugLabel:"play_uploaded_audio started"});this._sendCommand({type:"play_uploaded_audio",upload_id:e});const s=await i;if(typeof s.error=="string")throw new Error(s.error);return this._activeAudioUploadId=e,s}cancelAudio(e=null){const t=e??this._activeAudioUploadId;return t?(this._activeAudioUploadId===t&&(this._activeAudioUploadId=null),this._sendCommand({type:"cancel_audio",upload_id:t})):!1}_emit(e,t){this.dispatchEvent(new CustomEvent(e,{detail:t}))}_waitForBroadcast(e,{timeoutMs:t=5e3,debugLabel:i=""}={}){return new Promise((s,n)=>{const r={predicate:e,resolve:s,timer:setTimeout(()=>{const c=this._broadcastWaiters.indexOf(r);c!==-1&&this._broadcastWaiters.splice(c,1),n(new Error(`broadcast timeout (${t} ms): ${i}`))},t)};this._broadcastWaiters.push(r)})}async _awaitDataChannelDrain(){for(;this._dc&&this._dc.bufferedAmount>ne;)if(await new Promise(e=>setTimeout(e,30)),!this._dc||this._dc.readyState!=="open")throw new Error("data channel closed mid-upload")}async _sendToServer(e){if(!this._token)throw new Error("No token — authenticate() first");try{const t=await fetch(`${this._signalingUrl}/send`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this._token}`},body:JSON.stringify(e)});if(!t.ok){let i="";try{i=await t.text()}catch{}return console.warn(`[reachy-mini] /send rejected (${t.status}) for type=${e==null?void 0:e.type}; body=${i||"<empty>"}`),null}return await t.json()}catch(t){return console.error("Send error:",t),null}}_sendCommand(e){return!this._dc||this._dc.readyState!=="open"?!1:(this._dc.send(JSON.stringify(e)),!0)}_checkSessionReady(){this._iceConnected&&this._dcOpen&&this._sessionResolve&&(this._state="streaming",this.requestState(),this._stateRefreshInterval=setInterval(()=>this.requestState(),500),this._emit("streaming",{sessionId:this._sessionId,robotId:this._selectedRobotId}),this._sessionResolve(),this._sessionResolve=null,this._sessionReject=null)}async _handleSignalingMessage(e){switch(e.type){case"welcome":break;case"list":this._robots=e.producers||[],this._emit("robotsChanged",{robots:this._robots}),this._maybeAutoStart();break;case"peerStatusChanged":{const t=await this._sendToServer({type:"list"});t!=null&&t.producers&&(this._robots=t.producers,this._emit("robotsChanged",{robots:this._robots}),this._maybeAutoStart());break}case"sessionStarted":this._sessionId=e.sessionId??null;break;case"sessionRejected":this._failSessionRejected(e);break;case"endSession":this._handleEndSession(e);break;case"peer":this._handlePeerMessage(e);break}}_handleEndSession(e){const t=e.reason,i=t==="robot_busy_local_app"?"Robot is busy: a local Python app is running":t==="local_app_started"?"Disconnected: a local Python app started on the robot":t==="robot_busy_local"?"Robot is busy: another session is already active":null;if(this._sessionReject){const s=new Error(i||`Session ended before it could start: ${t||"unknown reason"}`);s.reason=t??null,this._emit("sessionRejected",{reason:t,activeApp:null}),this._clearIceGrace(),this._uninstallNetworkListeners(),this._pc&&(this._pc.close(),this._pc=null),this._micStream&&(this._micStream.getTracks().forEach(r=>r.stop()),this._micStream=null),this._iceConnected=!1,this._dcOpen=!1,this._micMuted=!0,this._micSupported=!1;const n=this._sessionReject;this._sessionResolve=null,this._sessionReject=null,n(s);return}this._state==="streaming"&&(this._emit("sessionStopped",{reason:t||"remote_end",message:i}),this.stopSession().catch(()=>{}))}async _handlePeerMessage(e){if(this._pc)try{if(e.sdp){const t=e.sdp;if(t.type==="offer"){const s=se(t.sdp);if(this._micSupported=s,this._emit("micSupported",{supported:s}),s&&this._micStream)for(const r of this._micStream.getAudioTracks())this._pc.addTrack(r,this._micStream);await this._pc.setRemoteDescription(new RTCSessionDescription(t));const n=await this._pc.createAnswer();await this._pc.setLocalDescription(n),await this._sendToServer({type:"peer",sessionId:this._sessionId,sdp:{type:"answer",sdp:n.sdp}})}else await this._pc.setRemoteDescription(new RTCSessionDescription(t));const i=this._pendingRemoteIce;if(i&&i.length){this._pendingRemoteIce=[];for(const s of i)try{await this._pc.addIceCandidate(new RTCIceCandidate(s))}catch(n){console.warn("[reachy-mini] buffered ICE candidate rejected:",n)}}}if(e.ice){if(!e.ice.candidate)return;this._pc.remoteDescription?await this._pc.addIceCandidate(new RTCIceCandidate(e.ice)):(this._pendingRemoteIce||(this._pendingRemoteIce=[]),this._pendingRemoteIce.push(e.ice))}}catch(t){console.error("WebRTC error:",t),this._emit("error",{source:"webrtc",error:t})}}_handleRobotMessage(e){if("version"in e&&this._versionResolve){this._versionResolve(e.version),this._versionResolve=null;return}if("hardware_id"in e&&this._hardwareIdResolve){this._hardwareIdResolve(e.hardware_id),this._hardwareIdResolve=null;return}if(e.command==="get_volume"||e.command==="set_volume"){this._volumeResolve&&(this._volumeResolve(e.status==="error"?null:e.volume),this._volumeResolve=null);return}if(e.command==="get_microphone_volume"||e.command==="set_microphone_volume"){this._micVolumeResolve&&(this._micVolumeResolve(e.status==="error"?null:e.volume),this._micVolumeResolve=null);return}if(e.command==="apply_audio_config"){this._applyAudioConfigResolve&&(this._applyAudioConfigResolve(e.error?!1:!!e.applied),this._applyAudioConfigResolve=null);return}if(e.command==="read_audio_parameter"){this._readAudioParameterResolve&&(this._readAudioParameterResolve(e.error?null:e.values??null),this._readAudioParameterResolve=null);return}if((e.command==="wake_up"||e.command==="goto_sleep")&&this._pendingMotionCompletions&&this._pendingMotionCompletions[e.command]){const t=this._pendingMotionCompletions[e.command];if(e.completed===!0&&t.length>0){const i=t.shift();clearTimeout(i.timer),i.resolve();return}if(e.error&&t.length>0){const i=t.shift();clearTimeout(i.timer),i.reject(new Error(`${e.command}: ${e.error}`));return}}if(e.type==="log_line"){for(const t of this._logSubscribers)try{t.onLine({timestamp:e.timestamp,line:e.line})}catch(i){console.error("subscribeLogs onLine threw:",i)}return}if(e.type==="log_stream_error"){for(const t of this._logSubscribers)if(typeof t.onError=="function")try{t.onError(e.error)}catch(i){console.error("subscribeLogs onError threw:",i)}return}if(e.state){const t=e.state;t.head_pose&&(this._robotState.head=t.head_pose.flat()),t.antennas&&(this._robotState.antennas=[t.antennas[0],t.antennas[1]]),typeof t.body_yaw=="number"&&(this._robotState.body_yaw=t.body_yaw),t.motor_mode&&(this._robotState.motor_mode=t.motor_mode),typeof t.is_move_running=="boolean"&&(this._robotState.is_move_running=t.is_move_running),this._emit("state",{...this._robotState})}if(e.error&&this._emit("error",{source:"robot",error:e.error}),this._broadcastWaiters.length>0)for(let t=this._broadcastWaiters.length-1;t>=0;t--){const i=this._broadcastWaiters[t];if(i.predicate(e)){this._broadcastWaiters.splice(t,1),clearTimeout(i.timer),i.resolve(e);return}}}_startLatencyMonitor(e){this._latencyMonitorId&&clearInterval(this._latencyMonitorId),this._latencyMonitorId=setInterval(()=>{if(!e.srcObject||e.paused)return;const t=e.buffered;if(t.length>0){const i=t.end(t.length-1),s=i-e.currentTime;s>.5&&(console.log(`Latency correction: was ${s.toFixed(2)}s behind`),e.currentTime=i-.1)}},2e3)}}window.ReachyMini=le;window.dispatchEvent(new Event("reachymini:ready"));const M=new URLSearchParams(window.location.search),ce=M.get("embedded")==="1"||M.get("embed")==="1",de=M.has("code")||M.has("error"),he=sessionStorage.getItem("cookaiware:launched")==="1";function F(){sessionStorage.setItem("cookaiware:launched","1");const o=document.getElementById("root");return o&&(o.innerHTML=""),U(async()=>{const{mountHost:a}=await import("./auto-CaTD3xKq.js");return{mountHost:a}},__vite__mapDeps([0,1])).then(({mountHost:a})=>{a({appName:"CookAIware",appIconUrl:"/icon.png",appEmoji:"🍲",enableMicrophone:!0,devToken:void 0,clientId:void 0})})}ce?U(()=>import("./embed-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};
|
|
@@ -59,7 +59,7 @@
|
|
| 59 |
}
|
| 60 |
})();
|
| 61 |
</script>
|
| 62 |
-
<script type="module" crossorigin src="/assets/index-
|
| 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
|
|
@@ -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",
|
|
@@ -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"
|
|
|
|
|
|
|
|
|
|
| 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;
|
|
@@ -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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 988 |
saveSettings(this.settings);
|
| 989 |
-
|
|
|
|
| 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 |
}
|
|
@@ -829,11 +829,18 @@ label {
|
|
| 829 |
flex: 1;
|
| 830 |
}
|
| 831 |
|
| 832 |
-
|
|
|
|
|
|
|
| 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;
|
|
@@ -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: {
|
|
@@ -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 |
-
|
| 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 =
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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);
|
|
@@ -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.",
|