Skip to content

Repository files navigation

Cook-Along Mode

A tablet-first, zero-touch cooking companion. Redesign of a recipe page for a cook whose hands are covered in flour, oil or water, reading a screen from 3 to 5 ft away.

The design rests on one finding from a recorded cook test: 15 device lookups in 50 minutes, and not one was a request for the next step. Every one was a question about the step the cook was already standing in. So this is a question answerer, not a step advancer.

Full reasoning lives in .docs/. Read those before changing anything structural:

Document What it settles
.docs/cook-along-mode-handoff.md Problem, field evidence, personas, jobs, principles, interaction model
.docs/cook-along-ia-sitemap.md Information architecture, sitemap, state machine, legibility maths
.docs/non-touch-interaction.md How the app is driven with your hands in a pan — the mic, the grammar, the alerts, and what each failure looks like
.docs/ai-layer-scope.md The import pipeline, the validator, and where the authored answers come from
.docs/parallel-cooking.md How several pans run at once — the clocks, the strip, which pan is allowed to interrupt you. Written for a non-engineer
.docs/ui-audit.md The design system: the token layer, the two zone scales, and the language rules
.docs/recipe-data-audit.md Where the two hand-authored dishes do and do not hold up their end of what the engine expects
.docs/decision-log.md Every choice taken, what forced it, what it rules out

This table is not the whole of .docs/. Also there, and not yet indexed: voice-interaction.md, app-content.md, user-journey-paneer-butter-masala.md and edge-cases-ai-layer.md.


Running it

Requires: Node 22+, npm 10+.

npm install
npm run dev

Then open http://localhost:5173.

The dev server runs with --host, so it also prints LAN addresses like http://192.168.1.x:5173. Use one of those to open it on the actual tablet — this is a tablet-first design and it cannot be judged in a desktop window.

Command What it does
npm run dev Vite dev server, exposed on the LAN for tablet testing
npm run build Typecheck, then production build to dist/
npm run check The one to run before committing. Typecheck + voice grammar + design system + render tests
npm run test:render Renders every route and every interrupt, and checks the content rules
npm run test:voice The phrase table: what each utterance means, and what must be ignored
npm run test:system The design system: no raw colours, no off-scale values, every button a variant

Stack

