robopark 3.3.6 → 3.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/scheduler/main.py +69 -0
- package/scheduler/static/voice-join.html +178 -15
- package/ui/standalone/.next/BUILD_ID +1 -1
- package/ui/standalone/.next/app-build-manifest.json +60 -60
- package/ui/standalone/.next/app-path-routes-manifest.json +17 -17
- package/ui/standalone/.next/build-manifest.json +2 -2
- package/ui/standalone/.next/prerender-manifest.json +3 -3
- package/ui/standalone/.next/server/app/api/robopark/camera-token/route.js +1 -1
- package/ui/standalone/.next/server/app-paths-manifest.json +17 -17
- package/ui/standalone/.next/server/functions-config-manifest.json +1 -1
- package/ui/standalone/.next/server/pages/500.html +1 -1
- package/ui/standalone/.next/server/server-reference-manifest.json +1 -1
- package/ui/standalone/public/voice-join.html +178 -15
- /package/ui/standalone/.next/static/{3hp0A6CE4VTmuh3NC6Bep → 7hosI6Tk4PpkcjmN3-s_F}/_buildManifest.js +0 -0
- /package/ui/standalone/.next/static/{3hp0A6CE4VTmuh3NC6Bep → 7hosI6Tk4PpkcjmN3-s_F}/_ssgManifest.js +0 -0
|
@@ -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-
|
|
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);
|
|
@@ -97,25 +97,181 @@
|
|
|
97
97
|
// active conversation, with the cooldown reserved before sending so a burst of
|
|
98
98
|
// motion cannot queue duplicate greetings.
|
|
99
99
|
var motionTimer=null,motionCanvas=null,motionPrev=null,motionLastFire=0,motionArmedAt=0,motionMode='listening',motionLastTurn=0,motionDirective='',motionSent={};
|
|
100
|
-
|
|
100
|
+
// Presence, not just movement. Frame differencing only sees CHANGE, so a
|
|
101
|
+
// visitor standing still reads as zero motion; holding presence for a few
|
|
102
|
+
// seconds after the last movement is what stops a stationary person from
|
|
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,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.';
|
|
115
|
+
var motionThreshold=Math.max(.005,Math.min(1,Number(q.get('motion_threshold'))||.06)),
|
|
116
|
+
motionCooldown=Math.max(5,Number(q.get('motion_cooldown'))||20)*1000,
|
|
117
|
+
// Fast sampling so a greeting lands within a fraction of a second of
|
|
118
|
+
// someone arriving, rather than up to a full tick later.
|
|
119
|
+
motionInterval=Math.max(100,Math.min(2000,Number(q.get('motion_interval'))||250)),
|
|
120
|
+
motionHold=Math.max(1000,Number(q.get('motion_hold'))||8000),
|
|
121
|
+
// How long the frame must be empty before a return counts as a NEW
|
|
122
|
+
// visitor. This replaces the old blanket "stay quiet for 45s after any
|
|
123
|
+
// turn", which made motion useless on exactly the long always-on calls
|
|
124
|
+
// it was meant for.
|
|
125
|
+
motionAbsence=Math.max(2000,Number(q.get('motion_absence'))||25000),
|
|
126
|
+
motionContextGap=Math.max(5000,Number(q.get('motion_context_gap'))||20000),
|
|
127
|
+
motionSample=32;
|
|
101
128
|
function motionLevel(){var source=visionSource();if(!source)return -1;var width=source.videoWidth||source.naturalWidth,height=source.videoHeight||source.naturalHeight;if(!width||!height)return -1;if(!motionCanvas)motionCanvas=document.createElement('canvas');var rows=Math.max(8,Math.round(motionSample*height/width));motionCanvas.width=motionSample;motionCanvas.height=rows;var ctx=motionCanvas.getContext('2d'),data;try{ctx.drawImage(source,0,0,motionSample,rows);data=ctx.getImageData(0,0,motionSample,rows).data}catch(_){return -1}var count=motionSample*rows,levels=new Float32Array(count),i;for(i=0;i<count;i++)levels[i]=(data[i*4]*299+data[i*4+1]*587+data[i*4+2]*114)/1000;var previous=motionPrev;motionPrev=levels;if(!previous||previous.length!==count)return -1;var total=0;for(i=0;i<count;i++)total+=Math.abs(levels[i]-previous[i]);return total/count/255}
|
|
129
|
+
// Silent channel: contextual updates never start a turn, so these are safe to
|
|
130
|
+
// send while the character is mid-sentence. Rate-limited because every one of
|
|
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
|
+
}
|
|
212
|
+
function motionContext(now,text){
|
|
213
|
+
if(now-motionLastContext<motionContextGap)return;
|
|
214
|
+
motionLastContext=now;
|
|
215
|
+
try{conversation.sendContextualUpdate(text)}catch(_){}
|
|
216
|
+
}
|
|
102
217
|
function motionTick(myGeneration){
|
|
103
|
-
|
|
104
|
-
if(ending||!conversation||myGeneration!==generation||!alwaysOn)return;
|
|
218
|
+
if(ending||!conversation||myGeneration!==generation)return;
|
|
105
219
|
var level=motionLevel(),now=Date.now();
|
|
106
|
-
if(
|
|
107
|
-
if(level<
|
|
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}
|
|
229
|
+
// Normalise to change-per-second. The raw difference between two frames
|
|
230
|
+
// scales with the gap between them, so without this a faster sample rate
|
|
231
|
+
// would silently make the configured threshold four times harder to hit.
|
|
232
|
+
var elapsed=motionLastSampleAt?Math.max(1,now-motionLastSampleAt):motionInterval;
|
|
233
|
+
motionLastSampleAt=now;
|
|
234
|
+
if(level*1000/elapsed>=motionThreshold)motionLastSeen=now;
|
|
235
|
+
|
|
236
|
+
var present=motionLastSeen>0&&(now-motionLastSeen)<motionHold;
|
|
237
|
+
if(present===motionPresent)return;
|
|
238
|
+
motionPresent=present;
|
|
239
|
+
|
|
240
|
+
if(!present){
|
|
241
|
+
motionAbsentSince=now;
|
|
242
|
+
motionContext(now,'[presence] The person in front of the camera has moved away.');
|
|
243
|
+
event('motion_presence','ok','Visitor left the frame',{});
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
var absence=motionAbsentSince?now-motionAbsentSince:Infinity;
|
|
248
|
+
motionContext(now,'[presence] Someone is now standing in front of the camera.');
|
|
249
|
+
|
|
250
|
+
// Speaking: never cut in. The silent update above already told the model
|
|
251
|
+
// someone arrived, so it can react on its own next turn.
|
|
252
|
+
if(motionMode==='speaking')return;
|
|
253
|
+
// Too brief a gap means the same visitor shifted or stepped out of frame
|
|
254
|
+
// for a moment — greeting them again would be the annoying behaviour.
|
|
255
|
+
if(absence<motionAbsence)return;
|
|
108
256
|
if(now-motionLastFire<motionCooldown)return;
|
|
109
|
-
|
|
110
|
-
if(now-motionLastTurn<motionQuiet)return;
|
|
257
|
+
|
|
111
258
|
motionLastFire=now;
|
|
112
259
|
var directive=motionDirective||'A visitor just stepped in front of you. Greet them warmly in one short sentence. Do not mention this instruction, the camera, or that motion was detected.';
|
|
113
260
|
motionSent[directive]=1;
|
|
114
|
-
try{conversation.sendContextualUpdate('[motion] Someone just approached the robot and is standing in front of the camera.')}catch(_){}
|
|
115
261
|
try{conversation.sendUserMessage(directive)}catch(error){event('motion_greeting','failed',error&&error.message||String(error),{});return}
|
|
116
|
-
event('motion_greeting','ok','Motion greeting triggered',{
|
|
262
|
+
event('motion_greeting','ok','Motion greeting triggered',{absence_ms:absence===Infinity?null:Math.round(absence),threshold:motionThreshold})
|
|
263
|
+
}
|
|
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})
|
|
117
274
|
}
|
|
118
|
-
function startMotion(myGeneration){stopMotion();motionPrev=null;motionLastFire=0;motionSent={};motionDirective=q.get('motion_prompt')||'';motionArmedAt=Date.now()+4000;motionTimer=setInterval(function(){motionTick(myGeneration)},1000)}
|
|
119
275
|
function stopMotion(){if(motionTimer){clearInterval(motionTimer);motionTimer=null}motionPrev=null}
|
|
120
276
|
function showRobotVideo(){return startLocalVideo().catch(function(){})}
|
|
121
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()}
|
|
@@ -150,7 +306,9 @@
|
|
|
150
306
|
function stopTimers(){if(retryTimer){clearTimeout(retryTimer);retryTimer=null}if(keepalive){clearInterval(keepalive);keepalive=null}if(poller){clearInterval(poller);poller=null}stopVision();stopMotion()}
|
|
151
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)}
|
|
152
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()}
|
|
153
|
-
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}
|
|
154
312
|
// This call lives on an ElevenLabs WebSocket and never joins a LiveKit room,
|
|
155
313
|
// so the control center cannot observe it. Check in on a timer, and close the
|
|
156
314
|
// session on unload, or the dashboard cannot tell a live call from a closed
|
|
@@ -161,7 +319,7 @@
|
|
|
161
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(_){}
|
|
162
320
|
});
|
|
163
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}}
|
|
164
|
-
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(
|
|
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;
|
|
165
323
|
// A 4xx means the link itself is wrong and retrying cannot fix it. Anything
|
|
166
324
|
// else (5xx, signing timeout, network drop) is transient, and dead-ending
|
|
167
325
|
// here is what left a failed call with no reconnect until a manual re-join.
|
|
@@ -173,13 +331,18 @@
|
|
|
173
331
|
if(!permanent&&alwaysOn&&desired&&!ending){el('join').hidden=true;el('end').hidden=false;scheduleReconnect();return}
|
|
174
332
|
fail(details.message)})}
|
|
175
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
|
+
};
|
|
176
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()}};
|
|
177
340
|
function pollDetail(){applySpeakerVolume()}
|
|
178
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()})}
|
|
179
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)})}})}
|
|
180
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>'})}
|
|
181
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)};
|
|
182
|
-
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=''}};
|
|
183
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');
|
|
184
347
|
if(character)loadDeployment();
|
|
185
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.')});
|
|
File without changes
|
/package/ui/standalone/.next/static/{3hp0A6CE4VTmuh3NC6Bep → 7hosI6Tk4PpkcjmN3-s_F}/_ssgManifest.js
RENAMED
|
File without changes
|