infinicode 2.8.66 → 2.8.68
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.
|
@@ -529,8 +529,34 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
529
529
|
.rp-modal-body .kv{display:grid;grid-template-columns:160px 1fr;gap:0.4rem 0.8rem;margin-bottom:0.4rem;}
|
|
530
530
|
.rp-modal-body .kv .k{color:var(--muted);font-size:0.66rem;text-transform:uppercase;letter-spacing:0.05em;}
|
|
531
531
|
.rp-modal-body .kv .v{color:var(--ink);word-break:break-word;white-space:pre-wrap;}
|
|
532
|
-
.rp-modal-body pre{background:var(--panel-solid);border:1px solid var(--glass-border);border-radius:8px;padding:0.5rem 0.7rem;
|
|
533
|
-
font-size:0.66rem;max-height:240px;overflow:auto;white-space:pre-wrap;word-break:break-word;color:var(--ink);}
|
|
532
|
+
.rp-modal-body pre{background:var(--panel-solid);border:1px solid var(--glass-border);border-radius:8px;padding:0.5rem 0.7rem;
|
|
533
|
+
font-size:0.66rem;max-height:240px;overflow:auto;white-space:pre-wrap;word-break:break-word;color:var(--ink);}
|
|
534
|
+
|
|
535
|
+
.rp-call-shell{display:grid;grid-template-columns:minmax(260px,0.8fr) minmax(320px,1.2fr);gap:0.8rem;min-height:500px;}
|
|
536
|
+
.rp-call-panel{border:1px solid var(--glass-border);border-radius:14px;background:var(--panel);box-shadow:var(--shadow),inset 0 1px 0 var(--glass-hi);padding:0.85rem;}
|
|
537
|
+
.rp-call-character{display:flex;align-items:center;gap:0.8rem;padding:0.7rem;border:1px solid var(--glass-border);border-radius:12px;background:var(--panel-2);margin-bottom:0.75rem;}
|
|
538
|
+
.rp-call-character img{width:78px;height:78px;object-fit:contain;filter:drop-shadow(0 8px 16px rgba(0,0,0,.35));}
|
|
539
|
+
.rp-call-character .name{font-size:1.05rem;color:var(--gold-bright);font-weight:700;}
|
|
540
|
+
.rp-call-state{display:flex;align-items:center;gap:0.55rem;font-family:var(--mono);font-size:0.72rem;padding:0.65rem 0;}
|
|
541
|
+
.rp-call-orb{width:10px;height:10px;border-radius:50%;background:var(--muted);box-shadow:0 0 0 transparent;}
|
|
542
|
+
.rp-call-orb.live{background:var(--ok);box-shadow:0 0 12px var(--ok);animation:pulse 1.6s infinite;}
|
|
543
|
+
.rp-call-orb.busy{background:var(--gold-bright);box-shadow:0 0 12px var(--gold-bright);animation:pulse 1s infinite;}
|
|
544
|
+
.rp-call-actions{display:flex;gap:0.45rem;flex-wrap:wrap;margin-top:0.7rem;}
|
|
545
|
+
.rp-call-transcript{height:430px;overflow-y:auto;display:flex;flex-direction:column;gap:0.55rem;padding:0.3rem;}
|
|
546
|
+
.rp-turn{max-width:86%;border:1px solid var(--glass-border);border-radius:12px;padding:0.55rem 0.7rem;font-family:var(--mono);font-size:0.72rem;line-height:1.45;background:var(--panel-2);}
|
|
547
|
+
.rp-turn.user{align-self:flex-end;border-color:color-mix(in srgb,var(--info) 45%,transparent);background:color-mix(in srgb,var(--info) 9%,var(--panel-2));}
|
|
548
|
+
.rp-turn.assistant{align-self:flex-start;border-color:color-mix(in srgb,var(--gold-bright) 42%,transparent);}
|
|
549
|
+
.rp-turn .who{font-size:0.57rem;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);margin-bottom:.18rem;}
|
|
550
|
+
.rp-detail-hero{display:flex;align-items:center;gap:.8rem;padding:.6rem;border:1px solid var(--glass-border);border-radius:12px;background:var(--panel-2);margin-bottom:.8rem;}
|
|
551
|
+
.rp-detail-hero img{width:72px;height:72px;object-fit:contain;}
|
|
552
|
+
.rp-pipeline{display:flex;flex-direction:column;gap:.3rem;margin-top:.5rem;}
|
|
553
|
+
.rp-pipeline-row{display:grid;grid-template-columns:76px 125px 70px 1fr;gap:.5rem;padding:.38rem .5rem;border-left:3px solid var(--glass-border);background:var(--panel-2);border-radius:5px;font-family:var(--mono);font-size:.64rem;}
|
|
554
|
+
.rp-pipeline-row.ok{border-left-color:var(--ok)} .rp-pipeline-row.failed,.rp-pipeline-row.blocked{border-left-color:var(--bad)}
|
|
555
|
+
.node .node-hero{display:flex;align-items:center;gap:.65rem;margin:.45rem 0 .55rem;}
|
|
556
|
+
.node .node-avatar{width:68px;height:68px;object-fit:contain;flex:none;filter:drop-shadow(0 7px 12px rgba(0,0,0,.3));}
|
|
557
|
+
.node .node-health{font-family:var(--mono);font-size:.61rem;line-height:1.45;color:var(--muted);}
|
|
558
|
+
.node .node-health b{color:var(--ink);}
|
|
559
|
+
@media(max-width:820px){.rp-call-shell{grid-template-columns:1fr}.rp-call-transcript{height:320px}.rp-pipeline-row{grid-template-columns:62px 105px 58px 1fr}}
|
|
534
560
|
|
|
535
561
|
/* ── TTS voice picker (used inside the generic modal) ── */
|
|
536
562
|
.rp-voice-picker{display:flex;flex-direction:column;gap:0.6rem;min-height:320px;}
|
|
@@ -658,7 +684,8 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
658
684
|
<div class="rp-subtabs" id="rp-subtabs" role="tablist">
|
|
659
685
|
<button class="rp-subtab active" data-sub="overview" id="rpsbtn-overview" role="tab" aria-selected="true">Overview</button>
|
|
660
686
|
<button class="rp-subtab" data-sub="incidents" id="rpsbtn-incidents" role="tab" aria-selected="false">Incidents <span class="badge" id="rps-incidents-badge">0</span></button>
|
|
661
|
-
<button class="rp-subtab" data-sub="live" id="rpsbtn-live" role="tab" aria-selected="false">Live Sessions <span class="badge" id="rps-live-badge">0</span></button>
|
|
687
|
+
<button class="rp-subtab" data-sub="live" id="rpsbtn-live" role="tab" aria-selected="false">Live Sessions <span class="badge" id="rps-live-badge">0</span></button>
|
|
688
|
+
<button class="rp-subtab" data-sub="voice" id="rpsbtn-voice" role="tab" aria-selected="false">Voice Call</button>
|
|
662
689
|
<button class="rp-subtab" data-sub="history" id="rpsbtn-history" role="tab" aria-selected="false">History</button>
|
|
663
690
|
<button class="rp-subtab" data-sub="audit" id="rpsbtn-audit" role="tab" aria-selected="false">Audit Log</button>
|
|
664
691
|
<button class="rp-subtab" data-sub="fleet" id="rpsbtn-fleet" role="tab" aria-selected="false">Fleet</button>
|
|
@@ -700,7 +727,30 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
700
727
|
</div>
|
|
701
728
|
|
|
702
729
|
<!-- ═══ History ═══ -->
|
|
703
|
-
<div class="rp-sub" id="rp-sub-
|
|
730
|
+
<div class="rp-sub" id="rp-sub-voice">
|
|
731
|
+
<div class="rp-call-shell">
|
|
732
|
+
<div class="rp-call-panel">
|
|
733
|
+
<div class="rp-section-h">Operator voice call <span class="meta">RoboVoice pipeline</span></div>
|
|
734
|
+
<div class="rp-call-character" id="rp-call-character"><div><div class="name">Choose a character</div><div class="meta">The selected preset and stack are frozen into session history.</div></div></div>
|
|
735
|
+
<div class="rp-field"><label>Character preset</label><select id="rp-call-character-select"><option value="">select character...</option></select></div>
|
|
736
|
+
<div class="rp-field" style="margin-top:.55rem"><label>Voice stack</label><select id="rp-call-stack-select"><option value="">character default</option></select></div>
|
|
737
|
+
<div class="rp-call-state"><span class="rp-call-orb" id="rp-call-orb"></span><span id="rp-call-status">ready to start</span></div>
|
|
738
|
+
<div class="rp-call-actions">
|
|
739
|
+
<button class="rp-btn primary" id="rp-call-start">Start voice call</button>
|
|
740
|
+
<button class="rp-btn" id="rp-call-mute" disabled>Mute microphone</button>
|
|
741
|
+
<button class="rp-btn danger" id="rp-call-end" disabled>End call</button>
|
|
742
|
+
</div>
|
|
743
|
+
<div class="pk-note" style="margin-top:.7rem">Uses your browser microphone. Agent audio plays here and every final transcript turn is saved to session history.</div>
|
|
744
|
+
</div>
|
|
745
|
+
<div class="rp-call-panel">
|
|
746
|
+
<div class="rp-section-h">Live transcript <span class="meta" id="rp-call-session">not connected</span></div>
|
|
747
|
+
<div class="rp-call-transcript" id="rp-call-transcript"><div class="rp-tbl-empty">Transcript appears when the call starts.</div></div>
|
|
748
|
+
<div id="rp-call-audio" hidden></div>
|
|
749
|
+
</div>
|
|
750
|
+
</div>
|
|
751
|
+
</div>
|
|
752
|
+
|
|
753
|
+
<div class="rp-sub" id="rp-sub-history">
|
|
704
754
|
<div class="rp-filterbar">
|
|
705
755
|
<div class="rp-field"><label>Search</label><input id="rp-hist-search" placeholder="session id, robot…"></div>
|
|
706
756
|
<div class="rp-field"><label>Robot</label><select id="rp-hist-robot"><option value="">all robots</option></select></div>
|
|
@@ -724,7 +774,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
724
774
|
<div class="rp-tbl-wrap" id="rp-hist-wrap">
|
|
725
775
|
<table class="rp-history-table" id="rp-hist-table">
|
|
726
776
|
<thead><tr>
|
|
727
|
-
<th>Session</th><th>Robot</th><th>Started</th><th>Ended</th><th>Duration</th><th>End reason</th><th>
|
|
777
|
+
<th>Session</th><th>Robot</th><th>Started</th><th>Ended</th><th>Duration</th><th>End reason</th><th>Turns</th><th>Pipeline</th><th>Latency</th><th>Actions</th>
|
|
728
778
|
</tr></thead>
|
|
729
779
|
<tbody id="rp-hist-tbody"></tbody>
|
|
730
780
|
</table>
|
|
@@ -1142,7 +1192,34 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
1142
1192
|
+ '<div class="bar-track"><div class="bar-fill '+loadClass(v)+'" style="width:'+v+'%"></div></div>'
|
|
1143
1193
|
+ '<span class="mv '+(cls||'')+'">'+esc(valTxt)+'</span></div>';
|
|
1144
1194
|
}
|
|
1145
|
-
function
|
|
1195
|
+
function rpAvatarFor(label){
|
|
1196
|
+
var key=String(label||'').toLowerCase();
|
|
1197
|
+
var presets=(typeof characterPresets!=='undefined' && Array.isArray(characterPresets))?characterPresets:[];
|
|
1198
|
+
for(var i=0;i<presets.length;i++){
|
|
1199
|
+
var p=presets[i], aliases=[p.id,p.name];
|
|
1200
|
+
for(var j=0;j<aliases.length;j++){
|
|
1201
|
+
var a=String(aliases[j]||'').toLowerCase();
|
|
1202
|
+
if(a && (key===a || key.indexOf(a)>=0 || a.indexOf(key)>=0)) return p.img||'';
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
if(key.indexOf('bmw')>=0 || key.indexOf('vixen')>=0) return '/static/robopark/bmw.png';
|
|
1206
|
+
var known=['volt','tesla','jaguar','panda'];
|
|
1207
|
+
for(var k=0;k<known.length;k++) if(key.indexOf(known[k])>=0) return '/static/robopark/'+known[k]+'.png';
|
|
1208
|
+
return '';
|
|
1209
|
+
}
|
|
1210
|
+
function rpTelemetryForNode(n){
|
|
1211
|
+
if(typeof roboparkByName==='undefined' || !roboparkByName) return null;
|
|
1212
|
+
var aliases=[n&&n.displayName,n&&n.name,n&&n.nodeId];
|
|
1213
|
+
for(var i=0;i<aliases.length;i++){
|
|
1214
|
+
var key=String(aliases[i]||'').toLowerCase();
|
|
1215
|
+
if(roboparkByName[key]) return roboparkByName[key];
|
|
1216
|
+
for(var existing in roboparkByName){
|
|
1217
|
+
if(existing===key || (key && existing.indexOf(key)>=0) || (existing && key.indexOf(existing)>=0)) return roboparkByName[existing];
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
return null;
|
|
1221
|
+
}
|
|
1222
|
+
function renderFleet(nodes){
|
|
1146
1223
|
var el = $('fleet');
|
|
1147
1224
|
if(!nodes || !nodes.length){ el.innerHTML = '<div class="empty">No nodes reporting yet. Start peers with <span class="mono">infinicode serve</span> or <span class="mono">robopark setup</span>.</div>'; return; }
|
|
1148
1225
|
// connected first, then robots/satellites above hubs so the fleet reads robot-first
|
|
@@ -1163,13 +1240,19 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
1163
1240
|
var disk = (hw&&hw.disk&&hw.disk.mounts&&hw.disk.mounts[0]) ? hw.disk.mounts[0].percent : null;
|
|
1164
1241
|
var nt = netRxTx(hw);
|
|
1165
1242
|
var upt = hw&&hw.system ? hw.system.uptimeSec : null;
|
|
1166
|
-
var caps = (n.capabilities||[]).filter(function(c){ return c.indexOf('backend:')!==0; }).slice(0,5);
|
|
1167
|
-
var capHtml=''; for(var c=0;c<caps.length;c++) capHtml += '<span class="cap">'+esc(caps[c])+'</span>';
|
|
1243
|
+
var caps = (n.capabilities||[]).filter(function(c){ return c.indexOf('backend:')!==0; }).slice(0,5);
|
|
1244
|
+
var capHtml=''; for(var c=0;c<caps.length;c++) capHtml += '<span class="cap">'+esc(caps[c])+'</span>';
|
|
1245
|
+
var tele=rpTelemetryForNode(n);
|
|
1246
|
+
var avatar=rpAvatarFor(tele&&(tele.character_name||tele.character_id)||n.displayName);
|
|
1247
|
+
var readiness=tele&&(tele.readiness_code||tele.status)||(!up?'offline':'mesh online');
|
|
1248
|
+
var hb=tele&&tele.heartbeat_age_seconds!=null?tele.heartbeat_age_seconds:null;
|
|
1168
1249
|
var site = n.site?(' · '+esc(n.site)):'';
|
|
1169
1250
|
|
|
1170
1251
|
html += '<div class="node '+(up?'up':'down')+(selected===n.nodeId?' sel':'')+'" tabindex="0" role="button" data-id="'+esc(n.nodeId)+'" data-name="'+esc(n.displayName)+'">'
|
|
1171
1252
|
+ '<div class="top"><span class="name"><span class="statdot '+(up?'on':'off')+'"></span>'+esc(n.displayName)+(isSelf?' <span class="selfbadge">this</span>':'')+'</span><span class="role">'+esc(n.role)+'</span></div>'
|
|
1172
|
-
+ '<div class="meta">'+esc(n.platform||'')+' '+esc((hw&&hw.cpu&&hw.cpu.model)?hw.cpu.model.replace(/\\s+/g,' ').slice(0,22):'')+site+' <span class="mono">v'+esc(n.version||'?')+'</span></div>'
|
|
1253
|
+
+ '<div class="meta">'+esc(n.platform||'')+' '+esc((hw&&hw.cpu&&hw.cpu.model)?hw.cpu.model.replace(/\\s+/g,' ').slice(0,22):'')+site+' <span class="mono">v'+esc(n.version||'?')+'</span></div>'
|
|
1254
|
+
+ (avatar?('<div class="node-hero"><img class="node-avatar" alt="'+esc(n.displayName)+'" src="'+esc(avatar+QS)+'"><div class="node-health"><b>'+esc(String(readiness).replace(/_/g,' '))+'</b><br>'
|
|
1255
|
+
+(tele?('sessions '+esc(tele.sessions_total||0)+' / triggers '+esc(tele.trigger_count||0)+'<br>latency '+esc(tele.avg_latency_ms!=null?Math.round(tele.avg_latency_ms)+'ms':'n/a')+' / drops '+esc(tele.drop_rate!=null?Math.round(tele.drop_rate*100)+'%':'n/a')+'<br>heartbeat '+esc(hb!=null?hb+'s ago':'unavailable')):'scheduler telemetry pending')+'</div></div>'):'');
|
|
1173
1256
|
|
|
1174
1257
|
if(up){
|
|
1175
1258
|
html += mrow('CPU', cpu, cpu+'%'+(cores?(' /'+cores+'c'):''), loadClass(cpu))
|
|
@@ -1610,12 +1693,12 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
1610
1693
|
});
|
|
1611
1694
|
|
|
1612
1695
|
var characterPresets=[];
|
|
1613
|
-
function loadCharacterPresets(){
|
|
1614
|
-
fetch('/robopark/api/character-presets'+QS,{cache:'no-store'}).then(function(r){return r.json();}).then(function(d){ characterPresets=Array.isArray(d)?d:(d&&d.presets)||[]; fillCharacterDropdowns(); fillTemplateCharacterList(); refreshDrawerPresetOptions(); }).catch(function(){});
|
|
1615
|
-
}
|
|
1616
|
-
function loadVoiceStacks(){
|
|
1617
|
-
fetch('/robopark/api/voice-stacks'+QS,{cache:'no-store'}).then(function(r){return r.json();}).then(function(d){ voiceStacks=Array.isArray(d)?d:(d&&d.stacks)||[]; fillVoiceStackDropdowns(); fillTemplateVoiceList(); refreshDrawerPresetOptions(); }).catch(function(){});
|
|
1618
|
-
}
|
|
1696
|
+
function loadCharacterPresets(){
|
|
1697
|
+
return fetch('/robopark/api/character-presets'+QS,{cache:'no-store'}).then(function(r){return r.json();}).then(function(d){ characterPresets=Array.isArray(d)?d:(d&&d.presets)||[]; fillCharacterDropdowns(); fillTemplateCharacterList(); refreshDrawerPresetOptions(); refreshVoiceCallOptions(); return characterPresets; }).catch(function(){ return []; });
|
|
1698
|
+
}
|
|
1699
|
+
function loadVoiceStacks(){
|
|
1700
|
+
return fetch('/robopark/api/voice-stacks'+QS,{cache:'no-store'}).then(function(r){return r.json();}).then(function(d){ voiceStacks=Array.isArray(d)?d:(d&&d.stacks)||[]; fillVoiceStackDropdowns(); fillTemplateVoiceList(); refreshDrawerPresetOptions(); refreshVoiceCallOptions(); return voiceStacks; }).catch(function(){ return []; });
|
|
1701
|
+
}
|
|
1619
1702
|
|
|
1620
1703
|
// ── TTS voice catalog (per provider) ──
|
|
1621
1704
|
// Holds the dropdown options for whichever TTS provider is currently
|
|
@@ -2080,7 +2163,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
2080
2163
|
for (var i=0;i<robots.length;i++){
|
|
2081
2164
|
var x = robots[i];
|
|
2082
2165
|
var name = robotName(x);
|
|
2083
|
-
var avatar =
|
|
2166
|
+
var avatar = rpAvatarFor(x.character_name||x.character_id||x.character_preset_id||name);
|
|
2084
2167
|
var lat = x.avg_latency_ms!=null?Math.round(x.avg_latency_ms)+'ms':'—';
|
|
2085
2168
|
var dcls = x.drop_rate!=null?(x.drop_rate>=0.2?'bad':(x.drop_rate>=0.05?'warn':'ok')):'';
|
|
2086
2169
|
var drop = x.drop_rate!=null?Math.round(x.drop_rate*100)+'%':'—';
|
|
@@ -2192,14 +2275,15 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
2192
2275
|
s.classList.toggle('active', s.id==='rp-sub-'+name);
|
|
2193
2276
|
}
|
|
2194
2277
|
// stop every sub-tab interval, then start only the active one
|
|
2195
|
-
var names = ['overview','incidents','live','history','audit','fleet','setup'];
|
|
2278
|
+
var names = ['overview','incidents','live','voice','history','audit','fleet','setup'];
|
|
2196
2279
|
for(var k=0;k<names.length;k++){
|
|
2197
2280
|
var n=names[k];
|
|
2198
2281
|
if(rpIntervals[n]){ clearInterval(rpIntervals[n]); rpIntervals[n]=null; }
|
|
2199
2282
|
}
|
|
2200
2283
|
if(name==='overview'){ pollOverview(); rpIntervals.overview = setInterval(pollOverview, 4000); }
|
|
2201
2284
|
if(name==='incidents'){ pollIncidents(); rpIntervals.incidents = setInterval(pollIncidents, 4000); }
|
|
2202
|
-
if(name==='live'){ pollLiveSessions(); startLiveAutoRefresh(); }
|
|
2285
|
+
if(name==='live'){ pollLiveSessions(); startLiveAutoRefresh(); }
|
|
2286
|
+
if(name==='voice'){ refreshVoiceCallOptions(); }
|
|
2203
2287
|
if(name==='history'){ rpHistoryPage=0; pollHistory(); }
|
|
2204
2288
|
if(name==='audit'){ pollAuditLog(); rpIntervals.audit = setInterval(pollAuditLog, 6000); }
|
|
2205
2289
|
if(name==='fleet'){ pollFleet(); rpIntervals.fleet = setInterval(pollFleet, 5000); }
|
|
@@ -2218,7 +2302,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
2218
2302
|
var h=String(location.hash||'').replace('#','');
|
|
2219
2303
|
var m = h.match(/^robopark\\/(.+)$/);
|
|
2220
2304
|
var initial = (m && m[1]) ? m[1] : 'overview';
|
|
2221
|
-
if(['overview','incidents','live','history','audit','fleet','setup'].indexOf(initial)<0) initial='overview';
|
|
2305
|
+
if(['overview','incidents','live','voice','history','audit','fleet','setup'].indexOf(initial)<0) initial='overview';
|
|
2222
2306
|
setRpSubTab(initial);
|
|
2223
2307
|
}
|
|
2224
2308
|
|
|
@@ -2513,7 +2597,111 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
2513
2597
|
})();
|
|
2514
2598
|
|
|
2515
2599
|
// ── History sub-tab ──
|
|
2516
|
-
var
|
|
2600
|
+
var rpVoiceCall={room:null,session:null,eventToken:null,muted:false,ending:false,seen:{}};
|
|
2601
|
+
function refreshVoiceCallOptions(){
|
|
2602
|
+
var cs=$('rp-call-character-select'), ss=$('rp-call-stack-select');
|
|
2603
|
+
if(!cs||!ss) return;
|
|
2604
|
+
var cv=cs.value, sv=ss.value;
|
|
2605
|
+
cs.innerHTML='<option value="">select character...</option>'+(characterPresets||[]).map(function(c){return '<option value="'+esc(c.id)+'">'+esc(c.name||c.id)+'</option>';}).join('');
|
|
2606
|
+
ss.innerHTML='<option value="">character default</option>'+(voiceStacks||[]).map(function(s){return '<option value="'+esc(s.id)+'">'+esc(s.name||s.id)+' - '+esc(s.stt_provider)+' / '+esc(s.llm_model)+' / '+esc(s.tts_provider)+'</option>';}).join('');
|
|
2607
|
+
if(cv) cs.value=cv; if(sv) ss.value=sv;
|
|
2608
|
+
paintVoiceCallCharacter();
|
|
2609
|
+
}
|
|
2610
|
+
function paintVoiceCallCharacter(){
|
|
2611
|
+
var sel=$('rp-call-character-select'), box=$('rp-call-character'); if(!sel||!box) return;
|
|
2612
|
+
var chosen=null; for(var i=0;i<characterPresets.length;i++) if(characterPresets[i].id===sel.value){chosen=characterPresets[i];break;}
|
|
2613
|
+
if(!chosen){ box.innerHTML='<div><div class="name">Choose a character</div><div class="meta">The selected preset and stack are frozen into session history.</div></div>'; return; }
|
|
2614
|
+
var img=chosen.img||rpAvatarFor(chosen.id||chosen.name);
|
|
2615
|
+
box.innerHTML=(img?'<img alt="'+esc(chosen.name)+'" src="'+esc(img+QS)+'">':'')+'<div><div class="name">'+esc(chosen.name||chosen.id)+'</div><div class="meta">'+esc(chosen.description||'Ready for an operator voice call')+'</div></div>';
|
|
2616
|
+
var stack=$('rp-call-stack-select'); if(stack && !stack.value && chosen.voice_stack_id) stack.value=chosen.voice_stack_id;
|
|
2617
|
+
}
|
|
2618
|
+
function setVoiceCallStatus(text,state){
|
|
2619
|
+
var s=$('rp-call-status'), o=$('rp-call-orb'); if(s)s.textContent=text; if(o)o.className='rp-call-orb '+(state||'');
|
|
2620
|
+
}
|
|
2621
|
+
function ensureVoiceLiveKit(){
|
|
2622
|
+
if(window.LivekitClient) return Promise.resolve(window.LivekitClient);
|
|
2623
|
+
return new Promise(function(resolve,reject){
|
|
2624
|
+
var old=document.querySelector('script[data-rp-voice-livekit]');
|
|
2625
|
+
if(old){ old.addEventListener('load',function(){resolve(window.LivekitClient);}); old.addEventListener('error',reject); return; }
|
|
2626
|
+
var script=document.createElement('script'); script.dataset.rpVoiceLivekit='1'; script.src='/vendor/livekit-client.umd.js'+QS;
|
|
2627
|
+
script.onload=function(){resolve(window.LivekitClient);}; script.onerror=function(){reject(new Error('LiveKit browser library failed to load'));}; document.head.appendChild(script);
|
|
2628
|
+
});
|
|
2629
|
+
}
|
|
2630
|
+
function appendVoiceTurn(role,text){
|
|
2631
|
+
var wrap=$('rp-call-transcript'); if(!wrap)return;
|
|
2632
|
+
if(wrap.querySelector('.rp-tbl-empty')) wrap.innerHTML='';
|
|
2633
|
+
var div=document.createElement('div'); div.className='rp-turn '+role;
|
|
2634
|
+
div.innerHTML='<div class="who">'+esc(role==='user'?'operator':'character')+'</div><div>'+esc(text)+'</div>';
|
|
2635
|
+
wrap.appendChild(div); wrap.scrollTop=wrap.scrollHeight;
|
|
2636
|
+
}
|
|
2637
|
+
function saveVoiceTurn(role,text,sequence){
|
|
2638
|
+
if(!rpVoiceCall.session||!rpVoiceCall.eventToken)return;
|
|
2639
|
+
fetch('/robopark/api/sessions/'+encodeURIComponent(rpVoiceCall.session)+'/transcript'+QS,{
|
|
2640
|
+
method:'POST',headers:Object.assign({'content-type':'application/json','X-RoboPark-Session-Token':rpVoiceCall.eventToken},operatorHeaders()),
|
|
2641
|
+
body:JSON.stringify({role:role,text:text,is_final:true,sequence:sequence,source:'control_center'})
|
|
2642
|
+
}).catch(function(){});
|
|
2643
|
+
}
|
|
2644
|
+
function startVoiceCall(){
|
|
2645
|
+
var cid=$('rp-call-character-select').value, sid=$('rp-call-stack-select').value;
|
|
2646
|
+
if(!cid){setVoiceCallStatus('select a character first','');return;}
|
|
2647
|
+
if(rpVoiceCall.room)return;
|
|
2648
|
+
setVoiceCallStatus('creating scheduled session...','busy'); $('rp-call-start').disabled=true;
|
|
2649
|
+
rpAction('POST','/robopark/api/sessions/voice-call',{character_preset_id:cid,voice_stack_id:sid||null,client_name:getOperatorName()||'RoboPark Operator'})
|
|
2650
|
+
.then(function(call){
|
|
2651
|
+
if(!call)throw new Error('scheduler returned no call');
|
|
2652
|
+
rpVoiceCall.session=call.session_id; rpVoiceCall.eventToken=call.event_token; rpVoiceCall.seen={}; rpVoiceCall.ending=false;
|
|
2653
|
+
$('rp-call-session').textContent=call.session_id; $('rp-call-transcript').innerHTML='<div class="rp-tbl-empty">Connecting to '+esc(call.character&&call.character.name||cid)+'...</div>';
|
|
2654
|
+
return ensureVoiceLiveKit().then(function(LK){
|
|
2655
|
+
var room=new LK.Room({adaptiveStream:true,dynacast:true}); rpVoiceCall.room=room;
|
|
2656
|
+
room.on(LK.RoomEvent.TrackSubscribed,function(track){ if(track.kind==='audio'){var el=track.attach();el.autoplay=true;$('rp-call-audio').appendChild(el);} });
|
|
2657
|
+
room.on(LK.RoomEvent.TranscriptionReceived,function(segments,participant){
|
|
2658
|
+
for(var i=0;i<(segments||[]).length;i++){
|
|
2659
|
+
var seg=segments[i], final=seg.final===true||seg.isFinal===true; if(!final||!String(seg.text||'').trim())continue;
|
|
2660
|
+
var key=String(seg.id||'')+'|'+String(seg.text); if(rpVoiceCall.seen[key])continue; rpVoiceCall.seen[key]=1;
|
|
2661
|
+
var identity=participant&&participant.identity||''; var role=identity.indexOf('operator:')===0?'user':'assistant';
|
|
2662
|
+
appendVoiceTurn(role,String(seg.text)); saveVoiceTurn(role,String(seg.text),Object.keys(rpVoiceCall.seen).length);
|
|
2663
|
+
}
|
|
2664
|
+
});
|
|
2665
|
+
room.on(LK.RoomEvent.DataReceived,function(payload,participant,kind,topic){
|
|
2666
|
+
if(topic && topic!=='lk-chat-topic')return;
|
|
2667
|
+
try{
|
|
2668
|
+
var msg=JSON.parse(new TextDecoder().decode(payload));
|
|
2669
|
+
var text=String(msg.message||msg.text||'').trim(); if(!text)return;
|
|
2670
|
+
var role=msg.role==='user'?'user':'assistant';
|
|
2671
|
+
var key=role+'|'+text; if(rpVoiceCall.seen[key])return; rpVoiceCall.seen[key]=1;
|
|
2672
|
+
appendVoiceTurn(role,text); saveVoiceTurn(role,text,Object.keys(rpVoiceCall.seen).length);
|
|
2673
|
+
}catch(_){}
|
|
2674
|
+
});
|
|
2675
|
+
room.on(LK.RoomEvent.Disconnected,function(){ if(!rpVoiceCall.ending) finishVoiceCall('connection_lost'); });
|
|
2676
|
+
return room.connect(call.server_url,call.token).then(function(){return room.localParticipant.setMicrophoneEnabled(true);});
|
|
2677
|
+
}).then(function(){
|
|
2678
|
+
setVoiceCallStatus('connected - listening','live'); $('rp-call-mute').disabled=false; $('rp-call-end').disabled=false;
|
|
2679
|
+
return fetch('/robopark/api/sessions/'+encodeURIComponent(rpVoiceCall.session)+'/joined'+QS,{method:'POST',headers:{'X-RoboPark-Session-Token':rpVoiceCall.eventToken}}).catch(function(){});
|
|
2680
|
+
});
|
|
2681
|
+
}).catch(function(e){
|
|
2682
|
+
setVoiceCallStatus('call failed: '+(e&&e.message||e),''); $('rp-call-start').disabled=false;
|
|
2683
|
+
if(rpVoiceCall.room){try{rpVoiceCall.room.disconnect();}catch(_){} rpVoiceCall.room=null;}
|
|
2684
|
+
});
|
|
2685
|
+
}
|
|
2686
|
+
function toggleVoiceMute(){
|
|
2687
|
+
if(!rpVoiceCall.room)return; rpVoiceCall.muted=!rpVoiceCall.muted;
|
|
2688
|
+
rpVoiceCall.room.localParticipant.setMicrophoneEnabled(!rpVoiceCall.muted);
|
|
2689
|
+
$('rp-call-mute').textContent=rpVoiceCall.muted?'Unmute microphone':'Mute microphone'; setVoiceCallStatus(rpVoiceCall.muted?'connected - microphone muted':'connected - listening','live');
|
|
2690
|
+
}
|
|
2691
|
+
function finishVoiceCall(reason){
|
|
2692
|
+
if(rpVoiceCall.ending)return; rpVoiceCall.ending=true; var session=rpVoiceCall.session, room=rpVoiceCall.room;
|
|
2693
|
+
rpVoiceCall.room=null; if(room){try{room.disconnect();}catch(_){}}
|
|
2694
|
+
if(session) rpAction('POST','/robopark/api/sessions/'+encodeURIComponent(session)+'/end?reason='+encodeURIComponent(reason||'operator'));
|
|
2695
|
+
setVoiceCallStatus('call ended',''); $('rp-call-start').disabled=false; $('rp-call-mute').disabled=true; $('rp-call-end').disabled=true;
|
|
2696
|
+
$('rp-call-mute').textContent='Mute microphone'; rpVoiceCall.session=null; rpVoiceCall.eventToken=null; rpVoiceCall.muted=false; setTimeout(function(){rpVoiceCall.ending=false;},300);
|
|
2697
|
+
}
|
|
2698
|
+
(function(){
|
|
2699
|
+
var cs=$('rp-call-character-select'), start=$('rp-call-start'), mute=$('rp-call-mute'), end=$('rp-call-end');
|
|
2700
|
+
if(cs)cs.addEventListener('change',paintVoiceCallCharacter); if(start)start.addEventListener('click',startVoiceCall);
|
|
2701
|
+
if(mute)mute.addEventListener('click',toggleVoiceMute); if(end)end.addEventListener('click',function(){finishVoiceCall('operator');});
|
|
2702
|
+
})();
|
|
2703
|
+
|
|
2704
|
+
var rpHistoryFilter = { robot: '', endReason: '', range: '7d', search: '' };
|
|
2517
2705
|
var rpHistoryPage = 0;
|
|
2518
2706
|
var RP_HISTORY_PAGE_SIZE = 25;
|
|
2519
2707
|
function pollHistory(){
|
|
@@ -2587,7 +2775,8 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
2587
2775
|
var dur = s.duration_seconds!=null ? s.duration_seconds : (started&&ended?Math.floor((ended-started)/1000):null);
|
|
2588
2776
|
var reason = s.end_reason || '';
|
|
2589
2777
|
var reasonCls = rpEndReasonClass(reason);
|
|
2590
|
-
var
|
|
2778
|
+
var turns = s.transcript_count!=null ? s.transcript_count : 0;
|
|
2779
|
+
var pipe = s.error_count ? (s.error_count+' errors') : ((s.pipeline_event_count||0)+' events');
|
|
2591
2780
|
var lat = s.avg_latency_ms!=null ? Math.round(s.avg_latency_ms)+'ms' : '—';
|
|
2592
2781
|
html += '<tr data-sid="'+esc(sid)+'">'
|
|
2593
2782
|
+ '<td><span class="sid" title="'+esc(sid)+'">'+esc(sid)+'</span></td>'
|
|
@@ -2596,7 +2785,8 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
2596
2785
|
+ '<td><span class="t-rel">'+esc(rpFmtRelative(ended))+'</span><span class="t-abs">'+esc(rpFmtAbs(ended))+'</span></td>'
|
|
2597
2786
|
+ '<td>'+esc(rpFmtDuration(dur))+'</td>'
|
|
2598
2787
|
+ '<td><span class="rp-end-badge '+(reasonCls||'')+'">'+esc(reason||'—')+'</span></td>'
|
|
2599
|
-
+ '<td>'+esc(
|
|
2788
|
+
+ '<td>'+esc(turns)+'</td>'
|
|
2789
|
+
+ '<td class="'+(s.error_count?'bad':'ok')+'">'+esc(pipe)+'</td>'
|
|
2600
2790
|
+ '<td>'+esc(lat)+'</td>'
|
|
2601
2791
|
+ '<td><button class="rp-btn" data-act="view" data-sid="'+esc(sid)+'">View</button></td>'
|
|
2602
2792
|
+ '</tr>';
|
|
@@ -2607,29 +2797,27 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
2607
2797
|
if(prev) prev.disabled = rpHistoryPage<=0;
|
|
2608
2798
|
if(next) next.disabled = rpHistoryPage>=totalPages-1;
|
|
2609
2799
|
}
|
|
2610
|
-
function
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
var html = ''
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
html += row('Room', s.room_name);
|
|
2617
|
-
html += row('Started', rpFmtAbs(s.started_at)
|
|
2618
|
-
html += row('
|
|
2619
|
-
html += row('
|
|
2620
|
-
html +=
|
|
2621
|
-
|
|
2622
|
-
html +=
|
|
2623
|
-
html +=
|
|
2624
|
-
html +=
|
|
2625
|
-
html
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
if(s.
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
showModal('Session '+(s.session_id||''), html);
|
|
2632
|
-
}
|
|
2800
|
+
function renderSessionDetail(s){
|
|
2801
|
+
function row(k,v){ return '<div class="kv"><div class="k">'+esc(k)+'</div><div class="v">'+esc(v==null?'—':v)+'</div></div>'; }
|
|
2802
|
+
var avatar=s.character_img||rpAvatarFor(s.character_id||s.character_name||s.robot_id), summary=s.summary||{};
|
|
2803
|
+
var html='<div class="rp-detail-hero">'+(avatar?'<img alt="'+esc(s.character_name||s.robot_id)+'" src="'+esc(avatar+QS)+'">':'')
|
|
2804
|
+
+'<div><div style="font-size:1rem;color:var(--gold-bright);font-weight:700">'+esc(s.character_name||s.robot_id||'Session')+'</div>'
|
|
2805
|
+
+'<div class="meta">'+esc(summary.turns||0)+' turns · '+esc(summary.pipeline_errors||0)+' pipeline errors</div></div></div>';
|
|
2806
|
+
html += row('Session ID', s.session_id)+row('Robot / source', s.robot_id)+row('Room', s.room_name);
|
|
2807
|
+
html += row('Started', rpFmtAbs(s.started_at)+' ('+rpFmtRelative(s.started_at)+')')+row('Ended',rpFmtAbs(s.ended_at)+' ('+rpFmtRelative(s.ended_at)+')');
|
|
2808
|
+
html += row('Duration',rpFmtDuration(s.duration_seconds))+row('End reason',s.end_reason||'active')+row('Join latency',s.latency_ms!=null?s.latency_ms+'ms':'—');
|
|
2809
|
+
html += row('Operator',s.initiated_by||'robot motion')+row('Character',s.character_name||s.character_id||'—')+row('Voice stack',s.voice_stack_name||s.voice_stack_id||'—');
|
|
2810
|
+
html += '<div class="rp-section-h" style="margin-top:1rem">Transcript <span class="meta">'+esc(summary.user_turns||0)+' user / '+esc(summary.assistant_turns||0)+' assistant</span></div>';
|
|
2811
|
+
var turns=(s.transcript||[]).filter(function(t){return t.is_final;});
|
|
2812
|
+
html += '<div class="rp-call-transcript" style="height:auto;max-height:320px">'+(turns.length?turns.map(function(t){return '<div class="rp-turn '+esc(t.role)+'"><div class="who">'+esc(t.speaker||t.role)+' · '+esc(clock(t.timestamp))+'</div><div>'+esc(t.text)+'</div></div>';}).join(''):'<div class="rp-tbl-empty">No transcript was recorded for this session.</div>')+'</div>';
|
|
2813
|
+
html += '<div class="rp-section-h" style="margin-top:1rem">Pipeline timeline <span class="meta">'+esc((s.pipeline_events||[]).length)+' events</span></div>';
|
|
2814
|
+
html += '<div class="rp-pipeline">'+((s.pipeline_events||[]).length?s.pipeline_events.map(function(e){return '<div class="rp-pipeline-row '+esc(e.status)+'"><span>'+esc(clock(e.timestamp))+'</span><b>'+esc(e.stage)+'</b><span>'+esc(e.status)+'</span><span>'+esc(e.message||'')+'</span></div>';}).join(''):'<div class="rp-tbl-empty">No pipeline events were recorded.</div>')+'</div>';
|
|
2815
|
+
return html;
|
|
2816
|
+
}
|
|
2817
|
+
function openSessionDetailModal(s){
|
|
2818
|
+
if(!s)return; showModal('Session '+(s.session_id||''),'<div class="rp-tbl-empty">Loading transcript and pipeline timeline...</div>');
|
|
2819
|
+
getJson('/robopark/api/sessions/'+encodeURIComponent(s.session_id)+'/detail').then(function(detail){$('rp-modal-body').innerHTML=renderSessionDetail(detail);}).catch(function(e){$('rp-modal-body').innerHTML='<div class="rp-tbl-empty">Session detail unavailable: '+esc(e&&e.message||e)+'</div>';});
|
|
2820
|
+
}
|
|
2633
2821
|
(function(){
|
|
2634
2822
|
var s=$('rp-hist-search'); if(s) s.addEventListener('input', function(){ rpHistoryFilter.search = String(s.value||'').toLowerCase(); rpHistoryPage=0; renderHistory(); });
|
|
2635
2823
|
var r=$('rp-hist-robot'); if(r) r.addEventListener('change', function(){ rpHistoryFilter.robot = r.value||''; rpHistoryPage=0; renderHistory(); });
|
|
@@ -3174,7 +3362,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
3174
3362
|
for(var i=0;i<top3.length;i++){
|
|
3175
3363
|
var x = top3[i];
|
|
3176
3364
|
var name = x.name || x.id || 'robot';
|
|
3177
|
-
var avatar =
|
|
3365
|
+
var avatar = rpAvatarFor(x.character_name||x.character_id||name);
|
|
3178
3366
|
thtml += '<div class="rp-card" style="padding:0.55rem 0.7rem;">'
|
|
3179
3367
|
+ '<div class="rp-card-top">'
|
|
3180
3368
|
+ (avatar?'<div class="rp-card-img"><img alt="'+esc(name)+'" src="'+esc(avatar+QS)+'"></div>':'')
|
|
@@ -5490,7 +5678,9 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
5490
5678
|
// same Windows camera twice and make both feeds unreliable.
|
|
5491
5679
|
function tryVisionOverlay(n, force){
|
|
5492
5680
|
var directImg=$('pk-vision-img'); if(!directImg) return;
|
|
5493
|
-
|
|
5681
|
+
// Names stay stable when a robot is re-enrolled and receives a new mesh
|
|
5682
|
+
// node id. The hub resolves the name to the currently connected peer.
|
|
5683
|
+
var directRef=n.displayName||n.name||n.nodeId||n.id;
|
|
5494
5684
|
if(!directRef){ visionOn=false; directImg.src=''; directImg.style.display='none'; return; }
|
|
5495
5685
|
var directCamNote=$('pk-camnote'), directLiveNote=$('pk-golivenote'), directRes=$('pk-camres');
|
|
5496
5686
|
visionOn=true;
|
|
@@ -37,6 +37,18 @@ export class PeerMesh {
|
|
|
37
37
|
manifest,
|
|
38
38
|
version: manifest.version,
|
|
39
39
|
};
|
|
40
|
+
const endpoint = url.replace(/\/+$/, '').toLowerCase();
|
|
41
|
+
const name = manifest.displayName.trim().toLowerCase();
|
|
42
|
+
for (const existing of this.peers.values()) {
|
|
43
|
+
if (existing.nodeId === peer.nodeId)
|
|
44
|
+
continue;
|
|
45
|
+
const sameEndpoint = existing.url.replace(/\/+$/, '').toLowerCase() === endpoint;
|
|
46
|
+
const sameNamedDevice = existing.role === manifest.role && existing.displayName.trim().toLowerCase() === name;
|
|
47
|
+
if (!sameEndpoint && !sameNamedDevice)
|
|
48
|
+
continue;
|
|
49
|
+
this.opts.logger.warn(`[federation] replacing stale peer ${existing.displayName} (${existing.nodeId}) with ${peer.nodeId}`);
|
|
50
|
+
this.remove(existing.nodeId);
|
|
51
|
+
}
|
|
40
52
|
this.peers.set(peer.nodeId, peer);
|
|
41
53
|
this.openStream(peer);
|
|
42
54
|
// Immediate heartbeat so the peer learns us (nodeId + port) right away —
|
package/package.json
CHANGED