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 CHANGED
@@ -158,38 +158,14 @@
158
 
159
 
160
 
161
- <div class="panel panel-features">
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:8px;">
191
  <!-- Cards injected here -->
192
- <div style="font-style:italic; color:var(--text-dim); text-align:center; margin-top:20px;">
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.0025) continue;
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 trackCount = $("#trackCount"); // Correct ID
 
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
- frameTrackList.innerHTML = '<div style="font-style:italic; color:var(--text-dim); text-align:center; margin-top:20px;">No objects tracked.</div>';
 
 
24
  return;
25
  }
26
 
27
- // Deterministic sort: ASSESSED first (by threat score), then UNASSESSED, then STALE
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 (Depth)`;
48
  } else if (det.gpt_distance_m) {
49
- rangeStr = `~${det.gpt_distance_m}m (est.)`;
50
  } else if (det.baseRange_m) {
51
- rangeStr = `${Math.round(det.baseRange_m)}m (Area)`;
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
- const desc = det.gpt_description
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
- RANGE: ${rangeStr} | BEARING: ${bearingStr}
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 { now, $ } = APP.core.utils;
6
- const { defaultAimpoint } = APP.core.physics;
7
 
8
  const canvas = $(`#${canvasId}`);
9
  if (!canvas) return;
10
 
11
- // Avoid double-drawing: If we are in Engage view (engageOverlay) and viewing the processed feed
12
- // (which has baked-in boxes from backend), we should NOT draw frontend predicted boxes.
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, w, h);
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 (aimpoints) disabled — backend boxes only.
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
- // User request: No overlays on first frame of video
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: 8px;
545
  min-height: 160px;
546
- max-height: 320px;
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 auto;
712
  gap: 12px;
713
  min-height: 0;
714
  }
715
 
716
- /* Video panel on left, track cards on right */
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.03);
898
- border: 1px solid var(--border-color);
899
- border-radius: 4px;
900
- padding: 8px;
901
- margin-bottom: 8px;
902
  cursor: pointer;
903
- transition: all 0.2s;
904
  }
905
 
906
  .track-card:hover {
907
- background: rgba(255, 255, 255, 0.08);
 
908
  }
909
 
910
  .track-card.active {
911
- border-color: var(--accent);
912
- background: rgba(34, 211, 238, 0.1);
 
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: 4px;
921
- font-size: 13px;
922
- color: var(--text-color);
 
923
  }
924
 
925
  .track-card-meta {
926
- font-size: 11px;
927
- color: var(--text-dim);
928
- margin-bottom: 4px;
 
 
929
  }
930
 
931
  .track-card-body {
932
  font-size: 11px;
933
- line-height: 1.4;
934
- color: #ccc;
935
- background: rgba(0, 0, 0, 0.2);
936
- padding: 6px;
937
- border-radius: 4px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
938
  }
939
 
940
  .gpt-badge {
@@ -947,7 +1000,7 @@ input[type="number"]:focus {
947
  }
948
 
949
  .gpt-text {
950
- color: #e0e0e0;
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: 280px;
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
  }