tiena2cva commited on
Commit
b3d9fda
·
1 Parent(s): 2a5c0c7

refactor(report): improve code readability with consistent formatting and indentation

Browse files
Files changed (1) hide show
  1. web/report.js +180 -58
web/report.js CHANGED
@@ -3,7 +3,12 @@ import React, {
3
  useRef,
4
  useState,
5
  } from "https://esm.sh/react@18.2.0";
6
- import { formatValue, h, label, percent } from "./common.js?v=20260614-modular-app";
 
 
 
 
 
7
 
8
  function ReviewInsights({ result }) {
9
  if (!result) return null;
@@ -114,18 +119,22 @@ function issuesForRep(report, repId) {
114
  }
115
 
116
  function topIssue(report) {
117
- return [...issueList(report)].sort((a, b) => b.severity - a.severity)[0] || null;
 
 
118
  }
119
 
120
  function bestRep(report) {
121
  const analysisById = repAnalysisById(report);
122
- return (report.reps?.reps || [])
123
- .map((rep) => {
124
- const score = repScore(analysisById.get(rep.rep_id));
125
- return { rep, score };
126
- })
127
- .filter((entry) => entry.score !== null)
128
- .sort((a, b) => b.score - a.score)[0] || null;
 
 
129
  }
130
 
131
  function reportDuration(report) {
@@ -165,20 +174,26 @@ function qualityVerdict(report) {
165
  if (adjusted >= 0.82) {
166
  return {
167
  title: issue ? "Strong set, one watchpoint" : "Clean training set",
168
- detail: issue ? `${issueTitle(issue)} is the main limiter.` : "No clear form issue dominated the set.",
 
 
169
  score: adjusted,
170
  };
171
  }
172
  if (adjusted >= 0.66) {
173
  return {
174
  title: "Usable reps, focused fix",
175
- detail: issue ? `${issueTitle(issue)} should be fixed first.` : "Quality is workable, but keep reps controlled.",
 
 
176
  score: adjusted,
177
  };
178
  }
179
  return {
180
  title: "Needs cleaner reps",
181
- detail: issue ? `${issueTitle(issue)} is limiting this set.` : "Repeat with slower reps before progressing.",
 
 
182
  score: adjusted,
183
  };
184
  }
@@ -216,7 +231,9 @@ function ScoreBar({ labelText, value, detail }) {
216
 
217
  function pointPath(points) {
218
  return points
219
- .map(([x, y], index) => `${index ? "L" : "M"} ${x.toFixed(1)} ${y.toFixed(1)}`)
 
 
220
  .join(" ");
221
  }
222
 
@@ -225,9 +242,7 @@ function pointList(points) {
225
  }
226
 
227
  function metricAverage(rows) {
228
- const values = rows
229
- .map((row) => row.value)
230
- .filter((value) => value !== null);
231
  if (!values.length) return null;
232
  return values.reduce((total, value) => total + value, 0) / values.length;
233
  }
@@ -252,7 +267,12 @@ function MetricRadar({ rows }) {
252
  row,
253
  x: center + Math.cos(angle) * (radius + 28),
254
  y: center + Math.sin(angle) * (radius + 28),
255
- anchor: Math.cos(angle) > 0.25 ? "start" : Math.cos(angle) < -0.25 ? "end" : "middle",
 
 
 
 
 
256
  };
257
  });
258
  const average = metricAverage(rows);
@@ -352,7 +372,8 @@ function RepTrendChart({ reps, metricKey, labelText, className }) {
352
  .map((rep, index) => {
353
  const value = metricScore(rep[metricKey]);
354
  if (value === null) return null;
355
- const x = pad + (reps.length <= 1 ? 0.5 : index / (reps.length - 1)) * innerWidth;
 
356
  const y = pad + (1 - value) * innerHeight;
357
  return { rep, value, x, y };
358
  })
@@ -422,7 +443,12 @@ function RepTrendChart({ reps, metricKey, labelText, className }) {
422
  h("text", { className: "trend-axis-label", x: pad, y: height - 4 }, "R1"),
423
  h(
424
  "text",
425
- { className: "trend-axis-label", x: width - pad, y: height - 4, textAnchor: "end" },
 
 
 
 
 
426
  reps.length ? `R${reps[reps.length - 1].rep_id}` : "R0",
427
  ),
428
  ),
@@ -453,7 +479,10 @@ function RepQualityStrip({ reps }) {
453
  ),
454
  h(
455
  "div",
456
- { className: "rep-quality-strip", "aria-label": "Average quality by rep" },
 
 
 
457
  scores.map(({ rep, score }) =>
458
  h(
459
  "div",
@@ -464,7 +493,10 @@ function RepQualityStrip({ reps }) {
464
  },
465
  h("span", {
466
  style: {
467
- height: score === null ? "8%" : `${Math.max(8, Math.round(score * 100))}%`,
 
 
 
468
  },
469
  "aria-hidden": "true",
470
  }),
@@ -496,9 +528,7 @@ function CoachOverview({ result }) {
496
  );
497
  const isFallback = coachSource.startsWith("fallback");
498
  const cueNow =
499
- summary.top_fixes?.[0] ||
500
- summary.next_session_plan?.[0] ||
501
- summary.summary;
502
 
503
  return h(
504
  "section",
@@ -530,21 +560,26 @@ function CoachOverview({ result }) {
530
  "A conservative fallback summary was used because the generated summary was unavailable or did not pass verification.",
531
  )
532
  : null,
533
- verifierBypassed
534
- ? h(
535
- "p",
536
- { className: "system-note warning" },
537
- "Verifier is disabled, so the coach summary is shown without automated verification.",
538
- )
539
- : null,
540
  )
541
  : null,
542
  h(
543
  "div",
544
  { className: "coach-overview-grid" },
545
- h(NoteList, { title: "Fix first", items: summary.top_fixes, className: "coach-note priority" }),
546
- h(NoteList, { title: "Next session", items: summary.next_session_plan, className: "coach-note next" }),
547
- h(NoteList, { title: "Keep", items: summary.what_looked_good, className: "coach-note" }),
 
 
 
 
 
 
 
 
 
 
 
 
548
  h(NoteList, {
549
  title: "Variation vs issue",
550
  items: summary.valid_variation_vs_issue,
@@ -572,7 +607,11 @@ function RepTimeline({
572
  const isPlayback = typeof onRepSelect === "function";
573
  const playheadLeft = `${Math.max(0, Math.min(100, (playbackTime / duration) * 100))}%`;
574
  if (!reps.length) {
575
- return h("p", { className: "empty-copy" }, "No complete reps were detected.");
 
 
 
 
576
  }
577
 
578
  return h(
@@ -614,7 +653,8 @@ function RepTimeline({
614
  if (isPlayback) {
615
  segmentProps.type = "button";
616
  segmentProps.onClick = () => onRepSelect(rep);
617
- segmentProps["aria-label"] = `Play from rep ${rep.rep_id}, ${timeRange(rep.start_sec, rep.end_sec)}`;
 
618
  segmentProps["aria-pressed"] = activeRepId === rep.rep_id;
619
  }
620
  return h(
@@ -663,7 +703,8 @@ function SummaryTab({ result }) {
663
  (issue
664
  ? issueEvidence(issue)
665
  : "Repeat the next set with the same controlled tempo.");
666
- const nextSession = summary.next_session_plan?.[0] || "Repeat the set with controlled reps.";
 
667
 
668
  return h(
669
  "section",
@@ -688,7 +729,9 @@ function SummaryTab({ result }) {
688
  : "no clear issue",
689
  issues.length ? severityLevel(issue) : "strong",
690
  ),
691
- metaChip(provider === "local_transformers" ? "local coach" : provider),
 
 
692
  ),
693
  ),
694
  h(
@@ -749,8 +792,14 @@ function SummaryTab({ result }) {
749
  "div",
750
  { className: "note-grid report-notes" },
751
  h(NoteList, { title: "What you did", items: summary.what_you_did }),
752
- h(NoteList, { title: "What looked good", items: summary.what_looked_good }),
753
- h(NoteList, { title: "What changed", items: summary.what_changed_across_reps }),
 
 
 
 
 
 
754
  ),
755
  warnings.length
756
  ? h(
@@ -786,7 +835,11 @@ function MetricsTab({ result }) {
786
  "section",
787
  { className: "summary metrics-panel" },
788
  h("h2", null, "Movement metrics"),
789
- h("p", null, "Charts show which qualities shaped the coach note and where the set started to drift."),
 
 
 
 
790
  h(
791
  "div",
792
  { className: "metric-chart-hero" },
@@ -802,7 +855,11 @@ function MetricsTab({ result }) {
802
  value: row.value,
803
  }),
804
  )
805
- : h("p", { className: "empty-copy" }, "No aggregate scores available."),
 
 
 
 
806
  ),
807
  ),
808
  trendSpecs.length
@@ -864,14 +921,24 @@ function MetricsTab({ result }) {
864
  );
865
  }
866
 
867
- function RepDetailPanel({ report, rep, result, playbackTime, compact = false }) {
 
 
 
 
 
 
868
  if (!rep) {
869
  return h(
870
  "aside",
871
  { className: `rep-detail-panel${compact ? " compact" : ""}` },
872
  h("span", { className: "rep-detail-kicker" }, "Current rep"),
873
  h("strong", null, "No rep selected"),
874
- h("p", null, "Play the video or click a rep segment to inspect its metrics."),
 
 
 
 
875
  );
876
  }
877
 
@@ -879,8 +946,14 @@ function RepDetailPanel({ report, rep, result, playbackTime, compact = false })
879
  const repIssues = issuesForRep(report, rep.rep_id);
880
  const score = repScore(analysis);
881
  const duration = Math.max(0, (rep.end_sec || 0) - (rep.start_sec || 0));
882
- const firstHalfDuration = Math.max(0, (rep.mid_sec || 0) - (rep.start_sec || 0));
883
- const secondHalfDuration = Math.max(0, (rep.end_sec || 0) - (rep.mid_sec || 0));
 
 
 
 
 
 
884
  const metricRows = [
885
  { name: "ROM", value: metricScore(analysis?.range_of_motion_score) },
886
  { name: "Stability", value: metricScore(analysis?.stability_score) },
@@ -931,7 +1004,10 @@ function RepDetailPanel({ report, rep, result, playbackTime, compact = false })
931
  h("strong", null, `Rep ${rep.rep_id}`),
932
  h("small", null, timeRange(rep.start_sec, rep.end_sec)),
933
  ),
934
- metaChip(score === null ? "no score" : percent(score), scoreTone(score)),
 
 
 
935
  ),
936
  h(
937
  "div",
@@ -946,7 +1022,13 @@ function RepDetailPanel({ report, rep, result, playbackTime, compact = false })
946
  "div",
947
  { className: "rep-fact" },
948
  h("span", null, "Focus"),
949
- h("strong", null, weakestMetric ? `${weakestMetric.name} ${percent(weakestMetric.value)}` : "n/a"),
 
 
 
 
 
 
950
  ),
951
  h(
952
  "div",
@@ -958,7 +1040,11 @@ function RepDetailPanel({ report, rep, result, playbackTime, compact = false })
958
  "div",
959
  { className: "rep-fact" },
960
  h("span", null, "Issues"),
961
- h("strong", null, repIssues.length ? String(repIssues.length) : "clear"),
 
 
 
 
962
  ),
963
  ),
964
  h(
@@ -1012,7 +1098,11 @@ function RepDetailPanel({ report, rep, result, playbackTime, compact = false })
1012
  { className: "rep-issue-meta" },
1013
  h("span", null, issueFocus(primaryIssue)),
1014
  h("span", null, issueClipText(result, primaryIssue, 0)),
1015
- h("span", null, `confidence ${percent(primaryIssue.evidence?.confidence)}`),
 
 
 
 
1016
  repIssues.length > 1
1017
  ? h("span", null, `+${repIssues.length - 1} more`)
1018
  : null,
@@ -1056,7 +1146,11 @@ function RepDetailPanel({ report, rep, result, playbackTime, compact = false })
1056
  "div",
1057
  { className: "rep-fact" },
1058
  h("span", null, "Tempo split"),
1059
- h("strong", null, `${firstHalfDuration.toFixed(2)}/${secondHalfDuration.toFixed(2)}s`),
 
 
 
 
1060
  ),
1061
  h(
1062
  "div",
@@ -1068,13 +1162,23 @@ function RepDetailPanel({ report, rep, result, playbackTime, compact = false })
1068
  "div",
1069
  { className: "rep-fact" },
1070
  h("span", null, "Focus"),
1071
- h("strong", null, weakestMetric ? `${weakestMetric.name} ${percent(weakestMetric.value)}` : "n/a"),
 
 
 
 
 
 
1072
  ),
1073
  h(
1074
  "div",
1075
  { className: "rep-fact" },
1076
  h("span", null, "Issues"),
1077
- h("strong", null, repIssues.length ? String(repIssues.length) : "clear"),
 
 
 
 
1078
  ),
1079
  ),
1080
  h(
@@ -1134,7 +1238,10 @@ function RepDetailPanel({ report, rep, result, playbackTime, compact = false })
1134
  ? repIssues.map((issue, index) =>
1135
  h(
1136
  "article",
1137
- { className: `issue-mini ${severityLevel(issue)}`, key: `${issue.issue}-${index}` },
 
 
 
1138
  h(
1139
  "div",
1140
  { className: "issue-mini-head" },
@@ -1150,7 +1257,11 @@ function RepDetailPanel({ report, rep, result, playbackTime, compact = false })
1150
  "div",
1151
  { className: "timeline-meta" },
1152
  h("span", null, issueClipText(result, issue, index)),
1153
- h("span", null, `evidence ${percent(issue.evidence?.confidence)}`),
 
 
 
 
1154
  ),
1155
  ),
1156
  )
@@ -1217,7 +1328,11 @@ function ReplayReviewPanel({ result, videoSrc, className = "" }) {
1217
  "div",
1218
  { className: "timeline-video-placeholder" },
1219
  h("strong", null, "No replay video available"),
1220
- h("span", null, "Upload a clip or use a run with an annotated video to sync the timeline."),
 
 
 
 
1221
  ),
1222
  ),
1223
  h(RepDetailPanel, {
@@ -1277,7 +1392,10 @@ function RepsTab({ result }) {
1277
  "div",
1278
  { className: "rep-card-head" },
1279
  h("strong", null, `Rep ${rep.rep_id}`),
1280
- metaChip(score === null ? "no score" : percent(score), scoreTone(score)),
 
 
 
1281
  ),
1282
  h("span", null, timeRange(rep.start_sec, rep.end_sec)),
1283
  h("span", null, `frames ${rep.start_frame}-${rep.end_frame}`),
@@ -1287,7 +1405,11 @@ function RepsTab({ result }) {
1287
  { className: "pill-row compact" },
1288
  repIssues.length
1289
  ? repIssues.map((issue, index) =>
1290
- metaChip(issueTitle(issue), `${severityLevel(issue)}`, `${issue.issue}-${index}`),
 
 
 
 
1291
  )
1292
  : metaChip("no issue marker", "strong"),
1293
  ),
 
3
  useRef,
4
  useState,
5
  } from "https://esm.sh/react@18.2.0";
6
+ import {
7
+ formatValue,
8
+ h,
9
+ label,
10
+ percent,
11
+ } from "./common.js?v=20260614-modular-app";
12
 
13
  function ReviewInsights({ result }) {
14
  if (!result) return null;
 
119
  }
120
 
121
  function topIssue(report) {
122
+ return (
123
+ [...issueList(report)].sort((a, b) => b.severity - a.severity)[0] || null
124
+ );
125
  }
126
 
127
  function bestRep(report) {
128
  const analysisById = repAnalysisById(report);
129
+ return (
130
+ (report.reps?.reps || [])
131
+ .map((rep) => {
132
+ const score = repScore(analysisById.get(rep.rep_id));
133
+ return { rep, score };
134
+ })
135
+ .filter((entry) => entry.score !== null)
136
+ .sort((a, b) => b.score - a.score)[0] || null
137
+ );
138
  }
139
 
140
  function reportDuration(report) {
 
174
  if (adjusted >= 0.82) {
175
  return {
176
  title: issue ? "Strong set, one watchpoint" : "Clean training set",
177
+ detail: issue
178
+ ? `${issueTitle(issue)} is the main limiter.`
179
+ : "No clear form issue dominated the set.",
180
  score: adjusted,
181
  };
182
  }
183
  if (adjusted >= 0.66) {
184
  return {
185
  title: "Usable reps, focused fix",
186
+ detail: issue
187
+ ? `${issueTitle(issue)} should be fixed first.`
188
+ : "Quality is workable, but keep reps controlled.",
189
  score: adjusted,
190
  };
191
  }
192
  return {
193
  title: "Needs cleaner reps",
194
+ detail: issue
195
+ ? `${issueTitle(issue)} is limiting this set.`
196
+ : "Repeat with slower reps before progressing.",
197
  score: adjusted,
198
  };
199
  }
 
231
 
232
  function pointPath(points) {
233
  return points
234
+ .map(
235
+ ([x, y], index) => `${index ? "L" : "M"} ${x.toFixed(1)} ${y.toFixed(1)}`,
236
+ )
237
  .join(" ");
238
  }
239
 
 
242
  }
243
 
244
  function metricAverage(rows) {
245
+ const values = rows.map((row) => row.value).filter((value) => value !== null);
 
 
246
  if (!values.length) return null;
247
  return values.reduce((total, value) => total + value, 0) / values.length;
248
  }
 
267
  row,
268
  x: center + Math.cos(angle) * (radius + 28),
269
  y: center + Math.sin(angle) * (radius + 28),
270
+ anchor:
271
+ Math.cos(angle) > 0.25
272
+ ? "start"
273
+ : Math.cos(angle) < -0.25
274
+ ? "end"
275
+ : "middle",
276
  };
277
  });
278
  const average = metricAverage(rows);
 
372
  .map((rep, index) => {
373
  const value = metricScore(rep[metricKey]);
374
  if (value === null) return null;
375
+ const x =
376
+ pad + (reps.length <= 1 ? 0.5 : index / (reps.length - 1)) * innerWidth;
377
  const y = pad + (1 - value) * innerHeight;
378
  return { rep, value, x, y };
379
  })
 
443
  h("text", { className: "trend-axis-label", x: pad, y: height - 4 }, "R1"),
444
  h(
445
  "text",
446
+ {
447
+ className: "trend-axis-label",
448
+ x: width - pad,
449
+ y: height - 4,
450
+ textAnchor: "end",
451
+ },
452
  reps.length ? `R${reps[reps.length - 1].rep_id}` : "R0",
453
  ),
454
  ),
 
479
  ),
480
  h(
481
  "div",
482
+ {
483
+ className: "rep-quality-strip",
484
+ "aria-label": "Average quality by rep",
485
+ },
486
  scores.map(({ rep, score }) =>
487
  h(
488
  "div",
 
493
  },
494
  h("span", {
495
  style: {
496
+ height:
497
+ score === null
498
+ ? "8%"
499
+ : `${Math.max(8, Math.round(score * 100))}%`,
500
  },
501
  "aria-hidden": "true",
502
  }),
 
528
  );
529
  const isFallback = coachSource.startsWith("fallback");
530
  const cueNow =
531
+ summary.top_fixes?.[0] || summary.next_session_plan?.[0] || summary.summary;
 
 
532
 
533
  return h(
534
  "section",
 
560
  "A conservative fallback summary was used because the generated summary was unavailable or did not pass verification.",
561
  )
562
  : null,
 
 
 
 
 
 
 
563
  )
564
  : null,
565
  h(
566
  "div",
567
  { className: "coach-overview-grid" },
568
+ h(NoteList, {
569
+ title: "Fix first",
570
+ items: summary.top_fixes,
571
+ className: "coach-note priority",
572
+ }),
573
+ h(NoteList, {
574
+ title: "Next session",
575
+ items: summary.next_session_plan,
576
+ className: "coach-note next",
577
+ }),
578
+ h(NoteList, {
579
+ title: "Keep",
580
+ items: summary.what_looked_good,
581
+ className: "coach-note",
582
+ }),
583
  h(NoteList, {
584
  title: "Variation vs issue",
585
  items: summary.valid_variation_vs_issue,
 
607
  const isPlayback = typeof onRepSelect === "function";
608
  const playheadLeft = `${Math.max(0, Math.min(100, (playbackTime / duration) * 100))}%`;
609
  if (!reps.length) {
610
+ return h(
611
+ "p",
612
+ { className: "empty-copy" },
613
+ "No complete reps were detected.",
614
+ );
615
  }
616
 
617
  return h(
 
653
  if (isPlayback) {
654
  segmentProps.type = "button";
655
  segmentProps.onClick = () => onRepSelect(rep);
656
+ segmentProps["aria-label"] =
657
+ `Play from rep ${rep.rep_id}, ${timeRange(rep.start_sec, rep.end_sec)}`;
658
  segmentProps["aria-pressed"] = activeRepId === rep.rep_id;
659
  }
660
  return h(
 
703
  (issue
704
  ? issueEvidence(issue)
705
  : "Repeat the next set with the same controlled tempo.");
706
+ const nextSession =
707
+ summary.next_session_plan?.[0] || "Repeat the set with controlled reps.";
708
 
709
  return h(
710
  "section",
 
729
  : "no clear issue",
730
  issues.length ? severityLevel(issue) : "strong",
731
  ),
732
+ metaChip(
733
+ provider === "local_transformers" ? "local coach" : provider,
734
+ ),
735
  ),
736
  ),
737
  h(
 
792
  "div",
793
  { className: "note-grid report-notes" },
794
  h(NoteList, { title: "What you did", items: summary.what_you_did }),
795
+ h(NoteList, {
796
+ title: "What looked good",
797
+ items: summary.what_looked_good,
798
+ }),
799
+ h(NoteList, {
800
+ title: "What changed",
801
+ items: summary.what_changed_across_reps,
802
+ }),
803
  ),
804
  warnings.length
805
  ? h(
 
835
  "section",
836
  { className: "summary metrics-panel" },
837
  h("h2", null, "Movement metrics"),
838
+ h(
839
+ "p",
840
+ null,
841
+ "Charts show which qualities shaped the coach note and where the set started to drift.",
842
+ ),
843
  h(
844
  "div",
845
  { className: "metric-chart-hero" },
 
855
  value: row.value,
856
  }),
857
  )
858
+ : h(
859
+ "p",
860
+ { className: "empty-copy" },
861
+ "No aggregate scores available.",
862
+ ),
863
  ),
864
  ),
865
  trendSpecs.length
 
921
  );
922
  }
923
 
924
+ function RepDetailPanel({
925
+ report,
926
+ rep,
927
+ result,
928
+ playbackTime,
929
+ compact = false,
930
+ }) {
931
  if (!rep) {
932
  return h(
933
  "aside",
934
  { className: `rep-detail-panel${compact ? " compact" : ""}` },
935
  h("span", { className: "rep-detail-kicker" }, "Current rep"),
936
  h("strong", null, "No rep selected"),
937
+ h(
938
+ "p",
939
+ null,
940
+ "Play the video or click a rep segment to inspect its metrics.",
941
+ ),
942
  );
943
  }
944
 
 
946
  const repIssues = issuesForRep(report, rep.rep_id);
947
  const score = repScore(analysis);
948
  const duration = Math.max(0, (rep.end_sec || 0) - (rep.start_sec || 0));
949
+ const firstHalfDuration = Math.max(
950
+ 0,
951
+ (rep.mid_sec || 0) - (rep.start_sec || 0),
952
+ );
953
+ const secondHalfDuration = Math.max(
954
+ 0,
955
+ (rep.end_sec || 0) - (rep.mid_sec || 0),
956
+ );
957
  const metricRows = [
958
  { name: "ROM", value: metricScore(analysis?.range_of_motion_score) },
959
  { name: "Stability", value: metricScore(analysis?.stability_score) },
 
1004
  h("strong", null, `Rep ${rep.rep_id}`),
1005
  h("small", null, timeRange(rep.start_sec, rep.end_sec)),
1006
  ),
1007
+ metaChip(
1008
+ score === null ? "no score" : percent(score),
1009
+ scoreTone(score),
1010
+ ),
1011
  ),
1012
  h(
1013
  "div",
 
1022
  "div",
1023
  { className: "rep-fact" },
1024
  h("span", null, "Focus"),
1025
+ h(
1026
+ "strong",
1027
+ null,
1028
+ weakestMetric
1029
+ ? `${weakestMetric.name} ${percent(weakestMetric.value)}`
1030
+ : "n/a",
1031
+ ),
1032
  ),
1033
  h(
1034
  "div",
 
1040
  "div",
1041
  { className: "rep-fact" },
1042
  h("span", null, "Issues"),
1043
+ h(
1044
+ "strong",
1045
+ null,
1046
+ repIssues.length ? String(repIssues.length) : "clear",
1047
+ ),
1048
  ),
1049
  ),
1050
  h(
 
1098
  { className: "rep-issue-meta" },
1099
  h("span", null, issueFocus(primaryIssue)),
1100
  h("span", null, issueClipText(result, primaryIssue, 0)),
1101
+ h(
1102
+ "span",
1103
+ null,
1104
+ `confidence ${percent(primaryIssue.evidence?.confidence)}`,
1105
+ ),
1106
  repIssues.length > 1
1107
  ? h("span", null, `+${repIssues.length - 1} more`)
1108
  : null,
 
1146
  "div",
1147
  { className: "rep-fact" },
1148
  h("span", null, "Tempo split"),
1149
+ h(
1150
+ "strong",
1151
+ null,
1152
+ `${firstHalfDuration.toFixed(2)}/${secondHalfDuration.toFixed(2)}s`,
1153
+ ),
1154
  ),
1155
  h(
1156
  "div",
 
1162
  "div",
1163
  { className: "rep-fact" },
1164
  h("span", null, "Focus"),
1165
+ h(
1166
+ "strong",
1167
+ null,
1168
+ weakestMetric
1169
+ ? `${weakestMetric.name} ${percent(weakestMetric.value)}`
1170
+ : "n/a",
1171
+ ),
1172
  ),
1173
  h(
1174
  "div",
1175
  { className: "rep-fact" },
1176
  h("span", null, "Issues"),
1177
+ h(
1178
+ "strong",
1179
+ null,
1180
+ repIssues.length ? String(repIssues.length) : "clear",
1181
+ ),
1182
  ),
1183
  ),
1184
  h(
 
1238
  ? repIssues.map((issue, index) =>
1239
  h(
1240
  "article",
1241
+ {
1242
+ className: `issue-mini ${severityLevel(issue)}`,
1243
+ key: `${issue.issue}-${index}`,
1244
+ },
1245
  h(
1246
  "div",
1247
  { className: "issue-mini-head" },
 
1257
  "div",
1258
  { className: "timeline-meta" },
1259
  h("span", null, issueClipText(result, issue, index)),
1260
+ h(
1261
+ "span",
1262
+ null,
1263
+ `evidence ${percent(issue.evidence?.confidence)}`,
1264
+ ),
1265
  ),
1266
  ),
1267
  )
 
1328
  "div",
1329
  { className: "timeline-video-placeholder" },
1330
  h("strong", null, "No replay video available"),
1331
+ h(
1332
+ "span",
1333
+ null,
1334
+ "Upload a clip or use a run with an annotated video to sync the timeline.",
1335
+ ),
1336
  ),
1337
  ),
1338
  h(RepDetailPanel, {
 
1392
  "div",
1393
  { className: "rep-card-head" },
1394
  h("strong", null, `Rep ${rep.rep_id}`),
1395
+ metaChip(
1396
+ score === null ? "no score" : percent(score),
1397
+ scoreTone(score),
1398
+ ),
1399
  ),
1400
  h("span", null, timeRange(rep.start_sec, rep.end_sec)),
1401
  h("span", null, `frames ${rep.start_frame}-${rep.end_frame}`),
 
1405
  { className: "pill-row compact" },
1406
  repIssues.length
1407
  ? repIssues.map((issue, index) =>
1408
+ metaChip(
1409
+ issueTitle(issue),
1410
+ `${severityLevel(issue)}`,
1411
+ `${issue.issue}-${index}`,
1412
+ ),
1413
  )
1414
  : metaChip("no issue marker", "strong"),
1415
  ),