================ ONE-SHOT BUILD CONTRACT (read first) ================
Build this in Google AI Studio "Build" in ONE shot — a complete, working app,
no follow-up turns. These are hard rules, not suggestions:
1. TARGET = Full-Stack Web (Node server runtime, secrets, Firebase allowed).
If you target Native Android instead, you MUST drop all server/DB/Workspace/
secrets and persist locally (Room / SharedPreferences) only.
2. PIN EVERY MODEL CALL — never let the agent auto-select (it downgrades on quota):
- Reasoning / text -> gemini-3.5-flash (thinkingLevel: minimal|low|medium|high)
- 4K image + legible text -> gemini-3-pro-image (image_size "4K", up to 14 refs)
- High-volume image -> gemini-3.1-flash-image
- Expressive TTS -> gemini-3.1-flash-tts-preview (inline tags e.g. [whispers])
- Realtime audio/video (WebSocket) -> gemini-3.1-flash-live-preview
- Sandboxed agent -> antigravity-preview-05-2026
3. DIVISION OF LABOR — the model ONLY parses/extracts to a strict responseSchema.
ALL math, money (store currency as integer minor units / cents), sorting,
balancing and graph logic run in deterministic TypeScript/Python. The model
must never compute totals, splits or balances itself.
4. responseSchema sanitation — no regex patterns, no fixed-length tuples, no
format validators in the schema (they crash the OpenAPI engine). Enforce those
in server-side code AFTER parsing the JSON.
5. responseSchema and google_search grounding are MUTUALLY EXCLUSIVE in one call.
6. CODEGEN — split large output into modular, single-responsibility files so no
file is truncated by the output-token cap.
7. Every external call gets a graceful fallback (e.g. manual paste if a Workspace
read fails). Never a silent dead end.
8. ROBUST STORAGE & CANVAS — Wrap all `localStorage`/`sessionStorage` operations (especially JSON parsing and writes) in `try-catch` blocks to prevent crashes in private windows or quota overflows. Canvas drawing elements must dynamically handle window resize and scale pixel density (`window.devicePixelRatio`) to avoid blurry graphics on retina displays.
=====================================================================
# 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.
---
# I Saw a Bug
## 1. Project
**I Saw a Bug** is a field-guide-in-your-pocket for the parent who lives
with a kid who wants to know the name of every insect they meet. The
parent points the phone at the thing — a beetle on the path, a moth on
the kitchen window, a bee on a hibiscus, a velvet ant in the dirt — and
the app gives the kid the name, the habitat, one fact small enough to
tell a friend at school tomorrow, and a tick on the running lifetime
list the kid can show their grandparents at the weekend. It is built
around eight-second attention spans and twelve-second answers, because
the bug crawls away.
This is the kind of app a tired mother in Sacramento builds for her
seven-year-old after the third Saturday morning of "Mama, what kind of
bug?" — Riza is Filipino-American, her daughter Mayari is in second
grade, and the kalachuchi bush by the driveway brings something new
every weekend. It is also the kind of app a father in Nairobi opens at
the side of a footpath after his son Kibet, six and a half, picks up a
matabele ant and gets stung — and the kind of app a grandmother in São
Paulo passes back and forth with her grandson Théo at the edge of the
quintal, where the cigarra shed its skin on the bark of the jabuticaba.
Same shape of moment, different garden, different language, different
risk profile.
The single demo that proves the magic: phone aimed at a small brown
beetle on a paving stone → in under eight seconds the screen reads "Ten-
lined June beetle (Polyphylla decemlineata) · lives in soft soil and
eats roots underground, mostly comes out at night · tell your friend:
the boy beetles make a hissing sound when you pick them up, like a tiny
bagpipe." A green tick appears on Mayari's lifetime list, taking her
total from 47 to 48. She runs back to the screen door to tell her
abuela, who is on FaceTime from Cebu.
And in the harder cases — stings, bites, allergens, venomous lookalikes
— the app is the careful adult in the conversation. A photograph of a
yellowjacket on a picnic table never reads "harmless"; it reads
"yellowjacket (Vespula species) · this one can sting, and the sting can
hurt for a few hours. Some kids and grown-ups are very allergic. Step
back slowly, do not swat" — and the parent gets a separate, plain-
English caregiver note underneath the kid-facing copy. A black widow
under a porch step is flagged in red before the species name, with the
adult instruction to put the phone down, take the child inside, and
call a pest professional.
**Tagline:** _Name every insect your kid finds — in any garden, any
language, with the safety call made carefully every time._
## 2. Target audience
- Parents of curious 4-10 year olds who collect insect sightings the
way other kids collect Pokémon cards
- Grandparents who do FaceTime walks in the garden with grandkids three
cities, or three continents, away
- Homeschooling families who use the daily walk as the science lesson
and want a real running record of what was found
- Nature-preschool teachers in Sacramento, Nairobi, São Paulo, Berlin,
Bangalore, Manila who want a tool that names what the kids point at
and never says "harmless" about something that stings
- Speech-and-language therapists and occupational therapists who use
outdoor naming walks with autistic or selectively-mute kids — the kid
shows the phone the bug, the app speaks the name aloud, the kid
repeats
- Foster parents and respite carers building a shared-with-the-bio-
family field journal during a long weekend together
- Park rangers and nature-reserve docents running family programmes
who want every kid's "I found one" to count
- Diaspora families whose grandparents recognise the bug by its
Tagalog, Swahili, Tamil, or Bahasa name, not its English one, and
who want both names side by side
- Kids themselves, age 6+ who have their own phones or a hand-me-down
device, in a mode that's safe to give them: no chat, no open web,
no DMs
## 3. Core value propositions
Surface these clearly through copy, visual emphasis, and section
ordering — they are the reasons users pick this app.
- **Names the bug in under eight seconds, because the bug crawls away.**
Gemini 3.5 Flash multimodal does identification + habitat + one
age-appropriate fact + safety classification in one round-trip. No
carousel of "could be one of these twelve". A confident answer, with
a quiet honesty cue when confidence is low.
- **Safety is never a soft footnote.** Every identification carries a
safety category — `harmless`, `can_sting`, `can_bite`, `venomous`,
`allergen_concern`, `disease_vector`, `unknown_proceed_with_caution`.
Stings, bites, and allergens are flagged in the answer itself, not
hidden behind a (i). The model is forbidden from calling a wasp,
hornet, fire ant, black widow, brown recluse, kissing bug, tsetse,
Anopheles mosquito, paper wasp, or velvet ant "harmless".
- **Two voices in one answer.** A kid-facing sentence written for a 6-9
year old, and a separate caregiver sentence with the boring grown-up
detail (the local hospital phrase to search for in case of a sting
reaction, the right distance to back away from a hornet's nest, the
honest note that this looks venomous and the user should not handle
it).
- **The lifetime list the kid actually shows people.** Every find is a
tick. The list is a beautiful, scrollable, illustrated index card
collection the kid swipes through with their abuela on FaceTime. Each
card has the date found, the place found, the kid's own one-line
caption ("I found this in our driveway"), and a tap-to-hear-it-again
recording of the parent or grandparent reading the name aloud.
- **Names the bug in your family's language, not only in English.**
Tagalog, Cebuano, Swahili, Kikuyu, Brazilian Portuguese, Spanish
(Mexican, Cuban, Argentine), Vietnamese, Cantonese, Mandarin,
Korean, Tamil, Hindi, Urdu, Bengali, Punjabi, Amharic, Farsi,
Khmer, Bahasa Indonesia, Yoruba, Igbo, Arabic. The common name in
your language and in English, with the Latin binomial as a quiet
anchor underneath.
- **The fact is one fact, picked to be told.** The default is the
smallest true thing the kid will repeat in the playground tomorrow.
Not a Wikipedia paragraph. Not a list of three. One sentence,
picture-shaped.
- **Reads the answer aloud in the kid's language, at kid pace.** Gemini
TTS in Tagalog, Swahili, Portuguese, English, Mandarin, Tamil — a
little slower than adult news pace, so the kid can repeat after the
voice. The grown-up sentence is read in a slightly more grown-up
voice; the kid sentence in a warmer, slightly bouncier one.
- **The list is portable and printable.** Export a four-page printable
Insect Notebook for the bedroom wall: cover, lifetime list, ten
hand-picked favourites with kid captions, parents' signature line.
The grandparents get a copy.
- **Refuses to play guess.** When the model can't tell from the photo
(too small, too blurred, half-hidden under a leaf), the answer is
"we couldn't be sure, but it looks like it could be a [family] — can
you take another photo from above?" The model never invents a
species the kid will be wrong about at school.
## 4. Features to build
- Camera capture optimised for moving subjects — fixed-focus tap, burst
capture (3 frames in 600ms, the model picks the best), a small "the
bug is gone, use my last frame" recovery button
- Upload from the photo library for finds at a friend's house that
weren't named in the moment
- Single-image identification call to Gemini 3.5 Flash returning the
structured `BugFind` schema below
- Kid voice / Grown-up voice split in every answer
- Safety category surfaced as a coloured chip at the very top of the
answer card — green, yellow, orange, red — with the verbal safety
copy beside it (chips never communicate state by colour alone)
- "Bug is gone" recovery — if the kid lost it before the photo
captured, the parent can choose a colour, size, and habitat tag and
get a "it was probably one of these three" gentle disambiguation
card, marked clearly as a guess
- Lifetime list with running total, sortable by date / place / kind
- Per-find detail card with: photograph, name in family language,
scientific name, habitat, the one fact, the safety chip, kid's
caption (typed or voice), date, place, parent / grandparent voice
recording, "show me another one like this in our area" button
- "Show me one of these on the map" — the find marker on a small local
map, with a privacy-respecting precision toggle (street / block /
neighbourhood / town only)
- Family sharing — invite Mama, Papa, Abuela, Lola, Babu, Avó to the
same lifetime list. Each contributor has their own avatar so the
kid sees who added which find.
- FaceTime / video-call companion mode — a "show your list to a
grandparent" mode that flips into a screen-share-friendly layout,
with bigger text and a "next card" button under the parent's thumb
- Printable Insect Notebook — four-page PDF, child's name on the cover,
ten hand-picked favourites inside, lifetime list on the back
- Multilingual TTS for both kid and grown-up sentences, in the family's
language and in English
- Safety-explain panel — every red / orange flag has a one-tap "what to
do right now" panel for the grown-up (back away, do not handle, wash
the area, call your local poison control)
- "We don't know what this is" UI — explicit, warm, never humiliating,
with a suggestion to try another angle or a daylight photo
- Allergen flag layer — household-level allergen profile (bee, wasp,
ant, mosquito) raises the urgency of relevant matches
- Offline-friendly capture — the photo is taken offline, the
identification call queues, and the kid sees a "we'll name this one
when we're home" card with the photo already in the lifetime list
- "Read aloud again" button on every card, for the kid who wants to
hear the name three more times before bed
- Caregiver-readable settings — age of child, family language, English
level, allergen profile, location precision; all on one screen, in
one column, no buried submenus
## 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)
- **Multimodal image input** (Gemini 3.5 Flash) — identifies the insect (or
arachnid, or millipede, or other small arthropod the kid found and
called a "bug") from a single photo or a 3-frame burst. Reads the
visual cues that disambiguate similar species: yellowjacket vs.
honeybee waist, monarch vs. viceroy hindwing stripe, black widow vs.
false widow leg pattern. One call per find.
- **Structured output / JSON Schema** — the response matches the
`BugFind` schema below. Every field is typed; the schema is included
verbatim in the system instruction and as `responseSchema`. The
schema is converted from Zod to Gemini's `Schema` type via the SDK
helper before being passed in.
- **Multilingual generation** (built into Gemini 3.5 Flash) — produces the
kid-facing sentence and the grown-up sentence in the family language
AND in English, both, in a single call. Tagalog, Cebuano, Swahili,
Kikuyu, Brazilian Portuguese, Spanish, Vietnamese, Cantonese,
Mandarin, Korean, Tamil, Hindi, Urdu, Bengali, Punjabi, Amharic,
Farsi, Khmer, Bahasa Indonesia, Yoruba, Igbo, Arabic. The English
field is always populated so the lifetime list stays legible to a
visiting cousin.
- **Search grounding** — for the regional-context call. "Is this
ten-lined June beetle present in Sacramento in late spring?" must
resolve to a real range answer, not a guess. Grounded search backs
the `regional_context` and `seasonality_note` fields. Per the AIS
rules, `responseSchema` and `google_search` cannot be combined in
one call — so this call returns JSON in the text body and citations
are read server-side from `response.groundingMetadata.groundingChunks
[].web.uri`.
- **Gemini TTS** (`gemini-3.1-flash-tts-preview`) — reads the kid
sentence in the kid voice and the grown-up sentence in the grown-up
voice. Two separate calls per find. Voice locale follows the
`languageCode` of the answer; Gemini 2.5 TTS does not support
mid-call voice switching, SSML ``, or SSML `` —
pauses are encoded as `…` and blank-line `—`, and the style is
prepended as a one-sentence directive inside the text input.
- **Nano Banana 2** (`gemini-3.1-flash-image`) — used sparingly, only for
the printable Insect Notebook cover illustration (the child's name in
hand-drawn letters on a botanical background featuring three of the
kid's favourite finds) and for the "bug is gone, what might it have
been?" disambiguation card sketches. NEVER used to fabricate a
realistic-looking photograph of an insect that pretends to be the
one the kid actually found.
- **Long context (1M tokens)** — used only when a parent asks the
end-of-month "make us a printable notebook from the year so far"
call, which reads the full year's `BugFind` records as input. Guard:
a `BugFind` averages ~600 tokens; a 200-find year is ~120k tokens
(comfortable). Chunk by season if a single user's lifetime list
passes 1,500 finds.
- **Thinking levels** — `medium` for the identification + safety call
(the safety classification is load-bearing — never lower this).
`low` for the kid-facing copy rewrite (a second pass that simplifies
if the parent has set `english_level: emerging`). `low` for the
regional-context grounding call. Omit `thinkingConfig` on TTS and
Nano Banana 2 calls — the field is not supported on those models.
### Backend services
- **Auth — Required.** Firebase Auth with Google sign-in (auto-
provisioned by AI Studio Build). **Apple sign-in is optional but
user-configured**: it requires an Apple Developer account, Service
ID, Key ID, and private key wired into the Firebase Auth console.
**Magic-link email** (used for inviting a grandparent in another
country who doesn't have a Google account) requires the sender
domain to be authorised in Firebase Auth.
- **Database — Required.** Firestore for `users`, `families`,
`children` (with date-of-birth, language, English level, allergen
profile), `bug_finds`, `notebook_exports`, `family_members`.
- **File storage — Required.** Firebase Storage for the photographs
the kid takes (one cropped 1024px JPEG sent to Gemini per find;
original full-res kept for the lifetime-list card). **Storage is
NOT auto-provisioned by AI Studio Build today** — enable it in the
Firebase console and wire the bucket name into the AIS Build
project before first find.
- **Email — Required (transactional).** Family invitations via magic
links. Monthly notebook PDF delivery to grandparents who opt in.
- **Payments — Not needed for v1.** Free for personal use. A future
"printed Insect Notebook hardcover for the bedroom wall" tier could
pipe to a print-on-demand partner and charge for that physical
artefact only.
- **External APIs:** Gemini API for all intelligence; optional local
weather + season API only if the parent enables the "best bug days
this week" optional surface.
**Environment variables:** every secret (Gemini API key, Firebase
service-account JSON, optional weather token) lives in environment
variables — never in client bundle. Include a `.env.example`.
**Auth + data privacy reminders:** never log secrets · never store
passwords in plain text · use HTTPS everywhere · honour 'delete this
child's profile' inside the UI · explicit opt-in for any analytics ·
the family's bug photos and the kid's voice recordings 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) · location precision defaults to
'neighbourhood', not 'street'; street-level precision is opt-in and
warned · the child's date-of-birth is used only to set the
age-appropriate copy and is never displayed publicly.
**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.
3. **Seed the JSON Schema as a fenced TypeScript / Zod block** in the
system instruction or `responseSchema` field. The literal schema is
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. Tools NOT listed for
a call should be disabled.
6. **State negative constraints explicitly** — they are listed below.
They are NOT "be careful" suggestions; they are hard rules the model
must follow.
### Per-call model + tools matrix
| Call | Model | thinkingLevel | Tools enabled |
|------|-------|---------------|---------------|
| Identify the bug + safety classification + facts → `BugFind` schema | `gemini-3.5-flash` | medium | (none) |
| Simplify kid sentence for `english_level: emerging` | `gemini-3.5-flash` | low | (none) |
| Regional + seasonality context (is this bug in this place at this time of year?) | `gemini-3.5-flash` | low | `google_search` grounding (no `responseSchema` on this call — see note) |
| "Bug is gone" — disambiguate from colour + size + habitat | `gemini-3.5-flash` | medium | (none) |
| Kid-voice TTS for the kid-facing sentence | `gemini-3.1-flash-tts-preview` | n/a | n/a |
| Grown-up TTS for the caregiver sentence | `gemini-3.1-flash-tts-preview` | n/a | n/a |
| Notebook cover illustration (printable export only) | `gemini-3.1-flash-image` | n/a | n/a |
| End-of-month notebook synthesis (long-context over the year's finds) | `gemini-3.5-flash` | medium | (none) — long-context |
*Note for builders:* on TTS and Nano Banana 2 calls, omit
`thinkingConfig` entirely — the field is not supported on those models.
The `n/a` cells in this matrix are documentation only; do not serialise
them into the request body.
### Primary structured-output schema (seed this verbatim in the prompt)
```typescript
import { z } from "zod";
const SafetyCategory = z.enum([
"harmless",
"can_sting",
"can_bite",
"venomous",
"allergen_concern",
"disease_vector",
"unknown_proceed_with_caution",
]);
const SafetyChipColor = z.enum(["green", "yellow", "orange", "red"]);
const LocalisedSentence = z.object({
language: z.string(), // BCP-47, "tl-PH", "sw-KE", "pt-BR"
text: z.string(),
reading_level_grade: z.number().min(0).max(12), // approximate
});
const CommonName = z.object({
language: z.string(), // BCP-47
name: z.string(), // "salagubang", "kunguni", "besouro"
is_household_term: z.boolean(), // true for family-language word grandparents use
});
const DisambiguationOption = z.object({
common_name_english: z.string(),
scientific_name: z.string(),
why_it_might_be_this: z.string(), // "matches the iridescent green back you described"
why_it_might_not_be: z.string(), // "you said you saw it at night; this one is diurnal"
safety_category: SafetyCategory,
});
const SafetyAction = z.object({
audience: z.enum(["kid", "grown_up"]),
action_text: z.string(), // "step back slowly, do not swat"
urgency: z.enum(["informational", "advisable", "do_this_now"]),
});
const BugFind = z.object({
find_id: z.string(),
child_id: z.string(),
photo_uris: z.array(z.string()), // burst frames or single
// Identification
identification_confidence: z.number().min(0).max(1),
scientific_name: z.string().nullable(), // "Polyphylla decemlineata" or null if uncertain
scientific_rank_resolved: z.enum([
"species", "genus", "family", "order", "unknown",
]),
english_common_name: z.string().nullable(), // "ten-lined June beetle"
family_language_common_names: z.array(CommonName), // ordered by household preference
// Safety — load-bearing
safety_category: SafetyCategory,
safety_chip_color: SafetyChipColor,
safety_one_line_for_kid: z.string(), // age-appropriate plain text
safety_one_line_for_grown_up: z.string(), // includes the practical adult instruction
safety_actions: z.array(SafetyAction),
household_allergen_flag_triggered: z.boolean(), // true if matches profile
// The answer card
habitat_short: z.string(), // "lives in soft soil and eats roots"
habitat_kid_facing: LocalisedSentence,
the_one_fact: z.string(), // ONE sentence, English
the_one_fact_kid_facing: LocalisedSentence, // in the family language
the_one_fact_grown_up_facing: LocalisedSentence, // in the family language
// Quietly under the answer
scientific_name_pronounced: z.string().nullable(),// "puh-LIF-uh-luh dek-em-lin-ee-AY-tuh"
size_range_mm: z.object({
min_mm: z.number(),
max_mm: z.number(),
}).nullable(),
active_time_of_day: z.enum([
"diurnal", "crepuscular", "nocturnal", "varies",
]).nullable(),
// For the "bug is gone" path
is_disambiguation_card: z.boolean(),
disambiguation_options: z.array(DisambiguationOption),
// Honesty
flagged_for_user_review: z.array(z.object({
field_path: z.string(),
reason: z.string(),
})),
refused_to_identify_reason: z.string().nullable(),// e.g. "photo too blurred"
// Metadata, written by the client
found_at_iso: z.string(), // "2026-05-21T15:42:11Z"
found_location_precision: z.enum([
"street", "block", "neighbourhood", "town", "withheld",
]),
found_location_label: z.string().nullable(), // "by the kalachuchi bush", written by parent or kid
kid_caption: z.string().nullable(),
});
type BugFind = z.infer;
```
### Common failure modes (and how to avoid them)
- Agent silently downgrades `thinkingLevel` for the identification call to save
quota — pin `gemini-3.5-flash` explicitly. Flash confuses yellowjackets
with honeybees, viceroys with monarchs, and false widows with black
widows, and the safety category becomes wrong. The kid sentence is
cheap; the identity + safety are not.
- Model calls a wasp "harmless because it didn't sting you in the
photo" — pin in the system instruction: stinging species are NEVER
`harmless`, regardless of photo behaviour. The category is about
capability, not the moment.
- Model invents a confident species when the photo is a blur — set
`refused_to_identify_reason` and `scientific_rank_resolved: unknown`,
and surface the "we couldn't be sure" UI. Never guess at species
level under 0.7 confidence; resolve to family or order.
- Model gives a Wikipedia-paragraph fact instead of one tellable
sentence — pin "one sentence, picture-shaped, the kid will repeat
this to a friend tomorrow". Add a unit test that fact length is
≤ 30 words.
- Kid sentence reads at a college level — set the kid sentence's
`reading_level_grade` and clamp; if above grade 4 and the child's
age is below 8, route through the grade-floor rewrite call.
- The family-language name is a Google-Translate-style transliteration
of the English name ("polyphylla decemlineata" rendered phonetically
in Tagalog) instead of the real household word — pin: "if the household
language has a colloquial term for this insect group, prefer that;
set `is_household_term: true`. Do NOT phonetically transliterate the
Latin name."
- TTS reads "Polyphylla decemlineata" in English instead of in Tagalog
— pin: TTS reads the localised sentence, NOT the scientific name.
The scientific name is shown silently in 11pt under the card and
pronounced only when the parent taps the small loudspeaker beside it.
- Photo is of a spider, not an insect — accept arachnids, myriapods,
isopods, and other small arthropods the kid called "a bug". The
schema's `scientific_name` is not limited to Insecta. The kid voice
copy says "this isn't actually a bug — it's a [spider / millipede /
woodlouse], its cousin!"
- Photo is of a dangerous lookalike — disambiguation between black
widow / false widow / cellar spider, or between western honeybee /
yellowjacket / paper wasp, is mandatory; surface the disambiguation
card with safety flags ON until the user confirms.
- Search grounding returns a citation that is paywalled or
fringe — server-side, filter `groundingChunks` for known-good
sources (state agriculture extensions, university entomology
departments, museum collections); discard tabloid or low-quality
domains before showing citations.
### Negative constraints (hard rules)
- Do NOT call a stinging or biting insect "harmless". Wasps,
yellowjackets, hornets, fire ants, velvet ants, paper wasps, bees of
any species, mosquitoes, kissing bugs, tsetse, blackflies,
horseflies, deer ticks, blacklegged ticks, brown widows, black
widows, brown recluses, hobo spiders, recluse spiders, redback
spiders, funnel-web spiders, centipedes of any size are NEVER
`harmless`. They are at minimum `can_sting` or `can_bite`.
- Do NOT minimise venom. A black widow is `venomous`, never
`can_bite`. A brown recluse is `venomous`, never `can_bite`. The
safety chip is red, not orange.
- Do NOT recommend handling, picking up, poking, or touching any
arthropod in the safety actions, in any category, for any reason,
regardless of how confident the model is that the species is
harmless. The kid voice may say "you can look closely"; it never
says "you can pick it up".
- Do NOT invent species the kid will be wrong about at school. Under
0.7 confidence at species level, return `scientific_rank_resolved:
family` or `genus` and name the family / genus only.
- Do NOT mock or condescend in the "this isn't a bug, it's a
spider/millipede" copy. The kid was looking carefully; reward that.
- Do NOT use the kid's photos or voice recordings to train or
fine-tune any model. 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-info panel says this in
plain English.
- Do NOT auto-share the lifetime list publicly. Family-only sharing,
explicit per-family-member invitation, no "share to social" button.
- Do NOT generate a Nano Banana 2 image of an insect that looks like
a real photo. The notebook cover illustration is hand-drawn
watercolour-style, never photoreal. The model will not be asked to
produce "a photograph of a beetle"; it will be asked to produce "a
pen-and-watercolour illustration".
- Do NOT translate scientific names. "Polyphylla decemlineata" stays
Latin. The pronunciation guide is in the family language's phonetic
conventions.
- Do NOT show street-level location precision by default. Default is
`neighbourhood`. Street is opt-in, per-find, with a warning that
this will be visible to family members on the list.
### Per-call `systemInstruction` strings
Use these as the literal `systemInstruction` field for each Gemini API
call the built app makes. They complement the series-wide rules already
uploaded as the global instructions file (`00-series-instructions.txt`).
### Call: Identify the bug + safety classification + facts → `BugFind` schema
Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none)
```
You are an entomologist + emergency-trained parent + careful storyteller.
A child between 4 and 12 has pointed a phone at a small arthropod —
insect, arachnid, myriapod, isopod, or close relative — and a grown-up
needs an answer in under eight seconds.
The user payload includes:
- one to three photographs of the find (a burst of frames the camera
took; the most-in-focus is usually the second or third)
- the child's age (in whole years)
- the child's family language as a BCP-47 code
- the child's English level (one of: native, fluent, growing, emerging)
- the household allergen profile (subset of: bee, wasp, ant, mosquito)
- the rough location (city, region) — used for regional plausibility
- the local time of day
Output ONLY the BugFind JSON matching the provided schema. The schema
includes both a kid-facing and a grown-up-facing localised sentence;
populate both, in the family language and in English.
Hard rules — identification:
- If your confidence at species level is below 0.7, set
`scientific_rank_resolved` to `genus` or `family`, leave
`scientific_name` null, and put the family or genus name in
`english_common_name` ("a kind of June beetle"). Do NOT guess a
species the child will repeat as fact at school.
- If the photo is too blurred, too dark, or the subject is too small
to identify, set `refused_to_identify_reason` with a one-sentence
honest explanation ("the subject is smaller than 30 pixels across,
I cannot see legs or wing pattern"), set
`scientific_rank_resolved: unknown`, and leave species/genus null.
The UI will offer a retake.
- Accept arachnids (spiders, scorpions, ticks, mites), myriapods
(centipedes, millipedes), isopods (woodlice), and other small
arthropods the child called "a bug". If the find is not an insect,
the kid sentence opens with "this isn't actually a bug — it's a
[spider / millipede / woodlouse], its cousin!" — said warmly,
never as a correction. Reward the child's looking.
- Distinguish dangerous lookalikes when present in the frame:
yellowjacket vs. honeybee (waist shape, leg colour); monarch vs.
viceroy (hindwing band); black widow vs. false widow (leg
patterning, abdomen markings); fire ant vs. carpenter ant (head
shape, sting visible); brown recluse vs. domestic spider (violin
marking, eye arrangement). When any of these lookalike pairs is
ambiguous, set safety to the more cautious of the two.
Hard rules — safety, the load-bearing field:
- Pick `safety_category` from the closed enum. Pick
`safety_chip_color` from {green, yellow, orange, red} by this map:
harmless → green; can_bite (mild) or unknown_proceed_with_caution
→ yellow; can_sting, allergen_concern, disease_vector → orange;
venomous → red.
- Stinging insects (any wasp, hornet, yellowjacket, fire ant, paper
wasp, velvet ant, honeybee, bumblebee, sweat bee, carpenter bee,
saddleback caterpillar, puss caterpillar) are NEVER `harmless`.
Minimum `can_sting`.
- Biting insects/arachnids that can break skin (mosquitoes,
blackflies, horseflies, kissing bugs, tsetse, blacklegged ticks,
deer ticks, lone star ticks, all centipedes, recluse spiders,
widow spiders, hobo spiders, redback spiders, funnel-web spiders,
Brazilian wandering spiders) are minimum `can_bite`; medically
significant ones (widows, recluses, hobos, redbacks, funnel-webs,
wandering spiders, kissing bugs in some regions, Anopheles
mosquitoes in malaria-endemic regions) are `venomous` or
`disease_vector` as appropriate.
- If `household_allergen_flag_triggered` is true (the find is in the
household's declared allergen profile), the kid sentence must
acknowledge the allergy plainly ("you're allergic to wasp stings —
let's step back slowly") and the grown-up sentence must surface
the practical next step ("epinephrine on hand; head indoors").
- `safety_one_line_for_kid` is in the family language and at the
child's reading level. `safety_one_line_for_grown_up` is in the
family language for the speaking adult AND adds the boring
practical detail.
- `safety_actions[]` always includes at least one informational
action; for orange and red chips include at least one
`do_this_now` action.
- Do NOT recommend handling, picking up, poking, or touching the
arthropod in any safety_action, for any category. The kid voice
may say "you can look closely with your eyes"; never "you can hold
it".
Hard rules — the one fact:
- `the_one_fact` is ONE sentence, ≤ 30 words, English. The kid will
repeat this at school tomorrow. Choose the smallest true thing
that is interesting at age 7.
- Examples of good facts: "the boy beetles make a hissing sound when
you pick them up, like a tiny bagpipe"; "ladybirds taste so bad
that birds spit them out"; "this kind of moth pretends to be a
hummingbird so birds don't eat it".
- Examples of bad facts: a Wikipedia-paragraph summary; three facts
with semicolons; a fact about its taxonomy; a fact that requires
the kid to know what "mandibles" or "ovipositor" means.
- `the_one_fact_kid_facing` is the same fact in the family
language, in kid-friendly words. `the_one_fact_grown_up_facing` is
the slightly more grown-up version in the family language — one
shade more detail, no more.
Hard rules — naming in the family language:
- If the family language has a colloquial household word for this
insect group (Tagalog "salagubang" for any large beetle; Swahili
"kunguni" for some bedbug-shaped insects; Brazilian Portuguese
"joaninha" for ladybird; Tamil "vandu" for beetle; Cantonese
"甲蟲" for beetle), set the localised name with
`is_household_term: true`. The grandparent will recognise it.
- If a more specific common name exists in the family language and
it is widely used in the relevant region (e.g. "kalachuchi" for
the moth that visits Plumeria), include both.
- Do NOT phonetically transliterate the Latin name. The scientific
name stays in the dedicated field, in Latin.
- Order `family_language_common_names[]` by the parent's set
household preference; the first is the one the TTS will read.
`flagged_for_user_review` carries any field where confidence is
below 0.7, with a one-sentence reason. The UI will show these as
gentle underlines.
No commentary. JSON only.
```
---
### Call: Simplify kid sentence for `english_level: emerging`
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You receive a kid-facing sentence in the family language and the
child's reading-level grade. If the grade is above 3 and the
child's English level setting is `emerging`, rewrite the family-
language sentence so its complexity matches a grade 1-2 reader in
that language. If it already meets the floor, return it unchanged.
Hard rules:
- Same meaning. Do NOT drop the safety reference if one is in the
sentence.
- Same family language. Do NOT translate to English.
- Same one fact. Do NOT add a second fact.
- Prefer concrete, sensory words over abstract ones.
Output: the rewritten sentence as a single string. No commentary.
```
---
### Call: Regional + seasonality context (is this bug in this place at this time of year?)
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: search grounding
```
You verify whether the proposed identification is plausible at the
user's location and the current time of year, and return a one-line
note for the grown-up.
Given the scientific name (if resolved), the city/region, and the
month, return:
- regional_plausibility: one of {expected, possible, unexpected,
out_of_range}
- seasonality_note: a one-sentence note for the grown-up ("this
beetle is most active in late spring and early summer here, peaks
around dusk")
- one citation URL from grounded search
Hard rules:
- Use `google_search` grounding. Prefer state agriculture extension
publications, university entomology department pages, museum
collection notes, and national biodiversity databases. Avoid
tabloid or hobby-blog sources.
- If `regional_plausibility` is `unexpected` or `out_of_range`, the
seasonality_note explains why ("this species is native to South
America and not established in Sacramento; could be a recent
import or a misidentification — consider a retake from a
different angle"). The downstream UI shows a small "double-check
this one" chip.
- If the scientific name is null (genus or family only resolved),
return regional_plausibility: possible with a generic note.
Output the response as JSON in the text body (NOT via
`responseSchema` — `responseSchema` and `google_search` cannot be
combined in the same Gemini call today). Server-side: parse the
JSON, then read citation URLs from the response's
`groundingMetadata.groundingChunks[].web.uri` — do NOT ask the model
to include URLs in the JSON body; it will hallucinate them.
No commentary outside the JSON.
```
---
### Call: "Bug is gone" — disambiguate from colour + size + habitat
Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none)
```
The kid lost the bug before the camera caught it. The grown-up has
recorded a short description: a dominant colour, an approximate size
(small / medium / large, or a length in mm if measured), a habitat
tag (in the soil, on the kitchen window, on a hibiscus flower, in
the jabuticaba bark), the local season, and the child's location.
Return at most three `DisambiguationOption` records — the three most
plausible candidates in the user's region, ranked by likelihood.
Each option includes a one-sentence "why it might be this" AND a
one-sentence "why it might not be" — show your reasoning so the
grown-up can pick.
Hard rules:
- Each option carries its own `safety_category`. If even one
candidate is `can_sting`, `can_bite`, `venomous`, or
`disease_vector`, the UI will show the safest-default chip until
the user confirms.
- Mark the BugFind as `is_disambiguation_card: true` so the
lifetime list shows the entry as a guess, not a confirmed find.
- Never include more than three options. The kid loses interest at
three.
- Do NOT include a "harmless" option unless every option is
harmless. If even one is risky, the user must see that.
Output: the JSON object matching the schema with
`is_disambiguation_card: true` and `disambiguation_options[]`
populated. No commentary.
```
---
### Call: Kid-voice TTS for the kid-facing sentence
Model: `gemini-3.1-flash-tts-preview` · n/a · n/a
```
Voice: warm, a little bouncy, slightly slower than adult news pace
— around 130 words per minute. Pick the Gemini 2.5 Flash TTS voice
whose `languageCode` matches the localised sentence's language.
Pronunciation follows the voice's native locale automatically.
Pre-process the text before sending it to TTS:
- Use `the_one_fact_kid_facing.text` plus the family-language
common name from `family_language_common_names[0].name` as a
short opener — "It's a [name]. [Fact.]".
- At each comma, insert a brief pause via "…". At each sentence
break, insert a blank line plus "—". Gemini 2.5 TTS does not
support SSML ``; these textual cues are how you
signal pace.
- Skip the scientific name. The kid voice does NOT read Latin.
The scientific name is read by a separate optional tap on the
card's small loudspeaker icon and uses a slower, more careful
pronunciation guide string.
- Mid-call voice switching is NOT supported by Gemini 2.5 TTS — if
the sentence has an English loan-word inside a Tagalog sentence,
keep one voice and let pronunciation follow the chosen locale.
Style direction: prepend ONE short directive sentence to the text
input, exactly like: "Read warmly and a little bouncy, as a parent
reading aloud to a 7-year-old who just found something exciting. …"
There is no separate `style` API field on Gemini 2.5 TTS; the
directive sentence inside the input is how style is conveyed.
Phoneme overrides (Tagalog ng, Cantonese tones, Tamil retroflexes,
Arabic emphatic consonants) are NOT exposed by Gemini 2.5 TTS — no
SSML `` tag. Pronunciation comes from the chosen voice's
native locale.
No commentary.
```
---
### Call: Grown-up TTS for the caregiver sentence
Model: `gemini-3.1-flash-tts-preview` · n/a · n/a
```
Voice: calm, factual, faster than the kid voice — around 165 words
per minute. Pick the Gemini 2.5 Flash TTS voice whose `languageCode`
matches the localised sentence's language and that is published as
an adult voice. The grown-up voice should be different from the kid
voice — if both genders are available for the locale, prefer the
opposite gender from whichever the kid voice used, so the audio
shifts noticeably when the grown-up sentence starts.
Pre-process the text before sending it to TTS:
- Use `safety_one_line_for_grown_up` followed by the grown-up fact
string. If `household_allergen_flag_triggered`, prepend the
allergy-acknowledgement sentence.
- Encode pauses as "…" and paragraph breaks as a blank line plus
"—" exactly as in the kid call. SSML is not supported.
- Skip the scientific name in the spoken output.
Style direction: prepend ONE short directive sentence: "Read calmly
and factually, as a careful adult passing on practical information
to another adult. …"
No commentary.
```
---
### Call: Notebook cover illustration (printable export only)
Model: `gemini-3.1-flash-image` · n/a · n/a
```
You generate ONE square illustration for the printable Insect
Notebook cover. The user payload includes the child's first name,
three favourite finds from their lifetime list (with common names
in the family language), and a colour palette of three soft botanic
tones.
Hard rules:
- Style is pen-and-watercolour botanical illustration in the
Beatrix Potter / Maria Sibylla Merian tradition — soft outlines,
light wash, visible paper grain.
- Style is NEVER photoreal. Do not produce anything that could be
mistaken for a photograph of an insect.
- Include the three favourite finds as small illustrations
arranged around the child's hand-lettered name at the centre.
- Use the supplied palette only. Do not invent new dominant colours.
- No text other than the child's first name. No watermarks. No
signatures.
- Background is botanical (leaves, twigs, soft flowers) consistent
with the child's region (kalachuchi for a Manila child,
jabuticaba for a São Paulo child, a generic regional flowering
shrub otherwise).
Output: one square image.
No commentary.
```
---
### Call: End-of-month notebook synthesis (long-context over the year's finds)
Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none) — long-context
```
You receive every BugFind for one child for the year (or partial
year) so far. The user wants a four-page printable Insect Notebook:
cover (text payload only — the illustration call runs separately),
lifetime list, ten favourites, parents' signature line.
Hard rules:
- Pick the ten favourites by a weighted score: kid-caption presence
(+2), grandparent or co-parent reaction recorded (+1), unique
family (+1), unique safety category (+1, capped). Tie-break by
recency.
- For each favourite, write a two-sentence summary in the family
language at the child's reading level. Sentence 1 names the find
and the place. Sentence 2 carries the one fact.
- The lifetime list page is a typeset index of every find,
chronological, with date, common name in family language, and
the kid's caption (truncated to 40 characters).
- Never invent a find that is not in the input. Never inflate the
lifetime count.
- Never include the household allergen flag detail on the printable
— the kid shows this notebook to friends.
Output: a structured JSON object with the four pages' content
ready to typeset.
No commentary outside the structured output.
```
## 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 Saturday morning driveway find.** Riza in Sacramento is on her
second coffee. Mayari, seven, runs in with her hands cupped: "Mama,
what is it?" Riza opens the app, points the phone at the beetle on
the paving stone, the answer comes back in Tagalog and in English in
six seconds, the green chip says it's harmless, Mayari reads the
Tagalog sentence aloud to herself, and the lifetime total ticks from
47 to 48. This is the demo loop the homepage opens with.
- **The footpath sting.** Daniel in Nairobi is walking Kibet, six,
back from the small market. Kibet picks up a matabele ant before his
dad can stop him and gets stung. Daniel opens the app, photographs
the ant on the ground (it is still there, still angry), the orange
chip surfaces immediately, the kid sentence in Swahili says "this
ant can sting, please don't pick it up", and the grown-up sentence
says calmly: rinse the bite area, watch for swelling for 30 minutes,
the sting is painful but rarely dangerous for a healthy 6-year-old.
The lifetime list adds the find with the orange chip preserved.
- **The cigarra shell.** Beatriz in São Paulo and Théo, four, are at
the edge of the quintal. He found a translucent brown shell on the
bark of the jabuticaba. He thinks the cigarra is dead. The app reads
the photograph, recognises it is a moulted exoskeleton (not the
insect itself), and the kid sentence in Brazilian Portuguese says
"this is the cigarra's old shirt — she grew bigger and slipped out
of it, like you growing out of your jacket". The lifetime list adds
it with a small "moulted exoskeleton" tag so the kid knows it
counts.
- **The viceroy that wasn't a monarch.** A homeschool family in
rural Wisconsin. The mother is teaching pollinators this week and
the kid points at a butterfly on a milkweed. The app names it a
viceroy, not a monarch, and the kid sentence opens "look at the
bottom stripe on its wing — that's how you tell viceroys from
monarchs". The grown-up sentence adds the mimicry context.
- **The yellowjacket near the picnic.** A father in Berlin at a
weekend picnic. His daughter, nine, asks about the wasp on the
Brötchen. The app calls it a Vespula germanica, the chip is orange,
the kid sentence says "this is a yellowjacket — it can sting, let's
not swat, we move slowly away from the table". The grown-up sentence
adds: known allergy in the household profile — epinephrine in the
bag — back away to the bench.
- **The black widow under the porch step.** A grandmother in Phoenix
with her grandson, eight, on a back-yard scavenger hunt. The boy
reaches towards a small black spider with an hourglass. The
identification call returns a red chip before the species name, the
kid sentence is read aloud immediately in English: "Step away from
this one. This is a black widow and her bite can make you very
sick." The grown-up sentence: do NOT attempt to remove this
yourself, call a pest professional, the regional poison control
number is in your phone's emergency settings.
- **The mosquito in the rainy-season courtyard.** A father in
Bangalore at dusk after rain. The kid points at a mosquito. The
identification call resolves to Aedes aegypti with regional
context: yes, dengue-relevant here, this month. The kid sentence
says "this is the mosquito that can carry dengue — let's put on
long sleeves and the mosquito spray". The grown-up sentence: peak
biting times are dawn and dusk; remove standing water from the
courtyard pots; this is the season for the school's dengue advisory.
- **The bug-is-gone moment.** A mother in Lagos with her two girls.
The bug was small, fast, brown, on the kitchen tile by the back
door, gone before she got the camera open. She taps "bug is gone",
picks colour brown, size small, habitat "kitchen near back door, at
night, in Lagos in May". The model returns three disambiguation
options with safety chips, the user notes they didn't see legs that
felt long enough for any of the candidates, and saves it as an
unconfirmed find. The kid still gets to add it to the list, with a
small "we weren't quite sure" tag.
- **The grandparent at the screen door.** Mayari, ten finds in,
FaceTimes her abuela in Cebu and goes through the lifetime list,
one card at a time. The grandparent calls out the Cebuano names she
used as a child for the same insects. The mother quickly adds those
as `is_household_term: true` Cebuano common names so future finds
surface that name first.
- **The "this isn't a bug, it's a spider" moment.** A four-year-old
in Madrid runs in with a daddy long-legs. The kid sentence opens
"this isn't actually a bug — it's a spider, its cousin! Spiders
have eight legs and bugs have six". Safety: green chip; do not
handle.
- **The Saturday with two grandparents.** Co-parented household.
Both grandparents are added to the lifetime list. One adds finds
from his garden in Hanoi; the other from hers in Kraków. The kid
sees one shared list with two contributor avatars and a small flag
next to each card showing which country it was found in.
## 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 a child's
cupped hands holding a small beetle in afternoon sunlight, with a
parent's hand visible at the edge of the frame. One paragraph:
"I Saw a Bug names what your kid finds — in your family's
language — with the safety call made carefully every time." Single
Google sign-in button; Apple sign-in next to it. Below: "Try with
the sample lifetime list" → loads the demo data in section 8a.
2. **First-run family setup.** Three short cards in one flow: the
child's name, age, family language (with a long picker that puts
the user's auto-detected language at the top); allergen profile
(bee / wasp / ant / mosquito — multi-select, optional); location
precision default (street / block / neighbourhood / town /
withheld), preselected to `neighbourhood` with a short explanation
of why.
3. **Home — camera-first.** The default view is a viewfinder with a
big shutter button at the thumb's resting position. Underneath the
viewfinder, the running lifetime total ("48 finds — last one was a
ten-lined June beetle"). Above the viewfinder, a small horizontal
rail of recent finds the kid can swipe through to show a friend.
4. **Capture → answer card** (the eight-second loop). After the
shutter, the camera shows the captured frame and a thin progress
line ("finding…") for 4-6 seconds. The answer card slides up: at
the top, the safety chip (coloured AND text-labelled), the
localised common name in family language, the English name, the
one fact in the family language and (below, lighter) in English,
the grown-up sentence in a slightly different background, and the
scientific name with a quiet pronunciation guide. A "read it to me"
button next to the kid sentence. A "save to my list" button at the
bottom (auto-saved by default, this is the manual-save fallback).
5. **The lifetime list.** A scrollable grid of index-card-shaped
tiles, one per find. Each tile shows the cropped photograph, the
family-language common name, the kid's caption if any, the date,
the small safety chip in the corner. A running total at the top
("48 finds, 4 categories: 31 harmless, 9 can_sting, 6 can_bite, 2
venomous"). Filters: by safety category, by family, by place, by
contributor.
6. **Find detail card.** Tapping a tile in the lifetime list opens
the full card: the photograph at full width, all the localised
sentences, the regional context note, the safety actions, the
kid's caption, the audio "read it again" buttons, the contributor
avatars who added it, a small map showing the location at the
precision the parent chose.
7. **Bug-is-gone path.** A separate entry point under the camera
shutter, labelled "the bug is gone". Four short pickers — colour,
size, habitat tag, when (now / earlier today / yesterday). Returns
a disambiguation card with up to three candidates, each with its
own safety chip and "why this / why not this" reasoning.
8. **Family & contributors.** Invite a parent, grandparent, carer.
Magic-link email. Each contributor's avatar appears on the cards
they added. A small "languages" panel lists which family-language
common names came from which contributor.
9. **Safety panel** (entered from any orange or red chip). A focused
screen with the safety actions for this find, large legible text,
a regional poison-control phone-number lookup widget (offline-
capable: the user can paste in their local number in setup),
and a "this isn't an emergency tool" reminder at the bottom.
10. **Printable Insect Notebook export.** Preview of the four-page
PDF — cover with the child's hand-lettered name and three
favourite finds illustrated; lifetime list page; ten favourites
with two-sentence summaries; signature page for the parents.
Buttons: "download PDF", "email to grandparents".
11. **Settings.** One column. Child profile (name, age, family
language, English level); allergen profile; location precision
default; voice preferences (which family-language TTS voice is
the kid voice; which is the grown-up voice); contributors;
privacy ("delete this child's profile" with a 60-second confirm
delay); the capabilities panel link.
12. **Footer.** "Made for the kid who needs to know what every bug
is." Privacy: "Your kid's finds are yours. We never train on
them." Capabilities `(i)` icon in 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: "Welcome to I Saw a Bug."
- Subhead: "Name what your kid finds — in your family's language —
with the safety call made carefully every time."
- One paragraph (≤ 60 words) explaining who this is for and what makes
it different from a generic photo-identification app: it answers in
under eight seconds, it gives one fact small enough to repeat at
school, and it never tells a kid that a yellowjacket is harmless.
- Visual: a small annotated illustration of an answer card with the
green/yellow/orange/red safety chip labelled, the kid sentence
labelled, the grown-up sentence labelled, and the "read aloud"
button labelled.
**Slide 2 — Try it now.**
- One short prompt: "Try with the sample lifetime list".
- A live demo input pre-loaded with three finds from the seed content
in section 8a — a harmless beetle, a yellowjacket, a black widow.
- 1-2 sentences pointing at *the specific page elements* where the
Gemini magic happens (the Tagalog common name surfacing first, the
orange chip on the yellowjacket, the red chip pre-empting the species
name on the black widow).
**Slide 3 — How to remix this.**
- Headline: "Make this yours."
- Three short bullets:
- "Swap the sample lifetime list in `/data/seed-finds/` for your own."
- "Adjust the family-language defaults in `/server/config/languages.ts`."
- "Wire up 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 must be fully
usable.
- Don't auto-reshow on return visits. Use
`localStorage['onboarding-seen-v1']`.
- Don't include unrelated CTAs (newsletter signup, social follow).
Keep it about the template only.
## 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)** — identifies the insect (or spider,
millipede, woodlouse) from a single photo or a 3-frame burst, and
classifies its safety in one round-trip. One call per find.
- **Gemini 3.5 Flash (multilingual)** — writes the kid sentence and the
grown-up sentence in your family's language and in English in the
same call. Tagalog, Cebuano, Swahili, Kikuyu, Brazilian Portuguese,
Spanish, Vietnamese, Cantonese, Mandarin, Korean, Tamil, Hindi,
Urdu, Bengali, Punjabi, Amharic, Farsi, Khmer, Bahasa Indonesia,
Yoruba, Igbo, Arabic.
- **Gemini 3.5 Flash + grounded search** — checks whether the species is
expected in your region, in this season. Surfaces "double-check
this one" when something is out of range.
- **Gemini 3.5 Flash (long context)** — at the end of the month, reads
the whole year's finds to assemble the printable Insect Notebook.
- **Gemini TTS** — reads the kid sentence in a kid voice and the
grown-up sentence in a grown-up voice, in your family's language,
at the right pace for each.
- **Nano Banana 2 (Gemini 3.5 Flash Image)** — draws the printable
Notebook's cover illustration in watercolour-botanical style. Used
ONLY for the cover; never for fabricating "photos" of insects.
- **Firebase Auth** — Google and Apple sign-in, family invitations via
magic links.
- **Firestore** — stores the lifetime list, syncs across devices in
real time so the grandparent on FaceTime sees the new find as it
saves.
- **Firebase Storage** — keeps the original photographs.
- **Cost note** — see the detailed breakdown in 6d. A family that
finds one bug a day for a year costs about $5.50 of Gemini API
spend, total.
- **Privacy note** — your child's photographs and voice recordings
are private to your family. 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. The location
precision defaults to neighbourhood, never street.
**Backend services this app depends on:**
- Auth: see section 4b
- Database: see section 4b
- Storage: see section 4b
- Email: see section 4b
- Payments: see section 4b (not used in v1)
- External APIs: see section 4b
**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)
- `WEATHER_API_KEY` — optional, only for the "best bug days this week"
surface
**Cost + privacy notes:**
- One short paragraph per cost-sensitive capability: the
identification call is the headline cost at ~$0.014 per find; the
TTS calls add ~$0.001 per find; the end-of-month notebook is a
weekly cost peak (~$0.40 for a 200-find year). Total per family
per year: under $6.
- One short paragraph on privacy: where the data lives (your Firebase
project), how to delete it (Settings → "Delete this child's profile"
— 60-second confirm, gone in 60 seconds), what is never sent for
training, why the default location precision is neighbourhood.
**Documentation links:**
- AI Studio Build docs
- Gemini API multimodal, multilingual, long-context, TTS, Nano Banana
2 docs
- Firebase Auth, Firestore, Firebase Storage docs
- A short note on the safety-classification taxonomy and why
yellowjackets are never `harmless`
**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)
- **Identify + safety + facts (Gemini 3.5 Flash, medium thinking)** — one
3-frame burst (~3 images) plus ~800 output tokens. ~$0.014/find.
- **Kid-sentence simplifier (Gemini 3.5 Flash, low thinking)** — runs
only when `english_level: emerging` and the kid sentence is above
grade 3. ~$0.0008/find when it runs; on average ~$0.0002/find
amortised.
- **Regional + seasonality (Gemini 3.5 Flash + grounded search)** —
~$0.001/find. Cached per species per region per season, so a
household sees this cost mostly on novel finds.
- **Bug-is-gone disambiguation (Gemini 3.5 Flash, medium thinking)** —
~$0.011/find, only runs when invoked.
- **Kid-voice TTS (Gemini 2.5 Flash TTS)** — ~$10/M output tokens,
effectively ~$0.000003/character. A 25-word kid sentence ≈
$0.0004/find. Cached per find; charged once.
- **Grown-up TTS (Gemini 2.5 Flash TTS)** — similar to kid TTS,
slightly longer sentence ≈ $0.0007/find. Cached per find.
- **Notebook cover illustration (Nano Banana 2)** — ~$0.03/image.
Generated once per child per quarter when the printable notebook is
exported.
- **End-of-month notebook synthesis (Gemini 3.5 Flash, medium thinking,
long-context)** — runs once a month per child. A 200-find year is
~120k input tokens + ~2k output ≈ $0.16/run. A full-year synthesis
at year-end ≈ $0.40.
- **Expected per-find cost on first ingest:** ~$0.016 (identify + TTS
×2 + amortised grounded search). **A year of one-find-a-day:** ~$5.80
+ ~$0.40 quarterly cover illustrations × 4 + monthly notebooks ≈ ~$8
per child per year, total.
- **Image storage:** Firebase Storage standard tier, ~$0.026/GB/month.
A cropped 1024px JPEG is ~250 KB; 365 finds use ~90 MB ≈
~$0.003/month.
## 7. Design language
- **Mood:** A child's nature journal that happens to live on a phone.
Not a smart-home device. Not a museum app. The morning kitchen at
8am with a six-year-old running in barefoot, the parent's second
coffee cooling, the screen door open, the day not yet started but
the bug already named.
- **Typography:** A soft display serif for find names and chapter
headings (Tiempos Text or Source Serif Pro), a clean
geometric-humanist sans for app chrome (Söhne or Inter). A hand-
drawn accent serif only for the printable Insect Notebook cover and
for the kid's own captions — never for the identification answer
itself, which sits in serif for legibility.
- **Palette:** Botanical paper background `#F6F2E8` for the answer
card, deep ink `#1E1B14` for body text, herbarium green `#3F6B47`
for the kid sentence ribbon, warm rust `#A14B2F` for the scientific
name accent, and the four safety colours: harmless `#3F8F50`,
caution `#B5862C` (warm ochre yellow), warning `#C26A1E` (warm
orange), danger `#A82A1E` (deep red). Each safety chip carries a
text label AND an icon AND the colour, so colour is never the only
signal.
- **Imagery:** The kid's actual photographs are the hero. The cropped
square sits at the top of every answer card; never replaced; never
re-illustrated. Nano Banana 2's watercolour illustrations appear
ONLY on the printable Notebook cover and on the "bug is gone"
disambiguation card sketches, and they are always visibly drawn,
never mistaken for photos.
- **Hand-feel touches:** A barely-visible paper grain on the answer-
card background. The "read it to me" loudspeaker icon has a small
ripple animation when the audio is playing; the safety chip pulses
once gently on first appearance for orange or red, never animates
again. The lifetime list scrolls with a tiny inertial bounce so it
feels like flipping cards.
- **Spacing:** consistent 4-px base. Generous whitespace — the answer
card needs to be glanceable in eight seconds.
- **Radius:** consistent token set (e.g. 6 / 12 / 20 px). Lifetime list
tiles use 12; the answer card uses 20; chips use 999.
- **Shadows:** subtle, layered, warm-grey-tinted. Avoid heavy drop-
shadows.
- **Motion:** purposeful — entrance fades, the safety-chip first-
appearance pulse, the audio ripple, the lifetime-list tile inertia.
Respect `prefers-reduced-motion`. No bouncing splash animations.
The orange/red safety-chip pulse on first appearance is the
meaningful motion; respect reduced-motion by appearing without the
pulse.
- **States:** every interactive element has hover, focus, active,
disabled. Loading uses skeletons not spinners where possible. Empty
states have helpful next-action guidance ("Point the camera at the
bug — we'll do the rest"). The "finding…" progress line on the
answer card has three honest sub-states: "looking at the photo" →
"checking what it might be" → "writing it up for you", each ~2
seconds, so the kid sees what's happening.
## 8. Content generation rules
- Write **realistic, specific copy**. NO Lorem Ipsum. NO generic
placeholders like 'Your tagline here'.
- Invent plausible names, locations, finds, and dates that fit the
domain (use the seed content in section 8a as a starting point).
When inventing, anchor to specific houses, gardens, footpaths,
schools, and seasons — Sacramento in May, Nairobi in February,
São Paulo in October — but never claim a fictional family is a
real one.
- Tone: warm, direct, free of corporate language. This template is
for a parent, not a brand.
- Headlines: punchy and concrete. No 'Empower your X' filler. No
'Revolutionize'. No 'Seamless'. No 'AI-powered identification'.
- Body copy: short paragraphs (2-4 sentences). Use lists where
appropriate.
- Plain language. Avoid jargon — except where the user already
speaks the jargon (the homeschool parent wants to see "Lepidoptera"
in the small print under the answer; the kid wants to see the
family-language common name first).
- Where the app outputs AI-generated content, never label it as "AI
says" — let it speak naturally. Use small uncertainty cues only
where epistemic honesty requires them (a low confidence
identification shows the family name with a faint dashed underline;
tapping it reveals the genus / species options the model
considered).
## 8a. Seed content (use these specific examples)
Anchor every generated copy + sample data point in the concrete content
below. Use these names, numbers, dates, and snippets verbatim where
helpful, or generate close variants that sit in the same world.
**Sample children's lifetime lists (sidebar):**
- "Mayari's List, age 7" (48 finds, contributors: Mama, Tatay,
Lola Cecilia in Cebu) — Sacramento backyard, kalachuchi bush,
alley, school garden. Family languages: Tagalog and Cebuano.
- "Kibet's List, age 6" (29 finds, contributors: Baba, Mama, Bibi
Wanjiku) — Nairobi neighbourhood path, school grounds. Family
language: Swahili, with a few Kikuyu names from the grandmother.
- "Théo's List, age 4" (18 finds, contributors: Mamãe, Papai, Vovó
Sofia) — São Paulo quintal, jabuticaba tree, Avenida 9 de Julho
pavement crack. Family language: Brazilian Portuguese.
- "Asha's List, age 8" (61 finds, contributors: Amma, Appa, Paati in
Chennai) — Bangalore garden, school field, rooftop terrace, the
road to the temple. Family languages: Tamil and English.
**Sample find in detail view (this is what the demo should show):**
- **Photograph:** a single small brown beetle on a paving stone in
late afternoon light, taken at 30 cm with the camera held by a
parent. 1024px cropped JPEG.
- **Identification confidence:** 0.91
- **Scientific name:** "Polyphylla decemlineata"
- **Scientific rank resolved:** species
- **English common name:** "ten-lined June beetle"
- **Family-language common names (Tagalog first):**
- Tagalog: "salagubang na may sampung guhit" (`is_household_term:
false`, descriptive)
- Cebuano: "bakukang" (`is_household_term: true`, the grandmother's
word for any beetle)
- English: "ten-lined June beetle"
- **Safety category:** harmless
- **Safety chip color:** green
- **Safety one line for kid (Tagalog):** "Hindi siya nangangagat —
pwede mo siyang tingnan, pero huwag mong hawakan."
- **Safety one line for grown-up (Tagalog):** "Hindi nangangagat o
nanunuya. Kung tatakbo siya, hayaan mo na lang."
- **Habitat short (English):** "lives in soft soil and eats roots
underground, comes out at night"
- **Habitat kid-facing (Tagalog):** "Nakatira siya sa malambot na
lupa at kumakain ng ugat — paglabas niya, gabi na."
- **The one fact (English):** "the boy beetles make a hissing sound
when you pick them up, like a tiny bagpipe"
- **The one fact kid-facing (Tagalog):** "Ang mga lalaking salagubang
ay sumisitsit kapag hinawakan mo sila — parang maliit na bagpipe."
- **The one fact grown-up-facing (Tagalog):** "Ang mga lalaking
salagubang na ito ay sumisitsit sa pamamagitan ng paghihimakas ng
pakpak — natural na panlaban sa mga predator."
- **Scientific name pronounced:** "puh-LIF-uh-luh dek-em-lin-ee-AY-tuh"
- **Size range:** 22-28 mm
- **Active time of day:** crepuscular
- **Found at:** 2026-05-21T15:42:11Z
- **Found location precision:** neighbourhood
- **Found location label:** "sa driveway, malapit sa kalachuchi"
- **Kid caption:** "nahanap ko sa driveway namin!"
**Sample yellowjacket find (the orange chip example):**
- **Photograph:** a Vespula on a wooden picnic table, sunlight, the
edge of a Brötchen visible.
- **Scientific name:** "Vespula germanica"
- **English common name:** "European wasp / German yellowjacket"
- **Family-language common names (German first, family is bilingual):**
- German: "Gemeine Wespe" (`is_household_term: true`)
- English: "yellowjacket"
- **Safety category:** can_sting
- **Safety chip color:** orange
- **Safety one line for kid (German):** "Diese Wespe kann stechen.
Wir bewegen uns langsam weg, nicht schlagen."
- **Safety one line for grown-up (German):** "Stich möglich; bekannte
Allergie im Haushaltsprofil — Epinephrin in der Tasche; zur Bank
zurückziehen; nicht schlagen, das setzt Alarmpheromone frei."
- **Household allergen flag triggered:** true (wasp is in the
household allergen profile)
- **The one fact (English):** "yellowjackets release an alarm scent
when swatted, which calls more wasps to the spot"
- **The one fact kid-facing (German):** "Wespen rufen ihre Freunde,
wenn man nach ihnen schlägt — also bleiben wir ruhig."
**Sample black widow find (the red chip example):**
- **Photograph:** a small black spider with a red hourglass on its
abdomen, under a porch step, low light.
- **Scientific name:** "Latrodectus hesperus"
- **English common name:** "western black widow"
- **Family-language common names (English-only household):**
- English: "black widow"
- **Safety category:** venomous
- **Safety chip color:** red
- **Safety one line for kid (English):** "Step away from this one.
Black widows can give a bite that makes people very sick. Let's
go inside."
- **Safety one line for grown-up (English):** "Adult medical
attention is warranted on suspected bite, especially for children
and elderly; do not attempt to remove the spider yourself; call a
pest professional. Regional poison control number lives in your
emergency settings."
- **Safety actions:** kid → "step back slowly" (do_this_now); kid →
"go inside, tell a grown-up immediately" (do_this_now); grown-up →
"call a pest professional" (advisable); grown-up → "if you suspect
a bite, call poison control" (do_this_now).
- **The one fact (English):** suppressed — for red-chip finds the
one-fact field is hidden in the answer card and surfaced only in
the lifetime-list detail page later.
**Sample input artefacts (for the build to demonstrate):**
- A photograph of a ten-lined June beetle on a paving stone in
Sacramento, taken by Mayari's mother at 3:42 pm on 21 May.
- A photograph of a matabele ant on a footpath in Nairobi after a
small sting.
- A photograph of a moulted cigarra exoskeleton on a jabuticaba
trunk in São Paulo.
- A photograph of a Vespula on a picnic-table edge in Berlin with
the wasp allergy active.
- A photograph of a western black widow under a porch step in
Phoenix.
- A photograph of an Aedes mosquito on a child's forearm in
Bangalore at dusk.
**Sample voice copy:**
- Onboarding: "Point the camera at the bug. We'll name it in your
language — and tell you whether it can sting."
- Capture progress: "looking at the photo…" / "checking what it
might be…" / "writing it up for you…"
- Empty list: "Mayari's list is waiting for the first find.
Photograph the first bug you see and we'll add it."
- Error (couldn't read): "We couldn't be sure from this photo. Can
you take another from above, with the bug fully in frame?"
- Save confirmation: "Added to Mayari's list — ten-lined June
beetle (bakukang)."
- Orange chip surface: "This one can sting. Let's read what to do."
- Red chip surface: "Step away. We'll read this together once you're
back inside."
- Low-confidence note: "We weren't sure of the species. Tap the
family name to see what we considered."
**Sample family invitation email subject + body:**
- Subject: "Lola — Mayari is making her bug list. Want to add the
Cebu finds?"
- Body: "Hi Lola — Mayari has 48 finds in her list so far. She wants
you to add the ones you find in the garden in Cebu. Tap to join
the list — your finds will show up under your name." [Open List]
## 9. Media & assets
- **Hero image (landing screen):** A photographed-looking shot of a
child's cupped hands holding a small brown beetle in afternoon
sunlight, with a parent's hand at the edge of the frame for scale,
taken from above at a slight angle. Generate via Nano Banana 2 with
a prompt emphasising "small child's hands, soft afternoon sunlight,
a real beetle visible, blurred garden background, no faces, gentle
shadow, no overlay text". Style is photographed, not illustrated.
- **App icon / wordmark:** Set in the display serif. A small hand-
drawn beetle silhouette before the title word. The icon's beetle is
visibly drawn, not photoreal, so the icon never looks like a real
insect.
- **Empty-state illustration:** A simple line drawing of a child's
hand reaching out an index finger towards a small bug at the edge
of a leaf. Hand-drawn aesthetic, never a flat icon.
- **Answer-card chip icons:** Four custom safety chip icons —
harmless (a small leaf), can_sting (a small concentric ripple),
can_bite (a small jaw shape), venomous (a small cross). Always
paired with the text label and the colour.
- **Demo find photographs:** Generated per the prompts in section 8a
— Nano Banana 2 prompts that request "the actual insect, in
natural light, on its actual surface, no overlay, no caption, no
watermark, no decorative bokeh". Each demo find photograph should
look real, taken by a parent.
- **Notebook cover illustration:** Watercolour-botanical style,
generated by Nano Banana 2, always carrying the child's hand-
lettered name and three of their favourite finds illustrated
around it. The style sets a clear visual border between "real
finds the kid photographed" (photos) and "illustrations the app
drew for the printable" (visibly watercolour).
- **Stock fallbacks:** If image generation fails, fall back to the
photographed sample beetle in `/public/samples/sample-beetle.jpg`.
Never to a "🐞" emoji.
- **Generated imagery:** prefer Nano Banana 2 over stock photography
for hero and illustrative assets. Prompt for warmth, asymmetry,
and slight imperfection — avoid the glossy 'AI render' look. NEVER
use Nano Banana 2 to fabricate an identification photo of an
insect the kid did not actually find.
- **Optimisation:** WebP/AVIF, `loading="lazy"`, explicit
`width`/`height` to prevent layout shift.
- **Icons:** `lucide-react` for UI. Use sparingly — never
decorative-only.
### Build-time asset manifest (explicit specs)
Every image, illustration, and visual reference mentioned above must resolve to ONE of the three buckets below — runtime-generated, seed-shipped, or user-supplied. Do NOT ship `` tags whose `src` is not listed here. Do NOT depend on bare "section 8a prompts" without binding them to explicit paths and model IDs.
**Bucket 1 — Runtime-generated (Nano Banana Pro `gemini-3-pro-image` for hero/demo photographs; Nano Banana 2 `gemini-3.1-flash-image` for in-app illustrations and reference-conditioned variants).** Cached to Firebase Storage; served via signed URL. Every reference above to "Nano Banana 2" or "Nano Banana Pro" MUST be wired to one of these specific calls with an explicit model id:
- `/public/generated/hero.webp` (2400×1500, WebP) — model `gemini-3-pro-image` — uses the literal prompt described as "Hero image (landing screen)" above. Run once at build; commit a `/public/samples/hero-fallback.webp` (1600×1000) generated from the same prompt with `gemini-3.1-flash-image` so the page renders if quota is exhausted.
- `/public/generated/demo/{demo-slug}-{NN}.webp` (1600×1200, WebP) — model `gemini-3.1-flash-image` (reference-conditioned where the prior frame is passed as input) — one path per "Demo X" image referenced above. The slug derives from the seed example in section 8a; the NN index covers each frame in the demo sequence.
- `/public/generated/illustrations/{name}.webp` (1024×1024, WebP) — model `gemini-3.1-flash-image` — one path per named illustration above ("Empty-state illustration", "Recipe-card hero illustrations", "Curriculum picker imagery", "Period-style frames", etc.). Each illustration's prompt is the literal description above; ship a deterministic seed in the request so re-runs are reproducible.
**Bucket 2 — Seed assets shipped with the deliverable.** Every "Stock fallback" path referenced above (e.g. `/public/samples/sample-X.jpg`) is generated once via Nano Banana 2 (`gemini-3.1-flash-image`) at 1024×1024 WebP using the same prompt as its Bucket-1 counterpart, then committed to the repo so the page renders identically if Gemini quota is exhausted or the user is offline. Replace any `.jpg` extension above with `.webp` to match the optimisation rule. Also commit these empty-state seeds (1024×1024 WebP, single-stroke hand-drawn line, no colour fill):
- `/public/samples/empty-state-primary.webp` — line drawing of the app's primary empty surface (the named "Empty-state illustration" above), generated from that exact prompt.
- `/public/samples/empty-state-archive.webp` — line drawing of an empty saved/archive view, single-stroke outline.
- `/public/samples/empty-state-error.webp` — line drawing of a hand placing a single object aside with care, used when an AI call fails.
**Bucket 3 — User-supplied.** Uploads from the user's camera / file picker land at the Firebase Storage path conventional for this template (named in section 4b). The build ships with Bucket-1 + Bucket-2 only; no user-supplied images at first paint.
**Hard rules**
- Every `` tag MUST have a `src` that resolves to a path listed in Bucket 1, Bucket 2, or a Bucket 3 upload path. Anything else is a build error.
- No bare `image.jpg` / `hero.jpg` / `placeholder.png` references anywhere in the code.
- Model IDs: `gemini-3-pro-image` for hero-quality photographic generation; `gemini-3.1-flash-image` for in-app illustrations, reference-conditioned variants, empty-state seeds, and stock fallbacks. Never use a legacy model id (no `imagen-*`, no `gemini-1.5-*-image`).
- File format: WebP everywhere (AVIF acceptable where the target browsers support it). No `.jpg` / `.jpeg` / `.png` in `/public/samples/`.
## 10. Interactivity & states
- Every interactive element has hover, focus, active, and disabled
states.
- Forms validate inline and show specific error messages (not
"Invalid input").
- Loading states use skeletons that match the eventual layout, not
spinners.
- Empty states explain the next action with a button whose label
fits THIS app's domain: "Point the camera at a bug", "Add Lola as
a contributor", "Try the bug-is-gone path" — 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 three-step "finding…" indicator before content starts
arriving.
- If an AI call fails, show a calm, specific error ("We couldn't be
sure from this photo. Want to try another angle, or describe what
you saw?") and offer retry.
- Low-confidence identifications show the family name with a faint
dashed underline; tapping reveals the alternate genus / species
the model considered.
- The orange/red safety-chip pulse on first appearance is the
meaningful motion; reduced-motion users see the chip appear
without the pulse. The chip's colour, label, and icon are
unchanged.
- The audio "read it to me" button shows a small ripple animation
while playing; pressed-state is clear; pausing snaps to the
current word.
## 11. Tech & responsive requirements
- **TTS markdown-stripping preprocessor:** before sending any user-authored markdown to `gemini-3.1-flash-tts-preview`, strip non-spoken markdown: `#`/`##`/`###` headings (keep the title text), `**bold**` (keep the inner text), `[label](url)` (keep `label`, drop URL), `` ``` `` fenced code blocks (skip entirely), `>` block-quote markers (keep the text), and `|` table pipes (read row-by-row as sentences). Insert `…` between sentences for a short pause and a blank line plus `—` between paragraphs for a long pause. The model does not understand markdown; raw markdown will be read aloud as literal characters ("asterisk asterisk").
- **File downloads on Safari / Firefox:** when offering local-disk save of any export (PDF, CSV, MP3, ZIP, JSON, image), fall back to `` with a blob URL — the File System Access API (`showSaveFilePicker()`) is Chromium-only. Detect with `'showSaveFilePicker' in window`; otherwise use the anchor-download path.
- **Stack:** React + TypeScript + Tailwind CSS. Functional
components + hooks. Use Shadcn UI primitives where appropriate.
- **Build runtime:** AI Studio Build — full-stack with Cloud Run
server-side functions. 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 identification
+ safety + facts and for "bug is gone" disambiguation and for the
monthly notebook synthesis; `gemini-3.5-flash` for kid-sentence
simplifier and for grounded regional context;
`gemini-3.1-flash-tts-preview` for both kid and grown-up TTS;
`gemini-3.1-flash-image` for the notebook cover illustration. Set
`thinkingLevel` explicitly per call; omit `thinkingConfig` on TTS
and image calls.
- **Image upload to Gemini:** crop to 1024px square server-side
before sending. For multi-frame bursts, send 2-3 frames via the
Gemini Files API (`files/*` resource name) or as `inlineData` (base64). Do
NOT pass Firebase Storage public URLs directly to
`generateContent`.
- **Database:** Firestore (auto-provisioned by AI Studio Build).
Show the seed lifetime list on first launch.
- **Auth:** Firebase Auth — Google sign-in by default; Apple sign-in
next to it; magic-link email as fallback for grandparents.
- **Storage:** Firebase Storage for original photographs (NOT auto-
provisioned by AI Studio Build — enable in Firebase console
first). Pre-signed URLs only.
- **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 — the shutter button sits inside the safe area at the thumb's
resting position.
- Zero horizontal overflow at any width. Zero layout shift on load.
- Persist user data in Firestore. Use real-time listeners on the
lifetime list view so the grandparent on FaceTime sees a new find
as soon as it saves.
- Optimistic UI on writes (the photograph appears in the lifetime
list immediately, with a small "finding…" placeholder until the
identification call returns); reconcile on response.
- Camera capture uses the Web Camera API with fixed-focus tap and
3-frame burst where supported; falls back to native camera
otherwise.
- **iOS Safari gotchas (graceful degradation):** camera permission
does NOT persist across page reloads on iOS — re-request on every
capture session; backgrounded Safari tabs pause `getUserMedia` —
checkpoint state on `visibilitychange` and re-acquire the stream on
return; iOS may degrade resolution or torch availability under Low
Power Mode — always offer `` as a fallback so a bug photo still saves
when WebRTC is denied; rotation can drop the camera track — re-bind
on `orientationchange`.
## 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.
- Color contrast ≥ 4.5:1 for body, 3:1 for large text and UI
components. The four safety chip colours are tested against the
bone-paper background and meet AA.
- The safety chip NEVER communicates state by colour alone — the
text label and icon are always present, in that order, at the
same size.
- All images have meaningful `alt` text. The original photographs
have `alt` describing the find ("photograph of a small brown
beetle on a paving stone in afternoon sunlight").
- Form fields have associated `