# MUST OBEY — Mobile-first build requirements
This app's PRIMARY surface is a mobile phone. Build it impeccably on mobile FIRST, then verify on tablet and desktop. Treat the rules below as non-negotiable hard constraints, not suggestions.
## Viewports to verify (every screen, every state)
- 320 px, 360 px, 375 px, 390 px, 414 px, 480 px
- 768 px, 834 px (iPad portrait / Pro 11)
- 1024 px, 1280 px, 1440 px, 1920 px, 2560 px
- Plus: 200% browser zoom, landscape orientation on every mobile width, iPhone with safe-area insets visible
## Hard layout rules
- Mobile-first CSS. Default styles target mobile; `@media (min-width: ...)` for larger viewports.
- Use `dvh` and `svh` instead of `vh` for full-height surfaces (iOS Safari URL-bar bug).
- Use `clamp()` for fluid typography across all viewports.
- Prefer container queries (`@container`) over media queries for component-level responsiveness.
- Use `min(100%, ...)` widths so content never overflows. Zero horizontal overflow at any viewport.
- Add `` to every page.
- Apply `padding: max(safe-area-inset-X, fallback)` on every edge-bleeding container so notched iPhones in landscape never clip content.
- Wide tables and code blocks scroll INSIDE their container (`overflow-x: auto`), never push the body.
- Use `background-attachment: scroll` on mobile, not `fixed` (iOS Safari repaint bug).
- Avoid `backdrop-filter` on animated elements. Use it sparingly on static surfaces only.
- **Canvas Scaling**: Canvases must dynamically scale with window resize events and properly handle high-DPI screens (`window.devicePixelRatio`). Set physical dimensions (`canvas.width`/`canvas.height`) using pixel ratio and render relative to this grid, using CSS to control responsive viewport scaling.
- **Robust Storage**: Every access to `localStorage`/`sessionStorage` (especially `JSON.parse` of loaded state or writes) MUST be wrapped in a `try-catch` block to handle disabled storage, private browsing mode, quota limits, or corrupted JSON gracefully. Fall back to a robust in-memory object store.
## Touch & accessibility
- Tap targets ≥ 44 × 44 px on touch (Apple HIG). Increase to 48 px under `@media (hover: none) and (pointer: coarse)`.
- All interactive controls reachable by keyboard with a visible focus ring; respect `:focus-visible`.
- Color contrast ≥ 4.5:1 for body text, 3:1 for UI components.
- All images have meaningful `alt`. Decorative images use `alt=""`.
- Respect `prefers-reduced-motion: reduce` — zero animation durations under that query.
- Forms validate inline; error messages are specific, not "Invalid input".
- Modals: focus trap, `Esc` closes, `role="dialog"`, `aria-modal="true"`, focus restored on close.
## Performance bar (Lighthouse mobile, throttled 3G/4G)
- LCP < 2.5 s · INP < 200 ms · CLS < 0.1
- JS bundle gzip < 200 KB mobile-first; lazy-load non-critical screens via `React.lazy` / dynamic imports.
- No render-blocking resources above the fold.
- Images: WebP/AVIF preferred, `loading="lazy"`, explicit `width`/`height` attributes (zero CLS), `srcset` for retina.
- Videos: `preload="metadata"`, low-resolution poster, max 720p mobile fallback. Never autoplay with audio.
- Fonts: `font-display: swap`; preload only the one used above the fold.
- Smooth scroll honoured via CSS `scroll-behavior: smooth` with reduced-motion fallback.
## Pre-ship mobile checklist (the deployer MUST verify before declaring done)
1. Open at 375 px in DevTools — every screen scrolls vertically only; zero horizontal scroll.
2. Browser zoom 200% — layout reflows without overlap.
3. iPhone Safari with the URL bar visible AND landscape — no content under the home indicator; no notch clipping.
4. iPad portrait (768 px) and landscape (1024 px) — no awkward gaps; tablet-specific breakpoints land cleanly.
5. Tap every interactive element with a thumb at real-device size — every target is easy to hit.
6. `prefers-reduced-motion: reduce` — every transition / animation skips cleanly, scroll-behavior becomes instant.
7. Lighthouse mobile score ≥ 90 across all 4 categories.
8. Zero `console.error` and zero CLS shift in real-device testing on a mid-tier Android (e.g. Pixel 6a) and an iPhone SE.
---
The original template starts below. All rules above apply on TOP of whatever this template specifies.
---
# Fridge → Dinner Tonight
## 1. Project
**Fridge → Dinner Tonight** turns a single photograph of an open
fridge into a paced, achievable dinner plan for the evening you are
actually in. It is 6:30 pm. The user has spent eleven minutes
holding the fridge door open, has closed it twice, has opened the
takeaway app and closed it, and is now in the small failure-state
where every option feels wrong. They photograph the fridge. In
twelve seconds the app comes back with a real dinner: a starter, a
main, an optional dessert, the order to start each in so everything
arrives at the table at the same time, the substitutions it had to
make ("you have onions, recipe wanted shallots — use the onions and
cut the cook time by two minutes"), and an honest note on what is
missing if anything fundamental is.
If the evening is for guests, one tap turns the plan into a
printable 4K dinner-party menu poster — the courses laid out in
editorial typography, the wines suggested, the date and the
hosts' names rendered legibly in-image. The poster is optional and
never the default. Most weeknight users will never see it. It is
there for the Friday when six people are coming and the user wants
the menu pinned to the fridge by 7:15 pm.
The single demo that proves the magic: the user takes a photo of a
fridge containing half a roast chicken, a fennel bulb, half a lemon,
a tub of crème fraîche, two eggs, a wilting bag of spinach, and a
sad-looking heel of parmesan. In twelve seconds the app returns a
three-course dinner — a quick fennel-and-lemon salad to start, a
chicken-spinach gratin with the crème fraîche and parmesan to
follow, and a soft-set lemon custard from the eggs and the rest of
the lemon for dessert. Each dish lists exactly what is being used,
the prep and cook time, the order to start in, and what to skip if
the user is short on time. No invented ingredients. No "you'll also
need…" demands that defeat the whole point.
**Tagline:** _Drop a fridge photo. Get back tonight's dinner —
paced, honest, and (optionally) a 4K menu poster you can hang._
## 2. Target audience
This is a job-to-be-done template. The job is universal: you have
food in the house, you are tired, you do not know what to make.
The audience is anyone who cooks a few times a week, lives in a
house with a fridge, and would rather not order in tonight.
The shape of the people who land on this in the gallery:
- Weeknight cooks at 6:30 pm who don't want to think anymore
- New cooks who can't yet "see" a meal in a set of ingredients
- People who hate food waste and want to use the wilting things up
- Hosts the morning of a dinner party, looking at a half-stocked
fridge and deciding what to top up at the shops
- People with one specific allergy or restriction (gluten, dairy,
shellfish, peanut, sesame, egg) who get tired of generic recipes
and want a plan that actually honours their list
- People cooking for one and tired of recipes that scale to four
- Parents at the end of the working day who need a 30-minute plan
and a 60-minute plan and want to choose between them
- Anyone who has ever stood at the open fridge for ten minutes
The 19-gallery rule: this is universal, not persona-led. The voice
opens on the job ("It is 6:30 pm and the fridge is open") not on a
named character.
## 3. Core value propositions
Surface these clearly in copy, visual emphasis, and screen
ordering. They are the reasons a visitor stops scrolling and taps.
- **One photo, real plan, twelve seconds.** Open the camera, take
the picture, put the phone down. Twelve seconds later the dinner
is on screen, paced and ordered, every ingredient anchored to
something visible in the fridge.
- **Never invents ingredients.** If the recipe wants shallots and
the fridge has onions, the app says so, picks the closest match,
and tells you what to adjust. If something fundamental is
missing (a stock cube for a risotto, an acid for the dressing)
the app says that too — and offers a different recipe that
doesn't need it. The line we never cross: telling the user they
have something they don't.
- **Paced to your evening.** Every plan returns prep time, cook
time, total wall-clock time, and the order to start each dish in
so the starter, main, and dessert finish within five minutes of
each other. "Start the gratin at 7:42 if you want to eat at
8:30" — that line is the difference between a recipe app and
a dinner app.
- **Substitutions are visible, not silent.** When the model
swaps an ingredient it shows the swap chip on the recipe card —
"shallots → onions (reduce 2 min)", "thyme → dried oregano (use
half)". The user sees every compromise made on their behalf.
- **Allergen-aware, strictly.** If the user names allergies on
first visit, the plan honours them as hard exclusions and the
app refuses to surface a recipe that touches them, even by
cross-contamination risk where relevant (shellfish, peanut,
sesame, egg, dairy, gluten, soy, fish, tree-nut). The exclusion
is visible in the plan ("dairy-free path; no butter, no
cream").
- **Dinner-party poster is one tap, not the default.** Most users
never see it. For the user who is cooking for guests, one tap
generates a 4K poster with the menu rendered in legible
editorial typography (the hero of the post-I/O 2026 Nano
Banana Pro capability). Two-line title, three courses, wines if
surfaced, date, hosts' names. Print-ready PDF below the image.
- **Honest about ambition.** A weeknight plan with 25 minutes of
work and one pan is the default. A "show off for the guests"
plan is opt-in. The app never demands the user roast a duck on
Tuesday because Tuesday is not duck night.
- **Hard rule on food safety.** The plan refuses to cook with
ingredients the photo suggests are spoiled (visible mould, slime
on protein, expired packaging dates if visible in the photo).
When in doubt the app says so plainly and offers a recipe that
doesn't use the questionable item. The disclaimer copy is
prominent: this is not a substitute for your own judgement,
your own nose, or your local food-safety authority.
## 4. Features to build
- **One-photo fridge capture** — open camera → frame fridge →
shoot. Or upload an existing photo. Multiple photos supported
(shelf-by-shelf for a packed fridge).
- **Ingredient inventory view** — what the model saw, as chips
the user can tap to remove (false positives) or add (things in
drawers it missed). Quantities estimated where visible.
- **Plan card** — starter / main / (optional dessert), each with
ingredients list (used: green chip, swapped: amber chip,
optional: grey chip), prep time, cook time, total time, ordered
steps with timing anchors.
- **Pacing strip** — a horizontal timeline: 7:42 start gratin →
7:55 toss salad → 8:05 dress salad → 8:25 plate main → 8:28
finish dessert. Adjustable target eat-time.
- **Substitution panel** — every swap shown as a chip with the
reason in plain English. Tap a chip to revert (forces the model
to pick a different recipe).
- **Allergies & restrictions** — single-pass setup on first
visit; persisted per device. Hard exclusions; soft preferences
(no liver, no fennel, prefer Italian, vegetarian Tuesdays).
- **Cuisine preference** — soft dial. The user can nudge toward
Italian, French, Japanese, Mexican, Levantine, Filipino, Indian,
or "surprise me". Not a hard filter.
- **Time budget** — slider: 15 min, 30 min, 45 min, 60 min, "I have
the evening". The plan re-runs against the new budget.
- **Skip dessert / Skip starter** — single tap to drop a course.
The plan re-paces.
- **Dinner-party mode (opt-in)** — toggle: number of guests,
occasion, vibe (casual / smart casual / smart), whether the user
wants the printable poster.
- **Poster generator** — Nano Banana Pro 4K poster with legible
in-image typography (the post-I/O 2026 capability). Three
templates: warm-tablecloth, white-marble, deep-green. PDF
download.
- **Wine pairing (optional)** — for dinner-party mode, the model
suggests two pairings per course at three price tiers. The user
picks one. Wine pairings cite the style ("a young Chianti
Classico", "an off-dry Riesling") not specific bottles.
- **Shopping top-up list** — if dinner-party mode requires
ingredients the fridge doesn't have, the app emits a precise
top-up list (with grams) the user can take to the shop. The
weeknight default emits nothing; the whole point is no shop.
- **Voice fallback** — if the camera is unavailable or the user
prefers, dictate the contents ("half a roast chicken, a fennel
bulb…") and the same plan generation runs.
- **Voice readback** — the recipe steps can be read aloud
hands-free while cooking (TTS), with a soft pause between
steps.
- **Live cook-along (opt-in, preview)** — point the phone at the
cooking and a Live API session narrates corrections at
conversational speed ("the onions are still translucent, give
them another two minutes"). Flagged as preview; the user opts
in per session.
- **Plan history** — last fourteen plans saved (so the user can
see what they cooked last Thursday).
- **Food-safety scrutiny** — every plan runs a safety pass on the
inventory before recipe selection. Anything flagged is excluded
from the recipe and surfaced in a banner.
## 4b. Required Gemini capabilities + backend services
**This template's intelligence comes from the Gemini capabilities
below. Wire them up explicitly — don't substitute generic LLM
calls.**
### Gemini capabilities (the load-bearing intelligence)
- **Fridge photo → ingredient inventory** (`gemini-3.5-flash`,
multimodal image, thinkingLevel `medium`) — accepts 1-4
photographs of the open fridge and returns a structured
`FridgeInventory` object: every visible ingredient as a chip
with a confidence score, an estimated quantity where visible
(a whole bag of spinach vs a half-used bag), a freshness note
where visible (the limp-looking fennel, the brown-edged
parsley), and a food-safety flag where the image shows
something obviously past it (visible mould, slime). 3.5 Flash
is the post-I/O 2026 default: it beats the previous Pro tier
on multimodal benchmarks at ~4× the speed.
- **Inventory + constraints → dinner plan** (`gemini-3.5-flash`,
thinkingLevel `medium`) — takes the inventory + allergens +
cuisine nudge + time budget + dinner-party flag and returns a
structured `DinnerPlan` with starter / main / dessert, each
recipe anchored only in inventory items, with substitutions
surfaced explicitly. Long-context is not needed here; the
payload is small.
- **4K poster with legible typography** (`gemini-3-pro-image` —
Nano Banana Pro) — the post-I/O 2026 hero. Accepts a structured
prompt naming the dishes, the date, the hosts' names, and a
template choice, and emits a 4K poster with the menu rendered
legibly in editorial typography. This is the new capability:
pre-I/O image models could not render legible text in-image at
4K; Nano Banana Pro can. The capability also supports up to 14
reference images for a style guide — we use this to let the
user supply 1-3 mood references (photographs of their dining
room, a favourite menu, a colour swatch) so the poster matches
the user's actual table.
- **Wine pairing fact grounding** (`gemini-3.5-flash` + grounded
search) — for dinner-party mode only. Pulls pairing logic and
style descriptors from cited sources. **Critical**: grounded
calls do NOT use `responseSchema`; the model emits JSON in the
text body and the server parses it, while citation URLs come
from `response.groundingMetadata.groundingChunks[].web.uri`.
- **TTS readback of recipe steps** (`gemini-3.1-flash-tts-preview`,
the post-I/O 2026 expressive TTS) — reads recipe steps aloud
with a calm, hands-free pace. SSML is not supported; pauses are
encoded as ellipses (`…`) and inter-step breaks as blank-line
em-dashes (`—`). A one-sentence style directive prepends the
text ("Read steadily and unhurriedly, like a friend talking you
through cooking…").
- **Live cook-along** (`gemini-3.1-flash-live-preview`, preview
status) — for users who opt in. Live audio + video session: the
phone watches the cooking, the model narrates corrections at
conversational speed, listens for "ok next" and "did I do that
right". Sessions cap at 2 minutes for audio+video; the app
cycles sessions transparently. Function calling is synchronous
on this preview; the model calls `setTimer(seconds, label)` and
`nextStep()` synchronously.
- **Hero / empty-state imagery** (`gemini-3.1-flash-image` — Nano
Banana 2, the cheap workhorse) — generates the welcome screen
imagery (a photographed-looking fridge half-open in a real
kitchen at evening). Cheaper than Pro; legible typography is
not needed here.
- **Thinking levels** — `medium` for inventory parse and plan
generation (real reasoning over visible-vs-not). `low` for the
food-safety scrutiny pass (cheap and routine). `high` is not
used; this is not a deep-reasoning task. Image-gen, TTS, and
Live API do not accept `thinkingConfig` — omit the field
entirely on those calls.
### Backend services
- **Auth — Required.** Firebase Auth with Google sign-in
(auto-provisioned by AI Studio Build post-I/O 2026). Apple
sign-in is optional and requires an Apple Developer account.
The user can also use the app fully offline of an account for
weeknight use; an account is only required for plan history
and poster downloads. **The post-I/O 2026 Workspace integration
is NOT used here** — this app deliberately doesn't touch the
user's Drive or Sheets.
- **Database — Required.** Firestore for `users`, `plans`,
`inventories`, `allergies`, `posters`. Plans persist 30 days;
the user can pin a plan to keep it longer.
- **File storage — Required.** Firebase Storage for fridge photos
(original quality, 30 days retention) and 4K poster PNGs +
PDFs (kept until the user deletes). **Storage is NOT
auto-provisioned by AI Studio Build today** — enable it in the
Firebase console and wire the bucket name into the project
before first photo upload.
- **Email — Optional.** Magic-link sign-in fallback; not strictly
required since Google sign-in is auto-provisioned. Sender
domain must be authorised in Firebase Auth if used.
- **Payments — Not needed for v1.** A future "host" tier might
charge a small subscription for unlimited posters; not built
in v1. The free tier allows 3 posters per month per user.
- **External APIs:** Gemini API for all intelligence. No required
external API beyond Gemini.
**Environment variables:** every secret (Gemini API key, Firebase
service-account JSON, Stripe key if host tier added) lives in
environment variables — never in client bundle. Include a
`.env.example`. The post-I/O 2026 free Cloud Run deploy from
Build is the intended hosting path.
**Auth + data privacy reminders:** never log secrets · never store
passwords in plain text · use HTTPS everywhere · honour 'delete my
account' inside the UI · explicit opt-in for any analytics · the
user's fridge photos and plan history are never sent to Gemini for
model training (use the Gemini API on the paid tier, where Google
does not use your content for model training, per the Gemini API
Additional Terms) · photos auto-delete after 30 days unless the
user pins a plan that references them.
**Read this first — prompt-craft rules that apply to every call in
this template:**
1. **Name the model variant explicitly** in every Gemini API call.
Do not let the agent pick the model. See the per-call matrix
below.
2. **Pin `thinkingLevel` explicitly** per call. See the matrix.
Models that don't take `thinkingConfig` (TTS, image-gen, Live):
omit the field entirely; matrix row shows `n/a`.
3. **Seed the JSON Schema as a fenced TypeScript / Zod block** in
the system instruction or `responseSchema` field. The literal
schemas are below. **Convert the Zod schema to Gemini's
`Schema` type via the SDK helper** before passing to
`responseSchema` — do NOT pass raw Zod. **Numeric `min`/`max`
constraints are documentation only inside `responseSchema`;
clamp on the server after the response arrives.**
4. **Pin the system instruction separately** from user input. Use
the `systemInstruction` field for persona + behavioural rules;
use `contents` for user input. Never concatenate.
5. **Pre-declare tools as an enable/disable list** per call. The
matrix below names which tools are enabled per call.
6. **`responseSchema` and `google_search` grounding are mutually
exclusive** in one call. The wine-pairing grounded call emits
JSON in the text body; parse server-side and read citation URLs
from `response.groundingMetadata.groundingChunks[].web.uri`.
7. **Multi-image input via Files API** (Developer API `files/*`
resource name, e.g. `files/abc123xyz`, passed via
`fileData.fileUri`) or `inlineData` (base64). Not Firebase
Storage public URLs — the API does not fetch them server-side.
`gs://` URIs belong to Vertex AI / Cloud Storage and are NOT
accepted by the Developer API surface this template runs on.
8. **Long context guardrail** — 1M tokens on Gemini 3.5 Flash. This
app never approaches the limit (the largest payload is 4 fridge
photos + a short plan history), so no chunking is needed.
9. **Grounded responses can wrap JSON in ```json fences or add
prose preamble.** Server-side, strip fences and brace-extract:
```typescript
function safeExtractJSON(raw: string): T {
const clean = raw.replace(/```json\s*|```/gi, '').trim();
const s = clean.indexOf('{'); const e = clean.lastIndexOf('}');
if (s === -1 || e === -1) throw new Error('No JSON boundaries in grounded response');
return JSON.parse(clean.slice(s, e + 1)) as T;
}
```
10. **Strip unsupported Zod modifiers before passing to
`responseSchema`** — Gemini's OpenAPI subset rejects `.regex()`
/ `pattern`, fixed-length `z.tuple()`, and other custom
validators. Use a sanitizer that flattens tuples to arrays and
removes regex patterns before serializing. Validate those
constraints in middleware AFTER parsing.
### Per-call model + tools matrix
| Call | Model | thinkingLevel | Tools enabled |
|------|-------|---------------|---------------|
| Fridge photo(s) → `FridgeInventory` | `gemini-3.5-flash` | medium | (none) |
| Inventory + constraints → `DinnerPlan` | `gemini-3.5-flash` | medium | (none) |
| Food-safety scrutiny pass on inventory | `gemini-3.5-flash` | low | (none) |
| 4K dinner-party poster | `gemini-3-pro-image` | n/a | n/a |
| Wine pairing (grounded) | `gemini-3.5-flash` | low | `google_search` (no `responseSchema`) |
| Recipe-step TTS readback | `gemini-3.1-flash-tts-preview` | n/a | n/a |
| Live cook-along (preview) | `gemini-3.1-flash-live-preview` | n/a | function calls: `setTimer`, `nextStep` (sync) |
| Welcome / empty-state imagery | `gemini-3.1-flash-image` | n/a | n/a |
*Note for builders:* on TTS, image-generation, and Live calls,
omit `thinkingConfig` entirely — the field is not supported. The
`n/a` cells in this matrix are documentation only; do not serialise
them into the request body. Grounded search calls emit JSON in the
text body — `responseSchema` and `google_search` cannot be combined
in the same Gemini call; parse the JSON server-side and read
citation URLs from
`response.groundingMetadata.groundingChunks[].web.uri`. The Live
API is preview — surface that in the UI when the user opts in.
### Primary structured-output schemas (seed verbatim in the prompt)
```typescript
import { z } from "zod";
const Confidence = z.number().min(0).max(1);
const Allergen = z.enum([
"dairy", "egg", "gluten", "peanut", "tree_nut", "sesame",
"soy", "shellfish", "fish", "mustard", "celery", "sulphite",
"lupin", "mollusc",
]);
const FreshnessNote = z.enum([
"fresh", "approaching_end", "questionable", "spoiled_visible",
"unknown",
]);
const InventoryItem = z.object({
item_id: z.string(),
name: z.string(), // "fennel bulb"
name_normalised: z.string(), // canonical
quantity_estimate: z.string().nullable(), // "1 bulb", "~200 g"
freshness: FreshnessNote,
freshness_reason: z.string().nullable(), // why we said so
allergens_present: z.array(Allergen),
confidence: Confidence,
visible_in_photo_index: z.number(), // which photo
flagged_for_user_review: z.boolean(),
review_reason: z.string().nullable(),
});
const FoodSafetyFlag = z.object({
item_id: z.string(),
concern: z.string(), // "visible mould on bread"
severity: z.enum(["advisory", "exclude_from_plan"]),
disclaimer_required: z.boolean(),
});
const FridgeInventory = z.object({
inventory_id: z.string(),
captured_at_iso: z.string(),
photo_count: z.number(),
items: z.array(InventoryItem),
safety_flags: z.array(FoodSafetyFlag),
parse_confidence_overall: Confidence,
notes_to_user: z.array(z.string()), // "I couldn't see in the door — anything in there?"
});
const IngredientUse = z.object({
inventory_item_id: z.string().nullable(), // null if pantry-staple assumption (salt, oil, water)
display_name: z.string(),
amount_text: z.string(), // "1 small bulb, sliced thin"
swap_chip: z.object({
is_swap: z.boolean(),
original_wanted: z.string().nullable(), // "shallot"
using_instead: z.string().nullable(), // "yellow onion"
adjustment_note: z.string().nullable(), // "reduce 2 min, slice thinner"
}),
optional: z.boolean(),
allergen_warning: z.string().nullable(),
});
const RecipeStep = z.object({
step_number: z.number(),
instruction: z.string(), // one sentence
active_minutes: z.number(), // hands-on
passive_minutes: z.number(), // simmer / oven
start_at_clock: z.string().nullable(), // "7:42" — server-computed
});
const Recipe = z.object({
recipe_id: z.string(),
course: z.enum(["starter", "main", "dessert"]),
title: z.string(), // "fennel & lemon salad with parmesan"
one_line_pitch: z.string(), // why this works tonight
prep_minutes: z.number(),
cook_minutes: z.number(),
total_minutes: z.number(),
serves: z.number(),
difficulty: z.enum(["easy", "moderate", "involved"]),
ingredients: z.array(IngredientUse),
steps: z.array(RecipeStep),
serving_note: z.string().nullable(), // "scatter parsley if you have any"
skip_note: z.string().nullable(), // "skip this if you're short on time"
});
const PacingMarker = z.object({
clock_time: z.string(), // "7:42"
what_happens: z.string(), // "start the gratin"
recipe_id: z.string(),
});
const DinnerPlan = z.object({
plan_id: z.string(),
inventory_id: z.string(),
target_eat_clock: z.string(), // "8:30"
served_at_clock_estimated: z.string(),
total_wall_minutes: z.number(),
diners_count: z.number(),
recipes: z.array(Recipe), // 1-3, ordered by course
pacing_strip: z.array(PacingMarker),
ingredients_used_from_inventory: z.array(z.string()), // item_ids
ingredients_assumed_pantry: z.array(z.string()), // "salt", "olive oil"
ingredients_missing_critical: z.array(z.object({
needed: z.string(),
why_critical: z.string(),
})),
substitutions_made: z.array(z.object({
recipe_id: z.string(),
original_wanted: z.string(),
using_instead: z.string(),
note: z.string(),
})),
allergens_respected: z.array(Allergen),
honesty_note: z.string().nullable(), // "I had to skip dessert — no sweet ingredient"
food_safety_disclaimer_required: z.boolean(),
party_mode: z.boolean(),
party_mode_top_up_list: z.array(z.object({
item: z.string(),
amount_grams: z.number(),
why: z.string(),
})),
});
const PosterRequest = z.object({
plan_id: z.string(),
template: z.enum([
"warm_tablecloth",
"white_marble",
"deep_green",
]),
hosts_line: z.string(), // "at home with R + S"
occasion_line: z.string().nullable(), // "Friday supper"
date_iso: z.string(),
style_reference_uris: z.array(z.string()), // up to 14, Files API `files/*` resource names
});
const WinePairing = z.object({
recipe_id: z.string(),
pairings: z.array(z.object({
style: z.string(), // "young Chianti Classico"
why_it_works: z.string(), // one sentence
price_tier: z.enum(["budget", "midrange", "splurge"]),
})),
source_authority: z.string(), // publisher
});
type FridgeInventory = z.infer;
type DinnerPlan = z.infer;
type PosterRequest = z.infer;
type WinePairing = z.infer;
```
### Common failure modes (and how to avoid them)
- **Model invents ingredients.** The plan lists "garlic" or
"butter" though neither is visible. **Fix:** the plan-generation
system instruction must require every `ingredients_used_from_
inventory` entry to be a strict subset of the inventory's
`item_id` list. Pantry staples (salt, pepper, water, neutral
oil) are explicitly allowed and listed in
`ingredients_assumed_pantry`; anything else must be an
inventory item. The server validates set membership; if any
ingredient is neither inventory nor on the allowed pantry list,
the plan is rejected and re-requested.
- **Silent substitution.** The model swaps shallots for onions
but doesn't tell the user. **Fix:** every `IngredientUse` with
`is_swap: true` must populate `original_wanted`, `using_instead`,
and `adjustment_note`. The UI shows an amber chip for every
swap.
- **Allergen leakage.** The user said "dairy-free" but the plan
has parmesan in it. **Fix:** the server filters the inventory
before passing to the plan call — items whose
`allergens_present` intersects the user's `hard_excluded` list
are stripped, with a `notes_to_user` line ("I'm leaving the
parmesan out of consideration — dairy-free path"). The plan
call's system instruction restates the exclusions; the server
re-validates after the response.
- **Plan exceeds time budget.** The user said 30 minutes and the
plan totals 75. **Fix:** the plan-generation system instruction
ranks recipes by `total_minutes ≤ budget` first; the server
rejects plans whose `total_wall_minutes > budget * 1.15` and
re-requests with a tighter prompt.
- **Pacing arithmetic done by the model.** The model says "start
the gratin at 7:42" but the math is wrong. **Fix:** the model
emits relative timing (active/passive minutes per step); the
server computes the absolute clock times from the user's
target eat time and writes them into `start_at_clock` and the
`pacing_strip`. The model never writes clock times directly.
- **Food-safety hallucination — calling fresh food spoiled.** The
model flags a perfectly good fennel bulb as "questionable"
because the photo is dark. **Fix:** the inventory parse system
instruction reserves `spoiled_visible` for unambiguous visible
spoilage (mould, slime, packaging breach, expiry-date text);
`questionable` requires a specific visible reason in
`freshness_reason`; ambiguous photos default to `unknown`, not
to "questionable".
- **Food-safety under-call — eating spoiled food.** The model
misses visible mould on the bread. **Fix:** the inventory parse
runs a second cheap scrutiny pass at thinkingLevel `low` that
re-reads only the photos with explicit instruction to find
spoilage; flags surface in `safety_flags` and the plan call
excludes those `item_id`s entirely.
- **Poster typography illegible.** Nano Banana 2 historically
rendered in-image text as a smear. **Fix:** the poster call
uses `gemini-3-pro-image` (Nano Banana Pro) which renders
legible 4K text — this is the post-I/O 2026 capability the
template demonstrates. The poster prompt enumerates the exact
strings to render (the menu lines, the date, the hosts' line)
and specifies font characteristics ("editorial serif, all caps
for the menu line, body sentence-case for the dishes").
- **TTS rate too fast or too monotone.** **Fix:** the recipe-step
TTS uses `gemini-3.1-flash-tts-preview` (the expressive post-I/O
2026 model). Style is conveyed by a directive sentence in the
input ("Read steadily and unhurriedly…") because SSML is not
supported. Inter-step pauses are encoded as blank-line em-dashes
(`—`); intra-step pauses as ellipses (`…`).
- **Live cook-along session times out mid-cook.** Live API
audio+video caps at 2 minutes. **Fix:** the client tracks the
session and silently re-establishes; the user's state (which
step, which timer) is preserved client-side and replayed in the
new session's opening turn.
- **Wine pairings cite specific real bottles by name.** A 2018
Chateau X may not exist or may be off-vintage. **Fix:** the
wine-pairing system instruction restricts output to styles
(e.g. "young Chianti Classico"), not specific producers or
vintages. The grounded call surfaces the source authority for
the style-level claim.
- **Plan history grows unbounded.** **Fix:** the client retains
last 14 plans; older plans pruned automatically. Photos
auto-delete from Firebase Storage after 30 days unless the
plan is pinned by the user.
### Negative constraints (hard rules)
- Do NOT invent ingredients the inventory doesn't contain. Every
ingredient in the plan must be either (a) an inventory item or
(b) on the explicitly allowed pantry-staples list (salt, black
pepper, water, neutral oil, white vinegar). If something else is
needed, the recipe is rejected and the plan-generation call is
re-requested with that recipe excluded.
- Do NOT make silent substitutions. Every swap is a visible chip
with the original ingredient, the substitute, and the
adjustment note.
- Do NOT violate stated allergens. Hard-excluded allergens are
filtered at the inventory level before the plan call, and
re-validated after the response. Soft preferences (no liver,
no fennel) are honoured at plan generation but not enforced
post-hoc.
- Do NOT propose a recipe that exceeds the user's time budget by
more than 15%. If no recipe can be built within budget, return
a plan with fewer courses and an honest note ("only a one-pan
main fits 25 minutes tonight — skip starter and dessert").
- Do NOT compute clock times in the model. The model emits
relative active/passive minutes; the server computes clock
times from the target eat time.
- Do NOT render in-image text via the cheap Nano Banana 2 image
model. The poster — and only the poster — uses Nano Banana Pro
(`gemini-3-pro-image`) because legible 4K typography is the
capability being demonstrated.
- Do NOT diagnose. The food-safety scrutiny describes ("a fuzzy
green patch on the bread heel") and recommends ("don't use this
loaf tonight"); it does not declare the food unsafe in absolute
terms. The disclaimer is always surfaced: this is not a
substitute for the user's judgement, their nose, or their local
food-safety authority.
- Do NOT moralise. No "you should cook more often", no "consider
a healthier option". The user is an adult.
- Do NOT auto-claim a dish is "authentic" to a cuisine. Use
language like "Italian-leaning", "Levantine-style", not
"authentic Sicilian". The app makes dinners; it does not
arbitrate cuisine authority.
- Do NOT push the dinner-party poster unprompted. It is opt-in.
The weeknight default never surfaces it.
- Do NOT generate posters that mimic real restaurant branding,
copyrighted menus, or specific brand identities. The poster's
typography is generic editorial; the wines suggested are by
style, not by producer.
- Do NOT send the user's fridge photos or plan history to Gemini
for model training. Use the Gemini API on the paid tier, where
Google does not use your content for model training, per the
Gemini API Additional Terms. The capabilities panel says this
in plain English.
- Do NOT auto-share. Plans, posters, and photos are private to
the user by default; sharing is explicit and per-plan.
- Do NOT use the dinner-party mode to charge for the basic plan.
The free tier permits unlimited weeknight plans; only the
poster has a soft cap (3 / month) on the free tier.
### Per-call `systemInstruction` strings
Use these as the literal `systemInstruction` field for each Gemini
API call the built app makes.
### Call: Fridge photo(s) → `FridgeInventory`
Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none)
```
You are parsing one to four photographs of an open fridge into a
structured FridgeInventory JSON object. The photographs were taken
on a phone, in a normal home kitchen, by someone who is hungry and
not a food stylist. Lighting is uneven. Items are partially
occluded. Some items are in clear containers; some are in opaque
ones the user has labelled in handwriting.
Your task is to list every food item you can see clearly enough to
cook with. For each item:
- name: how a home cook would say it ("half a roast chicken",
"wilting bag of spinach", "two eggs", "a heel of parmesan")
- name_normalised: canonical lower-case ("roasted chicken",
"spinach", "egg", "parmesan")
- quantity_estimate: a string a cook can use ("~200 g", "half a
bulb", "2 eggs", "a small handful") — only when visible
- freshness: one of fresh / approaching_end / questionable /
spoiled_visible / unknown
- freshness_reason: a short visible reason when freshness is not
"fresh" — "leaves are limp", "brown patches on the cut edge",
"visible mould on the cut side"
- allergens_present: every relevant allergen from the enum
- confidence: 0..1 — how sure you are this item is in the fridge
Hard rules:
- Do NOT invent items. If you cannot see it clearly, do not list
it. The user can add anything you missed via the inventory
chips in the UI.
- Reserve `spoiled_visible` for unambiguous visible spoilage.
Examples: mould, slime on protein, packaging breach with
visible contamination, an expiry date in the image text that
has passed. Ambiguous freshness defaults to `unknown`, NOT
`questionable`. Calling a fresh item questionable wastes the
user's food and erodes their trust.
- `questionable` requires a specific visible reason (limp leaves,
brown edges, dried-out cut surface). Always populate
`freshness_reason` for `questionable` and `spoiled_visible`
entries.
- For protein, dairy, and prepared food, be slightly more
conservative on freshness than for vegetables and condiments —
but still require a visible reason. Do not assume something is
spoiled because it is a protein.
- Populate `allergens_present` strictly. A wedge of parmesan has
`dairy`. A piece of salmon has `fish`. A jar of peanut butter
has `peanut`. Be thorough — under-calling allergens is the
most dangerous failure mode of this call.
- For items in opaque containers, list them only if you can read
a handwritten label or the container shape is unambiguous.
- For items in the door (condiments, juice, eggs), include them
in inventory even if partially visible; lower their
`confidence` accordingly.
- If you cannot see into a part of the fridge (the bottom drawer
is closed, the door is partly out of frame), add a line to
`notes_to_user` naming what you could not check.
- `flagged_for_user_review: true` whenever confidence < 0.6 or
the freshness call hinges on lighting. Write a one-sentence
`review_reason`.
- `parse_confidence_overall` reflects the photo's lighting,
framing, and your aggregate item confidence. Below 0.6 → ask
the user to retake or describe what you might have missed.
- `safety_flags`: populate aggressively for any visible mould,
slime on protein, broken seal on dairy, or visible date-past
text. Severity `exclude_from_plan` means the downstream plan
call will not use this item. Severity `advisory` means the
user is warned but the item may still be used (limp herbs,
bruised fruit).
Do NOT diagnose. Describe what you see. The phrase "the loaf has
a fuzzy green patch on the cut side, severity exclude_from_plan,
disclaimer_required true" is a description plus a recommendation
plus a disclaimer flag. The phrase "this bread is unsafe" is a
diagnosis — never output a diagnosis.
Do NOT translate item names from the language they appear in. A
hand-labelled jar of "miso" stays "miso"; a tube of "harissa"
stays "harissa".
Output ONLY the FridgeInventory JSON matching the provided schema.
No commentary. JSON only.
```
---
### Call: Inventory + constraints → `DinnerPlan`
Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none)
```
You receive a structured FridgeInventory (the cleaned inventory
after the safety scrutiny pass has stripped any items flagged
with severity "exclude_from_plan") and a set of constraints:
- diners_count (1..12)
- target_eat_clock ("8:30")
- time_budget_minutes (15 / 30 / 45 / 60 / 999 for "I have the
evening")
- hard_excluded_allergens (array of Allergen)
- soft_preferences (array of free-text strings the user gave at
setup or per-session, e.g. "no liver", "no fennel", "lean
Italian tonight", "kid-friendly")
- party_mode (boolean)
- vibe (null on weeknight; one of casual / smart_casual / smart
in party mode)
Your task: propose a dinner plan that is actually achievable in
the user's kitchen tonight, using only ingredients in the
inventory (plus the explicit pantry-staples list: salt, black
pepper, water, neutral oil, white vinegar). On weeknights the
plan is typically a single course (a one-pan main) when the time
budget is 30 minutes or less, two courses (main + starter or
main + dessert) at 45 minutes, three courses at 60+ minutes. In
party mode default to three courses.
Hard rules:
- Every ingredient used must reference either an inventory
item_id (populate `inventory_item_id`) or be one of the
explicit pantry staples (set `inventory_item_id: null` and
list the staple in `ingredients_assumed_pantry`). Anything
else is forbidden.
- Substitutions: when a recipe wants an ingredient that isn't in
inventory but a close alternative is, populate the
`swap_chip`. Examples: shallot → yellow onion (reduce 2 min,
slice thinner); thyme → dried oregano (use half); whole milk
→ crème fraîche thinned with water (richer, lift seasoning).
Do NOT silently swap; every swap is a visible chip.
- Allergens: never propose a recipe containing a hard-excluded
allergen. Even if a substitution would technically remove the
allergen, prefer a recipe that didn't want it in the first
place. List honoured allergens in `allergens_respected`.
- Total wall time: `total_wall_minutes` must be ≤ time_budget *
1.15. If no plan fits, return fewer courses and populate
`honesty_note` ("only a one-pan main fits 25 minutes
tonight").
- Pacing: emit only relative active/passive minutes per step.
The server computes absolute clock times. Do NOT write
`start_at_clock` values; leave them null. The server
populates them.
- Difficulty: be honest. A two-pan dish with a sauce reduction
is `moderate`, not `easy`.
- `one_line_pitch`: a single sentence that tells the user why
this recipe is the right call tonight — anchored in the
ingredients ("the wilting spinach disappears into the gratin
and the chicken stays moist under the crème fraîche").
- `skip_note`: present whenever a course is optional. ("skip
the dessert if the eggs are needed elsewhere — this plan
works as two courses.")
- `ingredients_missing_critical`: if a course needs something
the inventory truly lacks (e.g. an acid for a dressing, a
stock for a risotto) and no substitution works, do NOT
include that course. Add an entry to
`ingredients_missing_critical` so the user understands why.
Never propose a recipe whose backbone ingredient is missing.
- `honesty_note`: present whenever the plan compromises (only
two courses fit; dessert was skipped; the protein was lean
and we made a one-pan dish). One short sentence the user
can read in three seconds.
- Cuisine nudge: if the user said "lean Italian tonight" or
"Japanese feel", lean into descriptors and techniques from
that cuisine but do NOT claim authenticity. Use phrasing like
"Italian-leaning", "Japanese-inflected".
- `food_safety_disclaimer_required`: true whenever the plan
involves under-cooked protein, raw egg, raw fish, or anything
that touches food-safety judgement.
- Party mode: default to three courses, smart_casual vibe, with
one course that has a visible centrepiece (a roast, a tray
bake, a whole fish). Generate `party_mode_top_up_list` only
when a course truly needs something the fridge lacks — keep
the list precise (item + amount_grams + why).
- Do NOT claim dishes are "authentic" to any cuisine. Use
"-leaning" or "-style" instead.
Output ONLY the DinnerPlan JSON matching the provided schema.
No commentary. JSON only.
```
---
### Call: Food-safety scrutiny pass on inventory
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You receive the same FridgeInventory the parse call emitted and
the original photographs. Your only job is to look once more —
with explicit attention — at anything that might be spoiled, and
emit a clean `safety_flags` array.
What you are looking for:
- visible mould (any colour, any food)
- slime on protein
- packaging breach (a torn vacuum-pack, a cracked egg, a leaking
yogurt lid) with visible contamination
- expiry-date text in the image that has clearly passed
- discolouration on fish or meat (grey edges on red meat,
brown patches on fish flesh)
- bloated dairy containers (curdled, separated, swollen lid)
For each finding, emit a FoodSafetyFlag with:
- `item_id` from the inventory
- `concern` describing what you see in one short sentence
- `severity` "exclude_from_plan" for unambiguous spoilage,
"advisory" for borderline cases (limp herbs, bruised fruit
that's still usable)
- `disclaimer_required: true` whenever severity is
"exclude_from_plan"
Hard rules:
- Describe what you see, never diagnose. "A fuzzy green patch on
the cut side of the bread heel" is a description. "This bread
is unsafe" is a diagnosis — never output a diagnosis.
- Be honest about uncertainty. If the photo is dark and you
can't tell whether the brown patch on the chicken is char,
spice rub, or browning, default to NOT raising a flag — and
add a note_to_user line in the inventory call telling the
user to check that item with their own eyes.
- Do NOT raise a flag because food is "old looking". Lots of
food in fridges looks tired. Look for actual spoilage.
- Recommend the user's own senses ("sniff the chicken; if
there's any sour smell, set it aside") — never override the
user's judgement.
Output ONLY the safety_flags array, ready to merge back into the
FridgeInventory. No commentary.
```
---
### Call: 4K dinner-party poster
Model: `gemini-3-pro-image` (Nano Banana Pro) · n/a · n/a
```
You generate a single 4K printable dinner-party menu poster.
This is the post-I/O 2026 Nano Banana Pro capability: legible
typography in-image at 4K. Every word the user sees on the poster
must read crisply at print resolution.
Inputs you receive (assembled into a single prompt):
- `template`: one of `warm_tablecloth`, `white_marble`,
`deep_green` — the visual mood
- `hosts_line`: short, e.g. "at home with R + S"
- `occasion_line`: optional, e.g. "Friday supper"
- `date_iso`: ISO date, render as e.g. "Friday 7 June"
- `recipes`: array of three (or however many) recipes — title +
one_line_pitch
- `wine_styles_per_recipe`: optional, the user's chosen pairings
- up to 14 `style_reference_uris`: optional reference images the
user supplied (their dining room, a favourite menu, a colour
swatch). The Nano Banana Pro 14-reference style-guide
capability uses these to match the user's actual table.
*Note: the exact request payload shape for the 14-reference
style guide was not pinned by Google at I/O 2026 — verify the
SDK call structure against the live `gemini-3-pro-image`
reference before shipping.*
Visual rules per template:
- `warm_tablecloth`: a soft linen tablecloth in warm cream tones,
a sprig of rosemary in the corner, the menu set in a generous
editorial serif (think Times Modern, not Times New Roman),
rendered in deep umber on linen. Two columns: course label
(all caps, small) above dish title (sentence case, larger).
Hosts line in italic at the foot.
- `white_marble`: a slab of carrara marble, the menu set in a
modernist sans-serif (think Söhne, not Helvetica), rendered in
graphite. Centred composition, generous negative space.
- `deep_green`: a deep forest-green backdrop, the menu set in a
high-contrast serif (think Saol Display, not Bodoni), rendered
in warm cream. Each course separated by a hairline rule.
Hard rules:
- Render every text string exactly as supplied. Do NOT
paraphrase. Do NOT substitute words. The menu lines, the
date, the hosts' line, and any wine notes must match the
input strings character-for-character.
- Render text at 4K legibility. The body type must be readable
when the poster is printed at A3.
- Composition: leave generous margin. The menu is the hero;
illustrative elements (the rosemary sprig, the marble grain)
are supporting.
- Do NOT include restaurant branding, real-world menu
iconography from existing brands, or text that mimics a
specific real-world restaurant.
- No people in frame. No hands holding plates. Just the menu
and the surface.
- If `style_reference_uris` are supplied, treat them as a style
guide for palette and texture — match the warmth, the colour
temperature, the formality. Do NOT copy their content
verbatim.
- Output one 4K poster image. The server attaches the same plan
as a print-ready PDF after the image is generated.
```
---
### Call: Wine pairing (grounded)
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: `google_search` grounding
```
You answer "what wine works with this dish?" for each course in
a dinner-party plan. The answer is for a host, not a sommelier
— style-level, not bottle-level.
For each recipe, return two pairings:
- one budget pairing (≤ $15 / £12 / €13 retail bottle territory)
- one midrange pairing ($15-35 retail)
And one optional splurge pairing ($35+) when the dish is the
centrepiece of the meal.
Output JSON in the response text body (NOT via `responseSchema`
— `responseSchema` and `google_search` cannot be combined in the
same Gemini call). Server-side: parse the JSON, then read
citation URLs from
`response.groundingMetadata.groundingChunks[].web.uri` — do NOT
include URLs in the JSON body; the model will hallucinate them.
JSON shape per recipe:
{
"recipe_id": "",
"pairings": [
{
"style": "young Chianti Classico",
"why_it_works": "the bright acidity and lift of cherry cut the gratin's richness without overwhelming the chicken",
"price_tier": "midrange"
},
...
],
"source_authority": ""
}
Hard rules:
- Pairings are by STYLE, never by producer or vintage. "young
Chianti Classico" is allowed; "2021 Castello di Volpaia" is
not.
- `why_it_works` is one sentence, food-anchored, no waffle.
- If sources disagree, surface the disagreement in
`why_it_works`.
- Do NOT promote alcohol consumption. The pairing is offered;
the user decides whether to drink.
No commentary outside the JSON.
```
---
### Call: Recipe-step TTS readback
Model: `gemini-3.1-flash-tts-preview` · n/a · n/a
```
Voice: clear, calm, unhurried, slightly warm. Pick the voice
whose locale matches the user's UI locale.
Use case: hands-free readback while cooking. The user's hands
are full, oily, or wet; they want the next step read aloud
without picking up the phone.
Pre-process the text before sending to TTS:
- Source the text from the active recipe's RecipeStep array.
- At sentence boundaries within a step, insert an ellipsis
("…") for a natural pause.
- Between steps, insert a blank line plus an em-dash ("—"). The
TTS does not support SSML `` — these textual cues
are how pace is conveyed.
- Strip metadata that doesn't belong in spoken output (step
numbers spoken as "step five" rather than "5", active/passive
minutes only mentioned when they matter — "let it simmer for
about four minutes" yes, "active minutes two, passive minutes
four" no).
- Insert the recipe title at the start ("Reading the chicken
gratin steps…") and a calm closer at the end ("That's the
gratin. The salad is next when you're ready.").
Style direction: prepend ONE short directive sentence to the
text input, exactly like: "Read steadily and unhurriedly, like a
friend talking you through cooking. …". There is no separate
`style` API field on Gemini 3.1 Flash TTS; the directive
sentence inside the input is how style is conveyed.
Target rate: ~140 words per minute — slightly slower than
narrative pace because the listener is cooking, not reading
aloud.
Phoneme overrides for ingredient names from other languages
(harissa, sumac, gochujang, dashi) are NOT exposed by Gemini 3.1
Flash TTS — no SSML `` tag. Pronunciation comes from
the chosen voice's native locale.
```
---
### Call: Live cook-along (preview)
Model: `gemini-3.1-flash-live-preview` · n/a · synchronous function calls: `setTimer(seconds, label)`, `nextStep()`
```
Voice: warm, attentive, slightly informal. You are a friend
standing at the user's elbow while they cook. You see what they
see; you listen for them to say "ok next", "did I do that
right", "how long again".
Session opens with the recipe loaded as context. You know the
ingredients, the steps, the active/passive minutes per step.
Behaviour rules:
- Speak only when the user pauses or asks. Do NOT narrate
continuously — the kitchen has its own sounds.
- When the user says "ok next" or "moving on", call
`nextStep()` synchronously and read the next instruction.
- When the next step needs a timer (simmer 4 minutes, rest 5
minutes, bake 18 minutes), call `setTimer(seconds, label)`
synchronously and say "I've set a four-minute timer for the
simmer."
- When the user asks "is this right", look at what's on screen
and say one of: "yes, that's where you want it" / "needs
another minute or two — the edges aren't quite there yet" /
"I can't see clearly — can you tilt the phone toward the
pan?". Concrete, not hedging.
- When the user asks a question outside the recipe (a
substitution, a technique), answer briefly and return to the
step. You are not a chef-school instructor; you are a
cook-along partner.
- Acoustic awareness: if you hear the user say "ow" or "shit"
or a sudden crash, ask "are you okay?" before doing anything
else.
- Sessions cap at 2 minutes for audio+video. When the client
re-establishes the session, the opening turn is "still with
you — we were at step X" so the user does not feel dropped.
Hard rules:
- Do NOT moralise. No "you should preheat next time", no
"consider a healthier oil".
- Do NOT diagnose food safety. If the user shows you something
questionable, suggest they trust their nose and offer a
recipe pivot that doesn't use the item.
- Do NOT keep talking after the user says "thanks" or "I'm
good" — close cleanly.
- This is a preview capability. The UI flags it as such on
opt-in.
```
---
### Call: Welcome / empty-state imagery
Model: `gemini-3.1-flash-image` (Nano Banana 2) · n/a · n/a
```
You generate a single photographic-looking image for the welcome
screen or empty states. Subjects depict real kitchens in real
homes at the moment between work and dinner.
Prompt anchors that work well:
- "an open fridge in a real kitchen at evening, warm overhead
light, no people in frame, shelves visibly stocked with
ordinary food (a half-roast chicken on a plate, a fennel
bulb, some eggs, a wedge of cheese, leafy greens), shallow
depth of field, slight imperfection"
- "a small wooden kitchen table at dusk, a phone resting beside
a chopping board with a fennel bulb half-sliced, a glass of
water, a soft lamp, no people"
- "a packed home fridge photographed straight-on, shelf-by-shelf
visible, real condensation on the inside of the door, no
brand-name packaging visible, evening kitchen light"
Hard rules:
- Photographic, not cartoon, not illustration-style.
- No people in frame unless the user prompt asks for one.
- No commercial branding visible — no supermarket logos, no
brand-name packaging, no app icons.
- Warm lighting, slight imperfection, real surfaces. Avoid the
glossy AI-render look.
- Aspect ratios: 3:2 for hero, 1:1 for empty states, 9:16 for
mobile splash.
- Legible in-image text is NOT a requirement for this call —
that's what Nano Banana Pro is for. This call is the cheap
workhorse.
```
## 5. Use cases & content to include
Build dedicated UI sections or flows for each of these — they tell
you what content the app must support.
- **The 6:30 weeknight, single cook.** One photo, a 30-minute
budget, no party mode. Returns a one-pan main with a side from
whatever leafy thing is in the drawer. Pacing strip: 6:38 start
the pan → 6:52 dress the salad → 7:00 plate.
- **The Friday dinner party for six.** Three photos covering the
whole fridge, the freezer drawer, and the bottom drawer. Party
mode on, vibe smart_casual, eat by 8:30. Returns a three-course
plan with a roast at the centre, a top-up list of three things
the user can pick up at the shop ("100 g rocket, a lemon, a
small bunch of mint"), wine pairings at midrange tier, and a
one-tap poster generated with the warm-tablecloth template.
- **The "what do I actually have?" weekly audit.** Sunday evening.
The user shoots the fridge, the inventory view lists everything
visible, the user taps to remove the four false positives, adds
the two things in the drawer, and now has a working week-ahead
view. Plan generation is paused until the user asks for it.
- **The allergy household.** One household member is dairy-free
and one is gluten-free. The allergens setup includes both as
hard exclusions. Plans never propose a recipe touching either,
even via substitution. The plan card surfaces the honoured
exclusions visibly ("dairy-free + gluten-free path") so the
cook trusts what they're cooking for the table.
- **The cooking-for-one optimisation.** Diners count = 1. Plans
scale down honestly: the gratin becomes a single-portion bake;
the salad is a small bowl, not a salad-for-four halved. The
optional second course gets a smaller portion of an ingredient
the user already opened (don't open the last tin for one
spoonful).
- **The "tonight's questionable chicken".** The user photographs
a fridge containing a roast chicken whose surface looks a
little tacky and has a slight off-smell. The safety pass
flags the chicken with severity `exclude_from_plan` and a note
("the surface looks tacky and the colour at the cut edges is
slightly grey — set this aside and use your nose"). The plan
is built without the chicken; the food-safety disclaimer
surfaces prominently above the plan.
- **The Live cook-along on a tray-bake gratin.** User opts into
the preview. Phone props against the kettle. Live session
watches the gratin go in, sets a 22-minute timer, returns to
the salad with the user, then checks on the gratin when the
user says "is it done". Sessions transparently cycle every
2 minutes; the user never knows.
- **The dictation fallback.** Camera unavailable in a borrowed
flat. The user holds the record button and lists what's in the
fridge by voice. The same plan generation runs against the
voice-derived inventory.
- **The "I want to use up this thing" steer.** The user types
"tonight's plan around the half lamb shoulder before it goes".
The plan-generation prompt includes that steer as a soft
preference; the lamb anchors the main and the rest of the plan
fits around it.
- **The poster for the friend's birthday.** Party mode on, vibe
smart, three courses, occasion_line "Sarah's birthday supper",
hosts_line "from R + S". The user uploads a photograph of the
dining room (linen tablecloth, brass candlesticks) as a style
reference. The poster generator reads the warmth and palette
off the reference and renders the menu in a matching key.
## 6. Page structure
Build the following screens / sections in this order. Adjust copy
to fit the voice, but keep the structural intent.
1. **Welcome / sign-in.** A photographed-looking shot of an open
fridge at evening — half a roast chicken on a plate, a fennel
bulb, eggs in the door, a wedge of cheese visible on the
middle shelf. One paragraph: "It's 6:30 pm and the fridge is
open. Take one photo and we'll come back with a dinner —
paced for tonight, anchored to what you actually have. If
you're cooking for guests, we'll add a 4K poster." Camera
button. Below: Google sign-in (the auto-provisioned default).
"Try without an account" link below — works for the demo, no
plan history saved.
2. **Camera capture.** A clean, full-bleed camera view with a
subtle hint: "open the fridge, frame the shelves, hold
steady". Shutter button. Below: "use library", "add another
shelf", "switch to dictation". Multiple-photo support via a
thin filmstrip at the bottom (1-4 photos).
3. **Inventory view.** What the model saw, as chips on a soft
grid. Each chip shows the item name and a small confidence
bar. Tap a chip → toggle "remove" (false positive). The "+
Add what we missed" button opens a freeform input. Items
flagged by the safety pass surface as muted-red chips with a
warning icon; tap one to read the concern. A primary button:
"Plan dinner" → opens the constraints sheet.
4. **Constraints sheet.** Number of diners (stepper), target eat
time (defaults to 90 min from now, adjustable), time budget
(slider: 15 / 30 / 45 / 60 / "I have the evening"), cuisine
nudge (chip row: Italian, French, Japanese, Mexican,
Levantine, Filipino, Indian, "surprise me"), party mode
toggle. Allergies are global (set once at first visit, edit
in Settings). Primary button: "Generate plan" → spinner →
plan view.
5. **Plan view.** The hero card. At the top: target eat time +
total wall minutes + any honesty note ("only two courses fit
30 min tonight"). The pacing strip immediately below — a
horizontal timeline with the clock-time anchors. Three (or
one, or two) recipe cards stacked. Each recipe card shows:
- course label (small caps: STARTER / MAIN / DESSERT)
- title (display serif, large)
- one-line pitch (italic, muted)
- ingredients list — used (green chip), swapped (amber chip
with original → substitute), optional (grey chip)
- prep / cook / total time pill
- "expand steps" → numbered list with active/passive minutes
- skip note if present
- "read aloud" button (TTS) and "cook with me" button (Live
API, preview-flagged)
Bottom: substitutions panel ("3 swaps made — see them"), a
secondary "make it for guests" button (opens party mode).
Allergens respected chip row above the cards. Food-safety
disclaimer banner above the cards when present —
non-dismissable, role="status", aria-live="polite".
6. **Party mode panel** (opt-in, slides up from the plan view).
Number of guests, occasion, vibe, request poster toggle,
request wine pairings toggle. "Generate party plan" →
re-runs the plan call with party_mode=true → returns the
three-course plan + top-up list + (if requested) wine
pairings.
7. **Poster generator** (opt-in from party mode). Three template
thumbnails: warm_tablecloth, white_marble, deep_green. Hosts
line input, occasion line input, date picker (defaults to
today). "Upload reference images (optional, up to 3)" — the
Nano Banana Pro 14-reference capability, capped at 3 here for
sanity. "Generate poster" → spinner with a progress narration
("rendering the menu in editorial serif at 4K…") → 4K poster
preview with download buttons (PNG + print-ready PDF).
8. **Cook-along (Live API preview).** Full-screen camera view
with the current recipe step overlaid at the top. Live
transcript scrolls at the bottom. Mute button (kills the
microphone). "next step" and "what about…" voice triggers.
A preview banner at the top: "Cook-along is preview — sessions
cycle every 2 minutes, you won't notice."
9. **Plan history.** A simple list of the last fourteen plans.
Each row: date, the main course's title, two thumbnails of
the fridge photos, a "pin" toggle. Tap → re-open the plan
read-only.
10. **Settings & privacy.** Allergies editor (the hard exclusion
list), soft preferences editor (a freeform list of "things I
don't like" / "households I cook for"), unit preferences
(metric vs imperial; °C vs °F), notification preferences (a
soft local reminder at 6:30 pm — never push from server),
photo retention (default 30 days; "keep forever for pinned
plans"; "delete now"), "Delete my account" with a 60-second
cool-off, privacy panel restating the not-trained-on policy.
11. **Footer.** "Built for the 6:30 pm fridge." Privacy: "Your
photos and plans are yours. We never train on them."
Capabilities `(i)` icon in the header.
## 6b. First-visit onboarding
Show a **first-visit onboarding** the first time a visitor lands
on the app (detect via `localStorage` flag; do not show on return
visits). Three slides, dismissible at any time. Persistent
re-entry: a `?` icon in the header reopens it.
**Slide 1 — What this is.**
- Headline: "Photograph your fridge. Get tonight's dinner."
- Subhead: "Paced for the evening you're actually in, anchored
to what you actually have."
- One paragraph (≤ 60 words) explaining the job: it is 6:30 pm,
you don't want to think anymore, you have food in the house.
One photo, twelve seconds, real plan. The dinner-party poster
is opt-in and never the default.
- Visual: a small annotated illustration of the plan card with
the swap chips and the pacing strip labelled.
**Slide 2 — Tell us about your kitchen.**
- One short prompt: "Allergies and restrictions?"
- A chip grid of the fourteen allergens from the schema, plus a
"no restrictions" toggle. The user taps the chips that apply.
- A freeform field for soft preferences ("things you don't
like", "ingredients to avoid"). Optional.
- The setup is persisted per device; the user can edit it any
time in Settings.
**Slide 3 — How to remix this.**
- Headline: "Make this yours."
- Three short bullets:
- "Swap the cuisine chip row in `/data/cuisines.json` for the
kitchens you actually cook from."
- "Edit the recipe-generation system instruction in
`/server/prompts/plan.ts` to lean your weeknight style."
- "Wire your Gemini API key and Firebase project via the
env-var list in the capabilities panel."
- Primary CTA: "Use this template" → links to AI Studio Build
remix entry point.
- Secondary: "Just exploring — close" (sets localStorage flag,
never auto-shows again).
**Accessibility:** focus trap, `Esc` closes, `role="dialog"`,
`aria-modal="true"`, `aria-labelledby`, focus restored to trigger
on close. Respect `prefers-reduced-motion`.
**Don't:**
- Don't gate content behind the modal. The page beneath is
fully usable (you can shoot a photo without seeing it).
- Don't auto-reshow on return visits. Use
`localStorage['onboarding-seen-v1']`.
- Don't include unrelated CTAs (newsletter signup, social
follow).
## 6c. Capabilities info button (persistent in header)
Add a persistent `(i)` icon in the top-right of the header (next
to the primary nav). Click → opens a modal/panel titled **"What
powers this app"**.
**Panel contents (in this order):**
**Gemini capabilities used (the hero list):**
- **Gemini 3.5 Flash (multimodal image, post-I/O 2026 default)**
— reads your fridge photos and returns a structured inventory:
every visible item with a confidence score, an estimated
quantity, a freshness call, the allergens present. 3.5 Flash
is the new flagship: it beats the previous Pro tier on
multimodal benchmarks at four times the speed.
- **Gemini 3.5 Flash (structured output, medium thinking)** —
takes your inventory + your constraints (allergens, time
budget, cuisine nudge, party mode) and returns a structured
dinner plan: starter / main / dessert, ingredients anchored
only to what's in the photo, substitutions surfaced visibly,
pacing computed against your target eat time.
- **Gemini 3 Pro Image — Nano Banana Pro (4K, legible in-image
typography, 14-reference style guides)** — the new hero
capability shipped at I/O 2026. Generates a 4K printable
dinner-party menu poster with the menu lines, the date, and
the hosts' name rendered legibly in editorial typography. The
capability accepts up to 14 reference images so the poster
can match your actual dining room.
- **Gemini 3.5 Flash + grounded search** — for dinner-party
mode, suggests wine pairings by style with citations from
cited public sources.
- **Gemini 3.1 Flash TTS (post-I/O expressive voice)** — reads
recipe steps aloud with a calm, hands-free pace. SSML is not
supported; pauses are encoded in the text itself.
- **Gemini 3.1 Flash Live (preview)** — the opt-in cook-along
mode. Watches you cook, narrates corrections, sets timers.
Sessions cap at 2 minutes for audio+video; the client cycles
them transparently.
- **Nano Banana 2 (Gemini 3.1 Flash Image)** — the cheap
workhorse for welcome and empty-state imagery.
- **Firebase Auth (auto-provisioned)** — Google sign-in by
default. Apple sign-in optional (requires Apple Developer
account).
- **Firestore (auto-provisioned)** — stores your plans,
inventories, and allergens. Syncs across devices in real time.
- **Firebase Storage (manual enable)** — keeps your fridge
photos for 30 days (longer if you pin a plan); keeps your 4K
posters and PDFs until you delete them. NOT auto-provisioned
by AI Studio Build today — enable it once in the Firebase
console.
- **Cost note** — see the detailed breakdown in 6d. A typical
weeknight plan costs about $0.04 of Gemini API spend. A full
dinner-party plan with a 4K poster costs about $0.32.
- **Privacy note** — your photos and plans are private to you.
This app uses the Gemini API on the paid tier, where Google
does not use your content for model training, per the Gemini
API Additional Terms.
- **Food-safety note** — this app describes what it sees and
recommends what to skip; it does not declare food unsafe in
absolute terms. Whenever a question touches sanitation or
spoilage, the answer is flagged as not a substitute for your
own judgement, your own nose, or your local food-safety
authority.
**Backend services this app depends on:**
- Auth: see section 4b
- Database: see section 4b
- Storage: see section 4b — REQUIRES manual enable in Firebase
console; AIS Build does not auto-provision Storage today.
- Email: see section 4b — optional, magic-link fallback only.
- Apple sign-in: optional, requires an Apple Developer account
and Service-ID config.
- Payments: see section 4b (not used in v1; soft cap of 3
posters/month on free tier).
- External APIs: see section 4b — Gemini only.
**Environment variables you'll need to configure:**
- `GEMINI_API_KEY` — your Google AI Studio API key
- `FIREBASE_PROJECT_ID` — your Firebase project id
- `FIREBASE_SERVICE_ACCOUNT` — service-account JSON (server-side
only)
- `FIREBASE_STORAGE_BUCKET` — the bucket name you enabled
manually
**Cost + privacy notes:**
- A typical weeknight plan: ~1 inventory parse + 1 plan call +
optional 1 safety scrutiny + 0-1 TTS readback ≈ $0.03-0.05.
- A dinner-party plan with a 4K poster: ~1 inventory parse + 1
plan call + 1 safety scrutiny + 1 wine grounding + 1 Nano
Banana Pro poster ≈ $0.30-0.35.
- Privacy: photos auto-delete after 30 days unless the plan is
pinned. "Delete this plan" removes the photo and the plan
record in under 60 seconds.
**Documentation links:**
- AI Studio Build docs (post-I/O 2026 release notes)
- Gemini API multimodal image + structured output docs
- Gemini API Nano Banana Pro docs (4K, legible typography,
14-reference style guides)
- Gemini API Live preview docs (3.1 Flash Live)
- Gemini API expressive TTS docs (3.1 Flash TTS)
- Firebase Auth, Firestore, Firebase Storage docs
**Accessibility:** same standards as the onboarding modal —
focus trap, `Esc`, ARIA, restored focus.
**Behaviour:**
- Always available — single click from anywhere in the app.
- Tooltip on the `(i)` icon: "How this app is built".
- Mobile: opens as a full-screen sheet that slides up.
- Should be the most honest part of the app — never hand-wave
service requirements; never say "AI" without naming the
specific Gemini model and capability.
## 6d. Detailed cost breakdown (deployer reads this BEFORE shipping)
- **Fridge photo → `FridgeInventory` (Gemini 3.5 Flash, medium
thinking, multimodal image)** — a single 4-MP photo plus the
system instruction averages ~2,500 input tokens (the image
dominates) and ~800 output tokens. At Gemini 3.5 Flash global
pricing ($1.50/M input, $9/M output) that is ~$0.0110 per
photo parse. A typical capture is 1-2 photos → ~$0.01-0.02 per
inventory.
- **Food-safety scrutiny pass (Gemini 3.5 Flash, low thinking)**
— re-reads the same photos with a tighter instruction. Cached
input on the photo bytes reduces cost to ~$0.0008 per scrutiny
(Flash cached-input pricing is $0.15/M).
- **Inventory + constraints → `DinnerPlan` (Gemini 3.5 Flash,
medium thinking)** — payload is small (~1,200 input tokens,
~1,500 output tokens including three recipes with steps).
~$0.016 per plan generation.
- **4K dinner-party poster (Nano Banana Pro, `gemini-3-pro-image`)**
— flat $0.24 per 4K image at Pro tier ($2/M input, $12/M
output; effective per-image rate based on the standard image
output token count). Single generation per poster request.
- **Wine pairing (Gemini 3.5 Flash + grounded search)** —
~$0.002 per call, plus the grounded-search call cost. A
three-course party plan triggers one call.
- **Recipe-step TTS (Gemini 3.1 Flash TTS preview)** —
approximate ~$0.0010 per 200-word recipe step set (the exact
TTS character-token price was not pinned at I/O 2026; treat
as a working estimate and verify before shipping). Cached per
plan; charged once.
- **Live cook-along (Gemini 3.1 Flash Live preview)** — billed
per audio+video session-minute. A typical 30-minute cook-along
spent ~12 minutes actively talking with the model → ~$0.18.
Preview tier may be free or discounted as of June 2026; verify
current pricing.
- **Welcome / empty-state imagery (Nano Banana 2,
`gemini-3.1-flash-image`)** — ~$0.02/image. Generated once per
app install (cached client-side) or per regenerate-empty-state
action.
- **Expected per-plan cost:**
- Weeknight plan (no party mode, no poster, no Live): ~$0.04
- Weeknight plan with TTS readback: ~$0.04
- Dinner-party plan with 4K poster + wine pairings: ~$0.32
- Dinner-party plan with Live cook-along (30 min): ~$0.50
- **Storage:** fridge photos at upload quality average ~600 KB
per photo; 30 days retention at ~10 plans/month per active
user ≈ ~12 MB/user/month at Firebase Storage standard tier
($0.026/GB/month) ≈ ~$0.0003/user/month. Posters at 4K PNG
+ PDF average ~2 MB per saved poster → negligible.
## 7. Design language
- **Mood:** A real kitchen at 6:30 pm, not a SaaS product. Not a
food-blog. Not a recipe-app aesthetic with red-and-white
gingham. The light is the warm overhead of a real kitchen
after work. The surfaces are wood and ceramic, not polished
marble. The typography is editorial: confident, generous,
unhurried — like a small magazine, not a software dashboard.
- **Typography:** A workhorse grotesque for app chrome and
ingredient chips (Inter or Söhne). A display serif for recipe
titles and the dinner-party poster's heading style (Fraunces
or Saol Display). A small monospaced for the pacing strip
clock times (JetBrains Mono or IBM Plex Mono) — clock times
are data; they read better in mono.
- **Palette:**
- Warm paper background `#F6F1E8` for the main surface
- Deep ink `#1B1714` for body text
- Burnt sienna `#A75432` for the primary action (the camera
button, the "generate plan" button) — warm, kitchen-y, not
SaaS-blue
- Olive `#5B6E3A` for the "used from fridge" green chip
- Amber `#C7873A` for the "swap" chip — visible without
alarming
- Pale grey `#D5CFC2` for the "optional" chip and for muted
secondary text
- Tomato red `#C53A2C` only for the food-safety disclaimer
banner border and for hard-allergen exclusion chips
- Deep forest `#1F3A2D` and warm cream `#F4ECDC` for the
deep_green poster template
- **Imagery:** Photographic. Real kitchens at evening. Real
fridges, not styled product shots. Warm overhead light. Slight
imperfection (a smudge on the glass, a crumb on the shelf).
Generated via Nano Banana 2 with prompts emphasising real
surfaces, real food, no brand-name packaging. The dinner-party
posters are Nano Banana Pro — the editorial-magazine quality
is the demonstration.
- **Hand-feel touches:** The plan card slides up from below
with a slight paper-shadow when generation completes. The
swap chips have a soft amber glow on hover. The pacing strip
has a tiny ticking second-hand animation on the active step
during cook-along (respects prefers-reduced-motion). The
inventory chips have a satisfying tactile pop when removed.
The poster preview lifts with a slight tilt on hover, like a
printed page lifting off the table.
- **Spacing:** consistent 4-px base. Generous whitespace — the
plan card needs air to breathe.
- **Radius:** consistent token set (e.g. 8 / 14 / 22 px).
Ingredient chips use 8; recipe cards use 14; the welcome
hero uses 22.
- **Shadows:** subtle, warm-tinted, layered. The plan card has
one soft drop-shadow; nothing else does. Heavy drop-shadows
feel SaaS — avoid.
- **Motion:** purposeful — generation entrance, hover lifts on
cards, the pacing strip's ticking second hand. Respect
`prefers-reduced-motion`. No bouncing splash animations. No
hero parallax. The dinner-party poster generation has a small
progress narration with three steps ("composing the menu…
setting type… rendering at 4K…") because the wait is real (~8
seconds for Nano Banana Pro) and the narration earns the
patience.
- **States:** every interactive element has hover, focus,
active, and disabled states. Loading uses skeletons that
match the eventual layout, not spinners — except for the
poster generator, which uses the narrated progress because
the user is watching that specific render happen.
## 8. Content generation rules
- Write **realistic, specific copy**. NO Lorem Ipsum. NO generic
placeholders.
- Invent plausible fridges, plausible plans, plausible cooks.
Lean on realistic patterns — a wilting bag of spinach, a heel
of parmesan, a half-tub of crème fraîche, two leftover eggs
— but never claim a fictional ingredient came from a real
brand or that a fictional plan was tested by a real cook.
- Tone: warm, direct, free of corporate language. The user is
tired and hungry. Cut filler.
- Headlines: punchy and concrete. No 'Empower your X'. No
'Revolutionize'. No 'Seamless'. No 'AI-powered'.
- Body copy: short paragraphs (2-4 sentences). Use lists where
appropriate.
- Plain language. Avoid jargon — except where the cook already
speaks the jargon ("crème fraîche", "deglaze", "gratin",
"vinaigrette" because home cooks say those words).
- Where the app outputs AI-generated content, never label it
"AI says" — let it speak naturally. Use small uncertainty
cues only where epistemic honesty requires them (a
low-confidence inventory item shows a faint underline; tapping
reveals the model's reasoning).
- Substitution chip copy is one short clause: "shallots →
onions (reduce 2 min, slice thinner)". Not a paragraph.
- The food-safety disclaimer copy is exactly: "We describe what
we see and recommend what to skip. This is not a substitute
for your own judgement, your own nose, or your local
food-safety authority."
## 8a. Seed content (use these specific examples)
Anchor every generated copy + sample data point in the concrete
content below. Use these names, numbers, and snippets verbatim
where helpful, or generate close variants that sit in the same
world. Invent fictional households; do NOT use real public
figures.
**Sample fridge (the demo capture):**
- half a roast chicken on a white ceramic plate, breast and one
leg remaining
- a whole fennel bulb with fronds still attached
- half a lemon, cut side facing the front
- a tub of crème fraîche, ~200 g, lid on, ~half full
- two eggs in the door, in their original carton
- a wilting bag of spinach, ~80 g, leaves limp but still green
- a wedge of parmesan, ~120 g, slightly dried at the cut edge
- a glass jar of cornichons (closed, not used in the plan)
- a half-bottle of dry white wine (not used in the weeknight
plan; used as a pairing reference in party mode)
- a tube of harissa paste (closed, ~half full)
**Sample inventory parse result (the demo):**
- 10 items, parse_confidence_overall 0.86
- 1 advisory safety flag on the spinach ("leaves are limp;
still usable in cooked dishes, not raw")
- notes_to_user: "I couldn't see the bottom drawer clearly — is
there anything there?"
**Sample weeknight plan (the demo, 30-min budget, single cook):**
- target_eat_clock: "7:15" (set from a 6:43 pm capture)
- total_wall_minutes: 28
- One course: a one-pan chicken-spinach gratin with crème
fraîche and parmesan
- Recipe title: "chicken, spinach & parmesan, one pan"
- One-line pitch: "the wilting spinach disappears into the
gratin and the chicken stays moist under the crème fraîche"
- Prep 8 min, cook 20 min, total 28 min, serves 2
- Difficulty: easy
- Ingredients: roast chicken (used) · spinach (used, wilted into
the cream) · crème fraîche (used, ~150 g) · parmesan (used,
~80 g grated) · lemon (used, zest only) · salt + black pepper
+ olive oil (pantry assumed)
- Steps (5): preheat oven to 200 °C / heat oil, wilt spinach 3
min / pull chicken into bite pieces, layer with spinach,
cover with crème fraîche thinned with a splash of pasta water
/ scatter parmesan and lemon zest / bake 18 min until top is
bronzed
- Pacing strip: 6:47 preheat oven · 6:50 start spinach · 6:55
layer + scatter · 6:57 into oven · 7:15 plate
- Substitutions made: none
- Honesty note: "only one course fits 30 minutes tonight —
skip the salad or open the cornichons for crunch"
- Food-safety disclaimer required: false
**Sample dinner-party plan (the demo, 60-min budget, six
diners):**
- target_eat_clock: "8:30"
- total_wall_minutes: 95 (this exceeds the 60-min weeknight cap
but is appropriate in party mode where the budget shifts)
- Three courses:
- Starter: "fennel & lemon salad with parmesan shavings"
(prep 10 / cook 0 / total 10 min, serves 6)
- Main: "chicken & spinach gratin with parmesan crust" (prep
15 / cook 35 / total 50 min, serves 6)
- Dessert: "soft-set lemon custard with crème fraîche" (prep
10 / cook 25 / total 35 min, serves 6, requires top-up: 200
g sugar from the pantry — flagged in top-up if not visible)
- Substitutions: thyme → dried oregano (use half) on the gratin
topping
- Top-up list: 100 g rocket, 1 lemon, 200 g caster sugar — "the
rocket lifts the fennel salad; the lemon doubles for the
custard; sugar for the custard."
- Wine pairings:
- Starter: young Picpoul de Pinet (budget) / Vermentino di
Gallura (midrange)
- Main: village-level Chablis (midrange) / Beaujolais Cru
"Morgon" (midrange)
- Dessert: late-harvest Riesling (midrange) / Sauternes
(splurge)
- Honesty note: "this plan crosses the 60-min budget by 35
minutes — party mode default. Set time budget to 45 min to
tighten."
- Food-safety disclaimer required: false
**Sample dinner-party poster (the demo):**
- Template: warm_tablecloth
- Hosts line: "from R + S"
- Occasion line: "Friday supper"
- Date: "Friday 7 June"
- Menu lines as rendered legibly on the poster:
- STARTER · fennel & lemon salad, parmesan shavings
- MAIN · chicken & spinach gratin, parmesan crust
- DESSERT · soft-set lemon custard with crème fraîche
- The poster is generated at 3840 × 5400 px (4K-portrait),
ready to print at A3.
**Sample voice copy:**
- Onboarding: "Photograph your fridge. We'll build a dinner."
- Camera hint: "Open the fridge, hold steady, hit the shutter."
- Inventory empty state: "Nothing to see yet — point the camera
at the fridge."
- Parsing: "Reading what's in there…" / "Checking freshness…"
/ "Planning the courses…" / "Setting the clock for 7:15…"
- Plan confirmation: "Plan ready — start the oven at 6:47 to
eat at 7:15."
- Swap chip example: "shallots → onions (reduce 2 min, slice
thinner)"
- Allergen banner: "dairy-free path · no butter, no cream, no
cheese"
- Food-safety disclaimer: "We describe what we see and
recommend what to skip. This is not a substitute for your
own judgement, your own nose, or your local food-safety
authority."
- Poster CTA: "Cook for guests this week? One tap turns this
plan into a 4K menu poster you can pin to the fridge."
- Poster narration: "Composing the menu… setting type at 4K…
rendering the linen…"
- Live cook-along banner: "Cook-along is preview — sessions
cycle every 2 minutes, you won't notice."
- TTS opener: "Reading the chicken gratin steps…"
- Low-confidence note: "We weren't sure about this one. Tap
the chip to remove it or confirm what it is."
- Honesty note example: "only one course fits 30 minutes
tonight — skip the salad or open the cornichons for crunch."
**Sample fictional cooks (jobs-to-be-done framing, not persona
biographies):**
- A weeknight cook reheating around a half-roast chicken
- A first-time host preparing for six on a Friday
- A cook avoiding dairy for a partner whose intolerance flares
- A parent who has 30 minutes between the school run and dinner
- A flatmate cooking for one and wanting to use the wilting
spinach before it's gone
## 9. Media & assets
- **Hero image (landing screen):** A photographed-looking shot
of an open fridge at evening — half a roast chicken on a
ceramic plate, a fennel bulb, eggs in the door, a wedge of
parmesan, a half-lemon — under warm overhead kitchen light,
no people in frame, slight condensation on the door. Generate
via Nano Banana 2 (`gemini-3.1-flash-image`) — this is the
cheap workhorse call, not the Pro typography demo.
- **App icon / wordmark:** Set in the display serif. A small
illustration of an open fridge silhouette. No emoji.
- **Empty-state illustration:** A simple line drawing of a
fridge with a question mark on the door. Hand-drawn aesthetic,
not a flat icon. Generate once at build time via Nano Banana 2
(`gemini-3.1-flash-image`), 1:1 WebP at 1024×1024, prompt:
"single hand-drawn ink line illustration of a tall household
fridge in three-quarter view with a single hand-drawn question
mark on the door, off-white paper background, slight pen
imperfection, no shading, no colour fill, no text, no
commercial branding". Ship as a seed asset at
`/public/samples/empty-state-fridge.webp`.
- **Demo poster (Nano Banana Pro 4K):** Generated per the seed
content above — the warm_tablecloth template with the three
demo courses. This is the hero artefact that demonstrates the
post-I/O 2026 legible-typography capability. Show it in the
capabilities panel and below the dinner-party CTA on the
landing screen.
- **Stock fallbacks:** If image generation fails, fall back to
the photographed sample fridge at `/public/samples/sample-
fridge.jpg` (3:2 WebP, 2048×1365 — ship as a seed asset;
recreate via Nano Banana 2 (`gemini-3.1-flash-image`) with the
prompt: "photographic open fridge at evening, half a roast
chicken on a ceramic plate on the middle shelf, a fennel bulb,
eggs in the door tray, a wedge of parmesan, a half-lemon,
warm overhead kitchen light, no people in frame, slight
condensation on the inside of the door, no commercial
branding, real worn fridge"). Never to a "🍳" emoji.
- **Generated imagery:** prefer Nano Banana 2 over stock
photography for hero + empty states. Nano Banana Pro for the
user's posters only — cost-justified by the capability.
- **Optimisation:** WebP/AVIF, `loading="lazy"`, explicit
`width`/`height` to prevent layout shift.
- **Icons:** `lucide-react` for UI. Use sparingly — never
decorative-only.
## 10. Interactivity & states
- Every interactive element has hover, focus, active, and
disabled states.
- Forms validate inline and show specific error messages (not
"Invalid input"). "Eat time can't be in the past — pick a
later target" is the right shape.
- Loading states use skeletons that match the eventual layout,
not spinners — except for the poster generator, which uses
the three-step narrated progress because the wait is real.
- Empty states explain the next action with a button whose label
fits THIS app's domain: "Take a photo of your fridge", "Add
what we missed", "Tighten the time budget" — never a generic
"Add your first item".
- Smooth scroll for in-page anchors.
- All AI-generated content streams in token-by-token where
supported, with a clear "thinking…" indicator before content
starts arriving.
- If an AI call fails, show a calm, specific error ("The photo
is too dark to read confidently. Want to retake, or describe
what's in the fridge?") and offer retry.
- Low-confidence inventory items in the chip grid are faintly
underlined; tapping reveals the model's brief reasoning.
- Substitution chips have a tap action to revert the swap
(forces the plan call to re-run without that recipe).
- The pacing strip's ticking second-hand animation respects
`prefers-reduced-motion` and falls back to a static caret.
- The food-safety disclaimer banner is non-dismissable when
present, surfaces with `role="status"` and
`aria-live="polite"`, and stays pinned above the plan card.
- The poster generator's progress narration runs at ~1 phrase
every 2 seconds — earns the ~8-second Nano Banana Pro wait.
- Live cook-along surfaces a clear preview banner and a
prominent mute button.
## 11. Tech & responsive requirements
- **Stack:** React + TypeScript + Tailwind CSS. Functional
components + hooks. Use Shadcn UI primitives where
appropriate. Camera capture via the `MediaDevices.getUserMedia`
API with a fallback to the `` element on browsers that don't support
it.
- **Build runtime:** AI Studio Build — full-stack with Cloud Run
server-side functions (post-I/O 2026 free 2-app Cloud Run
deploy from Build). All Gemini API calls happen server-side;
API key lives in Secrets Manager, never in client bundle.
- **Model selection:** explicitly pin `gemini-3.5-flash` for the
inventory parse, the plan call, the safety scrutiny, and the
wine grounding; `gemini-3-pro-image` (Nano Banana Pro) for the
4K poster only; `gemini-3.1-flash-image` (Nano Banana 2) for
welcome/empty imagery; `gemini-3.1-flash-tts-preview` for the
recipe-step TTS; `gemini-3.1-flash-live-preview` for the
cook-along. Set `thinkingLevel` explicitly per call per the
matrix; omit `thinkingConfig` entirely on TTS, image, and Live
calls.
- **Database:** Firestore (auto-provisioned by AI Studio Build).
Show the seed inventory + sample plan on first launch (the
demo data above).
- **Auth:** Firebase Auth — Google sign-in by default (auto-
provisioned post-I/O 2026); Apple sign-in optional next to it;
magic-link email as fallback (sender domain must be authorised
in Firebase Auth if used). The app permits anonymous use for
the demo path; sign-in is only required for plan history and
poster downloads.
- **Storage:** Firebase Storage for fridge photos and poster
artefacts. Pre-signed URLs only. Photos are server-side
re-uploaded to the Gemini Developer API Files API and referenced
via `fileData.fileUri: "files/abc123xyz"` (the `files/*`
resource name; NOT a `gs://` URI — that's Vertex AI). Do NOT
pass Firebase Storage public URLs to `generateContent`.
- **Live API session handling + state recovery:** the cook-along
client tracks current step + active timers locally and
re-establishes the Live session transparently every 2 minutes
(audio+video) / 15 minutes (audio-only). Persist a
`SessionSyncState` to `sessionStorage` on every tick and pass a
concise context summary into the first system message of the
next handshake cycle:
```typescript
interface SessionSyncState {
activeSessionId: string;
accumulatedSegments: Array<{ speaker: string; text: string; timestamp: number }>;
currentActiveStepIndex: number; // recipe step cursor
activeTimers: Array<{ label: string; endsAtMs: number }>;
}
```
- **Mobile-first.** Verify layouts at 375 px (iPhone SE), 768 px
(iPad), 1024 px, 1440 px+.
- Use `clamp()` for fluid typography. Prefer container queries
over media queries for component-level responsiveness.
- Use `dvh` / `svh` instead of `vh`. Respect safe-area insets on
iOS.
- Zero horizontal overflow at any width. Zero layout shift on
load.
- Persist user data in Firestore. Use real-time listeners on
the plan view (so a user starting on phone can finish on
tablet).
- Optimistic UI on writes; reconcile on response.
- Cook-along mode uses the screen-wake-lock API to keep the
display on; falls back gracefully if unsupported.
- The poster image is generated server-side, written to Firebase
Storage, and served via a pre-signed URL with a `Content-
Disposition: attachment` header on the download endpoint.
- The PDF version of the poster is composed server-side via
`pdf-lib` or `pdfkit`, embedding the 4K PNG; print-ready at
A3 with 5 mm bleed.
## 12. Accessibility (WCAG 2.2 AA)
- Semantic HTML — `header`, `nav`, `main`, `section`, `article`,
`footer`.
- All interactive controls reachable by keyboard with a visible
focus ring. The camera capture button is reachable by keyboard
with `Enter` triggering shutter on browsers that allow it;
there is also a "use library" path for keyboard-only users
who cannot operate a camera UI.
- Colour contrast ≥ 4.5:1 for body, 3:1 for large text and UI
components. The amber swap-chip text on warm-paper background
is verified at 4.6:1; the burnt-sienna primary button text
(white on `#A75432`) is verified at 5.2:1.
- All images have meaningful `alt` text. The fridge photos
have `alt` describing what the model parsed ("photograph of
an open fridge containing half a roast chicken, a fennel
bulb, half a lemon, crème fraîche, two eggs, wilting spinach,
a wedge of parmesan").
- Form fields have associated `