ESP32-S3 Bench Guide
ESP32‑S3 N16R8 벤치 가이드: 실시간 핀아웃, 강의, 시뮬레이터 및 벤치 로그
유치원부터 8학년까지 수학 문제를 손으로 풀고 튜터가 표시와 힌트를 제공하는 화이트보드
A math whiteboard for kindergarten to 8th grade. Kids work problems by hand, and a tutor checks every line: a check mark for a right step, a circle around a slip, and a hint instead of the answer. Fre
교육TypeScript · SwiftGitHub @MntRushmore온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기A math whiteboard for kindergarten to 8th grade. Kids work problems by hand, and a tutor checks every line: a check mark for a right step, a circle around a slip, and a hint instead of the answer. Free for 7 days.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗Bring your own keys. No AI credentials ship with this source; whoever runs an instance supplies their own keys (see SETUP.md).
License: FSL-1.1-ALv2. Read, modify and self-host freely; do not offer it as a competing service. Each version becomes Apache-2.0 two years after release. See LICENSE, NOTICE, CONTRIBUTING.md and THIRD-PARTY-NOTICES.md. The tldraw SDK is licensed separately and needs its own key in production.
An AI whiteboard that tutors students in real time. Students draw on a tldraw canvas; after a short idle pause the app captures the viewport, sends it to a vision model and draws feedback, hints or a worked solution back onto the canvas. Handwritten math is recognised with Mathpix, and an optional voice tutor (OpenAI Realtime) can look at the workspace and talk the student through it.
browser (Next.js App Router, React 19, tldraw 4)
| Supabase JS: email/password auth, whiteboards/user_settings/bug_reports via RLS
| authedFetch(): POST /api/* with Authorization: Bearer <supabase access token>
v
Next.js route handlers (src/app/api/**, Node runtime on Vercel)
| verify JWT -> rate limit -> validate body (zod) -> call provider
+-> OpenRouter text + vision models (Live checks, hints, worked steps, workspace analysis, credits); never image generation
+-> Mathpix handwritten math -> LaTeX (optional)
+-> OpenAI Realtime voice token (optional)
v
Supabase: Postgres (RLS) + Auth + Storage (board-assets, training-data)
Key directories:
| Path | What |
|---|---|
src/app/page.tsx | Dashboard: list / create / rename / delete boards |
src/app/board/[id]/page.tsx | The canvas, AI loop, autosave, voice tutor |
src/app/train/page.tsx | Trainer-only page that records before/after samples |
src/app/api/** | Route handlers (all require a Supabase JWT) |
src/lib/api-client.ts | authedFetch / apiJson / ApiError used by the client |
src/lib/server/** | Auth, env, rate limiting and provider helpers for routes |
supabase/migrations | Database schema (see below) |
docs/ARCHITECTURE.md | Request flows, security model, known limitations |
Defined in supabase/migrations/20260911000000_init.sql. Every table has RLS enabled, anon has no grants, and authenticated users can only see rows where user_id = auth.uid().
| Table | Purpose |
|---|---|
whiteboards | One row per board: title, data (tldraw snapshot jsonb), preview thumbnail, version, deleted_at (soft delete). Owner CRUD. |
whiteboard_snapshots | Board history for an operator's restore: the state a save replaced, at most every 10 minutes and before a wipe; newest 8 + one a day for 7 days, within 4 MB per board (20261003000000_snapshot_retention.sql). Written by trigger only; owner read. |
board_assets | Registry of objects in the board-assets bucket, for garbage collection. Owner CRUD. |
user_settings | features jsonb (feature-lab toggles). Owner read/insert/update. |
bug_reports | Insert-only from the app (message, screenshot, diagnostics, client log ring buffer). Read via the dashboard/service role. |
trainers | Allow-list of user ids who may use /train. is_trainer() is used by RLS. Self read only. |
training_samples | Before/after samples recorded by trainers (append-only). |
Storage buckets:
board-assets (public read, 15 MB, images): owner writes only under <uid>/<boardId>/.... Target home for canvas images so whiteboards.data stops carrying base64.training-data (private, 10 MB, PNG): trainers write/read only under <uid>/<sampleId>/....npm install
npx supabase start # needs Docker; prints API URL + anon key
npm run db:reset # applies supabase/migrations from scratch
cp .env.example .env.local # fill in the keys below
npm run dev # http://localhost:3000 (pretty pino logs)
Other scripts: npm run typecheck, npm run lint, npm test (vitest), npm run build, npm run db:push.
.env.local)Mirrors .env.example (which carries per-variable comments, sources and local defaults; src/__tests__/envExample.test.ts keeps the two in step with the code). Cloud/Vercel placement: docs/RUNBOOK-supabase.md section 5.
| Name | Required? | Used for |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL | yes | Supabase project URL (browser client + server JWT verification + scripts) |
NEXT_PUBLIC_SUPABASE_ANON_KEY | yes | Supabase anon/publishable key (same users) |
OPENROUTER_API_KEY | yes | All image/vision/Live model calls and the credits banner |
OPENAI_API_KEY | no | Voice tutor (Realtime API). Missing -> /api/voice/token returns 503 |
MATHPIX_APP_ID | no | Handwritten math -> LaTeX (with MATHPIX_APP_KEY). Missing -> vision-only fallback |
MATHPIX_APP_KEY | no | See above; both must be set |
SUPABASE_SERVICE_ROLE_KEY | no | Server-only; declared in src/lib/env.ts but read by no route or script today |
NEXT_PUBLIC_TLDRAW_LICENSE_KEY | no | Removes the tldraw watermark (host-bound) |
NEXT_PUBLIC_SITE_URL | no | Sent as HTTP-Referer to OpenRouter; default http://localhost:3000 |
LOG_LEVEL | no | Server pino level, default info |
NEXT_PUBLIC_LOG_LEVEL | no | Browser pino level (console + bug-report log buffer), default info |
NEXT_PUBLIC_LIVE_MATH | no | 0 hides the Live Math pipeline/UI; default on |
LIVE_MODEL_CHECK | no | OpenRouter id override for Live check; default in src/lib/live/contracts.ts |
LIVE_MODEL_SOLVE | no | Same for Live solve |
LIVE_MODEL_VISION | no | Same for Live vision fallback |
BASE_URL | no (scripts) | scripts/live-smoke.mjs: dev server origin, default http://localhost:3113 |
MISTRAL_API_KEY is no longer used (the /api/ocr route it once powered has been removed with the image pipeline). Delete it from any environment.
The previous project was deleted, so production needs a fresh one:
npx supabase login
npx supabase link --project-ref <ref>
npm run db:push
This creates all tables, policies, triggers and both storage buckets. Re-running is safe.http://localhost:3000 to redirect URLs. Email/password sign-in is the only provider used.vercel env add NEXT_PUBLIC_SUPABASE_URL production
vercel env add NEXT_PUBLIC_SUPABASE_URL preview
vercel env add NEXT_PUBLIC_SUPABASE_URL development
vercel env add NEXT_PUBLIC_SUPABASE_ANON_KEY production
vercel env add NEXT_PUBLIC_SUPABASE_ANON_KEY preview
vercel env add NEXT_PUBLIC_SUPABASE_ANON_KEY development
Repeat for OPENROUTER_API_KEY, and for OPENAI_API_KEY / MATHPIX_* if you want voice and handwriting OCR. vercel env pull .env.local syncs the development values locally./train is gated server-side by the public.trainers table (the page also hides itself for non-trainers). After the trainer has signed up once, run in the SQL editor:
insert into public.trainers (user_id)
select id from auth.users where lower(email) = lower('[email protected]')
on conflict do nothing;
The app runs on Vercel (project whiteboardstaging). vercel.json is intentionally minimal; the Next.js preset handles everything.
vercel # preview deployment
vercel --prod # production
CI (.github/workflows/ci.yml) runs typecheck, lint, tests and a build on every push and PR to main with dummy env values, so no secrets are needed in GitHub.
| Symptom | Cause / fix |
|---|---|
Every /api/* call returns 401 unauthorized | The Supabase session expired or the token is for a different project. Sign out and back in; check NEXT_PUBLIC_SUPABASE_URL matches the project that issued the token. |
402 credits_exhausted | The OpenRouter account is out of credit. Top up at https://openrouter.ai/credits; the dashboard banner shows the balance. |
429 rate_limited | Per-user limit hit; the response carries Retry-After. Limits are per server instance. |
503 voice_unavailable | OPENAI_API_KEY is missing or invalid in that environment. Voice is optional; everything else keeps working. |
503 feature_unavailable | The route's optional provider (e.g. Mathpix) is not configured in this environment. |
502 upstream_error | OpenRouter/Mathpix/OpenAI returned an error; check server logs (LOG_LEVEL=debug). |
Board save fails with a timeout (Postgres 57014) | The snapshot is too large because images are still stored as base64 in whiteboards.data. Delete large pasted images, or ship the board-assets offload. |
npx supabase start fails | Docker is not running, or ports 54321-54329 are taken (npx supabase stop --no-backup then retry). |
All error responses from /api/* have the shape { "error": "<code>", "message": "<human text>" }.
Live Math is an always-on layer on the board (toggle next to the Off / Feedback / Suggest / Solve tabs, persisted per device in localStorage under agathon.live.v1). It turns each handwritten line into a native, editable KaTeX math shape about a second after pen-up and runs an offline mathjs engine for instant checks; a streaming LLM is used only for the why and for hints, and only when the help mode allows it.
What a student sees after writing a line:
| When | What |
|---|---|
| ~0.6 s | Quiet gate ends; strokes are clustered into lines and sent to POST /api/live/recognize (Mathpix v3/strokes, ~300 ms; vision-model fallback when Mathpix keys are absent). |
| ~1.2 s | A gray typeset "echo" appears right of the ink. The local engine adds a result chip (calculator rule), a green check or amber dot versus the previous line, a "Solved" chip on a correct final line, or a graph card for y = f(x). |
| ~2–3 s | In Suggest/Solve, a mismatch triggers POST /api/live/check (SSE); the first hint card streams in below the echo. "Solve steps" streams worked steps from POST /api/live/solve. |
Modes: Off = echoes and results only, never an LLM call. Feedback = location-only marks. Suggest = one Socratic hint per line. Solve = steps and full solutions on request. The legacy image-overlay pipeline is kept for non-math ink (diagrams) and via "Draw help" in the Live menu; its idle trigger is 4 s while Live is on.
What runs where:
src/lib/live/**, src/shapes/**, src/components/live/**).src/app/api/live/{recognize,check,solve}/route.ts behind the same auth, zod validation and per-user rate limits as every other route; prompts in src/lib/server/prompts/; streaming helpers in src/lib/server/sse.ts and openrouter.ts. Model ids default from LIVE_MODELS in src/lib/live/contracts.ts and can be overridden with LIVE_MODEL_CHECK, LIVE_MODEL_SOLVE, LIVE_MODEL_VISION.<Tldraw> mounts (board and train) so any saved board loads; NEXT_PUBLIC_LIVE_MATH=0 hides the pipeline and UI without unregistering the shapes.Keyboard: m selects the Math tool to type a LaTeX shape; double-click an echo to fix a misread line (the engine re-checks it without calling Mathpix).
Testing: npm test (356 unit tests incl. engine, clustering, policy, placement, shapes round-trip, SSE parsing), node scripts/live-smoke.mjs against a dev server + local Supabase (auth, recognition, streaming, rate limits), and the manual checklist in docs/LIVE-MATH-SPEC.md §10.3. Voice tools for Live (read_live_math, place_math, plot_function) are implemented and unit-tested but not yet wired into the voice session.
SMOKE_EMAIL | no (scripts) | scripts/live-smoke.mjs: throwaway local user, default [email protected] |
SMOKE_PASSWORD | no (scripts) | scripts/live-smoke.mjs: its password, default password123 |
SMOKE_SKIP_LLM | no (scripts) | scripts/live-smoke.mjs: 1 skips the LLM streams (no OpenRouter spend) |
RUN_DB_TESTS | no (tests) | 1 runs the DB/RLS integration tests in npm test against the local stack |