Spaces:
Running
Running
Update ER_MAP/dashboard.py
Browse filesAdded the dashboard we had in demo video to hf space UI for judges to interact previously it had a static html page
- ER_MAP/dashboard.py +85 -0
ER_MAP/dashboard.py
CHANGED
|
@@ -1070,6 +1070,8 @@ HTML_PAGE = r"""<!DOCTYPE html>
|
|
| 1070 |
const [outcome, setOutcome] = useState(null);
|
| 1071 |
const [persona, setPersona] = useState(null);
|
| 1072 |
const [modelSource, setModelSource] = useState(null);
|
|
|
|
|
|
|
| 1073 |
|
| 1074 |
// refs (audio + loop control β never trigger re-render)
|
| 1075 |
const audioQueueRef = useRef([]);
|
|
@@ -1077,6 +1079,11 @@ HTML_PAGE = r"""<!DOCTYPE html>
|
|
| 1077 |
const renderedCountRef = useRef(0);
|
| 1078 |
const stopRef = useRef(false);
|
| 1079 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1080 |
// ---------------- Audio queue (drives card activation) ----------------
|
| 1081 |
const processQueue = useCallback(async () => {
|
| 1082 |
if (isPlayingRef.current || audioQueueRef.current.length === 0) return;
|
|
@@ -1089,6 +1096,21 @@ HTML_PAGE = r"""<!DOCTYPE html>
|
|
| 1089 |
body: JSON.stringify({ text: item.text, agent: item.agent }),
|
| 1090 |
});
|
| 1091 |
if (!res.ok) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1092 |
isPlayingRef.current = false;
|
| 1093 |
processQueue();
|
| 1094 |
return;
|
|
@@ -1096,6 +1118,7 @@ HTML_PAGE = r"""<!DOCTYPE html>
|
|
| 1096 |
const blob = await res.blob();
|
| 1097 |
const url = URL.createObjectURL(blob);
|
| 1098 |
const audio = new Audio(url);
|
|
|
|
| 1099 |
|
| 1100 |
let finished = false;
|
| 1101 |
const finish = () => {
|
|
@@ -1117,6 +1140,21 @@ HTML_PAGE = r"""<!DOCTYPE html>
|
|
| 1117 |
playPromise.catch(() => { clearTimeout(watchdog); finish(); });
|
| 1118 |
}
|
| 1119 |
} catch (e) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1120 |
isPlayingRef.current = false;
|
| 1121 |
processQueue();
|
| 1122 |
}
|
|
@@ -1149,6 +1187,7 @@ HTML_PAGE = r"""<!DOCTYPE html>
|
|
| 1149 |
setTotalReward(0);
|
| 1150 |
setPhasesDone([]);
|
| 1151 |
setCurrentPhase(null);
|
|
|
|
| 1152 |
setStepCount(0);
|
| 1153 |
renderedCountRef.current = 0;
|
| 1154 |
audioQueueRef.current = [];
|
|
@@ -1213,6 +1252,10 @@ HTML_PAGE = r"""<!DOCTYPE html>
|
|
| 1213 |
if (stepData.phases_done) setPhasesDone(stepData.phases_done);
|
| 1214 |
if (stepData.current_phase !== undefined) setCurrentPhase(stepData.current_phase);
|
| 1215 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1216 |
if (stepData.done) {
|
| 1217 |
await waitForAudioDrained();
|
| 1218 |
break;
|
|
@@ -1368,6 +1411,48 @@ HTML_PAGE = r"""<!DOCTYPE html>
|
|
| 1368 |
</div>
|
| 1369 |
</div>
|
| 1370 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1371 |
</div>
|
| 1372 |
</div>
|
| 1373 |
|
|
|
|
| 1070 |
const [outcome, setOutcome] = useState(null);
|
| 1071 |
const [persona, setPersona] = useState(null);
|
| 1072 |
const [modelSource, setModelSource] = useState(null);
|
| 1073 |
+
const [conversationLog, setConversationLog] = useState([]);
|
| 1074 |
+
const logEndRef = useRef(null);
|
| 1075 |
|
| 1076 |
// refs (audio + loop control β never trigger re-render)
|
| 1077 |
const audioQueueRef = useRef([]);
|
|
|
|
| 1079 |
const renderedCountRef = useRef(0);
|
| 1080 |
const stopRef = useRef(false);
|
| 1081 |
|
| 1082 |
+
// Auto-scroll conversation log
|
| 1083 |
+
useEffect(() => {
|
| 1084 |
+
if (logEndRef.current) logEndRef.current.scrollIntoView({ behavior: 'smooth' });
|
| 1085 |
+
}, [conversationLog]);
|
| 1086 |
+
|
| 1087 |
// ---------------- Audio queue (drives card activation) ----------------
|
| 1088 |
const processQueue = useCallback(async () => {
|
| 1089 |
if (isPlayingRef.current || audioQueueRef.current.length === 0) return;
|
|
|
|
| 1096 |
body: JSON.stringify({ text: item.text, agent: item.agent }),
|
| 1097 |
});
|
| 1098 |
if (!res.ok) {
|
| 1099 |
+
// ElevenLabs unavailable β use browser speech
|
| 1100 |
+
try {
|
| 1101 |
+
const synth = window.speechSynthesis;
|
| 1102 |
+
if (synth) {
|
| 1103 |
+
const utter = new SpeechSynthesisUtterance(item.text);
|
| 1104 |
+
utter.rate = 1.05;
|
| 1105 |
+
utter.pitch = item.agent === 'patient' ? 0.9 : item.agent === 'nurse' ? 1.1 : 1.0;
|
| 1106 |
+
utter.volume = 1.0;
|
| 1107 |
+
setActiveAgent(item.agent);
|
| 1108 |
+
utter.onend = () => { setActiveAgent(null); isPlayingRef.current = false; processQueue(); };
|
| 1109 |
+
utter.onerror = () => { setActiveAgent(null); isPlayingRef.current = false; processQueue(); };
|
| 1110 |
+
synth.speak(utter);
|
| 1111 |
+
return;
|
| 1112 |
+
}
|
| 1113 |
+
} catch (_) {}
|
| 1114 |
isPlayingRef.current = false;
|
| 1115 |
processQueue();
|
| 1116 |
return;
|
|
|
|
| 1118 |
const blob = await res.blob();
|
| 1119 |
const url = URL.createObjectURL(blob);
|
| 1120 |
const audio = new Audio(url);
|
| 1121 |
+
audio.volume = 1.0;
|
| 1122 |
|
| 1123 |
let finished = false;
|
| 1124 |
const finish = () => {
|
|
|
|
| 1140 |
playPromise.catch(() => { clearTimeout(watchdog); finish(); });
|
| 1141 |
}
|
| 1142 |
} catch (e) {
|
| 1143 |
+
// ElevenLabs TTS failed β use browser Web Speech API as fallback
|
| 1144 |
+
try {
|
| 1145 |
+
const synth = window.speechSynthesis;
|
| 1146 |
+
if (synth) {
|
| 1147 |
+
const utter = new SpeechSynthesisUtterance(item.text);
|
| 1148 |
+
utter.rate = 1.05;
|
| 1149 |
+
utter.pitch = item.agent === 'patient' ? 0.9 : item.agent === 'nurse' ? 1.1 : 1.0;
|
| 1150 |
+
utter.volume = 1.0;
|
| 1151 |
+
setActiveAgent(item.agent);
|
| 1152 |
+
utter.onend = () => { setActiveAgent(null); isPlayingRef.current = false; processQueue(); };
|
| 1153 |
+
utter.onerror = () => { setActiveAgent(null); isPlayingRef.current = false; processQueue(); };
|
| 1154 |
+
synth.speak(utter);
|
| 1155 |
+
return;
|
| 1156 |
+
}
|
| 1157 |
+
} catch (_) {}
|
| 1158 |
isPlayingRef.current = false;
|
| 1159 |
processQueue();
|
| 1160 |
}
|
|
|
|
| 1187 |
setTotalReward(0);
|
| 1188 |
setPhasesDone([]);
|
| 1189 |
setCurrentPhase(null);
|
| 1190 |
+
setConversationLog([]);
|
| 1191 |
setStepCount(0);
|
| 1192 |
renderedCountRef.current = 0;
|
| 1193 |
audioQueueRef.current = [];
|
|
|
|
| 1252 |
if (stepData.phases_done) setPhasesDone(stepData.phases_done);
|
| 1253 |
if (stepData.current_phase !== undefined) setCurrentPhase(stepData.current_phase);
|
| 1254 |
|
| 1255 |
+
// Update conversation log
|
| 1256 |
+
const allMsgs = stepData.conversation || [];
|
| 1257 |
+
setConversationLog(allMsgs);
|
| 1258 |
+
|
| 1259 |
if (stepData.done) {
|
| 1260 |
await waitForAudioDrained();
|
| 1261 |
break;
|
|
|
|
| 1411 |
</div>
|
| 1412 |
</div>
|
| 1413 |
)}
|
| 1414 |
+
|
| 1415 |
+
{/* Section D: Live Conversation Log */}
|
| 1416 |
+
<div className="border-t border-slate-800/50 pt-6 pb-4">
|
| 1417 |
+
<div className="flex items-center justify-between mb-3">
|
| 1418 |
+
<h3 className="text-xs font-semibold text-slate-300 uppercase tracking-wider">Agent Conversation</h3>
|
| 1419 |
+
<span className="text-[10px] text-slate-500 font-mono">{conversationLog.length} msgs</span>
|
| 1420 |
+
</div>
|
| 1421 |
+
<div className="bg-slate-950/40 rounded-xl border border-slate-800/50 p-3 max-h-[280px] overflow-y-auto custom-scrollbar shadow-inner space-y-2">
|
| 1422 |
+
{conversationLog.length === 0 && (
|
| 1423 |
+
<div className="text-slate-600 italic text-[11px] text-center py-4">Start a case to see the conversation...</div>
|
| 1424 |
+
)}
|
| 1425 |
+
{conversationLog.map((m, i) => {
|
| 1426 |
+
const colors = {
|
| 1427 |
+
doctor: { bg: 'bg-indigo-500/10', border: 'border-indigo-500/20', name: 'text-indigo-400', icon: 'π©Ί' },
|
| 1428 |
+
nurse: { bg: 'bg-blue-500/10', border: 'border-blue-500/20', name: 'text-blue-400', icon: 'π©ββοΈ' },
|
| 1429 |
+
patient: { bg: 'bg-amber-500/10', border: 'border-amber-500/20', name: 'text-amber-400', icon: 'π₯' },
|
| 1430 |
+
system: { bg: 'bg-slate-800/30', border: 'border-slate-700/30', name: 'text-slate-400', icon: 'βοΈ' },
|
| 1431 |
+
};
|
| 1432 |
+
const c = colors[m.agent] || colors.system;
|
| 1433 |
+
return (
|
| 1434 |
+
<div key={i} className={`${c.bg} border ${c.border} rounded-lg p-2`}>
|
| 1435 |
+
<div className="flex items-center gap-1.5 mb-1">
|
| 1436 |
+
<span className="text-[10px]">{c.icon}</span>
|
| 1437 |
+
<span className={`text-[10px] font-bold uppercase tracking-wider ${c.name}`}>{m.agent}</span>
|
| 1438 |
+
{m.type && m.type !== 'speak_to' && (
|
| 1439 |
+
<span className="text-[8px] bg-slate-800/50 text-slate-500 px-1.5 py-0.5 rounded-full font-mono">{m.type}</span>
|
| 1440 |
+
)}
|
| 1441 |
+
{m.target && m.target !== m.agent && (
|
| 1442 |
+
<span className="text-[8px] text-slate-600">β {m.target}</span>
|
| 1443 |
+
)}
|
| 1444 |
+
</div>
|
| 1445 |
+
{m.thought && (
|
| 1446 |
+
<div className="text-[9px] text-slate-500 italic mb-1 pl-4 border-l border-slate-700/30">π {m.thought}</div>
|
| 1447 |
+
)}
|
| 1448 |
+
<div className="text-[10px] text-slate-300 leading-relaxed pl-4">{m.message || '(no message)'}</div>
|
| 1449 |
+
</div>
|
| 1450 |
+
);
|
| 1451 |
+
})}
|
| 1452 |
+
<div ref={logEndRef} />
|
| 1453 |
+
</div>
|
| 1454 |
+
</div>
|
| 1455 |
+
|
| 1456 |
</div>
|
| 1457 |
</div>
|
| 1458 |
|