robopark 3.3.7 → 3.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,55 +1,55 @@
1
1
  {
2
- "/_not-found/page": "app/_not-found/page.js",
3
- "/api/download-piper-model/route": "app/api/download-piper-model/route.js",
4
2
  "/api/device-character/route": "app/api/device-character/route.js",
5
- "/api/models/route": "app/api/models/route.js",
3
+ "/api/download-piper-model/route": "app/api/download-piper-model/route.js",
4
+ "/api/prewarm/route": "app/api/prewarm/route.js",
6
5
  "/api/prompt/route": "app/api/prompt/route.js",
7
6
  "/api/reload-tools/route": "app/api/reload-tools/route.js",
8
- "/api/prewarm/route": "app/api/prewarm/route.js",
7
+ "/api/models/route": "app/api/models/route.js",
9
8
  "/api/robopark/devices/[deviceId]/control/route": "app/api/robopark/devices/[deviceId]/control/route.js",
10
- "/api/robopark/sessions/[sessionId]/end/route": "app/api/robopark/sessions/[sessionId]/end/route.js",
11
9
  "/api/robopark/robots/[robotId]/operations/route": "app/api/robopark/robots/[robotId]/operations/route.js",
10
+ "/api/robopark/sessions/[sessionId]/end/route": "app/api/robopark/sessions/[sessionId]/end/route.js",
12
11
  "/api/robopark/transcripts/[sessionId]/route": "app/api/robopark/transcripts/[sessionId]/route.js",
13
- "/api/setup/test-groq/route": "app/api/setup/test-groq/route.js",
14
- "/api/setup/test-n8n/route": "app/api/setup/test-n8n/route.js",
15
12
  "/api/setup/complete/route": "app/api/setup/complete/route.js",
16
- "/api/setup/test-ollama/route": "app/api/setup/test-ollama/route.js",
17
13
  "/api/setup/status/route": "app/api/setup/status/route.js",
14
+ "/api/setup/test-groq/route": "app/api/setup/test-groq/route.js",
18
15
  "/api/setup/test-hass/route": "app/api/setup/test-hass/route.js",
16
+ "/api/setup/test-n8n/route": "app/api/setup/test-n8n/route.js",
17
+ "/api/setup/test-ollama/route": "app/api/setup/test-ollama/route.js",
19
18
  "/api/test-tts/route": "app/api/test-tts/route.js",
20
- "/api/wake-word/status/route": "app/api/wake-word/status/route.js",
21
19
  "/api/voices/route": "app/api/voices/route.js",
22
20
  "/api/wake-word/models/route": "app/api/wake-word/models/route.js",
21
+ "/api/wake-word/status/route": "app/api/wake-word/status/route.js",
23
22
  "/api/wake-word/upload/route": "app/api/wake-word/upload/route.js",
24
23
  "/favicon.ico/route": "app/favicon.ico/route.js",
24
+ "/_not-found/page": "app/_not-found/page.js",
25
25
  "/api/connection-details/route": "app/api/connection-details/route.js",
26
26
  "/api/kiosk-hardware/route": "app/api/kiosk-hardware/route.js",
27
27
  "/api/model-status/route": "app/api/model-status/route.js",
28
- "/api/robopark/camera-token/route": "app/api/robopark/camera-token/route.js",
29
28
  "/api/robopark/character-ai/route": "app/api/robopark/character-ai/route.js",
29
+ "/api/robopark/camera-token/route": "app/api/robopark/camera-token/route.js",
30
30
  "/api/robopark/device-access/route": "app/api/robopark/device-access/route.js",
31
31
  "/api/robopark/live-rooms/route": "app/api/robopark/live-rooms/route.js",
32
- "/api/robopark/robots/route": "app/api/robopark/robots/route.js",
33
- "/api/robopark/servers/[serverId]/metrics/route": "app/api/robopark/servers/[serverId]/metrics/route.js",
34
32
  "/api/robopark/monitor-token/route": "app/api/robopark/monitor-token/route.js",
33
+ "/api/robopark/servers/[serverId]/metrics/route": "app/api/robopark/servers/[serverId]/metrics/route.js",
34
+ "/api/robopark/robots/route": "app/api/robopark/robots/route.js",
35
35
  "/api/robopark/servers/[serverId]/route": "app/api/robopark/servers/[serverId]/route.js",
36
- "/api/robopark/sessions/route": "app/api/robopark/sessions/route.js",
37
36
  "/api/robopark/servers/route": "app/api/robopark/servers/route.js",
38
- "/api/robovoice/robots/[robotId]/media-health/route": "app/api/robovoice/robots/[robotId]/media-health/route.js",
37
+ "/api/robopark/sessions/route": "app/api/robopark/sessions/route.js",
39
38
  "/api/robopark/robots/[robotId]/voice-engine/route": "app/api/robopark/robots/[robotId]/voice-engine/route.js",
39
+ "/api/robovoice/robots/[robotId]/media-health/route": "app/api/robovoice/robots/[robotId]/media-health/route.js",
40
40
  "/api/robopark/vision-caption/route": "app/api/robopark/vision-caption/route.js",
41
41
  "/api/settings/route": "app/api/settings/route.js",
42
+ "/api/robopark/transcripts/route": "app/api/robopark/transcripts/route.js",
42
43
  "/api/turn-credentials/route": "app/api/turn-credentials/route.js",
43
- "/api/wake/route": "app/api/wake/route.js",
44
44
  "/camera-grid/route": "app/camera-grid/route.js",
45
- "/fed/media/robots/[robotId]/video_feed/route": "app/fed/media/robots/[robotId]/video_feed/route.js",
45
+ "/api/wake/route": "app/api/wake/route.js",
46
46
  "/robopark/[...path]/route": "app/robopark/[...path]/route.js",
47
- "/api/robopark/transcripts/route": "app/api/robopark/transcripts/route.js",
47
+ "/fed/media/robots/[robotId]/video_feed/route": "app/fed/media/robots/[robotId]/video_feed/route.js",
48
48
  "/fed/[...path]/route": "app/fed/[...path]/route.js",
49
- "/tailnet/route": "app/tailnet/route.js",
50
- "/(app)/opengraph-image-xg4ifa/route": "app/(app)/opengraph-image-xg4ifa/route.js",
51
49
  "/robopark/api/robots/[robotId]/stream/route": "app/robopark/api/robots/[robotId]/stream/route.js",
50
+ "/tailnet/route": "app/tailnet/route.js",
52
51
  "/vendor/livekit-client.umd.js/route": "app/vendor/livekit-client.umd.js/route.js",
52
+ "/(app)/opengraph-image-xg4ifa/route": "app/(app)/opengraph-image-xg4ifa/route.js",
53
53
  "/robopark/api/[...path]/route": "app/robopark/api/[...path]/route.js",
54
54
  "/robopark/page": "app/robopark/page.js",
55
55
  "/voice-tester/page": "app/voice-tester/page.js",
@@ -1 +1 @@
1
- <!DOCTYPE html><html><head><meta charSet="utf-8" data-next-head=""/><meta name="viewport" content="width=device-width" data-next-head=""/><title data-next-head="">500: Internal Server Error</title><noscript data-n-css=""></noscript><script defer="" noModule="" src="/_next/static/chunks/polyfills-42372ed130431b0a.js"></script><script src="/_next/static/chunks/webpack-5252e5254da079b5.js" defer=""></script><script src="/_next/static/chunks/framework-e60c938074ff7136.js" defer=""></script><script src="/_next/static/chunks/main-9e24e8708ff075fc.js" defer=""></script><script src="/_next/static/chunks/pages/_app-131c90850aef965b.js" defer=""></script><script src="/_next/static/chunks/pages/_error-e4ba546eb376bdf4.js" defer=""></script><script src="/_next/static/7hosI6Tk4PpkcjmN3-s_F/_buildManifest.js" defer=""></script><script src="/_next/static/7hosI6Tk4PpkcjmN3-s_F/_ssgManifest.js" defer=""></script></head><body><div id="__next"><div style="font-family:system-ui,&quot;Segoe UI&quot;,Roboto,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot;;height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center"><div style="line-height:48px"><style>body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}</style><h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding-right:23px;font-size:24px;font-weight:500;vertical-align:top">500</h1><div style="display:inline-block"><h2 style="font-size:14px;font-weight:400;line-height:28px">Internal Server Error<!-- -->.</h2></div></div></div></div><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{"statusCode":500}},"page":"/_error","query":{},"buildId":"7hosI6Tk4PpkcjmN3-s_F","nextExport":true,"isFallback":false,"gip":true,"scriptLoader":[]}</script></body></html>
1
+ <!DOCTYPE html><html><head><meta charSet="utf-8" data-next-head=""/><meta name="viewport" content="width=device-width" data-next-head=""/><title data-next-head="">500: Internal Server Error</title><noscript data-n-css=""></noscript><script defer="" noModule="" src="/_next/static/chunks/polyfills-42372ed130431b0a.js"></script><script src="/_next/static/chunks/webpack-5252e5254da079b5.js" defer=""></script><script src="/_next/static/chunks/framework-e60c938074ff7136.js" defer=""></script><script src="/_next/static/chunks/main-9e24e8708ff075fc.js" defer=""></script><script src="/_next/static/chunks/pages/_app-131c90850aef965b.js" defer=""></script><script src="/_next/static/chunks/pages/_error-e4ba546eb376bdf4.js" defer=""></script><script src="/_next/static/H5av7rSb0E_elgbludulF/_buildManifest.js" defer=""></script><script src="/_next/static/H5av7rSb0E_elgbludulF/_ssgManifest.js" defer=""></script></head><body><div id="__next"><div style="font-family:system-ui,&quot;Segoe UI&quot;,Roboto,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot;;height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center"><div style="line-height:48px"><style>body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}</style><h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding-right:23px;font-size:24px;font-weight:500;vertical-align:top">500</h1><div style="display:inline-block"><h2 style="font-size:14px;font-weight:400;line-height:28px">Internal Server Error<!-- -->.</h2></div></div></div></div><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{"statusCode":500}},"page":"/_error","query":{},"buildId":"H5av7rSb0E_elgbludulF","nextExport":true,"isFallback":false,"gip":true,"scriptLoader":[]}</script></body></html>
@@ -1,5 +1,5 @@
1
1
  {
2
- "/_app": "pages/_app.js",
3
2
  "/_error": "pages/_error.js",
3
+ "/_app": "pages/_app.js",
4
4
  "/_document": "pages/_document.js"
5
5
  }
