Spaces:
Running on Zero
Running on Zero
fix(report): show scan result insights
Browse files- web/app.js +57 -1
- web/styles.css +31 -0
web/app.js
CHANGED
|
@@ -141,10 +141,11 @@ function finalProgressState(result) {
|
|
| 141 |
|
| 142 |
function ProgressPanel({ steps }) {
|
| 143 |
if (!steps.length) return null;
|
|
|
|
| 144 |
return h(
|
| 145 |
"section",
|
| 146 |
{ className: "progress-panel", "aria-live": "polite" },
|
| 147 |
-
h("h3", null, "Your scan is moving"),
|
| 148 |
h(
|
| 149 |
"ol",
|
| 150 |
{ className: "progress-list" },
|
|
@@ -160,6 +161,60 @@ function ProgressPanel({ steps }) {
|
|
| 160 |
);
|
| 161 |
}
|
| 162 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
function SummaryTab({ result }) {
|
| 164 |
if (!result) {
|
| 165 |
return h(
|
|
@@ -922,6 +977,7 @@ function App() {
|
|
| 922 |
result?.annotated_video_url
|
| 923 |
? null
|
| 924 |
: h(ProgressPanel, { steps: progressSteps }),
|
|
|
|
| 925 |
),
|
| 926 |
),
|
| 927 |
h(ReportPanel, { result, activeTab, onTabChange: setActiveTab }),
|
|
|
|
| 141 |
|
| 142 |
function ProgressPanel({ steps }) {
|
| 143 |
if (!steps.length) return null;
|
| 144 |
+
const isComplete = steps.every((step) => step.status === "done");
|
| 145 |
return h(
|
| 146 |
"section",
|
| 147 |
{ className: "progress-panel", "aria-live": "polite" },
|
| 148 |
+
h("h3", null, isComplete ? "Scan results are ready" : "Your scan is moving"),
|
| 149 |
h(
|
| 150 |
"ol",
|
| 151 |
{ className: "progress-list" },
|
|
|
|
| 161 |
);
|
| 162 |
}
|
| 163 |
|
| 164 |
+
function ReviewInsights({ result }) {
|
| 165 |
+
if (!result) return null;
|
| 166 |
+
const report = result.report;
|
| 167 |
+
const warnings = report.video_manifest?.quality_warnings || [];
|
| 168 |
+
const exercise = label(report.exercise?.exercise || "movement");
|
| 169 |
+
const repCount = report.reps?.reps?.length || 0;
|
| 170 |
+
const issues = report.issue_markers?.issues || [];
|
| 171 |
+
const confidence = percent(report.exercise?.confidence);
|
| 172 |
+
const issueText = issues.length
|
| 173 |
+
? `${issues.length} coaching moment${issues.length === 1 ? "" : "s"}`
|
| 174 |
+
: "No clear form issues";
|
| 175 |
+
|
| 176 |
+
return h(
|
| 177 |
+
"div",
|
| 178 |
+
{ className: "scan-insights", "aria-label": "Scan results" },
|
| 179 |
+
h(
|
| 180 |
+
"article",
|
| 181 |
+
{ className: "scan-insight" },
|
| 182 |
+
h("span", null, "Movement"),
|
| 183 |
+
h("strong", null, exercise),
|
| 184 |
+
h("small", null, `confidence ${confidence}`),
|
| 185 |
+
),
|
| 186 |
+
h(
|
| 187 |
+
"article",
|
| 188 |
+
{ className: "scan-insight" },
|
| 189 |
+
h("span", null, "Reps counted"),
|
| 190 |
+
h("strong", null, String(repCount)),
|
| 191 |
+
h("small", null, repCount === 1 ? "clean rep detected" : "reps detected"),
|
| 192 |
+
),
|
| 193 |
+
h(
|
| 194 |
+
"article",
|
| 195 |
+
{ className: "scan-insight" },
|
| 196 |
+
h("span", null, "Coach review"),
|
| 197 |
+
h("strong", null, issueText),
|
| 198 |
+
h(
|
| 199 |
+
"small",
|
| 200 |
+
null,
|
| 201 |
+
issues.length ? "tap Issues for clips" : "nothing major stood out",
|
| 202 |
+
),
|
| 203 |
+
),
|
| 204 |
+
h(
|
| 205 |
+
"article",
|
| 206 |
+
{ className: "scan-insight" },
|
| 207 |
+
h("span", null, "Video quality"),
|
| 208 |
+
h("strong", null, warnings.length ? "Needs a quick note" : "Looks good"),
|
| 209 |
+
h(
|
| 210 |
+
"small",
|
| 211 |
+
null,
|
| 212 |
+
warnings.length ? warnings.map(label).join(", ") : "clear enough to coach",
|
| 213 |
+
),
|
| 214 |
+
),
|
| 215 |
+
);
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
function SummaryTab({ result }) {
|
| 219 |
if (!result) {
|
| 220 |
return h(
|
|
|
|
| 977 |
result?.annotated_video_url
|
| 978 |
? null
|
| 979 |
: h(ProgressPanel, { steps: progressSteps }),
|
| 980 |
+
h(ReviewInsights, { result }),
|
| 981 |
),
|
| 982 |
),
|
| 983 |
h(ReportPanel, { result, activeTab, onTabChange: setActiveTab }),
|
web/styles.css
CHANGED
|
@@ -399,6 +399,36 @@ input[type="text"]:focus,
|
|
| 399 |
box-shadow: 0 0 0 5px rgba(190, 104, 75, 0.14);
|
| 400 |
}
|
| 401 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 402 |
.result-stage {
|
| 403 |
display: grid;
|
| 404 |
min-height: 470px;
|
|
@@ -768,6 +798,7 @@ td {
|
|
| 768 |
.note-grid,
|
| 769 |
.stat-grid,
|
| 770 |
.metric-grid,
|
|
|
|
| 771 |
.rep-grid,
|
| 772 |
.issue-card-grid {
|
| 773 |
grid-template-columns: 1fr;
|
|
|
|
| 399 |
box-shadow: 0 0 0 5px rgba(190, 104, 75, 0.14);
|
| 400 |
}
|
| 401 |
|
| 402 |
+
.scan-insights {
|
| 403 |
+
display: grid;
|
| 404 |
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
| 405 |
+
gap: 10px;
|
| 406 |
+
margin-top: 14px;
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
.scan-insight {
|
| 410 |
+
min-width: 0;
|
| 411 |
+
border: 1px solid rgba(37, 44, 31, 0.12);
|
| 412 |
+
border-radius: 8px;
|
| 413 |
+
padding: 12px;
|
| 414 |
+
background: rgba(255, 253, 247, 0.74);
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
.scan-insight span,
|
| 418 |
+
.scan-insight small {
|
| 419 |
+
display: block;
|
| 420 |
+
color: var(--muted);
|
| 421 |
+
font-size: 0.8rem;
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
.scan-insight strong {
|
| 425 |
+
display: block;
|
| 426 |
+
overflow-wrap: anywhere;
|
| 427 |
+
margin: 4px 0;
|
| 428 |
+
color: var(--forest-deep);
|
| 429 |
+
font-size: 1.04rem;
|
| 430 |
+
}
|
| 431 |
+
|
| 432 |
.result-stage {
|
| 433 |
display: grid;
|
| 434 |
min-height: 470px;
|
|
|
|
| 798 |
.note-grid,
|
| 799 |
.stat-grid,
|
| 800 |
.metric-grid,
|
| 801 |
+
.scan-insights,
|
| 802 |
.rep-grid,
|
| 803 |
.issue-card-grid {
|
| 804 |
grid-template-columns: 1fr;
|