typesafe-typewriter

16 typed TypeSafe questions about whatever you type, re-asked on every keystroke.

File map

PathTypePurpose
index.tshttpHono app: serves the shell + assets, GET /api/questions, POST /api/judge
typesafe.tsscriptMinimal TypeSafe (System One) REST client + answer types
questions.tsscriptThe 16 question specs and the 3 display groups — the single source of truth
rate-limit.tsscriptBest-effort in-memory per-IP + global rate limit (burst control)
budget.tsscriptSQLite-backed lifetime spend ledger; the hard $2 cap
tests/budget.tsscriptChecks the budget guard without spending anything
frontend/root.tsxscriptHTML shell (Hono JSX); stamps immutable asset URLs
frontend/index.tsxscriptReact entrypoint
frontend/components/App.tsxscriptLayout, text state, status pill, group sections
frontend/components/Meter.tsxscriptOne question row: label, bar, answer
frontend/components/Notice.tsxscriptThe amber box for system states: no key, budget spent
frontend/lib/display.tsscriptAnswer → text, and the "did it meaningfully change" rules
frontend/lib/useJudge.tsscriptDebounced, abortable POST /api/judge
frontend/lib/useChanges.tsscriptLights up rows whose judgment moved
frontend/lib/types.tsscriptClient-side mirror of the API shapes
frontend/lib/presets.tsscriptSample texts
frontend/favicon.svgfileIcon

Notes

  • Plain by design. Black text on white, one bar style for every question type, no color scale, no chart vocabulary. Every row is label / bar / answer and the three columns line up down the page. If a row needs to say more, it says it in words.
  • Meter.tsx owns the only chart in the app: a single Bar (black fill on a bg-neutral-200 track). fill() maps each answer type to one 0–1 number — P(yes) for noul, position on the scale for score, winner probability for choice. Adding a question type means one line there, nothing else.
  • Styling is Twind (Tailwind utility classes, no build step), loaded from cdn.twind.style in root.tsx. Twind's theme is Tailwind v2: it has no 950 shades and no opacity modifiers, and it silently drops classes it can't resolve. Use explicit palette colors, and inline styles for computed values (bar widths, the faded "No" rows).
  • Two caps, two jobs. rate-limit.ts is burst control (600/min per IP, 6000/min globally) and is best-effort in memory. budget.ts is the hard cap: $2.00 lifetime, one row in the val's SQLite database, so it survives isolates and deploys. Every call reserves an estimate before it goes out and reconciles with usage.input_tokens after, which is why index.ts never calls ask() without a reservation. Set TYPESAFE_BUDGET_USD to move the cap. When it's spent, /api/judge returns 429 with the ledger in the body and the client stops asking by itself — don't add a retry loop around it.
  • No key is a UI state, not an error. /api/questions returns a setup object (configured, keyVar, envUrl, consoleUrl) built from isConfigured() in typesafe.ts, so the page can say what's missing before the first keystroke. App.tsx renders SetupNeeded and passes enabled: false to useJudge, which then asks nothing at all. envUrl is derived from parseVal(), so a fork links to its own settings. Nothing about the key's value crosses the wire — only whether one exists.
  • Never answer 502 from /api/judge. The CDN replaces an origin 502 with its own HTML error page, so the val's JSON body never arrives and the client dies on Unexpected token '<'. Unconfigured is 503, an upstream failure is 500; both pass through intact. useJudge's readJson() is the backstop — it parses the body itself and falls back to HTTP <status> rather than throwing a SyntaxError at whatever HTML it got.
  • data-q="<question id>" on each row, data-value on the answer cell, data-type on the row, data-status on the pill (ok, asking, error, budget-spent, needs-setup), data-budget-spent on the footer's spend line, data-budget="spent" on the budget notice and data-setup="needed" on the missing-key notice — for Playwright.
  • Env: TYPESAFE_API_KEY (from https://console.typesafe.ai/settings/keys). TYPESAFE_MODEL optionally overrides the model without a deploy.
  • The browser learns nothing about the questions except via GET /api/questions (which also returns the groups, so questions.ts is the only place group order and copy live). Edit questions.ts and the panel follows. Adding a question is one entry there; latency barely moves, since TypeSafe evaluates every question in a request in parallel.