@@ -1 +1 @@
1
- {"node":{},"edge":{},"encryptionKey":"oMzmrICjd1miwxs/vfayAYdzjnbywYaPFjZAUCXe3Eo="}
1
+ {"node":{},"edge":{},"encryptionKey":"v6yRoBy1CP0sc2/QCUk2IWQ98vup9njavuMLXuCteWs="}
@@ -1,12 +1,12 @@
1
1
  <!doctype html>
2
2
  <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
3
- <meta name="theme-color" content="#080704"><meta name="robopark-voice-join-build" content="production-motion-greeting-20260728-1">
3
+ <meta name="theme-color" content="#080704"><meta name="robopark-voice-join-build" content="production-motion-kiosk-20260729-1">
4
4
  <title>RoboPark Character Conversation</title>
5
5
  <style>
6
6
  .deployment{display:grid;gap:8px;margin:14px 0}.deployment-row{display:grid;grid-template-columns:minmax(150px,1fr) 2fr auto;gap:9px;align-items:center;padding:.7rem;border:1px solid var(--line);border-radius:12px;background:rgba(255,255,255,.025)}.deployment-row b{font:.62rem var(--mono);color:var(--pale)}.deployment-row span{font:.54rem var(--mono);color:var(--muted)}.deployment-row .ready{color:var(--ok)}.deployment-actions{display:flex;flex-wrap:wrap;gap:5px}.deployment-actions button{border:1px solid var(--line);border-radius:8px;background:rgba(244,207,114,.09);color:var(--gold);padding:.38rem .5rem;font:.5rem var(--mono);cursor:pointer}