React 19 + TypeScript + Vite. The recipe service is a Vite middleware plugin (server/aiPlugin.ts) mounted at /api/ai/*, so it runs under dev and preview and not in a static build — which is why an import fails with a message saying exactly that.

src/
  App.tsx               device frame, prototype chrome, routing
  ui/
    tokens.ts           the palette, the legibility maths, and every scale
    system.ts           the semantic layer, and the distance-derived CSS custom properties
    type.ts             mono() and sans() — the text styles
    Btn.tsx             every button in the app, as variants
  design/system.test.ts the guard: no raw colours, no off-scale values
  data/recipes.ts       Paneer Butter Masala, Chapati, Cherry Tomato Pasta — with provenance
  model/                types, scaling, clock formatting, photo handling, food safety
  ai/                   the import client, the two prompts, the response schema
  voice/                recogniser, grammar, speaker, and the controller that owns the mic
  session/
    engine.ts           the state machine — clock, timers, cursor, ledger, voice
    store.ts            the engine as a React external store
    derive.ts           gaps, attention, and flame visuals
    TimerStrip.tsx      one slot per pan, for the whole session
    IngredientRail.tsx  what is left, struck through live from the ledger
    StepColumn.tsx      the one step that matters, and the assistant behind it
    ElbowBand.tsx       50 mm a wet forearm can hit blind
  screens/              launcher, dishes, source, reading, triage, gaps, servings, ready, session, plate
  overlays/             S5 alert, S6 fallback, S7 wet hands, S8 back again, S11/S12 done, S13 recovery
  devtools/             prototype controls — not part of the design

Two zones, two scales

The seam is hand state, not app phase. Zone A is setup: read at arm's length with the pan still cold, sized in a fixed 4 px grid and a fixed em scale, and it does not move when the cook changes the distance. Zone B is the cook: read from across a kitchen, and every size in it is a ratio of --type-answer. Crossing them is the bug that once made setup type half-size at 2.5 ft. npm run test:system is what keeps them apart.

Why the state machine is not a hook

session/engine.ts is a plain class. The clock, the timers and the voice layer are imperative by nature, and keeping them outside React means a remount cannot restart the pan. store.ts exposes it through useSyncExternalStore. The same engine can later be driven by a real speech recogniser, or by a server, without touching a component.


The one thing to understand before editing

A recipe is a list of moments, not steps.

A conventional step — "fry the onions until golden, about 8 minutes, then add the tomatoes" — decomposes into three moments with three different primary objects: an order (Onions in), a threshold (Golden, and smell sweet), and another order (Purée in).

There is deliberately no field anywhere that holds prose. If you find yourself wanting one, that is the recipe-viewer failure mode the whole design exists to prevent.

Word budgets: prep 4, threshold 5, order 3. Do not raise a budget to make content fit. Cut the content.

The same rule holds for the design system. There is a radius scale, a spacing scale and a type scale, and npm run test:system fails the build on a value outside them. If a size looks wrong, the fix is a different rung — not a new one.

The gaps are the point

A published recipe is written for someone standing at the counter reading it. Cooking hands-free needs heat, a clock and a finish line at every step. data/recipes.ts records which of those the source actually gave ('r') and which we filled in ('i'), and the /setup #gaps screen shows the whole list before a pan is touched. Filled-in values stay marked while cooking — hatched flame icons, "no time given — cue decides" — and are never passed off as the recipe's own.

The clearest case: PBM step 3 says "turn off and let it cool" with no duration. The cook asked exactly this on 2 Aug. The app supplies a 10-minute floor, says so, and lets the cue decide.


Legibility

The entire type scale hangs off one number: how far away the cook stands.

cap height = viewing distance / 120        comfortable glance
cap height = 0.727 × em box                Roboto Flex

At 5 ft that is a ~12.7 mm cap height, which at the target panel's 5.33 px/mm works out around 93 px of body type. Emitted by rootVars() in ui/system.ts as CSS custom properties on the root; every size in the cook screen is a ratio of --type-answer.

The ready screen is where both of its claims get tested rather than stated. The sample line renders at true --type-answer — stand where you will cook and read it. Below it, the elbow band is drawn at its true 50 mm, because a promise that the screen stops asking for a finger is worth nothing until the cook has seen what replaces the finger.

--px-per-mm is measured for the target panel and needs verifying with a ruler on the real device before the layout can be fully trusted.


What is real and what is simulated

State
The state machine, timers, ledger, scaling, session restore Real. Timers are wall-clock epochs, so process death does not pause the pan.
Voice Real, and it is a lookup. The mic is open for the whole cook — no wake word — and every utterance goes through src/voice/grammar.ts into engine.hearUtterance. Answers come out of Moment.answers, authored at import time, so nothing said during a cook touches the network. Recognition itself is on-device where the tablet has a model and cloud where it does not; the band says which.
Recipe import Real. Fetch → extract → triage → compose → validate, against the Gemini API through server/aiPlugin.ts. Dev and preview only: a static build has no server.
Reference photos on the done-check Placeholders.

Known gotchas

Voice needs a secure context, and this is now the first thing to get right. SpeechRecognition only works on localhost or HTTPS. npm run dev -- --host serves a LAN address, which is not one — the page loads fine and the mic never opens. Pick one:

adb reverse tcp:5173 tcp:5173     # then open http://localhost:5173 on the tablet
  • adb reverse over USB is cleanest: no dependency, no certificate warning
  • or add @vitejs/plugin-basic-ssl and accept the self-signed certificate
  • or allowlist the origin in chrome://flags/#unsafely-treat-insecure-origin-as-secure

The app no longer fails quietly here: the ready screen reports Microphone needs https rather than claiming permission it never asked for.

On-device recognition is asked for first. Where Chrome has an en-IN model the audio never leaves the tablet and voice keeps working with the wifi off. Where it does not, the mic falls back to cloud recognition — the kitchen goes to the browser vendor for the whole cook — and both the ready screen and the elbow band say so. See D29.

Developer detail is behind the chrome flag. The adb line above, the missing-API-key instruction, the model and prompt version, the raw import error and the recognition-accuracy count are all real and all useless to a cook, so they render only when the prototype chrome is on (Esc toggles it). With chrome off, each has a plain replacement that says what the cook can actually do about it. See D33.

Target is an Android tablet, which is why the voice layer can use SpeechRecognition.continuous = true directly. On iPadOS that flag is unreliable and the layer would need rebuilding as a restart loop.


Where the recipe data comes from

Source What it decides
Quantities and timings Swasthi's Recipes What is true about the dish
Shape The 2 Aug cook test Which moment holds the screen, and what its primary object is

Most moments carry a sourceStep tracing them back to a published step; the rest are prep moments the source lists as ingredients rather than numbered steps. sourceGaps on each dish records where the published recipe is silent or contradicts itself.

About

Cook-Along Mode: a recipe app for when your hands are covered in oil. Voice-driven IA that splits on hand state, with the design system enforced by a build check instead of a document. React 19 + TypeScript.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages