Step 16 of 16Days 25–30 · Thu 19 – Tue 24 NovWeek 4Phase B · web
Next.js slice and demo
A minimal Next.js page that a non-coder can use: pick a template, upload a zip, watch training live, see the results, download the model. Then record the demo and write everything down.
Where Where to implement
Laptop
apps/web(Next.js App Router + Tailwind).env.local:NEXT_PUBLIC_API=http://<jetson-ip>:8000
Jetson
- FastAPI + worker from Step 15 keep running
- Add your laptop's address to CORS if you don't use localhost:3000
Do Do it
1Create the app
npx create-next-app@latest sutraai-web --typescript --app --tailwind --eslint --no-src-dir
cd sutraai-web
echo "NEXT_PUBLIC_API=http://<jetson-ip>:8000" > .env.local
npm run dev # http://localhost:30002One page: template → upload → train → results → download
"use client";
import { useState } from "react";
const API = process.env.NEXT_PUBLIC_API ?? "http://localhost:8000";
type Line = { epoch?: number; loss?: number; val_acc?: number; val_f1?: number; event?: string };
const TEMPLATES = [
{ id: "audio", name: "Bird sound (audio · MFCC)" },
{ id: "sensor", name: "Fall detection (accelerometer)" },
{ id: "image", name: "Defect QC (images)" },
{ id: "detect", name: "Driver drowsiness (object detection)" },
];
export default function Home() {
const [template, setTemplate] = useState("audio");
const [file, setFile] = useState<File | null>(null);
const [pid, setPid] = useState<string | null>(null);
const [lines, setLines] = useState<Line[]>([]);
const [status, setStatus] = useState("Pick a template and a zip file.");
const [metrics, setMetrics] = useState<any>(null);
async function start() {
if (!file) return setStatus("Choose a zip file first.");
setLines([]); setMetrics(null); setStatus("Creating project…");
const p = await fetch(`${API}/projects?template=${template}`, { method: "POST" }).then(r => r.json());
setPid(p.project_id);
setStatus("Uploading…");
const fd = new FormData(); fd.append("file", file);
await fetch(`${API}/projects/${p.project_id}/dataset`, { method: "POST", body: fd });
const j = await fetch(`${API}/projects/${p.project_id}/train`, { method: "POST" }).then(r => r.json());
setStatus("Training…");
const es = new EventSource(`${API}/jobs/${j.job_id}/events`);
es.onmessage = async (e) => {
const msg: Line = JSON.parse(e.data);
if (msg.event === "finished") {
es.close(); setStatus("Done.");
setMetrics(await fetch(`${API}/projects/${p.project_id}/metrics`).then(r => r.json()));
} else if (msg.epoch) setLines(l => [...l, msg]);
};
es.onerror = () => { es.close(); setStatus("Lost the connection to the training job."); };
}
const last = lines[lines.length - 1];
return (
<main className="mx-auto max-w-2xl p-6 space-y-6">
<h1 className="text-2xl font-semibold">SutraAI</h1>
<div className="space-y-3">
<label htmlFor="tpl" className="block text-sm font-medium">Template</label>
<select id="tpl" className="w-full border rounded p-2" value={template} onChange={e => setTemplate(e.target.value)}>
{TEMPLATES.map(t => <option key={t.id} value={t.id}>{t.name}</option>)}
</select>
<label htmlFor="zip" className="block text-sm font-medium">Dataset (.zip)</label>
<input id="zip" type="file" accept=".zip" onChange={e => setFile(e.target.files?.[0] ?? null)} />
<button onClick={start} className="rounded bg-blue-700 text-white px-4 py-2">Train</button>
<p className="text-sm text-gray-600">{status}</p>
</div>
{last && (
<div className="rounded border p-4">
<p>Epoch {last.epoch} · loss {last.loss} · validation {last.val_acc ?? last.val_f1}</p>
<progress className="w-full" value={last.epoch} max={100} />
</div>
)}
{metrics && pid && (
<div className="rounded border p-4 space-y-2">
<p className="font-medium">Test score: {(metrics.test_acc ?? metrics.test_f1)?.toFixed(3)}</p>
<div className="flex gap-3 flex-wrap">
{["onnx", "tflite_int8", "c_array", "pytorch"].map(f => (
<a key={f} className="underline" href={`${API}/projects/${pid}/download?fmt=${f}`}>Download {f}</a>
))}
</div>
</div>
)}
</main>
);
}Set the progress bar's max from the template's epoch count once it works. A loss/accuracy chart (recharts or a small SVG) is a good Day 26 addition, together with the Nemotron advisor text from Step 11 shown under the results.
3Demo day (Day 27)
- Run all four templates through the page, one after another.
- For the audio model, load the downloaded int8 file into the Step 10
FakeDeviceand show it predicting. - Record a 5-minute screen video: upload → live training → results → download → simulated device.
4Days 28–30: review and write it down
README.md: how to install on a fresh Jetson (Steps 01, 02, 14, 15 in order).PIPELINE.md: every notebook, what it proved, and the numbers.- Month 2 list: full Studio UI (see the SutraAI UI Flow canvas), SOC API v1 (the Step 10 simulator over HTTP), public project pages, cloud deploy.
- Show the demo to 1–2 people and note their questions.
Test Test it
- A person who has never seen the project trains the audio template from the page without your help.
- All four templates finish and offer downloads.
- The downloaded int8 audio model gives the same predictions in FakeDevice as in NB51.
Pass: upload → train (live) → results → download works for all four templates, and the demo video is recorded.
Done Done when
- Next.js page talks to the Jetson API (CORS OK)
- Live epoch progress during training
- Results and downloads shown when training finishes
- All four templates work through the page
- 5-minute demo video recorded
- README.md, PIPELINE.md and the Month 2 list written
If it breaks
| Symptom | Fix |
|---|---|
| CORS error in the browser console | Add your page's exact origin to allow_origins in app.py and restart the API. |
| EventSource closes at once | Check the job id; open /jobs/<id>/events directly in the browser to see the stream. |
| Upload is very slow | Large zips over Wi-Fi: use Ethernet, or downsample images and audio before zipping. |
| Download link 404 | The export step didn't run for that format; check projects/<id>/exports/ on the Jetson. |
Learn more
- Next.js App Router: getting started
- MDN: EventSource (Server-Sent Events)
- Next: the courses and models page for Month 2 learning