Spaces:
Running
Running
Abdelrahman Almatrooshi commited on
Commit ·
5627c54
1
Parent(s): 7b53d75
Fix FocusPageLocal.jsx syntax errors from merge
Browse files- Add missing closing brace on onSessionEnd callback
- Remove duplicate timeline/controls/frame-rate sections
- src/components/FocusPageLocal.jsx +21 -297
src/components/FocusPageLocal.jsx
CHANGED
|
@@ -37,122 +37,7 @@ function CameraIcon() {
|
|
| 37 |
);
|
| 38 |
}
|
| 39 |
|
| 40 |
-
function
|
| 41 |
-
const canvasRef = useRef(null);
|
| 42 |
-
const screenAspect = typeof window !== 'undefined'
|
| 43 |
-
? window.screen.width / window.screen.height
|
| 44 |
-
: 16 / 9;
|
| 45 |
-
|
| 46 |
-
const MAP_H = 100;
|
| 47 |
-
const MAP_W = Math.round(MAP_H * screenAspect);
|
| 48 |
-
|
| 49 |
-
useEffect(() => {
|
| 50 |
-
const cvs = canvasRef.current;
|
| 51 |
-
if (!cvs) return;
|
| 52 |
-
const ctx = cvs.getContext('2d');
|
| 53 |
-
const w = cvs.width;
|
| 54 |
-
const h = cvs.height;
|
| 55 |
-
|
| 56 |
-
ctx.clearRect(0, 0, w, h);
|
| 57 |
-
|
| 58 |
-
// Screen background
|
| 59 |
-
ctx.fillStyle = 'rgba(20, 20, 30, 0.85)';
|
| 60 |
-
ctx.fillRect(0, 0, w, h);
|
| 61 |
-
|
| 62 |
-
// Screen border
|
| 63 |
-
ctx.strokeStyle = 'rgba(255,255,255,0.25)';
|
| 64 |
-
ctx.lineWidth = 1;
|
| 65 |
-
ctx.strokeRect(0.5, 0.5, w - 1, h - 1);
|
| 66 |
-
|
| 67 |
-
// Grid lines
|
| 68 |
-
ctx.strokeStyle = 'rgba(255,255,255,0.07)';
|
| 69 |
-
ctx.lineWidth = 0.5;
|
| 70 |
-
for (let i = 1; i < 4; i++) {
|
| 71 |
-
ctx.beginPath();
|
| 72 |
-
ctx.moveTo((w * i) / 4, 0);
|
| 73 |
-
ctx.lineTo((w * i) / 4, h);
|
| 74 |
-
ctx.stroke();
|
| 75 |
-
}
|
| 76 |
-
for (let i = 1; i < 3; i++) {
|
| 77 |
-
ctx.beginPath();
|
| 78 |
-
ctx.moveTo(0, (h * i) / 3);
|
| 79 |
-
ctx.lineTo(w, (h * i) / 3);
|
| 80 |
-
ctx.stroke();
|
| 81 |
-
}
|
| 82 |
-
|
| 83 |
-
// Center crosshair
|
| 84 |
-
const cx = w / 2;
|
| 85 |
-
const cy = h / 2;
|
| 86 |
-
ctx.strokeStyle = 'rgba(255,255,255,0.15)';
|
| 87 |
-
ctx.lineWidth = 1;
|
| 88 |
-
ctx.beginPath();
|
| 89 |
-
ctx.moveTo(cx - 6, cy);
|
| 90 |
-
ctx.lineTo(cx + 6, cy);
|
| 91 |
-
ctx.moveTo(cx, cy - 6);
|
| 92 |
-
ctx.lineTo(cx, cy + 6);
|
| 93 |
-
ctx.stroke();
|
| 94 |
-
|
| 95 |
-
if (!gazeData || gazeData.gaze_x == null || gazeData.gaze_y == null) {
|
| 96 |
-
// No data label
|
| 97 |
-
ctx.fillStyle = 'rgba(255,255,255,0.3)';
|
| 98 |
-
ctx.font = '10px Arial';
|
| 99 |
-
ctx.textAlign = 'center';
|
| 100 |
-
ctx.fillText('No gaze data', cx, cy + 3);
|
| 101 |
-
ctx.textAlign = 'left';
|
| 102 |
-
return;
|
| 103 |
-
}
|
| 104 |
-
|
| 105 |
-
const gx = gazeData.gaze_x;
|
| 106 |
-
const gy = gazeData.gaze_y;
|
| 107 |
-
const onScreen = gazeData.on_screen;
|
| 108 |
-
|
| 109 |
-
// Draw gaze dot
|
| 110 |
-
const dotX = gx * w;
|
| 111 |
-
const dotY = gy * h;
|
| 112 |
-
|
| 113 |
-
// Trail / glow
|
| 114 |
-
const gradient = ctx.createRadialGradient(dotX, dotY, 0, dotX, dotY, 14);
|
| 115 |
-
gradient.addColorStop(0, onScreen ? 'rgba(74, 222, 128, 0.5)' : 'rgba(248, 113, 113, 0.5)');
|
| 116 |
-
gradient.addColorStop(1, 'rgba(0,0,0,0)');
|
| 117 |
-
ctx.fillStyle = gradient;
|
| 118 |
-
ctx.fillRect(dotX - 14, dotY - 14, 28, 28);
|
| 119 |
-
|
| 120 |
-
// Dot
|
| 121 |
-
ctx.beginPath();
|
| 122 |
-
ctx.arc(dotX, dotY, 5, 0, 2 * Math.PI);
|
| 123 |
-
ctx.fillStyle = onScreen ? '#4ade80' : '#f87171';
|
| 124 |
-
ctx.fill();
|
| 125 |
-
ctx.strokeStyle = '#fff';
|
| 126 |
-
ctx.lineWidth = 1.5;
|
| 127 |
-
ctx.stroke();
|
| 128 |
-
|
| 129 |
-
// Label
|
| 130 |
-
ctx.fillStyle = 'rgba(255,255,255,0.5)';
|
| 131 |
-
ctx.font = '9px Arial';
|
| 132 |
-
ctx.textAlign = 'right';
|
| 133 |
-
ctx.fillText(
|
| 134 |
-
`${(gx * 100).toFixed(0)}%, ${(gy * 100).toFixed(0)}%`,
|
| 135 |
-
w - 4,
|
| 136 |
-
h - 4
|
| 137 |
-
);
|
| 138 |
-
ctx.textAlign = 'left';
|
| 139 |
-
}, [gazeData]);
|
| 140 |
-
|
| 141 |
-
return (
|
| 142 |
-
<canvas
|
| 143 |
-
ref={canvasRef}
|
| 144 |
-
width={MAP_W}
|
| 145 |
-
height={MAP_H}
|
| 146 |
-
style={{
|
| 147 |
-
borderRadius: '8px',
|
| 148 |
-
border: '1px solid rgba(255,255,255,0.1)',
|
| 149 |
-
display: 'block',
|
| 150 |
-
}}
|
| 151 |
-
/>
|
| 152 |
-
);
|
| 153 |
-
}
|
| 154 |
-
|
| 155 |
-
function FocusPageLocal({ videoManager, sessionResult, setSessionResult, isActive }) {
|
| 156 |
const [currentFrame, setCurrentFrame] = useState(15);
|
| 157 |
const [timelineEvents, setTimelineEvents] = useState([]);
|
| 158 |
const [stats, setStats] = useState(null);
|
|
@@ -246,19 +131,10 @@ function FocusPageLocal({ videoManager, sessionResult, setSessionResult, isActiv
|
|
| 246 |
setFocusState(FOCUS_STATES.pending);
|
| 247 |
setCameraReady(false);
|
| 248 |
if (originalOnSessionEnd) originalOnSessionEnd(summary);
|
| 249 |
-
videoManager.callbacks.onCalibrationUpdate = (cal) => {
|
| 250 |
-
setCalibration(cal && cal.active ? { ...cal } : null);
|
| 251 |
};
|
| 252 |
|
| 253 |
-
videoManager.callbacks.onCalibrationUpdate = (
|
| 254 |
-
|
| 255 |
-
if (state && state.done && state.success) {
|
| 256 |
-
setIsCalibrated(true);
|
| 257 |
-
}
|
| 258 |
-
};
|
| 259 |
-
|
| 260 |
-
videoManager.callbacks.onGazeData = (data) => {
|
| 261 |
-
setGazeData(data);
|
| 262 |
};
|
| 263 |
|
| 264 |
const statsInterval = setInterval(() => {
|
|
@@ -276,7 +152,7 @@ function FocusPageLocal({ videoManager, sessionResult, setSessionResult, isActiv
|
|
| 276 |
};
|
| 277 |
}, [videoManager]);
|
| 278 |
|
| 279 |
-
// Fetch available models
|
| 280 |
useEffect(() => {
|
| 281 |
fetch('/api/models')
|
| 282 |
.then((res) => res.json())
|
|
@@ -287,14 +163,6 @@ function FocusPageLocal({ videoManager, sessionResult, setSessionResult, isActiv
|
|
| 287 |
if (data.l2cs_boost_available !== undefined) setL2csBoostAvailable(data.l2cs_boost_available);
|
| 288 |
})
|
| 289 |
.catch((err) => console.error('Failed to fetch models:', err));
|
| 290 |
-
|
| 291 |
-
fetch('/api/settings')
|
| 292 |
-
.then((res) => res.json())
|
| 293 |
-
.then((data) => {
|
| 294 |
-
if (data && data.l2cs_boost !== undefined) setL2csBoost(data.l2cs_boost);
|
| 295 |
-
if (data && data.l2cs_boost_available !== undefined) setL2csBoostAvailable(data.l2cs_boost_available);
|
| 296 |
-
})
|
| 297 |
-
.catch((err) => console.error('Failed to fetch settings:', err));
|
| 298 |
}, []);
|
| 299 |
|
| 300 |
useEffect(() => {
|
|
@@ -356,53 +224,6 @@ function FocusPageLocal({ videoManager, sessionResult, setSessionResult, isActiv
|
|
| 356 |
}
|
| 357 |
};
|
| 358 |
|
| 359 |
-
const handleL2csBoostToggle = async () => {
|
| 360 |
-
if (!l2csBoostAvailable) return;
|
| 361 |
-
const next = !l2csBoost;
|
| 362 |
-
try {
|
| 363 |
-
const res = await fetch('/api/settings', {
|
| 364 |
-
method: 'PUT',
|
| 365 |
-
headers: { 'Content-Type': 'application/json' },
|
| 366 |
-
body: JSON.stringify({ l2cs_boost: next })
|
| 367 |
-
});
|
| 368 |
-
if (res.ok) {
|
| 369 |
-
setL2csBoost(next);
|
| 370 |
-
} else {
|
| 371 |
-
const err = await res.json().catch(() => ({}));
|
| 372 |
-
alert(err.detail || 'Could not enable L2CS boost');
|
| 373 |
-
}
|
| 374 |
-
} catch (err) {
|
| 375 |
-
console.error('Failed to toggle L2CS boost:', err);
|
| 376 |
-
}
|
| 377 |
-
};
|
| 378 |
-
|
| 379 |
-
const handleEyeGazeToggle = async () => {
|
| 380 |
-
const next = !eyeGazeEnabled;
|
| 381 |
-
if (next) {
|
| 382 |
-
// Turning ON: save current model, switch to l2cs
|
| 383 |
-
setPrevModel(currentModel);
|
| 384 |
-
await handleModelChange('l2cs');
|
| 385 |
-
setEyeGazeEnabled(true);
|
| 386 |
-
} else {
|
| 387 |
-
// Turning OFF: switch back to previous model
|
| 388 |
-
const restoreTo = prevModel === 'l2cs' ? 'mlp' : prevModel;
|
| 389 |
-
await handleModelChange(restoreTo);
|
| 390 |
-
setEyeGazeEnabled(false);
|
| 391 |
-
setIsCalibrated(false);
|
| 392 |
-
setGazeData(null);
|
| 393 |
-
}
|
| 394 |
-
};
|
| 395 |
-
|
| 396 |
-
const [calibrationSetupOpen, setCalibrationSetupOpen] = useState(false);
|
| 397 |
-
|
| 398 |
-
const handleCalibrate = () => {
|
| 399 |
-
setCalibrationSetupOpen(true);
|
| 400 |
-
};
|
| 401 |
-
|
| 402 |
-
const handleCalibrationServerStart = () => {
|
| 403 |
-
if (videoManager) videoManager.startCalibration();
|
| 404 |
-
};
|
| 405 |
-
|
| 406 |
const handleEnableCamera = async () => {
|
| 407 |
if (!videoManager) return;
|
| 408 |
|
|
@@ -788,14 +609,6 @@ function FocusPageLocal({ videoManager, sessionResult, setSessionResult, isActiv
|
|
| 788 |
|
| 789 |
return (
|
| 790 |
<main id="page-b" className="page" style={pageStyle}>
|
| 791 |
-
<CalibrationOverlay
|
| 792 |
-
calibration={calibrationState}
|
| 793 |
-
videoManager={videoManager}
|
| 794 |
-
localVideoRef={localVideoRef}
|
| 795 |
-
onRequestStart={handleCalibrationServerStart}
|
| 796 |
-
setupOpen={calibrationSetupOpen}
|
| 797 |
-
setSetupOpen={setCalibrationSetupOpen}
|
| 798 |
-
/>
|
| 799 |
{renderIntroCard()}
|
| 800 |
|
| 801 |
<section id="display-area" className="focus-display-shell">
|
|
@@ -878,6 +691,22 @@ function FocusPageLocal({ videoManager, sessionResult, setSessionResult, isActiv
|
|
| 878 |
</div>
|
| 879 |
)}
|
| 880 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 881 |
</section>
|
| 882 |
|
| 883 |
{/* Server CPU / Memory (always visible) */}
|
|
@@ -1003,42 +832,6 @@ function FocusPageLocal({ videoManager, sessionResult, setSessionResult, isActiv
|
|
| 1003 |
{isStarting ? 'Starting...' : 'Start'}
|
| 1004 |
</button>
|
| 1005 |
|
| 1006 |
-
<button
|
| 1007 |
-
type="button"
|
| 1008 |
-
className="action-btn"
|
| 1009 |
-
style={{
|
| 1010 |
-
backgroundColor: eyeGazeEnabled ? '#8b5cf6' : '#475569',
|
| 1011 |
-
position: 'relative',
|
| 1012 |
-
}}
|
| 1013 |
-
onClick={handleEyeGazeToggle}
|
| 1014 |
-
title={eyeGazeEnabled
|
| 1015 |
-
? (isCalibrated ? 'Eye Gaze ON (Calibrated)' : 'Eye Gaze ON (Uncalibrated)')
|
| 1016 |
-
: 'Enable L2CS eye gaze tracking'}
|
| 1017 |
-
>
|
| 1018 |
-
Eye Gaze {eyeGazeEnabled ? 'ON' : 'OFF'}
|
| 1019 |
-
{eyeGazeEnabled && (
|
| 1020 |
-
<span style={{
|
| 1021 |
-
position: 'absolute', top: '-4px', right: '-4px',
|
| 1022 |
-
width: '10px', height: '10px', borderRadius: '50%',
|
| 1023 |
-
backgroundColor: isCalibrated ? '#4ade80' : '#fbbf24',
|
| 1024 |
-
border: '2px solid #1e1e2e',
|
| 1025 |
-
}} title={isCalibrated ? 'Calibrated' : 'Not calibrated'} />
|
| 1026 |
-
)}
|
| 1027 |
-
</button>
|
| 1028 |
-
|
| 1029 |
-
{eyeGazeEnabled ? (
|
| 1030 |
-
<button
|
| 1031 |
-
type="button"
|
| 1032 |
-
className="action-btn"
|
| 1033 |
-
style={{ backgroundColor: isCalibrated ? '#22c55e' : '#8b5cf6' }}
|
| 1034 |
-
onClick={handleCalibrate}
|
| 1035 |
-
disabled={!videoManager?.isStreaming}
|
| 1036 |
-
title="9-point gaze calibration for accurate tracking"
|
| 1037 |
-
>
|
| 1038 |
-
{isCalibrated ? 'Re-Calibrate' : 'Calibrate'}
|
| 1039 |
-
</button>
|
| 1040 |
-
) : null}
|
| 1041 |
-
|
| 1042 |
<button id="btn-floating" className="action-btn yellow" onClick={handlePiP}>
|
| 1043 |
Floating Window
|
| 1044 |
</button>
|
|
@@ -1051,32 +844,12 @@ function FocusPageLocal({ videoManager, sessionResult, setSessionResult, isActiv
|
|
| 1051 |
Preview Result
|
| 1052 |
</button>
|
| 1053 |
|
|
|
|
| 1054 |
<button id="btn-cam-stop" className="action-btn red" onClick={handleStop}>
|
| 1055 |
Stop
|
| 1056 |
</button>
|
| 1057 |
</section>
|
| 1058 |
|
| 1059 |
-
{eyeGazeEnabled && videoManager?.isStreaming ? (
|
| 1060 |
-
<section style={{
|
| 1061 |
-
display: 'flex',
|
| 1062 |
-
alignItems: 'center',
|
| 1063 |
-
justifyContent: 'center',
|
| 1064 |
-
gap: '14px',
|
| 1065 |
-
padding: '8px 14px',
|
| 1066 |
-
background: 'rgba(0,0,0,0.3)',
|
| 1067 |
-
borderRadius: '10px',
|
| 1068 |
-
margin: '6px auto',
|
| 1069 |
-
maxWidth: '400px',
|
| 1070 |
-
}}>
|
| 1071 |
-
<div style={{ textAlign: 'center' }}>
|
| 1072 |
-
<div style={{ fontSize: '11px', color: '#888', marginBottom: '4px', letterSpacing: '0.5px' }}>
|
| 1073 |
-
GAZE MAP {isCalibrated ? '(calibrated)' : '(raw)'}
|
| 1074 |
-
</div>
|
| 1075 |
-
<GazeMiniMap gazeData={gazeData} />
|
| 1076 |
-
</div>
|
| 1077 |
-
</section>
|
| 1078 |
-
) : null}
|
| 1079 |
-
|
| 1080 |
{cameraError ? (
|
| 1081 |
<div className="focus-inline-error focus-inline-error-standalone">{cameraError}</div>
|
| 1082 |
) : null}
|
|
@@ -1102,55 +875,6 @@ function FocusPageLocal({ videoManager, sessionResult, setSessionResult, isActiv
|
|
| 1102 |
</section>
|
| 1103 |
</>
|
| 1104 |
) : null}
|
| 1105 |
-
))}
|
| 1106 |
-
</div>
|
| 1107 |
-
<div id="timeline-line"></div>
|
| 1108 |
-
</section>
|
| 1109 |
-
|
| 1110 |
-
{/* 4. Control Buttons */}
|
| 1111 |
-
<section id="control-panel">
|
| 1112 |
-
<button id="btn-cam-start" className="action-btn green" onClick={handleStart}>
|
| 1113 |
-
Start
|
| 1114 |
-
</button>
|
| 1115 |
-
|
| 1116 |
-
<button id="btn-floating" className="action-btn yellow" onClick={handleFloatingWindow}>
|
| 1117 |
-
Floating Window
|
| 1118 |
-
</button>
|
| 1119 |
-
|
| 1120 |
-
<button
|
| 1121 |
-
id="btn-preview"
|
| 1122 |
-
className="action-btn"
|
| 1123 |
-
style={{ backgroundColor: '#6c5ce7' }}
|
| 1124 |
-
onClick={handlePreview}
|
| 1125 |
-
>
|
| 1126 |
-
Preview Result
|
| 1127 |
-
</button>
|
| 1128 |
-
|
| 1129 |
-
<button id="btn-cam-stop" className="action-btn red" onClick={handleStop}>
|
| 1130 |
-
Stop
|
| 1131 |
-
</button>
|
| 1132 |
-
</section>
|
| 1133 |
-
|
| 1134 |
-
{/* 5. Frame Control */}
|
| 1135 |
-
<section id="frame-control">
|
| 1136 |
-
<label htmlFor="frame-slider">Frame Rate (FPS)</label>
|
| 1137 |
-
<input
|
| 1138 |
-
type="range"
|
| 1139 |
-
id="frame-slider"
|
| 1140 |
-
min="10"
|
| 1141 |
-
max="30"
|
| 1142 |
-
value={currentFrame}
|
| 1143 |
-
onChange={(e) => handleFrameChange(e.target.value)}
|
| 1144 |
-
/>
|
| 1145 |
-
<input
|
| 1146 |
-
type="number"
|
| 1147 |
-
id="frame-input"
|
| 1148 |
-
min="10"
|
| 1149 |
-
max="30"
|
| 1150 |
-
value={currentFrame}
|
| 1151 |
-
onChange={(e) => handleFrameChange(e.target.value)}
|
| 1152 |
-
/>
|
| 1153 |
-
</section>
|
| 1154 |
|
| 1155 |
{/* Calibration overlay (fixed fullscreen, must be outside overflow:hidden containers) */}
|
| 1156 |
<CalibrationOverlay calibration={calibration} videoManager={videoManager} />
|
|
|
|
| 37 |
);
|
| 38 |
}
|
| 39 |
|
| 40 |
+
function FocusPageLocal({ videoManager, sessionResult, setSessionResult, isActive, role }) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
const [currentFrame, setCurrentFrame] = useState(15);
|
| 42 |
const [timelineEvents, setTimelineEvents] = useState([]);
|
| 43 |
const [stats, setStats] = useState(null);
|
|
|
|
| 131 |
setFocusState(FOCUS_STATES.pending);
|
| 132 |
setCameraReady(false);
|
| 133 |
if (originalOnSessionEnd) originalOnSessionEnd(summary);
|
|
|
|
|
|
|
| 134 |
};
|
| 135 |
|
| 136 |
+
videoManager.callbacks.onCalibrationUpdate = (cal) => {
|
| 137 |
+
setCalibration(cal && cal.active ? { ...cal } : null);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
};
|
| 139 |
|
| 140 |
const statsInterval = setInterval(() => {
|
|
|
|
| 152 |
};
|
| 153 |
}, [videoManager]);
|
| 154 |
|
| 155 |
+
// Fetch available models on mount
|
| 156 |
useEffect(() => {
|
| 157 |
fetch('/api/models')
|
| 158 |
.then((res) => res.json())
|
|
|
|
| 163 |
if (data.l2cs_boost_available !== undefined) setL2csBoostAvailable(data.l2cs_boost_available);
|
| 164 |
})
|
| 165 |
.catch((err) => console.error('Failed to fetch models:', err));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
}, []);
|
| 167 |
|
| 168 |
useEffect(() => {
|
|
|
|
| 224 |
}
|
| 225 |
};
|
| 226 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
const handleEnableCamera = async () => {
|
| 228 |
if (!videoManager) return;
|
| 229 |
|
|
|
|
| 609 |
|
| 610 |
return (
|
| 611 |
<main id="page-b" className="page" style={pageStyle}>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 612 |
{renderIntroCard()}
|
| 613 |
|
| 614 |
<section id="display-area" className="focus-display-shell">
|
|
|
|
| 691 |
</div>
|
| 692 |
)}
|
| 693 |
|
| 694 |
+
{role === 'admin' && stats && stats.isStreaming ? (
|
| 695 |
+
<div className="focus-debug-panel">
|
| 696 |
+
<div>Session: {stats.sessionId}</div>
|
| 697 |
+
<div>Sent: {stats.framesSent}</div>
|
| 698 |
+
<div>Processed: {stats.framesProcessed}</div>
|
| 699 |
+
<div>Latency: {stats.avgLatency.toFixed(0)}ms</div>
|
| 700 |
+
<div>Status: {stats.currentStatus ? 'Focused' : 'Not Focused'}</div>
|
| 701 |
+
<div>Confidence: {(stats.lastConfidence * 100).toFixed(1)}%</div>
|
| 702 |
+
{systemStats && systemStats.cpu_percent != null && (
|
| 703 |
+
<div style={{ marginTop: '6px', borderTop: '1px solid #444', paddingTop: '4px' }}>
|
| 704 |
+
<div>CPU: {systemStats.cpu_percent}%</div>
|
| 705 |
+
<div>RAM: {systemStats.memory_percent}% ({systemStats.memory_used_mb}/{systemStats.memory_total_mb} MB)</div>
|
| 706 |
+
</div>
|
| 707 |
+
)}
|
| 708 |
+
</div>
|
| 709 |
+
) : null}
|
| 710 |
</section>
|
| 711 |
|
| 712 |
{/* Server CPU / Memory (always visible) */}
|
|
|
|
| 832 |
{isStarting ? 'Starting...' : 'Start'}
|
| 833 |
</button>
|
| 834 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 835 |
<button id="btn-floating" className="action-btn yellow" onClick={handlePiP}>
|
| 836 |
Floating Window
|
| 837 |
</button>
|
|
|
|
| 844 |
Preview Result
|
| 845 |
</button>
|
| 846 |
|
| 847 |
+
|
| 848 |
<button id="btn-cam-stop" className="action-btn red" onClick={handleStop}>
|
| 849 |
Stop
|
| 850 |
</button>
|
| 851 |
</section>
|
| 852 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 853 |
{cameraError ? (
|
| 854 |
<div className="focus-inline-error focus-inline-error-standalone">{cameraError}</div>
|
| 855 |
) : null}
|
|
|
|
| 875 |
</section>
|
| 876 |
</>
|
| 877 |
) : null}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 878 |
|
| 879 |
{/* Calibration overlay (fixed fullscreen, must be outside overflow:hidden containers) */}
|
| 880 |
<CalibrationOverlay calibration={calibration} videoManager={videoManager} />
|