Spaces:
Paused
Paused
Zhen Ye
commited on
Commit
·
e7dfc36
1
Parent(s):
fb5e94d
Redesign track cards with inline features, remove separate features panel, clean up overlays
Browse files- frontend/index.html +3 -27
- frontend/js/core/tracker.js +6 -1
- frontend/js/core/video.js +0 -1
- frontend/js/main.js +1 -9
- frontend/js/ui/cards.js +46 -22
- frontend/js/ui/features.js +0 -35
- frontend/js/ui/overlays.js +6 -54
- frontend/style.css +88 -35
frontend/index.html
CHANGED
|
@@ -158,38 +158,14 @@
|
|
| 158 |
|
| 159 |
|
| 160 |
|
| 161 |
-
<div class="panel panel-
|
| 162 |
-
<h3>
|
| 163 |
-
<span>Selected Target · Features</span>
|
| 164 |
-
<span class="rightnote" id="selId">—</span>
|
| 165 |
-
</h3>
|
| 166 |
-
<table class="table" id="featureTable">
|
| 167 |
-
<thead>
|
| 168 |
-
<tr>
|
| 169 |
-
<th style="width:42%">Feature</th>
|
| 170 |
-
<th>Value</th>
|
| 171 |
-
</tr>
|
| 172 |
-
</thead>
|
| 173 |
-
<tbody>
|
| 174 |
-
<tr>
|
| 175 |
-
<td class="k">—</td>
|
| 176 |
-
<td class="mini">No target selected</td>
|
| 177 |
-
</tr>
|
| 178 |
-
</tbody>
|
| 179 |
-
</table>
|
| 180 |
-
<div class="hint mt-sm">You can replace feature generation via <span
|
| 181 |
-
class="kbd">externalFeatures()</span>. The UI will render whatever 10–12 key-value pairs you return.
|
| 182 |
-
</div>
|
| 183 |
-
</div>
|
| 184 |
-
|
| 185 |
-
<div class="panel panel-summary" style="display:flex; flex-direction:column; min-height: 0;">
|
| 186 |
<h3>
|
| 187 |
<span>Object Track Cards</span>
|
| 188 |
<span class="rightnote" id="trackCount">0</span>
|
| 189 |
</h3>
|
| 190 |
-
<div class="list" id="frameTrackList" style="flex:1; overflow-y:auto; padding:
|
| 191 |
<!-- Cards injected here -->
|
| 192 |
-
<div style="font-style:italic; color:var(--
|
| 193 |
No objects tracked.
|
| 194 |
</div>
|
| 195 |
</div>
|
|
|
|
| 158 |
|
| 159 |
|
| 160 |
|
| 161 |
+
<div class="panel panel-summary" style="display:flex; flex-direction:column; min-height: 0; overflow: hidden;">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
<h3>
|
| 163 |
<span>Object Track Cards</span>
|
| 164 |
<span class="rightnote" id="trackCount">0</span>
|
| 165 |
</h3>
|
| 166 |
+
<div class="list" id="frameTrackList" style="flex:1; overflow-y:auto; padding:6px; max-height:none;">
|
| 167 |
<!-- Cards injected here -->
|
| 168 |
+
<div style="font-style:italic; color:var(--faint); text-align:center; margin-top:20px; font-size:12px;">
|
| 169 |
No objects tracked.
|
| 170 |
</div>
|
| 171 |
</div>
|
frontend/js/core/tracker.js
CHANGED
|
@@ -112,7 +112,7 @@ APP.core.tracker.matchAndUpdateTracks = function (dets, dtSec) {
|
|
| 112 |
if (used.has(i)) continue;
|
| 113 |
// create new track only if big enough
|
| 114 |
const a = detObjs[i].bbox.w * detObjs[i].bbox.h;
|
| 115 |
-
if (a < (w * h) * 0.
|
| 116 |
|
| 117 |
const newId = `T${String(state.tracker.nextId++).padStart(2, "0")}`;
|
| 118 |
const ap = defaultAimpoint(detObjs[i].label);
|
|
@@ -239,6 +239,11 @@ APP.core.tracker.syncWithBackend = async function (frameIdx) {
|
|
| 239 |
state.tracker.tracks = newTracks;
|
| 240 |
state.detections = newTracks; // Keep synced
|
| 241 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
} catch (e) {
|
| 243 |
console.warn("Track sync failed", e);
|
| 244 |
}
|
|
|
|
| 112 |
if (used.has(i)) continue;
|
| 113 |
// create new track only if big enough
|
| 114 |
const a = detObjs[i].bbox.w * detObjs[i].bbox.h;
|
| 115 |
+
if (a < (w * h) * 0.0005) continue;
|
| 116 |
|
| 117 |
const newId = `T${String(state.tracker.nextId++).padStart(2, "0")}`;
|
| 118 |
const ap = defaultAimpoint(detObjs[i].label);
|
|
|
|
| 239 |
state.tracker.tracks = newTracks;
|
| 240 |
state.detections = newTracks; // Keep synced
|
| 241 |
|
| 242 |
+
// Re-render track cards (same renderer as Tab 1)
|
| 243 |
+
if (APP.ui.cards && APP.ui.cards.renderFrameTrackList) {
|
| 244 |
+
APP.ui.cards.renderFrameTrackList();
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
} catch (e) {
|
| 248 |
console.warn("Track sync failed", e);
|
| 249 |
}
|
frontend/js/core/video.js
CHANGED
|
@@ -187,7 +187,6 @@ APP.core.video.unloadVideo = async function (options = {}) {
|
|
| 187 |
// Re-render UI components
|
| 188 |
if (APP.ui.radar.renderFrameRadar) APP.ui.radar.renderFrameRadar();
|
| 189 |
if (APP.ui.cards.renderFrameTrackList) APP.ui.cards.renderFrameTrackList();
|
| 190 |
-
if (APP.ui.features.renderFeatures) APP.ui.features.renderFeatures(null);
|
| 191 |
if (APP.ui.trade.renderTrade) APP.ui.trade.renderTrade();
|
| 192 |
|
| 193 |
setStatus("warn", "STANDBY · No video loaded");
|
|
|
|
| 187 |
// Re-render UI components
|
| 188 |
if (APP.ui.radar.renderFrameRadar) APP.ui.radar.renderFrameRadar();
|
| 189 |
if (APP.ui.cards.renderFrameTrackList) APP.ui.cards.renderFrameTrackList();
|
|
|
|
| 190 |
if (APP.ui.trade.renderTrade) APP.ui.trade.renderTrade();
|
| 191 |
|
| 192 |
setStatus("warn", "STANDBY · No video loaded");
|
frontend/js/main.js
CHANGED
|
@@ -14,11 +14,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
| 14 |
// UI Renderers
|
| 15 |
const { renderFrameOverlay, renderEngageOverlay } = APP.ui.overlays;
|
| 16 |
const { renderFrameTrackList } = APP.ui.cards;
|
| 17 |
-
const { renderFeatures } = APP.ui.features;
|
| 18 |
const { tickAgentCursor, moveCursorToRect } = APP.ui.cursor;
|
| 19 |
const { matchAndUpdateTracks, predictTracks } = APP.core.tracker;
|
| 20 |
const { defaultAimpoint } = APP.core.physics;
|
| 21 |
-
const { normBBox } = APP.core.utils;
|
| 22 |
|
| 23 |
// DOM Elements
|
| 24 |
const videoEngage = $("#videoEngage");
|
|
@@ -155,7 +153,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
| 155 |
state.selectedId = null;
|
| 156 |
renderFrameTrackList();
|
| 157 |
renderFrameOverlay();
|
| 158 |
-
renderFeatures(null);
|
| 159 |
|
| 160 |
log("Detections cleared.", "t");
|
| 161 |
});
|
|
@@ -213,14 +210,12 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
| 213 |
|
| 214 |
|
| 215 |
|
| 216 |
-
// Track selection event
|
| 217 |
document.addEventListener("track-selected", (e) => {
|
| 218 |
state.selectedId = e.detail.id;
|
| 219 |
state.tracker.selectedTrackId = e.detail.id;
|
| 220 |
renderFrameTrackList();
|
| 221 |
renderFrameOverlay();
|
| 222 |
-
const det = state.detections.find(d => d.id === state.selectedId);
|
| 223 |
-
renderFeatures(det);
|
| 224 |
});
|
| 225 |
|
| 226 |
// Cursor mode toggle
|
|
@@ -333,8 +328,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
| 333 |
state.selectedId = null;
|
| 334 |
renderFrameTrackList();
|
| 335 |
renderFrameOverlay();
|
| 336 |
-
renderFeatures(null);
|
| 337 |
-
|
| 338 |
|
| 339 |
setStatus("warn", "REASONING · Running perception pipeline");
|
| 340 |
|
|
@@ -574,7 +567,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
| 574 |
state.selectedId = state.detections[0]?.id || null;
|
| 575 |
|
| 576 |
renderFrameTrackList();
|
| 577 |
-
renderFeatures(state.detections[0] || null);
|
| 578 |
renderFrameOverlay();
|
| 579 |
|
| 580 |
log(`Detected ${state.detections.length} objects in first frame.`, "g");
|
|
|
|
| 14 |
// UI Renderers
|
| 15 |
const { renderFrameOverlay, renderEngageOverlay } = APP.ui.overlays;
|
| 16 |
const { renderFrameTrackList } = APP.ui.cards;
|
|
|
|
| 17 |
const { tickAgentCursor, moveCursorToRect } = APP.ui.cursor;
|
| 18 |
const { matchAndUpdateTracks, predictTracks } = APP.core.tracker;
|
| 19 |
const { defaultAimpoint } = APP.core.physics;
|
|
|
|
| 20 |
|
| 21 |
// DOM Elements
|
| 22 |
const videoEngage = $("#videoEngage");
|
|
|
|
| 153 |
state.selectedId = null;
|
| 154 |
renderFrameTrackList();
|
| 155 |
renderFrameOverlay();
|
|
|
|
| 156 |
|
| 157 |
log("Detections cleared.", "t");
|
| 158 |
});
|
|
|
|
| 210 |
|
| 211 |
|
| 212 |
|
| 213 |
+
// Track selection event — re-renders cards (with inline features for active card)
|
| 214 |
document.addEventListener("track-selected", (e) => {
|
| 215 |
state.selectedId = e.detail.id;
|
| 216 |
state.tracker.selectedTrackId = e.detail.id;
|
| 217 |
renderFrameTrackList();
|
| 218 |
renderFrameOverlay();
|
|
|
|
|
|
|
| 219 |
});
|
| 220 |
|
| 221 |
// Cursor mode toggle
|
|
|
|
| 328 |
state.selectedId = null;
|
| 329 |
renderFrameTrackList();
|
| 330 |
renderFrameOverlay();
|
|
|
|
|
|
|
| 331 |
|
| 332 |
setStatus("warn", "REASONING · Running perception pipeline");
|
| 333 |
|
|
|
|
| 567 |
state.selectedId = state.detections[0]?.id || null;
|
| 568 |
|
| 569 |
renderFrameTrackList();
|
|
|
|
| 570 |
renderFrameOverlay();
|
| 571 |
|
| 572 |
log(`Detected ${state.detections.length} objects in first frame.`, "g");
|
frontend/js/ui/cards.js
CHANGED
|
@@ -4,28 +4,29 @@ APP.ui.cards.renderFrameTrackList = function () {
|
|
| 4 |
const { state } = APP.core;
|
| 5 |
const { $ } = APP.core.utils;
|
| 6 |
const frameTrackList = $("#frameTrackList");
|
| 7 |
-
const
|
|
|
|
| 8 |
|
| 9 |
-
if (!frameTrackList) return;
|
| 10 |
-
frameTrackList.innerHTML = "";
|
|
|
|
| 11 |
|
| 12 |
// Filter: only show mission-relevant detections (or all in LEGACY mode)
|
| 13 |
const dets = (state.detections || []).filter(d => {
|
| 14 |
-
// LEGACY mode: mission_relevant is null -> show all
|
| 15 |
if (d.mission_relevant === null || d.mission_relevant === undefined) return true;
|
| 16 |
-
// MISSION mode: only show relevant
|
| 17 |
return d.mission_relevant === true;
|
| 18 |
});
|
| 19 |
|
| 20 |
if (trackCount) trackCount.textContent = dets.length;
|
| 21 |
|
| 22 |
if (dets.length === 0) {
|
| 23 |
-
|
|
|
|
|
|
|
| 24 |
return;
|
| 25 |
}
|
| 26 |
|
| 27 |
-
//
|
| 28 |
-
// Within each group, sort by threat_level_score descending, then by confidence
|
| 29 |
const statusOrder = { "ASSESSED": 0, "UNASSESSED": 1, "STALE": 2 };
|
| 30 |
const sorted = [...dets].sort((a, b) => {
|
| 31 |
const statusA = statusOrder[a.assessment_status] ?? 1;
|
|
@@ -39,16 +40,17 @@ APP.ui.cards.renderFrameTrackList = function () {
|
|
| 39 |
|
| 40 |
sorted.forEach((det, i) => {
|
| 41 |
const id = det.id || `T${String(i + 1).padStart(2, '0')}`;
|
|
|
|
| 42 |
|
| 43 |
let rangeStr = "---";
|
| 44 |
let bearingStr = "---";
|
| 45 |
|
| 46 |
if (det.depth_valid && det.depth_est_m != null) {
|
| 47 |
-
rangeStr = `${Math.round(det.depth_est_m)}m
|
| 48 |
} else if (det.gpt_distance_m) {
|
| 49 |
-
rangeStr = `~${det.gpt_distance_m}m
|
| 50 |
} else if (det.baseRange_m) {
|
| 51 |
-
rangeStr = `${Math.round(det.baseRange_m)}m
|
| 52 |
}
|
| 53 |
|
| 54 |
if (det.gpt_direction) {
|
|
@@ -56,8 +58,7 @@ APP.ui.cards.renderFrameTrackList = function () {
|
|
| 56 |
}
|
| 57 |
|
| 58 |
const card = document.createElement("div");
|
| 59 |
-
card.className = "track-card";
|
| 60 |
-
if (state.selectedId === id) card.classList.add("active");
|
| 61 |
card.id = `card-${id}`;
|
| 62 |
|
| 63 |
card.onclick = () => {
|
|
@@ -65,11 +66,7 @@ APP.ui.cards.renderFrameTrackList = function () {
|
|
| 65 |
document.dispatchEvent(ev);
|
| 66 |
};
|
| 67 |
|
| 68 |
-
|
| 69 |
-
? `<div class="track-card-body"><span class="gpt-text">${det.gpt_description}</span></div>`
|
| 70 |
-
: "";
|
| 71 |
-
|
| 72 |
-
// Assessment status badge (INV-6: UNASSESSED distinct from score 0)
|
| 73 |
let statusBadge = "";
|
| 74 |
const assessStatus = det.assessment_status || "UNASSESSED";
|
| 75 |
if (assessStatus === "UNASSESSED") {
|
|
@@ -80,19 +77,46 @@ APP.ui.cards.renderFrameTrackList = function () {
|
|
| 80 |
statusBadge = `<span class="badgemini" style="background:${det.threat_level_score >= 8 ? '#ff4d4d' : '#ff9f43'}; color:white">T-${det.threat_level_score}</span>`;
|
| 81 |
}
|
| 82 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
card.innerHTML = `
|
| 84 |
<div class="track-card-header">
|
| 85 |
<span>${id} · ${det.label}</span>
|
| 86 |
-
<div style="display:flex; gap:4px">
|
| 87 |
${statusBadge}
|
| 88 |
-
<span class="badgemini">${(det.score * 100).toFixed(0)}%</span>
|
| 89 |
</div>
|
| 90 |
</div>
|
| 91 |
<div class="track-card-meta">
|
| 92 |
-
|
| 93 |
</div>
|
| 94 |
${desc}
|
|
|
|
| 95 |
`;
|
| 96 |
-
frameTrackList.appendChild(card);
|
|
|
|
| 97 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
};
|
|
|
|
| 4 |
const { state } = APP.core;
|
| 5 |
const { $ } = APP.core.utils;
|
| 6 |
const frameTrackList = $("#frameTrackList");
|
| 7 |
+
const trackList = $("#trackList"); // Tab 2 (Engage) uses the same card logic
|
| 8 |
+
const trackCount = $("#trackCount");
|
| 9 |
|
| 10 |
+
if (!frameTrackList && !trackList) return;
|
| 11 |
+
if (frameTrackList) frameTrackList.innerHTML = "";
|
| 12 |
+
if (trackList) trackList.innerHTML = "";
|
| 13 |
|
| 14 |
// Filter: only show mission-relevant detections (or all in LEGACY mode)
|
| 15 |
const dets = (state.detections || []).filter(d => {
|
|
|
|
| 16 |
if (d.mission_relevant === null || d.mission_relevant === undefined) return true;
|
|
|
|
| 17 |
return d.mission_relevant === true;
|
| 18 |
});
|
| 19 |
|
| 20 |
if (trackCount) trackCount.textContent = dets.length;
|
| 21 |
|
| 22 |
if (dets.length === 0) {
|
| 23 |
+
const emptyMsg = '<div style="font-style:italic; color:var(--faint); text-align:center; margin-top:20px; font-size:12px;">No objects tracked.</div>';
|
| 24 |
+
if (frameTrackList) frameTrackList.innerHTML = emptyMsg;
|
| 25 |
+
if (trackList) trackList.innerHTML = emptyMsg;
|
| 26 |
return;
|
| 27 |
}
|
| 28 |
|
| 29 |
+
// Sort: ASSESSED first (by threat score), then UNASSESSED, then STALE
|
|
|
|
| 30 |
const statusOrder = { "ASSESSED": 0, "UNASSESSED": 1, "STALE": 2 };
|
| 31 |
const sorted = [...dets].sort((a, b) => {
|
| 32 |
const statusA = statusOrder[a.assessment_status] ?? 1;
|
|
|
|
| 40 |
|
| 41 |
sorted.forEach((det, i) => {
|
| 42 |
const id = det.id || `T${String(i + 1).padStart(2, '0')}`;
|
| 43 |
+
const isActive = state.selectedId === id;
|
| 44 |
|
| 45 |
let rangeStr = "---";
|
| 46 |
let bearingStr = "---";
|
| 47 |
|
| 48 |
if (det.depth_valid && det.depth_est_m != null) {
|
| 49 |
+
rangeStr = `${Math.round(det.depth_est_m)}m`;
|
| 50 |
} else if (det.gpt_distance_m) {
|
| 51 |
+
rangeStr = `~${det.gpt_distance_m}m`;
|
| 52 |
} else if (det.baseRange_m) {
|
| 53 |
+
rangeStr = `${Math.round(det.baseRange_m)}m`;
|
| 54 |
}
|
| 55 |
|
| 56 |
if (det.gpt_direction) {
|
|
|
|
| 58 |
}
|
| 59 |
|
| 60 |
const card = document.createElement("div");
|
| 61 |
+
card.className = "track-card" + (isActive ? " active" : "");
|
|
|
|
| 62 |
card.id = `card-${id}`;
|
| 63 |
|
| 64 |
card.onclick = () => {
|
|
|
|
| 66 |
document.dispatchEvent(ev);
|
| 67 |
};
|
| 68 |
|
| 69 |
+
// Assessment status badge
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
let statusBadge = "";
|
| 71 |
const assessStatus = det.assessment_status || "UNASSESSED";
|
| 72 |
if (assessStatus === "UNASSESSED") {
|
|
|
|
| 77 |
statusBadge = `<span class="badgemini" style="background:${det.threat_level_score >= 8 ? '#ff4d4d' : '#ff9f43'}; color:white">T-${det.threat_level_score}</span>`;
|
| 78 |
}
|
| 79 |
|
| 80 |
+
// GPT description (collapsed summary)
|
| 81 |
+
const desc = det.gpt_description
|
| 82 |
+
? `<div class="track-card-body"><span class="gpt-text">${det.gpt_description}</span></div>`
|
| 83 |
+
: "";
|
| 84 |
+
|
| 85 |
+
// Inline features (only shown when active/expanded)
|
| 86 |
+
let featuresHtml = "";
|
| 87 |
+
if (isActive && det.features && Object.keys(det.features).length > 0) {
|
| 88 |
+
const entries = Object.entries(det.features).slice(0, 12);
|
| 89 |
+
const rows = entries.map(([k, v]) =>
|
| 90 |
+
`<div class="feat-row"><span class="feat-key">${k}</span><span class="feat-val">${String(v)}</span></div>`
|
| 91 |
+
).join("");
|
| 92 |
+
featuresHtml = `<div class="track-card-features">${rows}</div>`;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
card.innerHTML = `
|
| 96 |
<div class="track-card-header">
|
| 97 |
<span>${id} · ${det.label}</span>
|
| 98 |
+
<div style="display:flex; gap:4px; align-items:center">
|
| 99 |
${statusBadge}
|
| 100 |
+
<span class="badgemini" style="background:rgba(255,255,255,.08); color:rgba(255,255,255,.7)">${(det.score * 100).toFixed(0)}%</span>
|
| 101 |
</div>
|
| 102 |
</div>
|
| 103 |
<div class="track-card-meta">
|
| 104 |
+
RNG ${rangeStr} · BRG ${bearingStr}
|
| 105 |
</div>
|
| 106 |
${desc}
|
| 107 |
+
${featuresHtml}
|
| 108 |
`;
|
| 109 |
+
if (frameTrackList) frameTrackList.appendChild(card);
|
| 110 |
+
if (trackList) trackList.appendChild(card.cloneNode(true));
|
| 111 |
});
|
| 112 |
+
|
| 113 |
+
// Wire up click handlers on cloned Tab 2 cards
|
| 114 |
+
if (trackList) {
|
| 115 |
+
trackList.querySelectorAll(".track-card").forEach(card => {
|
| 116 |
+
const id = card.id.replace("card-", "");
|
| 117 |
+
card.onclick = () => {
|
| 118 |
+
document.dispatchEvent(new CustomEvent("track-selected", { detail: { id } }));
|
| 119 |
+
};
|
| 120 |
+
});
|
| 121 |
+
}
|
| 122 |
};
|
frontend/js/ui/features.js
CHANGED
|
@@ -1,36 +1 @@
|
|
| 1 |
APP.ui.features = {};
|
| 2 |
-
|
| 3 |
-
APP.ui.features.renderFeatures = function (det) {
|
| 4 |
-
const { $ } = APP.core.utils;
|
| 5 |
-
const featureTable = $("#featureTable");
|
| 6 |
-
const selId = $("#selId"); // Correct ID
|
| 7 |
-
|
| 8 |
-
if (!featureTable || !selId) return;
|
| 9 |
-
|
| 10 |
-
selId.textContent = det ? det.id : "—";
|
| 11 |
-
const tbody = featureTable.querySelector("tbody");
|
| 12 |
-
tbody.innerHTML = "";
|
| 13 |
-
|
| 14 |
-
if (!det) {
|
| 15 |
-
tbody.innerHTML = `<tr><td class="k">—</td><td class="mini">No target selected</td></tr>`;
|
| 16 |
-
return;
|
| 17 |
-
}
|
| 18 |
-
|
| 19 |
-
const feats = det.features || {};
|
| 20 |
-
const keys = Object.keys(feats);
|
| 21 |
-
const show = keys.slice(0, 12);
|
| 22 |
-
|
| 23 |
-
show.forEach(k => {
|
| 24 |
-
const tr = document.createElement("tr");
|
| 25 |
-
tr.innerHTML = `<td class="k">${k}</td><td>${String(feats[k])}</td>`;
|
| 26 |
-
tbody.appendChild(tr);
|
| 27 |
-
});
|
| 28 |
-
|
| 29 |
-
if (show.length < 10) {
|
| 30 |
-
for (let i = show.length; i < 10; i++) {
|
| 31 |
-
const tr = document.createElement("tr");
|
| 32 |
-
tr.innerHTML = `<td class="k">—</td><td class="mini">awaiting additional expert outputs</td>`;
|
| 33 |
-
tbody.appendChild(tr);
|
| 34 |
-
}
|
| 35 |
-
}
|
| 36 |
-
};
|
|
|
|
| 1 |
APP.ui.features = {};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/ui/overlays.js
CHANGED
|
@@ -2,70 +2,24 @@ APP.ui.overlays = {};
|
|
| 2 |
|
| 3 |
APP.ui.overlays.render = function (canvasId, trackSource) {
|
| 4 |
const { state } = APP.core;
|
| 5 |
-
const {
|
| 6 |
-
const { defaultAimpoint } = APP.core.physics;
|
| 7 |
|
| 8 |
const canvas = $(`#${canvasId}`);
|
| 9 |
if (!canvas) return;
|
| 10 |
|
| 11 |
-
//
|
| 12 |
-
//
|
| 13 |
-
// Except maybe selected highlight? For now, hide all to avoid clutter/mismatch.
|
| 14 |
if (canvasId === "engageOverlay" && state.useProcessedFeed) {
|
| 15 |
const ctx = canvas.getContext("2d");
|
| 16 |
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
| 17 |
return;
|
| 18 |
}
|
| 19 |
|
| 20 |
-
// Resize to match DOM if needed (handling HiDPI optionally, or just verify size)
|
| 21 |
-
const w = canvas.width, h = canvas.height;
|
| 22 |
const ctx = canvas.getContext("2d");
|
| 23 |
-
ctx.clearRect(0, 0,
|
| 24 |
-
|
| 25 |
-
const source = trackSource || state.detections || [];
|
| 26 |
-
if (!source || !source.length) return;
|
| 27 |
-
|
| 28 |
-
// Helpers
|
| 29 |
-
function roundRect(ctx, x, y, w, h, r, fill, stroke) {
|
| 30 |
-
if (w < 2 * r) r = w / 2;
|
| 31 |
-
if (h < 2 * r) r = h / 2;
|
| 32 |
-
ctx.beginPath();
|
| 33 |
-
ctx.moveTo(x + r, y);
|
| 34 |
-
ctx.arcTo(x + w, y, x + w, y + h, r);
|
| 35 |
-
ctx.arcTo(x + w, y + h, x, y + h, r);
|
| 36 |
-
ctx.arcTo(x, y + h, x, y, r);
|
| 37 |
-
ctx.arcTo(x, y, x + w, y, r);
|
| 38 |
-
ctx.closePath();
|
| 39 |
-
if (fill) ctx.fill();
|
| 40 |
-
if (stroke) ctx.stroke();
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
function drawAimpoint(ctx, x, y, isSel) {
|
| 44 |
-
ctx.save();
|
| 45 |
-
ctx.strokeStyle = "rgba(239,68,68,.95)";
|
| 46 |
-
ctx.lineWidth = isSel ? 3 : 2;
|
| 47 |
-
ctx.beginPath();
|
| 48 |
-
ctx.arc(x, y, isSel ? 10 : 9, 0, Math.PI * 2);
|
| 49 |
-
ctx.stroke();
|
| 50 |
-
|
| 51 |
-
ctx.strokeStyle = "rgba(255,255,255,.70)";
|
| 52 |
-
ctx.lineWidth = 1.5;
|
| 53 |
-
ctx.beginPath();
|
| 54 |
-
ctx.moveTo(x - 14, y); ctx.lineTo(x - 4, y);
|
| 55 |
-
ctx.moveTo(x + 4, y); ctx.lineTo(x + 14, y);
|
| 56 |
-
ctx.moveTo(x, y - 14); ctx.lineTo(x, y - 4);
|
| 57 |
-
ctx.moveTo(x, y + 4); ctx.lineTo(x, y + 14);
|
| 58 |
-
ctx.stroke();
|
| 59 |
-
|
| 60 |
-
ctx.fillStyle = "rgba(239,68,68,.95)";
|
| 61 |
-
ctx.beginPath();
|
| 62 |
-
ctx.arc(x, y, 2.5, 0, Math.PI * 2);
|
| 63 |
-
ctx.fill();
|
| 64 |
-
ctx.restore();
|
| 65 |
-
}
|
| 66 |
|
| 67 |
// Bounding boxes and labels are baked into the video by the backend.
|
| 68 |
-
// Frontend overlay drawing
|
| 69 |
};
|
| 70 |
|
| 71 |
APP.ui.overlays.renderFrameOverlay = function () {
|
|
@@ -74,7 +28,6 @@ APP.ui.overlays.renderFrameOverlay = function () {
|
|
| 74 |
const canvas = $("#frameOverlay");
|
| 75 |
if (!canvas) return;
|
| 76 |
|
| 77 |
-
// Only show overlay for the selected detection
|
| 78 |
if (state.selectedId) {
|
| 79 |
const sel = (state.detections || []).filter(d => d.id === state.selectedId);
|
| 80 |
if (sel.length) {
|
|
@@ -82,7 +35,6 @@ APP.ui.overlays.renderFrameOverlay = function () {
|
|
| 82 |
return;
|
| 83 |
}
|
| 84 |
}
|
| 85 |
-
// Nothing selected — clear
|
| 86 |
const ctx = canvas.getContext("2d");
|
| 87 |
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
| 88 |
};
|
|
@@ -91,7 +43,7 @@ APP.ui.overlays.renderEngageOverlay = function () {
|
|
| 91 |
const { state } = APP.core;
|
| 92 |
const { $ } = APP.core.utils;
|
| 93 |
|
| 94 |
-
//
|
| 95 |
const video = $("#videoEngage");
|
| 96 |
if (video && (video.currentTime < 0.25 || (video.paused && video.currentTime === 0))) {
|
| 97 |
const canvas = $("#engageOverlay");
|
|
|
|
| 2 |
|
| 3 |
APP.ui.overlays.render = function (canvasId, trackSource) {
|
| 4 |
const { state } = APP.core;
|
| 5 |
+
const { $ } = APP.core.utils;
|
|
|
|
| 6 |
|
| 7 |
const canvas = $(`#${canvasId}`);
|
| 8 |
if (!canvas) return;
|
| 9 |
|
| 10 |
+
// If viewing the processed feed (which has baked-in boxes from backend),
|
| 11 |
+
// do not draw frontend predicted boxes to avoid clutter/mismatch.
|
|
|
|
| 12 |
if (canvasId === "engageOverlay" && state.useProcessedFeed) {
|
| 13 |
const ctx = canvas.getContext("2d");
|
| 14 |
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
| 15 |
return;
|
| 16 |
}
|
| 17 |
|
|
|
|
|
|
|
| 18 |
const ctx = canvas.getContext("2d");
|
| 19 |
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
// Bounding boxes and labels are baked into the video by the backend.
|
| 22 |
+
// Frontend overlay drawing disabled — backend boxes only.
|
| 23 |
};
|
| 24 |
|
| 25 |
APP.ui.overlays.renderFrameOverlay = function () {
|
|
|
|
| 28 |
const canvas = $("#frameOverlay");
|
| 29 |
if (!canvas) return;
|
| 30 |
|
|
|
|
| 31 |
if (state.selectedId) {
|
| 32 |
const sel = (state.detections || []).filter(d => d.id === state.selectedId);
|
| 33 |
if (sel.length) {
|
|
|
|
| 35 |
return;
|
| 36 |
}
|
| 37 |
}
|
|
|
|
| 38 |
const ctx = canvas.getContext("2d");
|
| 39 |
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
| 40 |
};
|
|
|
|
| 43 |
const { state } = APP.core;
|
| 44 |
const { $ } = APP.core.utils;
|
| 45 |
|
| 46 |
+
// No overlays on first frame of video
|
| 47 |
const video = $("#videoEngage");
|
| 48 |
if (video && (video.currentTime < 0.25 || (video.paused && video.currentTime === 0))) {
|
| 49 |
const canvas = $("#engageOverlay");
|
frontend/style.css
CHANGED
|
@@ -81,7 +81,18 @@ header {
|
|
| 81 |
min-height: 0;
|
| 82 |
}
|
| 83 |
|
| 84 |
-
aside
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
main {
|
| 86 |
background: rgba(255, 255, 255, .02);
|
| 87 |
border: 1px solid var(--stroke);
|
|
@@ -541,10 +552,10 @@ input[type="number"]:focus {
|
|
| 541 |
.list {
|
| 542 |
display: flex;
|
| 543 |
flex-direction: column;
|
| 544 |
-
gap:
|
| 545 |
min-height: 160px;
|
| 546 |
-
max-height:
|
| 547 |
-
overflow: auto;
|
| 548 |
padding-right: 4px;
|
| 549 |
}
|
| 550 |
|
|
@@ -708,25 +719,21 @@ input[type="number"]:focus {
|
|
| 708 |
.frame-grid {
|
| 709 |
display: grid;
|
| 710 |
grid-template-columns: 1.6fr 0.9fr;
|
| 711 |
-
grid-template-rows: auto auto
|
| 712 |
gap: 12px;
|
| 713 |
min-height: 0;
|
| 714 |
}
|
| 715 |
|
| 716 |
-
/* Video panel on left
|
| 717 |
.frame-grid .panel-monitor {
|
| 718 |
grid-column: 1;
|
| 719 |
grid-row: 1 / 3;
|
| 720 |
}
|
| 721 |
|
|
|
|
| 722 |
.frame-grid .panel-summary {
|
| 723 |
grid-column: 2;
|
| 724 |
-
grid-row: 1;
|
| 725 |
-
}
|
| 726 |
-
|
| 727 |
-
.frame-grid .panel-features {
|
| 728 |
-
grid-column: 2;
|
| 729 |
-
grid-row: 2;
|
| 730 |
}
|
| 731 |
|
| 732 |
.intel {
|
|
@@ -894,22 +901,24 @@ input[type="number"]:focus {
|
|
| 894 |
|
| 895 |
/* Track Cards */
|
| 896 |
.track-card {
|
| 897 |
-
background: rgba(255, 255, 255, 0.
|
| 898 |
-
border: 1px solid
|
| 899 |
-
border-radius:
|
| 900 |
-
padding:
|
| 901 |
-
margin-bottom:
|
| 902 |
cursor: pointer;
|
| 903 |
-
transition: all 0.
|
| 904 |
}
|
| 905 |
|
| 906 |
.track-card:hover {
|
| 907 |
-
background: rgba(255, 255, 255, 0.
|
|
|
|
| 908 |
}
|
| 909 |
|
| 910 |
.track-card.active {
|
| 911 |
-
border-color:
|
| 912 |
-
background: rgba(34, 211, 238,
|
|
|
|
| 913 |
}
|
| 914 |
|
| 915 |
.track-card-header {
|
|
@@ -917,24 +926,68 @@ input[type="number"]:focus {
|
|
| 917 |
justify-content: space-between;
|
| 918 |
align-items: center;
|
| 919 |
font-weight: 600;
|
| 920 |
-
margin-bottom:
|
| 921 |
-
font-size:
|
| 922 |
-
color:
|
|
|
|
| 923 |
}
|
| 924 |
|
| 925 |
.track-card-meta {
|
| 926 |
-
font-size:
|
| 927 |
-
|
| 928 |
-
|
|
|
|
|
|
|
| 929 |
}
|
| 930 |
|
| 931 |
.track-card-body {
|
| 932 |
font-size: 11px;
|
| 933 |
-
line-height: 1.
|
| 934 |
-
color:
|
| 935 |
-
background: rgba(0, 0, 0, 0.
|
| 936 |
-
padding:
|
| 937 |
-
border-radius:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 938 |
}
|
| 939 |
|
| 940 |
.gpt-badge {
|
|
@@ -947,7 +1000,7 @@ input[type="number"]:focus {
|
|
| 947 |
}
|
| 948 |
|
| 949 |
.gpt-text {
|
| 950 |
-
color:
|
| 951 |
}
|
| 952 |
|
| 953 |
/* =========================================
|
|
@@ -955,9 +1008,9 @@ input[type="number"]:focus {
|
|
| 955 |
========================================= */
|
| 956 |
|
| 957 |
.panel-chat {
|
| 958 |
-
grid-column: 1;
|
| 959 |
grid-row: 3;
|
| 960 |
-
max-height:
|
| 961 |
display: flex;
|
| 962 |
flex-direction: column;
|
| 963 |
}
|
|
|
|
| 81 |
min-height: 0;
|
| 82 |
}
|
| 83 |
|
| 84 |
+
aside {
|
| 85 |
+
background: rgba(255, 255, 255, .02);
|
| 86 |
+
border: 1px solid var(--stroke);
|
| 87 |
+
border-radius: 16px;
|
| 88 |
+
box-shadow: var(--shadow);
|
| 89 |
+
overflow-y: auto;
|
| 90 |
+
overflow-x: hidden;
|
| 91 |
+
display: flex;
|
| 92 |
+
flex-direction: column;
|
| 93 |
+
min-height: 0;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
main {
|
| 97 |
background: rgba(255, 255, 255, .02);
|
| 98 |
border: 1px solid var(--stroke);
|
|
|
|
| 552 |
.list {
|
| 553 |
display: flex;
|
| 554 |
flex-direction: column;
|
| 555 |
+
gap: 6px;
|
| 556 |
min-height: 160px;
|
| 557 |
+
max-height: none;
|
| 558 |
+
overflow-y: auto;
|
| 559 |
padding-right: 4px;
|
| 560 |
}
|
| 561 |
|
|
|
|
| 719 |
.frame-grid {
|
| 720 |
display: grid;
|
| 721 |
grid-template-columns: 1.6fr 0.9fr;
|
| 722 |
+
grid-template-rows: auto auto;
|
| 723 |
gap: 12px;
|
| 724 |
min-height: 0;
|
| 725 |
}
|
| 726 |
|
| 727 |
+
/* Video panel on left */
|
| 728 |
.frame-grid .panel-monitor {
|
| 729 |
grid-column: 1;
|
| 730 |
grid-row: 1 / 3;
|
| 731 |
}
|
| 732 |
|
| 733 |
+
/* Track cards on right, spanning full height */
|
| 734 |
.frame-grid .panel-summary {
|
| 735 |
grid-column: 2;
|
| 736 |
+
grid-row: 1 / 3;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 737 |
}
|
| 738 |
|
| 739 |
.intel {
|
|
|
|
| 901 |
|
| 902 |
/* Track Cards */
|
| 903 |
.track-card {
|
| 904 |
+
background: rgba(255, 255, 255, 0.025);
|
| 905 |
+
border: 1px solid rgba(255, 255, 255, .10);
|
| 906 |
+
border-radius: 10px;
|
| 907 |
+
padding: 10px 12px;
|
| 908 |
+
margin-bottom: 0;
|
| 909 |
cursor: pointer;
|
| 910 |
+
transition: all 0.15s ease;
|
| 911 |
}
|
| 912 |
|
| 913 |
.track-card:hover {
|
| 914 |
+
background: rgba(255, 255, 255, 0.06);
|
| 915 |
+
border-color: rgba(255, 255, 255, .18);
|
| 916 |
}
|
| 917 |
|
| 918 |
.track-card.active {
|
| 919 |
+
border-color: rgba(124, 58, 237, .55);
|
| 920 |
+
background: linear-gradient(135deg, rgba(124, 58, 237, .12), rgba(34, 211, 238, .06));
|
| 921 |
+
box-shadow: 0 0 0 2px rgba(124, 58, 237, .15);
|
| 922 |
}
|
| 923 |
|
| 924 |
.track-card-header {
|
|
|
|
| 926 |
justify-content: space-between;
|
| 927 |
align-items: center;
|
| 928 |
font-weight: 600;
|
| 929 |
+
margin-bottom: 6px;
|
| 930 |
+
font-size: 12px;
|
| 931 |
+
color: rgba(255, 255, 255, .92);
|
| 932 |
+
letter-spacing: .02em;
|
| 933 |
}
|
| 934 |
|
| 935 |
.track-card-meta {
|
| 936 |
+
font-size: 10px;
|
| 937 |
+
font-family: var(--mono);
|
| 938 |
+
color: rgba(255, 255, 255, .50);
|
| 939 |
+
margin-bottom: 6px;
|
| 940 |
+
letter-spacing: .03em;
|
| 941 |
}
|
| 942 |
|
| 943 |
.track-card-body {
|
| 944 |
font-size: 11px;
|
| 945 |
+
line-height: 1.45;
|
| 946 |
+
color: rgba(255, 255, 255, .72);
|
| 947 |
+
background: rgba(0, 0, 0, 0.25);
|
| 948 |
+
padding: 8px 10px;
|
| 949 |
+
border-radius: 8px;
|
| 950 |
+
border: 1px solid rgba(255, 255, 255, .05);
|
| 951 |
+
}
|
| 952 |
+
|
| 953 |
+
.badgemini {
|
| 954 |
+
font-size: 10px;
|
| 955 |
+
font-weight: 600;
|
| 956 |
+
padding: 2px 6px;
|
| 957 |
+
border-radius: 6px;
|
| 958 |
+
letter-spacing: .03em;
|
| 959 |
+
line-height: 1;
|
| 960 |
+
}
|
| 961 |
+
|
| 962 |
+
.track-card-features {
|
| 963 |
+
margin-top: 8px;
|
| 964 |
+
border-top: 1px solid rgba(255, 255, 255, .06);
|
| 965 |
+
padding-top: 8px;
|
| 966 |
+
display: grid;
|
| 967 |
+
grid-template-columns: 1fr 1fr;
|
| 968 |
+
gap: 4px 12px;
|
| 969 |
+
}
|
| 970 |
+
|
| 971 |
+
.track-card-features .feat-row {
|
| 972 |
+
display: flex;
|
| 973 |
+
justify-content: space-between;
|
| 974 |
+
align-items: baseline;
|
| 975 |
+
font-size: 10px;
|
| 976 |
+
padding: 2px 0;
|
| 977 |
+
}
|
| 978 |
+
|
| 979 |
+
.track-card-features .feat-key {
|
| 980 |
+
font-family: var(--mono);
|
| 981 |
+
color: rgba(255, 255, 255, .50);
|
| 982 |
+
letter-spacing: .02em;
|
| 983 |
+
white-space: nowrap;
|
| 984 |
+
margin-right: 6px;
|
| 985 |
+
}
|
| 986 |
+
|
| 987 |
+
.track-card-features .feat-val {
|
| 988 |
+
color: rgba(255, 255, 255, .85);
|
| 989 |
+
text-align: right;
|
| 990 |
+
font-size: 10px;
|
| 991 |
}
|
| 992 |
|
| 993 |
.gpt-badge {
|
|
|
|
| 1000 |
}
|
| 1001 |
|
| 1002 |
.gpt-text {
|
| 1003 |
+
color: rgba(255, 255, 255, .78);
|
| 1004 |
}
|
| 1005 |
|
| 1006 |
/* =========================================
|
|
|
|
| 1008 |
========================================= */
|
| 1009 |
|
| 1010 |
.panel-chat {
|
| 1011 |
+
grid-column: 1 / -1;
|
| 1012 |
grid-row: 3;
|
| 1013 |
+
max-height: 260px;
|
| 1014 |
display: flex;
|
| 1015 |
flex-direction: column;
|
| 1016 |
}
|