7
7
  .audio-controls,.call-controls{position:relative;width:min(440px,94%);margin-top:12px;border:1px solid var(--line);border-radius:14px;background:rgba(0,0,0,.3);text-align:left}.audio-controls summary,.call-controls summary{padding:.65rem .8rem;cursor:pointer;color:var(--gold);font:600 .58rem var(--mono);letter-spacing:.07em;text-transform:uppercase}.audio-controls summary::marker,.call-controls summary::marker{color:var(--gold)}.audio-panel,.call-panel{display:grid;gap:.75rem;padding:0 .85rem .85rem}.audio-row{display:grid;grid-template-columns:105px 1fr 45px;gap:.55rem;align-items:center}.audio-row label,.audio-value,.call-row label{font:600 .54rem var(--mono);color:var(--muted)}.audio-value{text-align:right;color:var(--pale)}.audio-row input[type=range]{width:100%;accent-color:var(--gold)}.audio-note,.call-note{margin:0;color:#7e755f;font:.5rem/1.45 var(--mono)}.call-row{display:grid;gap:.3rem}.call-row select{width:100%;min-width:0;border:1px solid var(--line);border-radius:9px;background:#090805;color:var(--ink);padding:.52rem;font:600 .55rem var(--mono)}.call-actions{display:grid;grid-template-columns:1fr 1fr;gap:7px}.call-actions button{border:1px solid var(--line);border-radius:9px;background:rgba(244,207,114,.1);color:var(--gold);padding:.52rem;font:600 .52rem var(--mono);cursor:pointer}.call-actions button.primary{background:linear-gradient(135deg,#f5d77f,#b87b1d);color:#171005}.call-status.ok{color:var(--ok)}.call-status.bad{color:var(--bad)}.mode-badge{display:inline-flex;align-items:center;gap:.35rem;padding:.28rem .48rem;border:1px solid var(--line);border-radius:999px;color:var(--muted);font:600 .5rem var(--mono)}.mode-badge.production{color:var(--ok);border-color:rgba(100,223,160,.4)}
8
8
  :root{--bg:#080704;--panel:rgba(20,17,11,.88);--line:rgba(247,211,116,.22);--gold:#f4cf72;--pale:#fff0bd;--ink:#f7f1df;--muted:#a99e84;--ok:#64dfa0;--warn:#f0bd55;--bad:#ff776c;--mono:"JetBrains Mono","Cascadia Code",monospace;--display:Georgia,"Times New Roman",serif}*{box-sizing:border-box}html,body{margin:0;min-height:100%;background:radial-gradient(circle at 50% 8%,#332510 0,#100d08 35%,#050403 78%);color:var(--ink);font-family:Verdana,Geneva,sans-serif}body:before{content:"";position:fixed;inset:0;pointer-events:none;background:linear-gradient(115deg,transparent 0 48%,rgba(244,207,114,.025) 49% 50%,transparent 51%),repeating-linear-gradient(0deg,transparent 0 34px,rgba(255,255,255,.012) 35px)}button,input{font:inherit}.shell{width:min(1120px,100%);min-height:100vh;margin:auto;padding:clamp(14px,3vw,34px);display:grid;grid-template-rows:auto 1fr auto;gap:18px}.top{display:flex;align-items:center;justify-content:space-between;gap:16px}.brand{font:700 .65rem var(--mono);letter-spacing:.18em;text-transform:uppercase;color:var(--gold)}.ops-toggle{border:1px solid var(--line);border-radius:999px;background:rgba(0,0,0,.35);color:var(--muted);padding:.55rem .8rem;cursor:pointer}.ops-toggle.on{color:var(--pale);border-color:var(--gold)}.stage{position:relative;min-height:620px;display:grid;grid-template-columns:minmax(0,1.3fr) minmax(280px,.7fr);overflow:hidden;border:1px solid var(--line);border-radius:28px;background:linear-gradient(145deg,rgba(34,27,14,.82),rgba(8,7,5,.92));box-shadow:0 28px 90px rgba(0,0,0,.45)}.hero{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:34px;text-align:center;overflow:hidden}.hero:before{content:"";position:absolute;width:500px;height:500px;border:1px solid rgba(244,207,114,.1);border-radius:50%;box-shadow:0 0 0 55px rgba(244,207,114,.018),0 0 0 120px rgba(244,207,114,.012)}.avatar-wrap{position:relative;width:min(330px,72vw);aspect-ratio:1;display:grid;place-items:center;border:1px solid rgba(244,207,114,.3);border-radius:50%;background:radial-gradient(circle,rgba(244,207,114,.16),rgba(8,7,5,.62) 58%,#050403 74%);box-shadow:0 0 90px rgba(214,164,55,.16)}.avatar-wrap.live{animation:breathe 2.2s ease-in-out infinite}.avatar-wrap.speaking{box-shadow:0 0 110px rgba(244,207,114,.42);border-color:var(--pale)}@keyframes breathe{50%{transform:scale(1.018)}}.avatar{position:relative;z-index:2;width:78%;height:78%;object-fit:contain;filter:drop-shadow(0 20px 28px rgba(0,0,0,.58))}.fallback{position:absolute;font:700 4rem var(--display);color:var(--gold)}h1{position:relative;margin:22px 0 5px;font:700 clamp(2rem,5vw,3.5rem) var(--display);color:var(--pale)}.subtitle{position:relative;color:var(--muted);font-size:.8rem}.state{position:relative;margin-top:14px;font:600 .68rem var(--mono);letter-spacing:.08em;text-transform:uppercase;color:var(--gold)}.join{position:relative;margin-top:22px;min-width:210px;border:1px solid var(--gold);border-radius:999px;padding:.9rem 1.25rem;background:linear-gradient(135deg,#f5d77f,#b87b1d);color:#171005;font-weight:800;cursor:pointer;box-shadow:0 12px 30px rgba(184,123,29,.24)}.join:disabled{opacity:.48;cursor:not-allowed}.end{position:relative;margin-top:10px;border:0;background:transparent;color:var(--muted);cursor:pointer}.side{position:relative;border-left:1px solid var(--line);padding:25px;display:flex;flex-direction:column;gap:14px;background:rgba(0,0,0,.2)}.vision{position:relative;aspect-ratio:16/10;border:1px solid var(--line);border-radius:17px;overflow:hidden;background:#050403}.vision img,.vision video{width:100%;height:100%;object-fit:contain;background:#050403}.vision .label{position:absolute;left:9px;top:9px;padding:.32rem .45rem;border-radius:7px;background:rgba(0,0,0,.7);font:600 .5rem var(--mono);color:var(--gold)}.transcript{flex:1;min-height:220px;max-height:360px;overflow:auto;padding:.65rem;border:1px solid var(--line);border-radius:15px;background:rgba(0,0,0,.22)}.turn{margin:.45rem 0;padding:.55rem .65rem;border-radius:11px;font-size:.75rem;line-height:1.45}.turn.user{margin-left:13%;background:rgba(244,207,114,.12);border:1px solid rgba(244,207,114,.18)}.turn.assistant{margin-right:13%;background:rgba(255,255,255,.045)}.compose{display:flex;gap:8px}.compose input{min-width:0;flex:1;border:1px solid var(--line);border-radius:11px;background:#090805;color:var(--ink);padding:.7rem}.compose button{border:1px solid var(--line);border-radius:11px;background:rgba(244,207,114,.12);color:var(--gold);padding:.7rem;cursor:pointer}.ops{display:none;position:absolute;inset:12px;z-index:8;padding:16px;border:1px solid rgba(244,207,114,.35);border-radius:18px;background:rgba(5,4,3,.94);backdrop-filter:blur(16px);overflow:auto}.ops.open{display:block}.ops-head{display:flex;align-items:center;justify-content:space-between}.ops h2{margin:0;font:700 1.2rem var(--display);color:var(--pale)}.close{border:1px solid var(--line);border-radius:9px;background:transparent;color:var(--muted);padding:.4rem .55rem;cursor:pointer}.facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;margin:14px 0}.fact{padding:.7rem;border:1px solid var(--line);border-radius:12px;background:rgba(255,255,255,.025)}.fact span{display:block;color:var(--muted);font:500 .49rem var(--mono);text-transform:uppercase;letter-spacing:.11em}.fact b{display:block;margin-top:.3rem;color:var(--ink);font:600 .64rem var(--mono);word-break:break-all}.pipeline{display:grid;grid-template-columns:repeat(5,1fr);gap:7px;margin:15px 0}.step{padding:.65rem .3rem;border:1px solid var(--line);border-radius:11px;text-align:center;color:var(--muted);font:600 .52rem var(--mono);text-transform:uppercase}.step.done{color:var(--ok);border-color:rgba(100,223,160,.4)}.step.live{color:#171005;border-color:var(--gold);background:var(--gold);box-shadow:0 0 24px rgba(244,207,114,.25)}.step.bad{color:var(--bad);border-color:rgba(255,119,108,.5)}.events{display:grid;gap:7px}.event{display:grid;grid-template-columns:95px 70px 1fr;gap:8px;padding:.6rem;border-bottom:1px solid var(--line);font:500 .55rem var(--mono)}.event .ok{color:var(--ok)}.event .error,.event .failed{color:var(--bad)}.foot{text-align:center;color:#716956;font:500 .52rem var(--mono);letter-spacing:.08em}.error-box{display:none;margin-top:12px;color:var(--bad);font:.65rem var(--mono)}@media(max-width:780px){.shell{padding:10px}.stage{grid-template-columns:1fr;min-height:0}.hero{min-height:600px;padding:24px 15px}.side{border-left:0;border-top:1px solid var(--line)}.pipeline{grid-template-columns:1fr 1fr}.top{padding:4px}.event{grid-template-columns:75px 55px 1fr}}
9
- </style></head><body><main class="shell"><header class="top"><div class="brand">RoboPark / managed character channel</div><div><button class="ops-toggle on" id="always-on" aria-pressed="true" hidden>Always on: ON</button> <button class="ops-toggle" id="ops-toggle">Operations</button></div></header><section class="stage"><div class="hero"><div class="avatar-wrap" id="avatar-wrap"><div class="fallback" id="fallback">RP</div><img class="avatar" id="avatar" alt=""></div><h1 id="character-name">Loading character...</h1><div class="subtitle" id="subtitle">Secure ElevenLabs conversation managed by RoboPark</div><div class="state" id="state">validating link</div><button class="join" id="join" disabled>Join conversation</button><button class="end" id="end" hidden>End conversation</button><div class="error-box" id="error"></div></div><aside class="side"><div class="vision" id="vision" hidden><img id="robot-video" alt="Live robot camera"><span class="label">LIVE ROBOT VISION</span></div><div class="transcript" id="transcript"><div class="turn assistant">The transcript will appear here when the conversation begins.</div></div><form class="compose" id="compose"><input id="message" placeholder="Type a message" disabled><button id="send" disabled>Send</button></form></aside><section class="ops" id="ops"><div class="ops-head"><h2>Live management overlay</h2><button class="close" id="ops-close">Close</button></div><div class="facts"><div class="fact"><span>Session</span><b id="f-session">not started</b></div><div class="fact"><span>Character</span><b id="f-character">--</b></div><div class="fact"><span>Agent</span><b id="f-agent">--</b></div><div class="fact"><span>Branch</span><b id="f-branch">default</b></div><div class="fact"><span>Robot</span><b id="f-robot">operator only</b></div><div class="fact"><span>Share link</span><b id="f-share">--</b></div></div><div class="pipeline" id="pipeline"><div class="step live" data-stage="configured">configured</div><div class="step" data-stage="permission">permission</div><div class="step" data-stage="signed_session">signed</div><div class="step" data-stage="connected">connected</div><div class="step" data-stage="conversation">conversation</div></div><div class="events" id="events"><div class="event"><span>waiting</span><span>info</span><span>Session telemetry will stream here.</span></div></div></section></section><footer class="foot">No API credentials are stored in this link. Each visit creates an isolated signed session.</footer></main><script src="/vendor/elevenlabs-client.iife.js"></script><script>
9
+ </style></head><body><main class="shell"><header class="top"><div class="brand">RoboPark / managed character channel</div><div><button class="ops-toggle on" id="always-on" aria-pressed="true" hidden>Always on: ON</button><button class="ops-toggle" id="kiosk" aria-pressed="false">Motion calls: OFF</button> <button class="ops-toggle" id="ops-toggle">Operations</button></div></header><section class="stage"><div class="hero"><div class="avatar-wrap" id="avatar-wrap"><div class="fallback" id="fallback">RP</div><img class="avatar" id="avatar" alt=""></div><h1 id="character-name">Loading character...</h1><div class="subtitle" id="subtitle">Secure ElevenLabs conversation managed by RoboPark</div><div class="state" id="state">validating link</div><button class="join" id="join" disabled>Join conversation</button><button class="end" id="end" hidden>End conversation</button><div class="error-box" id="error"></div></div><aside class="side"><div class="vision" id="vision" hidden><img id="robot-video" alt="Live robot camera"><span class="label">LIVE ROBOT VISION</span></div><div class="transcript" id="transcript"><div class="turn assistant">The transcript will appear here when the conversation begins.</div></div><form class="compose" id="compose"><input id="message" placeholder="Type a message" disabled><button id="send" disabled>Send</button></form></aside><section class="ops" id="ops"><div class="ops-head"><h2>Live management overlay</h2><button class="close" id="ops-close">Close</button></div><div class="facts"><div class="fact"><span>Session</span><b id="f-session">not started</b></div><div class="fact"><span>Character</span><b id="f-character">--</b></div><div class="fact"><span>Agent</span><b id="f-agent">--</b></div><div class="fact"><span>Branch</span><b id="f-branch">default</b></div><div class="fact"><span>Robot</span><b id="f-robot">operator only</b></div><div class="fact"><span>Share link</span><b id="f-share">--</b></div></div><div class="pipeline" id="pipeline"><div class="step live" data-stage="configured">configured</div><div class="step" data-stage="permission">permission</div><div class="step" data-stage="signed_session">signed</div><div class="step" data-stage="connected">connected</div><div class="step" data-stage="conversation">conversation</div></div><div class="events" id="events"><div class="event"><span>waiting</span><span>info</span><span>Session telemetry will stream here.</span></div></div></section></section><footer class="foot">No API credentials are stored in this link. Each visit creates an isolated signed session.</footer></main><script src="/vendor/elevenlabs-client.iife.js"></script><script>
10
10
  (function(){
11
11
  var deployment=document.createElement('div');deployment.id='deployment';deployment.className='deployment';deployment.innerHTML='<div class="deployment-row"><b>Character hardware</b><span>Loading registered voice, vision, and motor nodes...</span><span></span></div>';document.getElementById('pipeline').before(deployment);
12
12
  var audioControls=document.createElement('details');audioControls.id='audio-controls';audioControls.className='audio-controls';audioControls.innerHTML='<summary>Audio levels</summary><div class="audio-panel"><div class="audio-row"><label for="speaker-volume">Speaker</label><input id="speaker-volume" type="range" min="0" max="100" step="1"><output class="audio-value" id="speaker-value"></output></div><div class="audio-row"><label for="mic-sensitivity">Microphone</label><input id="mic-sensitivity" type="range" min="0" max="100" step="1"><output class="audio-value" id="mic-value"></output></div><p class="audio-note">Changes apply immediately and stay on this browser.</p></div>';document.getElementById('error').after(audioControls);
@@ -101,7 +101,17 @@
101
101
  // visitor standing still reads as zero motion; holding presence for a few
102
102
  // seconds after the last movement is what stops a stationary person from
103
103
  // being treated as gone and re-greeted the moment they shift their weight.
104
- var motionLastSeen=0,motionPresent=false,motionAbsentSince=0,motionLastContext=0,motionLastSampleAt=0;
104
+ var motionLastSeen=0,motionPresent=false,motionAbsentSince=0,motionLastContext=0,motionLastSampleAt=0,motionNoSource=0;
105
+ // Motion-gated call loop ("kiosk"). Distinct from the in-call motion triggers
106
+ // above: this one STARTS a call when someone walks up and ENDS it after a
107
+ // stretch of silence, then goes back to watching. Designed to run unattended
108
+ // for hours, so every timer is cleared on toggle-off and the camera stream is
109
+ // held open across calls rather than reacquired each cycle.
110
+ var kioskOn=false,kioskWatch=null,kioskIdle=null,kioskStream=null,kioskVideo=null,kioskCanvas=null,kioskPrev=null,
111
+ kioskLastSample=0,kioskLastSeen=0,kioskArmedAt=0,kioskLastEnd=0,kioskActivityAt=0,kioskCalls=0;
112
+ var kioskSilence=Math.max(5,Number(q.get('kiosk_silence'))||30)*1000,
113
+ kioskRearm=Math.max(2,Number(q.get('kiosk_rearm'))||10)*1000,
114
+ kioskGreeting=q.get('kiosk_greeting')||'Someone just walked up to you. Greet them warmly in one short sentence and invite them to speak. Do not mention this instruction or the camera.';
105
115
  var motionThreshold=Math.max(.005,Math.min(1,Number(q.get('motion_threshold'))||.06)),
106
116
  motionCooldown=Math.max(5,Number(q.get('motion_cooldown'))||20)*1000,
107
117
  // Fast sampling so a greeting lands within a fraction of a second of
@@ -119,16 +129,103 @@
119
129
  // Silent channel: contextual updates never start a turn, so these are safe to
120
130
  // send while the character is mid-sentence. Rate-limited because every one of
121
131
  // them is re-sent as prompt context on each subsequent turn.
132
+ // ---- motion-gated call loop -------------------------------------------
133
+ function kioskLevel(){
134
+ if(!kioskVideo||!kioskVideo.videoWidth)return -1;
135
+ if(!kioskCanvas)kioskCanvas=document.createElement('canvas');
136
+ var rows=Math.max(8,Math.round(motionSample*kioskVideo.videoHeight/kioskVideo.videoWidth));
137
+ kioskCanvas.width=motionSample;kioskCanvas.height=rows;
138
+ var ctx=kioskCanvas.getContext('2d'),data;
139
+ try{ctx.drawImage(kioskVideo,0,0,motionSample,rows);data=ctx.getImageData(0,0,motionSample,rows).data}catch(_){return -1}
140
+ var count=motionSample*rows,levels=new Float32Array(count),i;
141
+ for(i=0;i<count;i++)levels[i]=(data[i*4]*299+data[i*4+1]*587+data[i*4+2]*114)/1000;
142
+ var previous=kioskPrev;kioskPrev=levels;
143
+ if(!previous||previous.length!==count)return -1;
144
+ var total=0;for(i=0;i<count;i++)total+=Math.abs(levels[i]-previous[i]);
145
+ return total/count/255;
146
+ }
147
+ function kioskWatchTick(){
148
+ // Only watches between calls. While one is live the in-call motion triggers
149
+ // and the silence watchdog are in charge.
150
+ if(!kioskOn||desired||conversation||connecting||ending)return;
151
+ var now=Date.now();
152
+ if(now<kioskArmedAt)return;
153
+ var level=kioskLevel();
154
+ if(level<0)return;
155
+ var elapsed=kioskLastSample?Math.max(1,now-kioskLastSample):motionInterval;
156
+ kioskLastSample=now;
157
+ if(level*1000/elapsed<motionThreshold)return;
158
+ // Require the frame to have been quiet before triggering, so the visitor who
159
+ // just hung up standing in front of the lens does not immediately redial.
160
+ if(kioskLastSeen&&now-kioskLastSeen<kioskRearm){kioskLastSeen=now;return}
161
+ kioskLastSeen=now;
162
+ kioskCalls++;
163
+ event('kiosk_call','ok','Motion started a call',{call_number:kioskCalls});
164
+ text('state','someone approached - connecting');
165
+ start();
166
+ }
167
+ function kioskIdleTick(){
168
+ if(!kioskOn||!conversation||ending)return;
169
+ if(!kioskActivityAt)return;
170
+ var quiet=Date.now()-kioskActivityAt;
171
+ if(quiet<kioskSilence)return;
172
+ event('kiosk_call','ok','Ending call after silence',{silent_ms:Math.round(quiet)});
173
+ // Rearm only after the visitor has actually left the frame; kioskArmedAt
174
+ // gates the watcher and kioskLastSeen forces a fresh quiet period.
175
+ kioskArmedAt=Date.now()+kioskRearm;
176
+ end('kiosk_idle');
177
+ }
178
+ function kioskNote(){kioskActivityAt=Date.now()}
179
+ function kioskStart(){
180
+ if(kioskWatch)return Promise.resolve();
181
+ return navigator.mediaDevices.getUserMedia({video:cameraDeviceId?{deviceId:{exact:cameraDeviceId}}:true})
182
+ .then(function(stream){
183
+ kioskStream=stream;
184
+ if(!kioskVideo){kioskVideo=document.createElement('video');kioskVideo.autoplay=true;kioskVideo.muted=true;kioskVideo.playsInline=true;kioskVideo.style.display='none';document.body.appendChild(kioskVideo)}
185
+ kioskVideo.srcObject=stream;
186
+ var play=kioskVideo.play();if(play&&play.catch)play.catch(function(){});
187
+ kioskPrev=null;kioskLastSample=0;kioskLastSeen=0;kioskArmedAt=Date.now()+3000;
188
+ kioskWatch=setInterval(kioskWatchTick,motionInterval);
189
+ kioskIdle=setInterval(kioskIdleTick,1000);
190
+ event('kiosk_armed','ok','Motion-gated calling armed',{silence_ms:kioskSilence,rearm_ms:kioskRearm,threshold:motionThreshold});
191
+ text('state','watching for visitors');
192
+ })
193
+ .catch(function(error){
194
+ kioskOn=false;syncKioskButton();
195
+ event('kiosk_armed','failed',error&&error.message||String(error),{});
196
+ text('state','camera unavailable for motion calling');
197
+ });
198
+ }
199
+ function kioskStop(){
200
+ if(kioskWatch){clearInterval(kioskWatch);kioskWatch=null}
201
+ if(kioskIdle){clearInterval(kioskIdle);kioskIdle=null}
202
+ if(kioskStream){kioskStream.getTracks().forEach(function(track){track.stop()});kioskStream=null}
203
+ if(kioskVideo)kioskVideo.srcObject=null;
204
+ kioskPrev=null;
205
+ }
206
+ function syncKioskButton(){
207
+ var button=el('kiosk');if(!button)return;
208
+ button.classList.toggle('on',kioskOn);
209
+ button.setAttribute('aria-pressed',String(kioskOn));
210
+ button.textContent='Motion calls: '+(kioskOn?'ON':'OFF');
211
+ }
122
212
  function motionContext(now,text){
123
213
  if(now-motionLastContext<motionContextGap)return;
124
214
  motionLastContext=now;
125
215
  try{conversation.sendContextualUpdate(text)}catch(_){}
126
216
  }
127
217
  function motionTick(myGeneration){
128
- // Checked per tick, not at start, so the always-on toggle takes effect live.
129
- if(ending||!conversation||myGeneration!==generation||!alwaysOn)return;
218
+ if(ending||!conversation||myGeneration!==generation)return;
130
219
  var level=motionLevel(),now=Date.now();
131
- if(level<0||now<motionArmedAt)return;
220
+ if(now<motionArmedAt)return;
221
+ if(level<0){
222
+ // No usable frame: the camera element is hidden, has no dimensions yet,
223
+ // or the canvas is tainted. Silence here is indistinguishable from "no
224
+ // one is there", so say it once instead of never triggering in secret.
225
+ if(!motionNoSource){motionNoSource=now;event('motion_source','warn','No readable camera frame; motion triggers are inactive',{})}
226
+ return;
227
+ }
228
+ if(motionNoSource){event('motion_source','ok','Camera frame available; motion triggers active',{blind_ms:now-motionNoSource});motionNoSource=0}
132
229
  // Normalise to change-per-second. The raw difference between two frames
133
230
  // scales with the gap between them, so without this a faster sample rate
134
231
  // would silently make the configured threshold four times harder to hit.
@@ -164,7 +261,17 @@
164
261
  try{conversation.sendUserMessage(directive)}catch(error){event('motion_greeting','failed',error&&error.message||String(error),{});return}
165
262
  event('motion_greeting','ok','Motion greeting triggered',{absence_ms:absence===Infinity?null:Math.round(absence),threshold:motionThreshold})
166
263
  }
167
- function startMotion(myGeneration){stopMotion();motionPrev=null;motionLastFire=0;motionSent={};motionLastSeen=0;motionPresent=false;motionAbsentSince=0;motionLastContext=0;motionLastSampleAt=0;motionDirective=q.get('motion_prompt')||'';motionArmedAt=Date.now()+4000;motionTimer=setInterval(function(){motionTick(myGeneration)},motionInterval)}
264
+ function startMotion(myGeneration){
265
+ stopMotion();motionPrev=null;motionLastFire=0;motionSent={};motionLastSeen=0;motionPresent=false;motionAbsentSince=0;motionLastContext=0;motionLastSampleAt=0;motionNoSource=0;
266
+ motionDirective=q.get('motion_prompt')||'';
267
+ if(q.get('motion')==='off'){event('motion_armed','skipped','Motion triggers disabled by ?motion=off',{});return}
268
+ motionArmedAt=Date.now()+4000;
269
+ motionTimer=setInterval(function(){motionTick(myGeneration)},motionInterval);
270
+ // Announce the configuration once. Motion used to be gated on both
271
+ // production mode and the always-on toggle, and failing either produced
272
+ // total silence with nothing to indicate why.
273
+ event('motion_armed','ok','Motion presence tracking armed',{interval_ms:motionInterval,threshold:motionThreshold,hold_ms:motionHold,absence_ms:motionAbsence,cooldown_ms:motionCooldown})
274
+ }
168
275
  function stopMotion(){if(motionTimer){clearInterval(motionTimer);motionTimer=null}motionPrev=null}
169
276
  function showRobotVideo(){return startLocalVideo().catch(function(){})}
170
277
  function applyCallMode(){mode=el('call-mode').value==='production'?'production':'test';el('robot-row').style.display=mode==='production'?'grid':'none';el('mode-badge').textContent=mode==='production'?'PRODUCTION ? ROBOT VIDEO + TOOLS':'TEST ? BROWSER ONLY';el('mode-badge').className='mode-badge '+(mode==='production'?'production':'');if(mode==='test')robot='';else robot=el('call-robot').value==='none'?'':el('call-robot').value;text('f-mode',mode);text('f-robot',mode==='production'?(robot||'missing robot'):'not requested');showRobotVideo()}
@@ -199,7 +306,9 @@
199
306
  function stopTimers(){if(retryTimer){clearTimeout(retryTimer);retryTimer=null}if(keepalive){clearInterval(keepalive);keepalive=null}if(poller){clearInterval(poller);poller=null}stopVision();stopMotion()}
200
307
  function scheduleReconnect(){if(!desired||!alwaysOn||ending)return;connecting=false;conversation=null;conversationId=null;var attempt=++retryAttempt,delay=Math.min(30000,Math.round(750*Math.pow(2,Math.min(attempt-1,6))+Math.random()*500));text('state','reconnecting in '+Math.ceil(delay/1000)+'s');stage('connected');el('avatar-wrap').classList.remove('speaking');if(retryTimer)clearTimeout(retryTimer);retryTimer=setTimeout(function(){retryTimer=null;connectAttempt('reconnect')},delay)}
201
308
  function recover(myGeneration,details){if(myGeneration!==generation||ending||!desired)return;var captured=plainDetails(details);captured.last_provider_error=lastProviderError;event('provider_error','warning','ElevenLabs disconnected; automatic recovery is running',captured);closeManaged('provider_disconnected',captured);stopTimers();session=null;eventToken=null;conversation=null;conversationId=null;connecting=false;el('avatar-wrap').classList.remove('live','speaking');el('join').hidden=true;el('end').hidden=false;el('message').disabled=true;el('send').disabled=true;scheduleReconnect()}
202
- function end(reason){if(ending)return;ending=true;desired=false;generation++;stopTimers();var active=conversation,details={manual:true,reason:reason||'visitor'};if(active)try{active.endSession()}catch(_){};if(cameraRoom){cameraRoom.disconnect();cameraRoom=null}if(localVideoStream){localVideoStream.getTracks().forEach(function(track){track.stop()});localVideoStream=null}var robotStream=el('robot-stream');if(robotStream){robotStream.removeAttribute('src');robotStream.hidden=true}el('robot-video').hidden=false;closeManaged(reason||'visitor',details);session=null;eventToken=null;conversation=null;conversationId=null;connecting=false;retryAttempt=0;stage('conversation','done');text('state','manually disconnected - supervisor stopped');el('avatar-wrap').classList.remove('live','speaking');el('join').hidden=false;el('join').disabled=false;el('join').textContent='Start conversation';el('end').hidden=true;el('message').disabled=true;el('send').disabled=true;ending=false}
309
+ function end(reason){if(ending)return;ending=true;desired=false;generation++;stopTimers();var active=conversation,details={manual:true,reason:reason||'visitor'};if(active)try{active.endSession()}catch(_){};if(cameraRoom){cameraRoom.disconnect();cameraRoom=null}if(localVideoStream){localVideoStream.getTracks().forEach(function(track){track.stop()});localVideoStream=null}var robotStream=el('robot-stream');if(robotStream){robotStream.removeAttribute('src');robotStream.hidden=true}el('robot-video').hidden=false;closeManaged(reason||'visitor',details);session=null;eventToken=null;conversation=null;conversationId=null;connecting=false;retryAttempt=0;stage('conversation','done');text('state','manually disconnected - supervisor stopped');el('avatar-wrap').classList.remove('live','speaking');el('join').hidden=false;el('join').disabled=false;el('join').textContent='Start conversation';el('end').hidden=true;el('message').disabled=true;el('send').disabled=true;ending=false;kioskActivityAt=0;// Back to watching. kioskArmedAt already holds off the re-trigger so a
310
+ // visitor still standing there cannot immediately redial.
311
+ if(kioskOn&&!kioskArmedAt)kioskArmedAt=Date.now()+kioskRearm}
203
312
  // This call lives on an ElevenLabs WebSocket and never joins a LiveKit room,
204
313
  // so the control center cannot observe it. Check in on a timer, and close the
205
314
  // session on unload, or the dashboard cannot tell a live call from a closed
@@ -210,7 +319,7 @@
210
319
  try{navigator.sendBeacon('/robopark/api/sessions/'+encodeURIComponent(session)+'/end?reason=page_closed',new Blob([JSON.stringify({details:{page_closed:true},source:'public_join'})],{type:'application/json'}))}catch(_){}
211
320
  });
212
321
  function callPayload(){return {character_preset_id:character,voice_stack_id:stack||null,client_name:'Public character link',engine:'elevenlabs',agent_id:agent,branch_id:branch||null,robot_id:robot||null,join_link_id:share||null}}
213
- function connectAttempt(kind){if(!desired||ending||connecting||conversation)return;connecting=true;var myGeneration=++generation;text('state',kind==='reconnect'?'obtaining fresh voice session':'connecting');stage('signed_session');api('/api/sessions/voice-call',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(callPayload())}).then(function(call){if(myGeneration!==generation||!desired)throw new Error('connection superseded');session=call.session_id;eventToken=call.event_token;text('f-session',session);text('f-agent',call.agent_id);text('f-branch',call.branch_id||'default');text('f-robot',call.robot_id||'operator only');if(call.robot_video_ref&&robot){el('vision').hidden=false;el('robot-stream').src='/fed/media/robots/'+encodeURIComponent(call.robot_video_ref)+'/video_feed';el('robot-stream').hidden=false;el('robot-video').hidden=true}stage('connected');event('signed_session','ok','Fresh signed ElevenLabs session created',{join_link_id:share,robot_id:robot,reconnect_attempt:retryAttempt});return ensureElevenLabsClient().then(function(SDK){return SDK.Conversation.startSession({signedUrl:call.signed_url,connectionType:'websocket',useWakeLock:true,onConnect:function(info){if(myGeneration!==generation)return;conversationId=info.conversationId;lastProviderError=null;retryAttempt=0;text('state','listening');el('error').style.display='none';el('avatar-wrap').classList.add('live');stage('conversation');management('/joined',{conversation_id:conversationId,connection_type:'websocket',engine:'elevenlabs',robot_id:robot,join_link_id:share,source:'public_join'});event('provider_connected','ok','ElevenLabs conversation connected',{conversation_id:conversationId,supervised:true});startVision(myGeneration);motionLastTurn=Date.now();if(mode==='production')startMotion(myGeneration)},onDisconnect:function(info){recover(myGeneration,info||{reason:'provider_disconnected'})},onError:function(message,context){if(myGeneration!==generation)return;lastProviderError={message:String(message||'Provider error'),context:plainDetails(context)};event('provider_error','failed',lastProviderError.message,lastProviderError);text('state','provider error')},onStatusChange:function(info){if(myGeneration===generation)event('provider_status',info&&info.status==='disconnected'?'warning':'ok',info&&info.status||'unknown',{})},onMessage:function(info){if(myGeneration!==generation)return;var message=String(info&&info.message||'').trim(),role=info&&info.role==='user'?'user':'assistant';if(role==='user'&&motionSent[message])return;motionLastTurn=Date.now();append(role,message);if(message){history.push({role:role,text:message});history=history.slice(-20)}saveTurn(role,message)},onModeChange:function(info){if(myGeneration!==generation)return;var speaking=info.mode==='speaking';motionMode=speaking?'speaking':'listening';el('avatar-wrap').classList.toggle('speaking',speaking);text('state',speaking?'speaking':'listening');event('mode','ok',info.mode,{})},onInterruption:function(info){event('interruption','ok','Agent interrupted',info||{})},onMCPToolCall:function(info){event('mcp_tool','running','Tool call',info||{})}})})}).then(function(active){connecting=false;if(myGeneration!==generation||!desired){try{active.endSession()}catch(_){}return}conversation=active;startKeepalive();el('join').hidden=true;el('end').hidden=false;el('message').disabled=false;el('send').disabled=false;pollDetail()}).catch(function(error){connecting=false;if(myGeneration!==generation||!desired)return;var details={message:error&&error.message||String(error),last_provider_error:lastProviderError};event('provider_error','failed','Voice connection attempt failed',details);closeManaged('connection_failed',details);session=null;eventToken=null;conversation=null;conversationId=null;
322
+ function connectAttempt(kind){if(!desired||ending||connecting||conversation)return;connecting=true;var myGeneration=++generation;text('state',kind==='reconnect'?'obtaining fresh voice session':'connecting');stage('signed_session');api('/api/sessions/voice-call',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(callPayload())}).then(function(call){if(myGeneration!==generation||!desired)throw new Error('connection superseded');session=call.session_id;eventToken=call.event_token;text('f-session',session);text('f-agent',call.agent_id);text('f-branch',call.branch_id||'default');text('f-robot',call.robot_id||'operator only');if(call.robot_video_ref&&robot){el('vision').hidden=false;el('robot-stream').src='/fed/media/robots/'+encodeURIComponent(call.robot_video_ref)+'/video_feed';el('robot-stream').hidden=false;el('robot-video').hidden=true}stage('connected');event('signed_session','ok','Fresh signed ElevenLabs session created',{join_link_id:share,robot_id:robot,reconnect_attempt:retryAttempt});return ensureElevenLabsClient().then(function(SDK){return SDK.Conversation.startSession({signedUrl:call.signed_url,connectionType:'websocket',useWakeLock:true,onConnect:function(info){if(myGeneration!==generation)return;conversationId=info.conversationId;lastProviderError=null;retryAttempt=0;text('state','listening');el('error').style.display='none';el('avatar-wrap').classList.add('live');stage('conversation');management('/joined',{conversation_id:conversationId,connection_type:'websocket',engine:'elevenlabs',robot_id:robot,join_link_id:share,source:'public_join'});event('provider_connected','ok','ElevenLabs conversation connected',{conversation_id:conversationId,supervised:true});startVision(myGeneration);motionLastTurn=Date.now();startMotion(myGeneration);kioskNote();if(kioskOn){try{motionSent[kioskGreeting]=1;conversation.sendUserMessage(kioskGreeting)}catch(_){}}},onDisconnect:function(info){recover(myGeneration,info||{reason:'provider_disconnected'})},onError:function(message,context){if(myGeneration!==generation)return;lastProviderError={message:String(message||'Provider error'),context:plainDetails(context)};event('provider_error','failed',lastProviderError.message,lastProviderError);text('state','provider error')},onStatusChange:function(info){if(myGeneration===generation)event('provider_status',info&&info.status==='disconnected'?'warning':'ok',info&&info.status||'unknown',{})},onMessage:function(info){if(myGeneration!==generation)return;var message=String(info&&info.message||'').trim(),role=info&&info.role==='user'?'user':'assistant';if(role==='user'&&motionSent[message])return;motionLastTurn=Date.now();kioskNote();append(role,message);if(message){history.push({role:role,text:message});history=history.slice(-20)}saveTurn(role,message)},onModeChange:function(info){if(myGeneration!==generation)return;var speaking=info.mode==='speaking';motionMode=speaking?'speaking':'listening';if(speaking)kioskNote();el('avatar-wrap').classList.toggle('speaking',speaking);text('state',speaking?'speaking':'listening');event('mode','ok',info.mode,{})},onInterruption:function(info){event('interruption','ok','Agent interrupted',info||{})},onMCPToolCall:function(info){event('mcp_tool','running','Tool call',info||{})}})})}).then(function(active){connecting=false;if(myGeneration!==generation||!desired){try{active.endSession()}catch(_){}return}conversation=active;startKeepalive();el('join').hidden=true;el('end').hidden=false;el('message').disabled=false;el('send').disabled=false;pollDetail()}).catch(function(error){connecting=false;if(myGeneration!==generation||!desired)return;var details={message:error&&error.message||String(error),last_provider_error:lastProviderError};event('provider_error','failed','Voice connection attempt failed',details);closeManaged('connection_failed',details);session=null;eventToken=null;conversation=null;conversationId=null;
214
323
  // A 4xx means the link itself is wrong and retrying cannot fix it. Anything
215
324
  // else (5xx, signing timeout, network drop) is transient, and dead-ending
216
325
  // here is what left a failed call with no reconnect until a manual re-join.
@@ -222,13 +331,18 @@
222
331
  if(!permanent&&alwaysOn&&desired&&!ending){el('join').hidden=true;el('end').hidden=false;scheduleReconnect();return}
223
332
  fail(details.message)})}
224
333
  function start(){if(desired||conversation||connecting)return;if(mode==='production'&&!robot){fail('Select a registered production robot first.');return}alwaysOn=true;el('always-on').classList.add('on');el('always-on').setAttribute('aria-pressed','true');el('always-on').textContent='Always on: ON';if(mode==='test')ensureMicAudioContext();desired=true;retryAttempt=0;el('join').disabled=true;text('state','requesting microphone permission');stage('permission');var audio=inputDeviceId?{deviceId:{exact:inputDeviceId}}:true;navigator.mediaDevices.getUserMedia({audio:audio}).then(function(stream){stream.getTracks().forEach(function(track){track.stop()});return startLocalVideo()}).then(function(){connectAttempt('initial')}).catch(function(error){if(desired){text('state','microphone unavailable; retrying');scheduleReconnect()}else{fail(error&&error.message||String(error))}})}
334
+ el('kiosk').onclick=function(){
335
+ kioskOn=!kioskOn;syncKioskButton();
336
+ if(kioskOn){kioskStart()}
337
+ else{kioskStop();text('state',conversation?'connected':'motion calling off')}
338
+ };
225
339
  el('always-on').onclick=function(){alwaysOn=!alwaysOn;this.classList.toggle('on',alwaysOn);this.setAttribute('aria-pressed',String(alwaysOn));this.textContent='Always on: '+(alwaysOn?'ON':'OFF');if(!alwaysOn&&retryTimer){clearTimeout(retryTimer);retryTimer=null;text('state','disconnected - automatic recovery paused')}else if(alwaysOn&&desired&&!conversation&&!connecting){scheduleReconnect()}};
226
340
  function pollDetail(){applySpeakerVolume()}
227
341
  function deploymentRequest(path,options){var token=q.get('token')||'',suffix=token?(path.indexOf('?')>=0?'&':'?')+'token='+encodeURIComponent(token):'';return fetch('/robopark'+path+suffix,Object.assign({cache:'no-store'},options||{})).then(function(response){if(!response.ok)throw new Error('request '+response.status);return response.json()})}
228
342
  function renderDeployment(data){var token=q.get('token')||'',rows=[];[['voice_vision_boxes','Windows voice + vision',['vision','conversation']],['motor_nodes','Pi motor server',['motor']]].forEach(function(group){var devices=data[group[0]]||[];if(!devices.length){rows.push('<div class="deployment-row"><b>'+group[1]+'</b><span>not registered</span><span></span></div>');return}devices.forEach(function(device){var seen=Date.parse(device.last_heartbeat||''),live=isFinite(seen)&&Date.now()-seen<90000,buttons=token?group[2].map(function(service){return '<button data-device="'+device.id+'" data-service="'+service+'" data-action="start">Start '+service+'</button><button data-device="'+device.id+'" data-service="'+service+'" data-action="restart">Restart</button><button data-device="'+device.id+'" data-service="'+service+'" data-action="stop">Stop</button>'}).join(''):'management token required for controls';rows.push('<div class="deployment-row"><b>'+group[1]+'</b><span class="'+(live?'ready':'')+'">'+String(device.name||device.id)+' / '+(live?'online':'stale')+'</span><div class="deployment-actions">'+buttons+'</div></div>')})});deployment.innerHTML=rows.join('');deployment.querySelectorAll('button[data-action]').forEach(function(button){button.onclick=function(){var action=button.getAttribute('data-action'),service=button.getAttribute('data-service'),device=button.getAttribute('data-device');if((action==='stop'||action==='restart')&&!confirm(action+' '+service+'?'))return;button.disabled=true;deploymentRequest('/api/devices/'+encodeURIComponent(device)+'/services/'+encodeURIComponent(service)+'/'+action,{method:'POST',headers:{'content-type':'application/json'}}).then(function(){button.textContent='queued'}).catch(function(error){button.textContent='failed';fail(error.message)}).then(function(){setTimeout(function(){button.disabled=false},1800)})}})}
229
343
  function loadDeployment(){deploymentRequest('/api/characters/'+encodeURIComponent(character)+'/deployment').then(renderDeployment).catch(function(){deployment.innerHTML='<div class="deployment-row"><b>Character hardware</b><span>deployment status unavailable</span><span></span></div>'})}
230
344
  el('speaker-volume').value=String(speakerVolume);text('speaker-value',speakerVolume+'%');el('mic-sensitivity').value=String(micSensitivity);text('mic-value',micSensitivity+'%');el('speaker-volume').oninput=function(){speakerVolume=Math.max(0,Math.min(100,Number(this.value)||0));text('speaker-value',speakerVolume+'%');persistNumber('robopark.voice.speaker',speakerVolume);applySpeakerVolume()};el('mic-sensitivity').oninput=function(){setMicSensitivity(this.value);text('mic-value',micSensitivity+'%');persistNumber('robopark.voice.micSensitivity',micSensitivity)};
231
- el('ops-toggle').onclick=function(){el('ops').classList.toggle('open');this.classList.toggle('on',el('ops').classList.contains('open'))};el('ops-close').onclick=function(){el('ops').classList.remove('open');el('ops-toggle').classList.remove('on')};el('join').onclick=start;el('end').onclick=function(){end('visitor')};el('compose').onsubmit=function(e){e.preventDefault();var message=el('message').value.trim();if(message&&conversation){conversation.sendUserMessage(message);el('message').value=''}};
345
+ el('ops-toggle').onclick=function(){el('ops').classList.toggle('open');this.classList.toggle('on',el('ops').classList.contains('open'))};el('ops-close').onclick=function(){el('ops').classList.remove('open');el('ops-toggle').classList.remove('on')};el('join').onclick=start;if(q.get('kiosk')==='1'||q.get('kiosk')==='on'){kioskOn=true;syncKioskButton();kioskStart()}el('end').onclick=function(){end('visitor')};el('compose').onsubmit=function(e){e.preventDefault();var message=el('message').value.trim();if(message&&conversation){conversation.sendUserMessage(message);el('message').value=''}};
232
346
  text('f-mode',mode);text('f-character',character);text('f-agent',agent);text('f-branch',branch||'default');text('f-robot',mode==='production'?(robot||'missing robot'):'not requested');text('f-share',share||'untracked');
233
347
  if(character)loadDeployment();
234
348
  if(!character||!agent){fail('This join link is incomplete. Ask the operator for a new character link.');return}if(mode==='production'&&!robot){fail('This production link has no registered robot. Ask the operator for a corrected link.');return}api('/api/character-presets/'+encodeURIComponent(character)).then(function(preset){text('character-name',preset.name||preset.id);text('subtitle',(mode==='test'?'Browser-only test call · ':'Production on-site call · ')+(preset.description||'Secure ElevenLabs conversation managed by RoboPark'));text('f-character',preset.name||preset.id);text('fallback',String(preset.name||preset.id).slice(0,2).toUpperCase());if(preset.img){el('avatar').src=preset.img;el('avatar').onload=function(){el('fallback').style.display='none'}};text('state','ready to connect');el('join').disabled=false}).catch(function(){fail('Character configuration is unavailable.')});