Pozify / web /report.js
tiena2cva's picture
refactor(report): improve code readability with consistent formatting and indentation
b3d9fda
Raw
History Blame Contribute Delete
50 kB
import React, {
useEffect,
useRef,
useState,
} from "https://esm.sh/react@18.2.0";
import {
formatValue,
h,
label,
percent,
} from "./common.js?v=20260614-modular-app";
function ReviewInsights({ result }) {
if (!result) return null;
const report = result.report;
const warnings = report.video_manifest?.quality_warnings || [];
const exercise = label(report.exercise?.exercise || "movement");
const repCount = report.reps?.reps?.length || 0;
const issues = report.issue_markers?.issues || [];
const confidence = percent(report.exercise?.confidence);
const issueText = issues.length
? `${issues.length} coaching moment${issues.length === 1 ? "" : "s"}`
: "No clear form issues";
return h(
"div",
{ className: "scan-insights", "aria-label": "Scan results" },
h(
"article",
{ className: "scan-insight" },
h("span", null, "Movement"),
h("strong", null, exercise),
h("small", null, `confidence ${confidence}`),
),
h(
"article",
{ className: "scan-insight" },
h("span", null, "Reps counted"),
h("strong", null, String(repCount)),
h("small", null, repCount === 1 ? "clean rep detected" : "reps detected"),
),
h(
"article",
{ className: "scan-insight" },
h("span", null, "Coach review"),
h("strong", null, issueText),
h(
"small",
null,
issues.length ? "tap Issues for clips" : "nothing major stood out",
),
),
h(
"article",
{ className: "scan-insight" },
h("span", null, "Video quality"),
h("strong", null, warnings.length ? "Needs a quick note" : "Looks good"),
h(
"small",
null,
warnings.length
? warnings.map(label).join(", ")
: "clear enough to coach",
),
),
);
}
function metricScore(value) {
return typeof value === "number" && Number.isFinite(value)
? Math.max(0, Math.min(1, value))
: null;
}
function metricScoreRows(report) {
const aggregate = report.rep_analysis?.aggregate_metrics || {};
return [
["ROM", aggregate.avg_rom_score],
["Stability", aggregate.avg_stability_score],
["Symmetry", aggregate.avg_symmetry_score],
["Pose coverage", aggregate.pose_valid_ratio],
]
.map(([name, value]) => ({ name, value: metricScore(value) }))
.filter((item) => item.value !== null);
}
function scoreTone(score) {
if (score === null) return "neutral";
if (score >= 0.8) return "strong";
if (score >= 0.62) return "watch";
return "risk";
}
function repScore(item) {
if (!item) return null;
const values = [
item.range_of_motion_score,
item.stability_score,
item.symmetry_score,
]
.map(metricScore)
.filter((value) => value !== null);
if (!values.length) return null;
return values.reduce((total, value) => total + value, 0) / values.length;
}
function repAnalysisById(report) {
return new Map(
(report.rep_analysis?.items || []).map((item) => [item.rep_id, item]),
);
}
function issueList(report) {
return report.issue_markers?.issues || [];
}
function issuesForRep(report, repId) {
return issueList(report).filter((issue) => issue.rep_id === repId);
}
function topIssue(report) {
return (
[...issueList(report)].sort((a, b) => b.severity - a.severity)[0] || null
);
}
function bestRep(report) {
const analysisById = repAnalysisById(report);
return (
(report.reps?.reps || [])
.map((rep) => {
const score = repScore(analysisById.get(rep.rep_id));
return { rep, score };
})
.filter((entry) => entry.score !== null)
.sort((a, b) => b.score - a.score)[0] || null
);
}
function reportDuration(report) {
const manifestDuration = report.video_manifest?.duration_sec;
const repEnd = Math.max(
0,
...(report.reps?.reps || []).map((rep) => rep.end_sec || 0),
...issueList(report).map((issue) => issue.end_sec || 0),
);
return Math.max(manifestDuration || 0, repEnd, 1);
}
function repAtTime(report, playbackTime) {
return (
(report.reps?.reps || []).find(
(rep) => playbackTime >= rep.start_sec && playbackTime <= rep.end_sec,
) || null
);
}
function qualityVerdict(report) {
const scores = metricScoreRows(report)
.filter((row) => row.name !== "Pose coverage")
.map((row) => row.value);
const average = scores.length
? scores.reduce((total, score) => total + score, 0) / scores.length
: null;
const issue = topIssue(report);
if (average === null) {
return {
title: "Report ready",
detail: "Coach notes are grounded in the scan.",
score: null,
};
}
const adjusted = Math.max(0, average - (issue?.severity || 0) * 0.08);
if (adjusted >= 0.82) {
return {
title: issue ? "Strong set, one watchpoint" : "Clean training set",
detail: issue
? `${issueTitle(issue)} is the main limiter.`
: "No clear form issue dominated the set.",
score: adjusted,
};
}
if (adjusted >= 0.66) {
return {
title: "Usable reps, focused fix",
detail: issue
? `${issueTitle(issue)} should be fixed first.`
: "Quality is workable, but keep reps controlled.",
score: adjusted,
};
}
return {
title: "Needs cleaner reps",
detail: issue
? `${issueTitle(issue)} is limiting this set.`
: "Repeat with slower reps before progressing.",
score: adjusted,
};
}
function timeRange(start, end) {
return `${Number(start || 0).toFixed(2)}s-${Number(end || 0).toFixed(2)}s`;
}
function metaChip(text, tone = "", key = undefined) {
const props = { className: `meta-chip${tone ? ` ${tone}` : ""}` };
if (key !== undefined) props.key = key;
return h("span", props, text);
}
function ScoreBar({ labelText, value, detail }) {
const score = metricScore(value);
const width = score === null ? "0%" : `${Math.round(score * 100)}%`;
return h(
"div",
{ className: `score-card ${scoreTone(score)}` },
h(
"div",
{ className: "score-card-head" },
h("span", null, labelText),
h("strong", null, score === null ? "n/a" : percent(score)),
),
h(
"span",
{ className: "score-track", "aria-hidden": "true" },
h("span", { className: "score-fill", style: { width } }),
),
detail ? h("small", null, detail) : null,
);
}
function pointPath(points) {
return points
.map(
([x, y], index) => `${index ? "L" : "M"} ${x.toFixed(1)} ${y.toFixed(1)}`,
)
.join(" ");
}
function pointList(points) {
return points.map(([x, y]) => `${x.toFixed(1)},${y.toFixed(1)}`).join(" ");
}
function metricAverage(rows) {
const values = rows.map((row) => row.value).filter((value) => value !== null);
if (!values.length) return null;
return values.reduce((total, value) => total + value, 0) / values.length;
}
function MetricRadar({ rows }) {
if (!rows.length) return null;
const size = 220;
const center = size / 2;
const radius = 72;
const rings = [0.33, 0.66, 1];
const points = rows.map((row, index) => {
const angle = -Math.PI / 2 + (index / rows.length) * Math.PI * 2;
const score = row.value ?? 0;
return [
center + Math.cos(angle) * radius * score,
center + Math.sin(angle) * radius * score,
];
});
const labelPoints = rows.map((row, index) => {
const angle = -Math.PI / 2 + (index / rows.length) * Math.PI * 2;
return {
row,
x: center + Math.cos(angle) * (radius + 28),
y: center + Math.sin(angle) * (radius + 28),
anchor:
Math.cos(angle) > 0.25
? "start"
: Math.cos(angle) < -0.25
? "end"
: "middle",
};
});
const average = metricAverage(rows);
return h(
"article",
{ className: "metric-radar-card" },
h(
"div",
{ className: "metric-chart-head" },
h(
"div",
null,
h("span", null, "Aggregate profile"),
h("strong", null, average === null ? "n/a" : percent(average)),
),
h("small", null, "set average"),
),
h(
"svg",
{
className: "metric-radar-svg",
viewBox: `0 0 ${size} ${size}`,
role: "img",
"aria-label": "Aggregate movement metric radar chart",
},
h("title", null, "Aggregate movement metric radar chart"),
rings.map((ring) => {
const ringPoints = rows.map((_, index) => {
const angle = -Math.PI / 2 + (index / rows.length) * Math.PI * 2;
return [
center + Math.cos(angle) * radius * ring,
center + Math.sin(angle) * radius * ring,
];
});
return h("polygon", {
key: ring,
className: "radar-ring",
points: pointList(ringPoints),
});
}),
rows.map((_, index) => {
const angle = -Math.PI / 2 + (index / rows.length) * Math.PI * 2;
return h("line", {
key: index,
className: "radar-axis",
x1: center,
y1: center,
x2: center + Math.cos(angle) * radius,
y2: center + Math.sin(angle) * radius,
});
}),
h("polygon", {
className: "radar-shape",
points: pointList(points),
}),
points.map(([x, y], index) =>
h("circle", {
key: `${rows[index].name}-dot`,
className: `radar-dot ${scoreTone(rows[index].value)}`,
cx: x,
cy: y,
r: 4,
}),
),
labelPoints.map(({ row, x, y, anchor }) =>
h(
"text",
{
key: row.name,
className: "radar-label",
x,
y,
textAnchor: anchor,
},
h("tspan", { x, dy: 0 }, row.name),
h("tspan", { x, dy: 13 }, percent(row.value)),
),
),
),
);
}
const repMetricSpecs = [
{ key: "range_of_motion_score", label: "ROM", className: "rom" },
{ key: "stability_score", label: "Stability", className: "stability" },
{ key: "symmetry_score", label: "Symmetry", className: "symmetry" },
];
function RepTrendChart({ reps, metricKey, labelText, className }) {
const width = 360;
const height = 150;
const pad = 22;
const innerWidth = width - pad * 2;
const innerHeight = height - pad * 2;
const trendPoints = reps
.map((rep, index) => {
const value = metricScore(rep[metricKey]);
if (value === null) return null;
const x =
pad + (reps.length <= 1 ? 0.5 : index / (reps.length - 1)) * innerWidth;
const y = pad + (1 - value) * innerHeight;
return { rep, value, x, y };
})
.filter(Boolean);
const values = trendPoints.map((point) => point.value);
const average = values.length
? values.reduce((total, value) => total + value, 0) / values.length
: null;
const lowPoint = trendPoints.length
? [...trendPoints].sort((a, b) => a.value - b.value)[0]
: null;
const linePath = trendPoints.map((point) => [point.x, point.y]);
const areaPath =
trendPoints.length > 1
? `${pointPath(linePath)} L ${trendPoints[trendPoints.length - 1].x.toFixed(1)} ${(height - pad).toFixed(1)} L ${trendPoints[0].x.toFixed(1)} ${(height - pad).toFixed(1)} Z`
: "";
return h(
"article",
{ className: `metric-trend-card ${className}` },
h(
"div",
{ className: "metric-chart-head" },
h(
"div",
null,
h("span", null, labelText),
h("strong", null, average === null ? "n/a" : percent(average)),
),
h("small", null, lowPoint ? `low R${lowPoint.rep.rep_id}` : "no data"),
),
h(
"svg",
{
className: "metric-trend-svg",
viewBox: `0 0 ${width} ${height}`,
role: "img",
"aria-label": `${labelText} metric trend by rep`,
},
h("title", null, `${labelText} metric trend by rep`),
[0, 0.5, 1].map((level) =>
h("line", {
key: level,
className: "trend-grid-line",
x1: pad,
y1: pad + (1 - level) * innerHeight,
x2: width - pad,
y2: pad + (1 - level) * innerHeight,
}),
),
areaPath ? h("path", { className: "trend-area", d: areaPath }) : null,
linePath.length
? h("path", {
className: "trend-line",
d: pointPath(linePath),
})
: null,
trendPoints.map((point) =>
h("circle", {
key: point.rep.rep_id,
className: `trend-dot ${scoreTone(point.value)}`,
cx: point.x,
cy: point.y,
r: 4,
}),
),
h("text", { className: "trend-axis-label", x: pad, y: height - 4 }, "R1"),
h(
"text",
{
className: "trend-axis-label",
x: width - pad,
y: height - 4,
textAnchor: "end",
},
reps.length ? `R${reps[reps.length - 1].rep_id}` : "R0",
),
),
);
}
function RepQualityStrip({ reps }) {
if (!reps.length) return null;
const scores = reps.map((rep) => ({ rep, score: repScore(rep) }));
const average = metricAverage(scores.map(({ score }) => ({ value: score })));
const weakest = scores
.filter(({ score }) => score !== null)
.sort((a, b) => a.score - b.score)[0];
return h(
"article",
{ className: "rep-quality-card" },
h(
"div",
{ className: "metric-chart-head" },
h(
"div",
null,
h("span", null, "Rep quality strip"),
h("strong", null, average === null ? "n/a" : percent(average)),
),
h("small", null, weakest ? `review R${weakest.rep.rep_id}` : "no data"),
),
h(
"div",
{
className: "rep-quality-strip",
"aria-label": "Average quality by rep",
},
scores.map(({ rep, score }) =>
h(
"div",
{
className: `rep-quality-bar ${scoreTone(score)}`,
key: rep.rep_id,
title: `Rep ${rep.rep_id}: ${score === null ? "n/a" : percent(score)}`,
},
h("span", {
style: {
height:
score === null
? "8%"
: `${Math.max(8, Math.round(score * 100))}%`,
},
"aria-hidden": "true",
}),
h("strong", null, `R${rep.rep_id}`),
),
),
),
);
}
function BriefTile({ eyebrow, title, body, tone = "" }) {
return h(
"article",
{ className: `brief-tile${tone ? ` ${tone}` : ""}` },
h("span", null, eyebrow),
h("strong", null, title),
h("p", null, body),
);
}
function CoachOverview({ result }) {
if (!result) return null;
const summary = result.report.coach_summary;
const coachArtifacts = result.report.artifacts || {};
const coachSource = coachArtifacts.coach_summary_source || "";
const coachModel = coachArtifacts.coach_summary_model || "n/a";
const verifierBypassed = Boolean(
coachArtifacts.coach_summary_verifier_bypassed,
);
const isFallback = coachSource.startsWith("fallback");
const cueNow =
summary.top_fixes?.[0] || summary.next_session_plan?.[0] || summary.summary;
return h(
"section",
{ className: "coach-spotlight" },
h(
"div",
{ className: "coach-command" },
h(
"div",
{ className: "coach-command-copy" },
h("span", null, "Coach intelligence"),
h("strong", null, cueNow),
h("p", null, summary.summary),
),
h(
"div",
{ className: "coach-meta-stack" },
metaChip(`Model: ${coachModel}`),
),
),
isFallback || verifierBypassed
? h(
"div",
{ className: "coach-system-row" },
isFallback
? h(
"p",
{ className: "system-note" },
"A conservative fallback summary was used because the generated summary was unavailable or did not pass verification.",
)
: null,
)
: null,
h(
"div",
{ className: "coach-overview-grid" },
h(NoteList, {
title: "Fix first",
items: summary.top_fixes,
className: "coach-note priority",
}),
h(NoteList, {
title: "Next session",
items: summary.next_session_plan,
className: "coach-note next",
}),
h(NoteList, {
title: "Keep",
items: summary.what_looked_good,
className: "coach-note",
}),
h(NoteList, {
title: "Variation vs issue",
items: summary.valid_variation_vs_issue,
className: "coach-note",
}),
h(NoteList, {
title: "Confidence notes",
items: summary.confidence_notes,
className: "coach-note muted",
}),
),
);
}
function RepTimeline({
report,
compact = false,
activeRepId = null,
playbackTime = 0,
onRepSelect = null,
}) {
const reps = report.reps?.reps || [];
const duration = reportDuration(report);
const analysisById = repAnalysisById(report);
const isPlayback = typeof onRepSelect === "function";
const playheadLeft = `${Math.max(0, Math.min(100, (playbackTime / duration) * 100))}%`;
if (!reps.length) {
return h(
"p",
{ className: "empty-copy" },
"No complete reps were detected.",
);
}
return h(
"div",
{
className: `rep-timeline${compact ? " compact" : ""}${isPlayback ? " playback" : ""}`,
},
h(
"div",
{ className: "timeline-ruler", "aria-hidden": "true" },
h("span", null, "0s"),
h("span", null, `${duration.toFixed(1)}s`),
),
h(
"div",
{
className: `rep-track${isPlayback ? " interactive" : ""}`,
"aria-label": isPlayback ? "Interactive rep timeline" : "Rep timeline",
},
isPlayback
? h("span", {
className: "timeline-playhead",
style: { left: playheadLeft },
"aria-hidden": "true",
})
: null,
reps.map((rep) => {
const left = `${Math.max(0, Math.min(98, (rep.start_sec / duration) * 100))}%`;
const width = `${Math.max(6, Math.min(100, ((rep.end_sec - rep.start_sec) / duration) * 100))}%`;
const score = repScore(analysisById.get(rep.rep_id));
const repIssues = issuesForRep(report, rep.rep_id);
const tagName = isPlayback ? "button" : "div";
const segmentProps = {
className: `rep-segment ${scoreTone(score)}${repIssues.length ? " has-issue" : ""}${activeRepId === rep.rep_id ? " active" : ""}`,
key: rep.rep_id,
style: { left, width },
title: `Rep ${rep.rep_id} ${timeRange(rep.start_sec, rep.end_sec)}`,
};
if (isPlayback) {
segmentProps.type = "button";
segmentProps.onClick = () => onRepSelect(rep);
segmentProps["aria-label"] =
`Play from rep ${rep.rep_id}, ${timeRange(rep.start_sec, rep.end_sec)}`;
segmentProps["aria-pressed"] = activeRepId === rep.rep_id;
}
return h(
tagName,
segmentProps,
h("strong", null, `R${rep.rep_id}`),
repIssues.map((issue, index) =>
h("span", {
className: `issue-pin ${severityLevel(issue)}`,
key: `${issue.issue}-${index}`,
style: {
left: `${Math.max(8, Math.min(92, ((issue.start_sec - rep.start_sec) / Math.max(0.01, rep.end_sec - rep.start_sec)) * 100))}%`,
},
title: issueTitle(issue),
}),
),
);
}),
),
);
}
function SummaryTab({ result }) {
if (!result) {
return h(
"section",
{ className: "summary" },
h("h2", null, "Ready for review"),
h("p", null, "Choose the movement context and start the analyzer."),
);
}
const report = result.report;
const summary = report.coach_summary;
const warnings = report.video_manifest.quality_warnings || [];
const issues = issueList(report);
const issue = topIssue(report);
const best = bestRep(report);
const verdict = qualityVerdict(report);
const scores = metricScoreRows(report);
const provider = report.artifacts?.coach_summary_provider || "n/a";
const model = report.artifacts?.coach_summary_model || "n/a";
const repCount = report.reps?.reps?.length || 0;
const cueNow =
summary.top_fixes?.[0] ||
(issue
? issueEvidence(issue)
: "Repeat the next set with the same controlled tempo.");
const nextSession =
summary.next_session_plan?.[0] || "Repeat the set with controlled reps.";
return h(
"section",
{ className: "summary overview-panel" },
h(
"div",
{ className: "report-hero" },
h(
"div",
{ className: "report-copy" },
h("p", { className: "eyebrow" }, "Movement report"),
h("h2", null, `${label(report.exercise.exercise)} review`),
h("p", { className: "report-lede" }, summary.summary),
h(
"div",
{ className: "pill-row" },
metaChip(`confidence ${percent(report.exercise.confidence)}`, "blue"),
metaChip(`${repCount} rep${repCount === 1 ? "" : "s"}`),
metaChip(
issues.length
? `${issues.length} coaching moment${issues.length === 1 ? "" : "s"}`
: "no clear issue",
issues.length ? severityLevel(issue) : "strong",
),
metaChip(
provider === "local_transformers" ? "local coach" : provider,
),
),
),
h(
"aside",
{ className: `verdict-card ${scoreTone(verdict.score)}` },
h("span", null, "Set verdict"),
h("strong", null, verdict.title),
h("p", null, verdict.detail),
h("small", null, model),
),
),
h(
"div",
{ className: "brief-grid" },
h(BriefTile, {
eyebrow: "Best rep",
title: best ? `Rep ${best.rep.rep_id}` : "Not enough data",
body: best
? `Most consistent rep at ${timeRange(best.rep.start_sec, best.rep.end_sec)} with ${percent(best.score)} quality.`
: "Complete reps are needed before the app can pick a best rep.",
tone: "mint",
}),
h(BriefTile, {
eyebrow: "Main limiter",
title: issue ? issueTitle(issue) : "Nothing major",
body: issue
? `${issueClipText(result, issue, 0)}. ${issueEvidence(issue)}.`
: "No sustained threshold violations were found in this set.",
tone: issue ? severityLevel(issue) : "mint",
}),
h(BriefTile, {
eyebrow: "Cue now",
title: "Try this first",
body: cueNow,
tone: "volt",
}),
h(BriefTile, {
eyebrow: "Next session",
title: "Keep it simple",
body: nextSession,
}),
),
h(
"div",
{ className: "score-board" },
scores.length
? scores.map((row) =>
h(ScoreBar, {
key: row.name,
labelText: row.name,
value: row.value,
}),
)
: h("p", { className: "empty-copy" }, "No movement scores available."),
),
h(CoachOverview, { result }),
h(
"div",
{ className: "note-grid report-notes" },
h(NoteList, { title: "What you did", items: summary.what_you_did }),
h(NoteList, {
title: "What looked good",
items: summary.what_looked_good,
}),
h(NoteList, {
title: "What changed",
items: summary.what_changed_across_reps,
}),
),
warnings.length
? h(
"div",
{ className: "quality-list" },
warnings.map((warning) =>
h("span", { key: warning }, label(warning)),
),
)
: h(
"div",
{ className: "quality-list" },
h("span", null, "No quality warnings"),
),
);
}
function MetricsTab({ result }) {
if (!result)
return h(
"section",
{ className: "summary" },
h("h2", null, "Movement metrics"),
h("p", null, "Metrics appear after analysis."),
);
const report = result.report;
const reps = report.rep_analysis?.items || [];
const scoreRows = metricScoreRows(report);
const trendSpecs = repMetricSpecs.filter((spec) =>
reps.some((rep) => metricScore(rep[spec.key]) !== null),
);
return h(
"section",
{ className: "summary metrics-panel" },
h("h2", null, "Movement metrics"),
h(
"p",
null,
"Charts show which qualities shaped the coach note and where the set started to drift.",
),
h(
"div",
{ className: "metric-chart-hero" },
h(MetricRadar, { rows: scoreRows }),
h(
"div",
{ className: "score-board metric-score-board" },
scoreRows.length
? scoreRows.map((row) =>
h(ScoreBar, {
key: row.name,
labelText: row.name,
value: row.value,
}),
)
: h(
"p",
{ className: "empty-copy" },
"No aggregate scores available.",
),
),
),
trendSpecs.length
? h(
"div",
{ className: "metric-trend-grid" },
trendSpecs.map((spec) =>
h(RepTrendChart, {
key: spec.key,
reps,
metricKey: spec.key,
labelText: spec.label,
className: spec.className,
}),
),
)
: h("p", { className: "empty-copy" }, "No per-rep metrics available."),
h(RepQualityStrip, { reps }),
h(
"div",
{ className: "metric-table-head" },
h("span", null, "Raw values"),
h("small", null, "per rep detail"),
),
h(
"div",
{ className: "table-wrap" },
h(
"table",
null,
h(
"thead",
null,
h(
"tr",
null,
["Rep", "Duration", "ROM", "Stability", "Symmetry"].map((heading) =>
h("th", { key: heading }, heading),
),
),
),
h(
"tbody",
null,
reps.map((rep) =>
h(
"tr",
{ key: rep.rep_id },
h("td", null, rep.rep_id),
h("td", null, `${formatValue(rep.duration_sec)}s`),
h("td", null, percent(rep.range_of_motion_score)),
h("td", null, percent(rep.stability_score)),
h("td", null, percent(rep.symmetry_score)),
),
),
),
),
),
);
}
function RepDetailPanel({
report,
rep,
result,
playbackTime,
compact = false,
}) {
if (!rep) {
return h(
"aside",
{ className: `rep-detail-panel${compact ? " compact" : ""}` },
h("span", { className: "rep-detail-kicker" }, "Current rep"),
h("strong", null, "No rep selected"),
h(
"p",
null,
"Play the video or click a rep segment to inspect its metrics.",
),
);
}
const analysis = repAnalysisById(report).get(rep.rep_id);
const repIssues = issuesForRep(report, rep.rep_id);
const score = repScore(analysis);
const duration = Math.max(0, (rep.end_sec || 0) - (rep.start_sec || 0));
const firstHalfDuration = Math.max(
0,
(rep.mid_sec || 0) - (rep.start_sec || 0),
);
const secondHalfDuration = Math.max(
0,
(rep.end_sec || 0) - (rep.mid_sec || 0),
);
const metricRows = [
{ name: "ROM", value: metricScore(analysis?.range_of_motion_score) },
{ name: "Stability", value: metricScore(analysis?.stability_score) },
{ name: "Symmetry", value: metricScore(analysis?.symmetry_score) },
].filter((item) => item.value !== null);
const weakestMetric = metricRows.length
? [...metricRows].sort((a, b) => a.value - b.value)[0]
: null;
const repProgress =
duration > 0
? Math.max(0, Math.min(1, (playbackTime - rep.start_sec) / duration))
: 0;
const phase =
playbackTime < rep.start_sec
? "setup"
: playbackTime > rep.end_sec
? "review"
: playbackTime <= rep.mid_sec
? "half 1"
: "half 2";
const focusText = weakestMetric
? `${weakestMetric.name} is the main review cue at ${percent(weakestMetric.value)}.`
: "Metric detail is not available for this rep.";
const scoreText =
score === null
? "Metric data is limited for this rep."
: score >= 0.8
? "This rep is one of the steadier parts of the set."
: score >= 0.62
? "This rep is usable, but one metric deserves attention."
: "This rep is the kind to slow down and review before progressing.";
const issueText = repIssues.length
? `${repIssues.length} issue marker${repIssues.length === 1 ? "" : "s"} attached to this rep.`
: "No issue marker is attached, so use the lowest metric as the review cue.";
const primaryIssue = repIssues[0] || null;
if (compact) {
return h(
"aside",
{ className: `rep-detail-panel compact ${scoreTone(score)}` },
h(
"div",
{ className: "rep-detail-head" },
h(
"div",
null,
h("span", { className: "rep-detail-kicker" }, "Current rep"),
h("strong", null, `Rep ${rep.rep_id}`),
h("small", null, timeRange(rep.start_sec, rep.end_sec)),
),
metaChip(
score === null ? "no score" : percent(score),
scoreTone(score),
),
),
h(
"div",
{ className: "rep-fact-grid compact" },
h(
"div",
{ className: "rep-fact" },
h("span", null, "Duration"),
h("strong", null, `${duration.toFixed(2)}s`),
),
h(
"div",
{ className: "rep-fact" },
h("span", null, "Focus"),
h(
"strong",
null,
weakestMetric
? `${weakestMetric.name} ${percent(weakestMetric.value)}`
: "n/a",
),
),
h(
"div",
{ className: "rep-fact" },
h("span", null, "Frames"),
h("strong", null, `${rep.start_frame}-${rep.end_frame}`),
),
h(
"div",
{ className: "rep-fact" },
h("span", null, "Issues"),
h(
"strong",
null,
repIssues.length ? String(repIssues.length) : "clear",
),
),
),
h(
"div",
{ className: "rep-progress-card compact" },
h(
"div",
{ className: "rep-now" },
h("span", null, "playhead"),
h("strong", null, `${playbackTime.toFixed(2)}s`),
),
h(
"div",
{ className: "rep-progress-track", "aria-hidden": "true" },
h("span", {
className: "rep-progress-fill",
style: { width: `${Math.round(repProgress * 100)}%` },
}),
h("span", { className: "rep-progress-mid" }),
),
h(
"div",
{ className: "rep-progress-labels" },
h("span", null, `${rep.start_sec.toFixed(2)}s`),
h("span", null, `${rep.mid_sec.toFixed(2)}s mid`),
h("span", null, `${rep.end_sec.toFixed(2)}s`),
),
),
primaryIssue
? h(
"article",
{ className: `rep-issue-brief ${severityLevel(primaryIssue)}` },
h(
"div",
{ className: "rep-issue-brief-head" },
h(
"div",
null,
h("span", null, "Issue detail"),
h("strong", null, issueTitle(primaryIssue)),
),
h(
"span",
{ className: `severity-chip ${severityLevel(primaryIssue)}` },
severityText(primaryIssue),
),
),
h("p", null, issueEvidence(primaryIssue)),
h(
"div",
{ className: "rep-issue-meta" },
h("span", null, issueFocus(primaryIssue)),
h("span", null, issueClipText(result, primaryIssue, 0)),
h(
"span",
null,
`confidence ${percent(primaryIssue.evidence?.confidence)}`,
),
repIssues.length > 1
? h("span", null, `+${repIssues.length - 1} more`)
: null,
),
)
: null,
);
}
return h(
"aside",
{ className: `rep-detail-panel ${scoreTone(score)}` },
h(
"div",
{ className: "rep-detail-head" },
h(
"div",
null,
h("span", { className: "rep-detail-kicker" }, "Current rep"),
h("strong", null, `Rep ${rep.rep_id}`),
h("small", null, timeRange(rep.start_sec, rep.end_sec)),
),
metaChip(score === null ? "no score" : percent(score), scoreTone(score)),
),
h(
"div",
{ className: "rep-fact-grid" },
h(
"div",
{ className: "rep-fact" },
h("span", null, "Duration"),
h("strong", null, `${duration.toFixed(2)}s`),
),
h(
"div",
{ className: "rep-fact" },
h("span", null, "Frames"),
h("strong", null, `${rep.start_frame}-${rep.end_frame}`),
),
h(
"div",
{ className: "rep-fact" },
h("span", null, "Tempo split"),
h(
"strong",
null,
`${firstHalfDuration.toFixed(2)}/${secondHalfDuration.toFixed(2)}s`,
),
),
h(
"div",
{ className: "rep-fact" },
h("span", null, "Phase"),
h("strong", null, phase),
),
h(
"div",
{ className: "rep-fact" },
h("span", null, "Focus"),
h(
"strong",
null,
weakestMetric
? `${weakestMetric.name} ${percent(weakestMetric.value)}`
: "n/a",
),
),
h(
"div",
{ className: "rep-fact" },
h("span", null, "Issues"),
h(
"strong",
null,
repIssues.length ? String(repIssues.length) : "clear",
),
),
),
h(
"div",
{ className: "rep-progress-card" },
h(
"div",
{ className: "rep-now" },
h("span", null, "playhead"),
h("strong", null, `${playbackTime.toFixed(2)}s`),
),
h(
"div",
{ className: "rep-progress-track", "aria-hidden": "true" },
h("span", {
className: "rep-progress-fill",
style: { width: `${Math.round(repProgress * 100)}%` },
}),
h("span", { className: "rep-progress-mid" }),
),
h(
"div",
{ className: "rep-progress-labels" },
h("span", null, `${rep.start_sec.toFixed(2)}s`),
h("span", null, `${rep.mid_sec.toFixed(2)}s mid`),
h("span", null, `${rep.end_sec.toFixed(2)}s`),
),
),
h(
"article",
{ className: "rep-readout" },
h("span", null, "Movement read"),
h("strong", null, scoreText),
h("p", null, `${focusText} ${issueText}`),
),
h(
"div",
{ className: "rep-mini-metrics" },
h(ScoreBar, {
labelText: "ROM",
value: analysis?.range_of_motion_score,
}),
h(ScoreBar, {
labelText: "Stability",
value: analysis?.stability_score,
}),
h(ScoreBar, {
labelText: "Symmetry",
value: analysis?.symmetry_score,
}),
),
h(
"div",
{ className: "issue-mini-list" },
h("h3", null, repIssues.length ? "Issue in this rep" : "Issue check"),
repIssues.length
? repIssues.map((issue, index) =>
h(
"article",
{
className: `issue-mini ${severityLevel(issue)}`,
key: `${issue.issue}-${index}`,
},
h(
"div",
{ className: "issue-mini-head" },
h("strong", null, issueTitle(issue)),
h(
"span",
{ className: `severity-chip ${severityLevel(issue)}` },
severityText(issue),
),
),
h("p", null, issueEvidence(issue)),
h(
"div",
{ className: "timeline-meta" },
h("span", null, issueClipText(result, issue, index)),
h(
"span",
null,
`evidence ${percent(issue.evidence?.confidence)}`,
),
),
),
)
: h("p", null, "No issue marker is attached to this rep."),
),
);
}
function ReplayReviewPanel({ result, videoSrc, className = "" }) {
const [playbackTime, setPlaybackTime] = useState(0);
const [selectedRepId, setSelectedRepId] = useState(null);
const replayVideoRef = useRef(null);
useEffect(() => {
setPlaybackTime(0);
setSelectedRepId(null);
}, [result?.run_id, videoSrc]);
const report = result.report;
const reps = report.reps?.reps || [];
const timedRep = repAtTime(report, playbackTime);
const selectedRep = reps.find((rep) => rep.rep_id === selectedRepId) || null;
const activeRep = timedRep || selectedRep || reps[0] || null;
function playFromRep(rep) {
const startTime = Math.max(0, (rep.start_sec || 0) - 0.04);
setSelectedRepId(rep.rep_id);
setPlaybackTime(startTime);
if (!replayVideoRef.current) return;
replayVideoRef.current.currentTime = startTime;
const playPromise = replayVideoRef.current.play();
if (playPromise?.catch) playPromise.catch(() => undefined);
}
return h(
"div",
{ className: `replay-review${className ? ` ${className}` : ""}` },
h(
"div",
{ className: "replay-main" },
h(
"div",
{ className: "replay-video-card" },
videoSrc
? h("video", {
className: "timeline-video",
controls: true,
muted: true,
playsInline: true,
preload: "metadata",
ref: replayVideoRef,
src: videoSrc,
onLoadedMetadata: (event) => {
setPlaybackTime(event.currentTarget.currentTime || 0);
},
onTimeUpdate: (event) => {
setPlaybackTime(event.currentTarget.currentTime || 0);
},
onSeeked: (event) => {
setPlaybackTime(event.currentTarget.currentTime || 0);
},
})
: h(
"div",
{ className: "timeline-video-placeholder" },
h("strong", null, "No replay video available"),
h(
"span",
null,
"Upload a clip or use a run with an annotated video to sync the timeline.",
),
),
),
h(RepDetailPanel, {
report,
rep: activeRep,
result,
playbackTime,
compact: true,
}),
),
h(
"div",
{ className: "replay-timeline-card" },
h(RepTimeline, {
report,
activeRepId: activeRep?.rep_id || null,
playbackTime,
onRepSelect: playFromRep,
}),
),
);
}
function RepsTab({ result }) {
if (!result)
return h(
"section",
{ className: "summary" },
h("h2", null, "Rep list"),
h("p", null, "Rep segments appear after analysis."),
);
const report = result.report;
const reps = report.reps?.reps || [];
const analysisById = repAnalysisById(report);
return h(
"section",
{ className: "summary reps-panel" },
h("h2", null, "All reps"),
h(
"p",
null,
"A compact pass over every detected rep, with timing, frames, movement score, and issue markers.",
),
reps.length
? h(
"div",
{ className: "rep-grid" },
reps.map((rep) => {
const analysis = analysisById.get(rep.rep_id);
const repIssues = issuesForRep(report, rep.rep_id);
const score = repScore(analysis);
return h(
"article",
{ className: `rep-card ${scoreTone(score)}`, key: rep.rep_id },
h(
"div",
{ className: "rep-card-head" },
h("strong", null, `Rep ${rep.rep_id}`),
metaChip(
score === null ? "no score" : percent(score),
scoreTone(score),
),
),
h("span", null, timeRange(rep.start_sec, rep.end_sec)),
h("span", null, `frames ${rep.start_frame}-${rep.end_frame}`),
h("span", null, `midpoint ${rep.mid_sec.toFixed(2)}s`),
h(
"div",
{ className: "pill-row compact" },
repIssues.length
? repIssues.map((issue, index) =>
metaChip(
issueTitle(issue),
`${severityLevel(issue)}`,
`${issue.issue}-${index}`,
),
)
: metaChip("no issue marker", "strong"),
),
);
}),
)
: h("p", null, "No complete reps were detected."),
);
}
function issueEvidence(issue) {
const degreeEntry = Object.entries(issue.evidence || {}).find(
([key, value]) =>
key.endsWith("_deg") && typeof value === "number" && !Number.isNaN(value),
);
if (degreeEntry) {
return `${label(degreeEntry[0].replace("_deg", ""))} about ${Math.round(degreeEntry[1])} deg`;
}
const cues = {
shallow_depth: "Lower the hips a little more before standing up",
hip_sag: "Keep hips in line with shoulders and ankles",
incomplete_depth: "Bend deeper at the bottom of the rep",
knee_valgus: "Keep knees tracking over the toes",
excessive_torso_lean: "Keep the chest taller through the bottom",
incomplete_lockout: "Finish by straightening the elbows",
asymmetry: "Keep both sides moving evenly",
};
return cues[issue.issue] || "Review this part of the rep";
}
function issueTitle(issue) {
const titles = {
shallow_depth: "Squat depth is shallow",
hip_sag: "Hips are dropping",
incomplete_depth: "Rep is not deep enough",
knee_valgus: "Knees are caving inward",
excessive_torso_lean: "Torso leans too far forward",
incomplete_lockout: "Lockout is incomplete",
asymmetry: "Left and right sides are uneven",
};
return titles[issue.issue] || label(issue.issue);
}
function issueFocus(issue) {
const focus = {
shallow_depth: "Focus on hips and knees",
hip_sag: "Focus on trunk and hips",
incomplete_depth: "Focus on shoulders, elbows, and wrists",
knee_valgus: "Focus on knees and ankles",
excessive_torso_lean: "Focus on chest and hips",
incomplete_lockout: "Focus on elbows and wrists",
asymmetry: "Focus on left-right balance",
};
if (focus[issue.issue]) return focus[issue.issue];
return `Focus on ${issue.affected_joints.map(label).join(", ")}`;
}
function severityText(issue) {
const severity = Math.round(issue.severity * 100);
if (severity >= 70) return `high attention ${severity}%`;
if (severity >= 40) return `moderate attention ${severity}%`;
return `minor attention ${severity}%`;
}
function severityLevel(issue) {
const severity = Math.round(issue.severity * 100);
if (severity >= 70) return "high";
if (severity >= 40) return "moderate";
return "minor";
}
function thumbnailForIssue(result, issue, index) {
const thumbnails = result?.issue_thumbnail_urls || [];
return (
thumbnails.find(
(thumbnail) =>
thumbnail.rep_id === issue.rep_id && thumbnail.issue === issue.issue,
) || thumbnails[index]
);
}
function clipForIssue(result, issue, index) {
const clips = result?.issue_clip_urls || [];
return (
clips.find(
(clip) => clip.rep_id === issue.rep_id && clip.issue === issue.issue,
) || clips[index]
);
}
function IssueMedia({ result, issue, index }) {
const clip = clipForIssue(result, issue, index);
if (clip?.url) {
return h("video", {
className: "issue-clip",
src: clip.url,
controls: true,
muted: true,
playsInline: true,
preload: "metadata",
});
}
const thumbnail = thumbnailForIssue(result, issue, index);
if (thumbnail?.url) {
return h("img", {
className: "issue-thumb",
src: thumbnail.url,
alt: `${label(issue.issue)} thumbnail`,
});
}
return h("div", { className: "issue-thumb empty" }, "No clip");
}
function issueClipText(result, issue, index) {
const clip = clipForIssue(result, issue, index);
if (
!clip ||
typeof clip.clip_start_sec !== "number" ||
typeof clip.clip_end_sec !== "number"
) {
return `Rep ${issue.rep_id} · ${issue.start_sec.toFixed(2)}s-${issue.end_sec.toFixed(2)}s`;
}
return `Rep ${issue.rep_id} · clip ${clip.clip_start_sec.toFixed(2)}s-${clip.clip_end_sec.toFixed(2)}s`;
}
function IssuesTab({ result }) {
const issues = result?.report?.issue_markers?.issues || [];
return h(
"section",
{ className: "summary issue-board", "aria-label": "Issue timeline" },
h(
"div",
{ className: "timeline-head" },
h("h3", null, "Coaching moments"),
h(
"span",
null,
issues.length
? `${issues.length} interval${issues.length === 1 ? "" : "s"}`
: "clear",
),
),
issues.length
? h(
"div",
{ className: "issue-card-grid" },
issues.map((issue, index) =>
h(
"article",
{
className: `issue-card issue-card-feature ${severityLevel(issue)}`,
key: `${issue.rep_id}-${issue.issue}-${index}`,
},
h(IssueMedia, { result, issue, index }),
h(
"div",
{ className: "issue-body" },
h(
"div",
{ className: "issue-card-head" },
h(
"span",
{ className: `severity-chip ${severityLevel(issue)}` },
severityText(issue),
),
h("strong", null, issueTitle(issue)),
h("small", null, issueClipText(result, issue, index)),
),
h("p", null, issueEvidence(issue)),
h(
"div",
{ className: "timeline-meta" },
h(
"span",
null,
`issue ${timeRange(issue.start_sec, issue.end_sec)}`,
),
h(
"span",
null,
`evidence ${percent(issue.evidence?.confidence)}`,
),
h("span", null, issueFocus(issue)),
issue.affected_joints.length
? h(
"span",
null,
`joints ${issue.affected_joints.map(label).join(", ")}`,
)
: null,
),
),
),
),
)
: h("p", null, "No sustained threshold violations were found."),
);
}
function JsonTab({ result }) {
return h(
"pre",
{ className: "json-block" },
result ? JSON.stringify(result.report, null, 2) : "{}",
);
}
function ArtifactsTab({ result }) {
const links = result?.artifact_urls || [];
return h(
"section",
{ className: "artifact-panel" },
h(
"div",
{ className: "artifact-links" },
links.length
? links.map((artifact) =>
h(
"a",
{
className: "artifact-link",
key: artifact.url,
href: artifact.url,
download: artifact.name,
},
artifact.name,
h("span", null, "download"),
),
)
: h("p", null, "Artifacts appear after analysis."),
),
h(
"details",
{ className: "json-details" },
h("summary", null, "Raw report JSON"),
h(JsonTab, { result }),
),
);
}
const reportTabs = [
["summary", "Overview"],
["reps", "Reps"],
["metrics", "Metrics"],
["issues", "Issues"],
["artifacts", "Artifacts"],
];
function ReportPanel({ result, activeTab, onTabChange }) {
const content = {
summary: h(SummaryTab, { result }),
metrics: h(MetricsTab, { result }),
reps: h(RepsTab, { result }),
issues: h(IssuesTab, { result }),
artifacts: h(ArtifactsTab, { result }),
}[activeTab];
return h(
React.Fragment,
null,
h(
"div",
{ className: "tabs", role: "tablist", "aria-label": "Report sections" },
reportTabs.map(([key, name]) =>
h(
"button",
{
className: `tab${activeTab === key ? " active" : ""}`,
"aria-selected": activeTab === key,
key,
onClick: () => onTabChange(key),
role: "tab",
type: "button",
},
name,
),
),
),
content,
);
}
function NoteList({ title, items, className = "" }) {
return h(
"article",
{ className: `note${className ? ` ${className}` : ""}` },
h("h3", null, title),
h(
"ul",
null,
items.map((item) => h("li", { key: item }, item)),
),
);
}
export { ReportPanel, ReplayReviewPanel, ReviewInsights };