import React, { useEffect, useMemo, useState, } from "https://esm.sh/react@18.2.0"; import { createRoot } from "https://esm.sh/react-dom@18.2.0/client"; import { defaults, h, label } from "./common.js?v=20260614-modular-app"; import { ReportPanel, ReplayReviewPanel, ReviewInsights, } from "./report.js?v=20260614-modular-app"; function Field({ labelText, children, full = false }) { return h( "label", { className: `field${full ? " full" : ""}` }, h("span", { className: "label" }, labelText), children, ); } function SelectField({ labelText, value, onChange, options, name }) { return h( Field, { labelText }, h( "select", { name, value, onChange: (event) => onChange(event.target.value) }, options.map((option) => h("option", { key: option, value: option }, label(option)), ), ), ); } function KineticFigure({ compact = false }) { const imageUrl = compact ? "https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?auto=format&fit=crop&fm=jpg&w=900&q=84" : "https://images.unsplash.com/photo-1517836357463-d25dfeac3438?auto=format&fit=crop&fm=jpg&w=1100&q=84"; return h( "div", { className: `kinetic-photo${compact ? " compact" : ""}`, "aria-hidden": "true" }, h("img", { alt: "", className: "motion-photo", draggable: false, src: imageUrl, }), h("span", { className: "photo-vignette" }), h("span", { className: "photo-scanline" }), ); } function SignalStack() { return h( "div", { className: "signal-stack", "aria-label": "Analysis pipeline" }, ["Video QC", "Pose Map", "Rep Count", "Coach Notes"].map((item, index) => h( "div", { className: "signal-row", key: item }, h("span", null, `0${index + 1}`), h("strong", null, item), ), ), ); } const runningProgressSteps = [ { id: "quality", text: "First up, I am checking if the video is clear enough to coach from.", delayMs: 0, }, { id: "pose", text: "Now I am mapping your posture and tracking the key body landmarks.", delayMs: 900, }, { id: "exercise", text: "Let me figure out which exercise you are doing.", delayMs: 1800, }, { id: "reps", text: "Counting your reps now. One clean rep at a time.", delayMs: 2800, }, { id: "issues", text: "Almost there. I am checking the moments that may need a small fix.", delayMs: 3900, }, { id: "render", text: "I am preparing your annotated video and issue clips.", delayMs: 4800, }, { id: "coach", text: "I am turning the scan into coaching notes you can use right away.", delayMs: 5600, }, ]; function pendingProgressState() { return runningProgressSteps.map((step, index) => ({ id: step.id, text: step.text, status: index === 0 ? "active" : "pending", })); } function finalProgressState(result) { 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 || []; return [ { id: "quality", status: "done", text: warnings.length ? `Quick note: the video has a few things to watch, like ${warnings.map(label).join(", ")}.` : "Nice, your video quality looks solid.", }, { id: "pose", status: "done", text: "Posture tracking is done. I found the key landmarks I need.", }, { id: "exercise", status: "done", text: `Looks like you are doing ${exercise}.`, }, { id: "reps", status: "done", text: `I counted ${repCount} ${exercise} reps in this set.`, }, { id: "issues", status: "done", text: issues.length ? `I found ${issues.length} coaching point${issues.length === 1 ? "" : "s"} worth reviewing.` : "Good news, I did not spot any clear form issues in this set.", }, { id: "render", status: "done", text: result.annotated_video_url ? "Your annotated video is ready." : "I could not render an annotated video, but the report is ready.", }, { id: "coach", status: "done", text: "Coach notes are ready.", }, ]; } function applyProgressEvent(currentSteps, event) { const baseSteps = currentSteps.length ? currentSteps : pendingProgressState(); const knownStepIds = new Set(runningProgressSteps.map((step) => step.id)); if (!knownStepIds.has(event.step)) return baseSteps; return baseSteps.map((step) => step.id === event.step ? { ...step, status: event.status || "active", text: event.text || step.text, } : step, ); } async function readAnalysisStream(response, onEvent) { if (!response.body) throw new Error("Streaming is not available in this browser."); const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; let result = null; while (true) { const { value, done } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n"); buffer = lines.pop() || ""; for (const line of lines) { if (!line.trim()) continue; const event = JSON.parse(line); if (event.type === "progress") onEvent(event); if (event.type === "complete") result = event.result; if (event.type === "error") throw new Error(event.detail || "Analysis failed."); } } if (buffer.trim()) { const event = JSON.parse(buffer); if (event.type === "progress") onEvent(event); if (event.type === "complete") result = event.result; if (event.type === "error") throw new Error(event.detail || "Analysis failed."); } if (!result) throw new Error("Analysis finished without a report."); return result; } function ProgressPanel({ steps }) { if (!steps.length) return null; const isComplete = steps.every((step) => step.status === "done"); return h( "section", { className: "progress-panel", "aria-live": "polite" }, h( "h3", null, isComplete ? "Scan results are ready" : "Your scan is moving", ), h( "ol", { className: "progress-list" }, steps.map((step) => h( "li", { className: `progress-step ${step.status}`, key: step.id }, h("span", { className: "progress-dot", "aria-hidden": "true" }), h("span", null, step.text), ), ), ), ); } function StageEmpty() { return h( "div", { className: "result-empty" }, h(KineticFigure, { compact: true }), h("strong", null, "Your annotated replay will land here"), h( "span", null, "Upload a set and Pozify will paint the movement path, rep timing, and coaching moments on top of the video.", ), ); } function App() { const [config, setConfig] = useState(defaults); const [file, setFile] = useState(null); const [goal, setGoal] = useState("beginner_practice"); const [experience, setExperience] = useState("beginner"); const [exercise, setExercise] = useState("auto"); const [equipment, setEquipment] = useState("bodyweight"); const [limitations, setLimitations] = useState([]); const [result, setResult] = useState(null); const [activeTab, setActiveTab] = useState("summary"); const [status, setStatus] = useState("idle"); const [error, setError] = useState(""); const [progressSteps, setProgressSteps] = useState([]); const previewUrl = useMemo( () => (file ? URL.createObjectURL(file) : ""), [file], ); useEffect(() => { fetch("/api/config") .then((response) => response.json()) .then(setConfig) .catch(() => setConfig(defaults)); }, []); useEffect(() => { return () => { if (previewUrl) URL.revokeObjectURL(previewUrl); }; }, [previewUrl]); function toggleLimitation(value) { setLimitations((current) => current.includes(value) ? current.filter((item) => item !== value) : [...current, value], ); } async function analyze(event) { event.preventDefault(); setError(""); setStatus("running"); setResult(null); setProgressSteps(pendingProgressState()); const payload = new FormData(); if (file) payload.append("video", file); payload.append("goal", goal); payload.append("experience_level", experience); payload.append("intended_exercise", exercise); payload.append("intended_variation", ""); payload.append("limitations", JSON.stringify(limitations)); payload.append("equipment", equipment); payload.append("bypass_verifier", "true"); try { const response = await fetch("/api/analyze/stream", { method: "POST", body: payload, }); if (!response.ok) { const body = await response.json().catch(() => ({})); throw new Error(body.detail || "Analysis failed."); } const body = await readAnalysisStream(response, (progressEvent) => { setProgressSteps((currentSteps) => applyProgressEvent(currentSteps, progressEvent), ); }); setResult(body); setProgressSteps(finalProgressState(body)); setStatus("complete"); } catch (caught) { setError(caught.message || "Analysis failed."); setProgressSteps([ { id: "error", status: "active", text: "The scan did not finish. Try another video or check the connection, and we can run it again.", }, ]); setStatus("idle"); } } return h( "main", { className: "app" }, h( "header", { className: "hero" }, h( "div", { className: "hero-content" }, h( "p", { className: "eyebrow" }, "Pose intelligence for coached training", ), h("h1", null, "Pozify"), h("p", null, config.description), h( "div", { className: "hero-actions", "aria-label": "Demo strengths" }, h("span", null, "Realtime stream"), h("span", null, "Annotated replay"), h("span", null, "Grounded coach notes"), ), ), h( "aside", { className: "hero-lab", "aria-label": "Motion analysis preview" }, h(KineticFigure, null), h(SignalStack, null), h( "div", { className: "hero-metrics", "aria-label": "Pipeline highlights" }, h( "div", { className: "metric" }, h("strong", null, "17"), h("span", null, "pose landmarks"), ), h( "div", { className: "metric" }, h("strong", null, "60s"), h("span", null, "clip ceiling"), ), h( "div", { className: "metric" }, h("strong", null, "JSON"), h("span", null, "audit trail"), ), ), ), ), h( "form", { className: "workspace", onSubmit: analyze }, h( "section", { className: "panel" }, h( "div", { className: "panel-head" }, h( "div", null, h("h2", null, "Session setup"), h("p", null, "Movement context"), ), h( "span", { className: "status-pill" }, status === "running" ? "Analyzing" : result ? "Complete" : "Ready", ), ), h( "label", { className: "dropzone" }, h("input", { name: "video", type: "file", accept: "video/*", onChange: (event) => setFile(event.target.files?.[0] || null), }), previewUrl ? h("video", { className: "dropzone-preview", src: previewUrl, controls: true, }) : h( "span", { className: "dropzone-empty" }, h("span", { className: "upload-icon" }, "↑"), h("strong", null, "Drop a workout clip"), h("span", null, "or click to upload an MP4, MOV, or WebM file"), ), ), h( "div", { className: "form-grid" }, h(SelectField, { labelText: "Goal", name: "goal", value: goal, onChange: setGoal, options: config.goals, }), h(SelectField, { labelText: "Experience", name: "experience_level", value: experience, onChange: setExperience, options: config.experience_levels, }), h(SelectField, { labelText: "Exercise", name: "intended_exercise", value: exercise, onChange: setExercise, options: config.exercises, }), h(SelectField, { labelText: "Equipment", name: "equipment", value: equipment, onChange: setEquipment, options: config.equipment, }), h( "div", { className: "field full" }, h("span", { className: "label" }, "Known limitations"), h( "div", { className: "check-grid" }, config.limitations.map((item) => h( "label", { className: "check-chip", key: item }, h("input", { name: "limitations", type: "checkbox", checked: limitations.includes(item), onChange: () => toggleLimitation(item), }), h("span", null, label(item)), ), ), ), ), ), h( "button", { className: "primary", disabled: status === "running", type: "submit" }, status === "running" ? "Analyzing…" : "Analyze Form", ), error ? h("div", { className: "error" }, error) : null, ), h( "section", { className: "panel" }, h( "div", { className: "panel-head" }, h( "div", null, h("h2", null, "Review output"), h("p", null, "Annotated movement"), ), result ? h("span", { className: "status-pill" }, result.run_id) : null, ), result ? h(ReplayReviewPanel, { result, videoSrc: result?.annotated_video_url || previewUrl, className: "review-output-replay", }) : h( React.Fragment, null, h( "div", { className: "result-stage" }, h(StageEmpty, null), ), h(ProgressPanel, { steps: progressSteps }), ), h(ReviewInsights, { result }), ), ), h(ReportPanel, { result, activeTab, onTabChange: setActiveTab, }), ); } createRoot(document.getElementById("root")).render(h(App));