================ 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. --- # Mobile Groom ## 1. Project **Mobile Groom** is a between-appointments voice notebook for solo mobile dog-groomers — the people who drive a converted van from driveway to driveway, six to ten dogs a day, blow-dryer on a generator, water tank under the floor. The user finishes a groom, walks back to the van, presses one button, and talks for thirty seconds: which dog, what their owner asked for, how the coat came in, what was different about today, what to do next time. The app turns each memo into a structured per-pet profile — Bella the nine-year-old cockapoo whose left ear has a sebaceous cyst the vet already knows about, Mochi the shih tzu who tolerates the scissors but hyperventilates at the dryer, Rocky the staffie cross whose mum likes him kept long over the spine in winter — and the app quietly resurfaces every preference the next time that dog is on the day's list. By the eighth driveway the groomer hasn't typed a single note, and every dog in the diary knows itself. This is the kind of app a mobile groomer in São Paulo opens between a poodle in Vila Madalena and a labradoodle in Pinheiros, because she runs eight dogs a Saturday solo and the only paper she keeps is a folded notebook on the dashboard that the rain wrecked last June. It is also the kind of app a mobile groomer in Manila opens between an aspin in Mandaluyong and a maltese in Quezon City, working out of an aircon-equipped converted L300 with the OFW remittance she saved for two years to buy. And the kind of app a single-van groomer in Birmingham opens between a labradoodle in Moseley and a beardie in Kings Heath in the December dark, because her clients all message her at midnight to ask if she remembered that their dog hates the dryer and she'd like to be able to answer "yes — it's in his profile". Same shape of day, different city, different breed mix, same need for the dog's own preferences to ride along into the next visit. The single demo that proves the magic: stand at the side of the van after the fourth groom of the day, press one button, talk for twenty seconds ("Bella the cockapoo, Mrs Chen's place in Vila Madalena, Bella's mum says shorter on the ears today, took about ten millimetres off the topknot, used the seven-blade on the body, she was great with the scissors but jumped at the dryer again so I switched to the low setting after five minutes, paid R$180 via Pix"). Three seconds later Bella's profile updates: the ear length preference is now "shorter — 10 mm topknot, per Mrs Chen, last visit"; the dryer sensitivity flag stays at "low setting after warm-up — confirmed again 28 May"; a note appears under "preferences" saying "Mrs Chen confirmed shorter ears this visit — apply by default at next appointment unless she says otherwise". When Bella shows up on the calendar in six weeks, the prompt the groomer sees before knocking on the door reads: "Bella, 9, cockapoo. Shorter ears — Mrs Chen confirmed 28 May. Dryer: low setting after warm-up. Last visit: R$180, paid Pix." And in the harder cases — the dog with a recent surgical site the owner mentioned in passing, the dog who bit last time and the groomer needs to flag the muzzle policy without losing the relationship, the elderly dog whose owner is in the early stages of dementia and gives different instructions each visit, the foster dog who arrives with no history at all — the app keeps the groomer honest about what the owner said versus what the groomer observed, never invents a health diagnosis the groomer didn't make, and surfaces the contradictions for the groomer to resolve before next visit. The owner's voice is the owner's voice; the groomer's voice is the groomer's voice; the app does not pretend to be the vet. **Tagline:** _The dog's preferences travel with the dog — in any breed, any city, in the words the owner actually said._ ## 2. Target audience - Solo mobile dog-groomers running a one-van operation — six to ten dogs a day, no salon front, no receptionist, no second pair of hands - Mobile groomers in any country where the model is dominant — São Paulo, Rio, Manila, Bangkok, Mexico City, Cape Town, Birmingham, Dublin, Auckland, Melbourne, Toronto, Madrid, Lisbon, Berlin, Warsaw, Tel Aviv, Jakarta, Bandung - Groomers whose customer base speaks one language (Portuguese, Tagalog, Cantonese, Yoruba, Spanish, Arabic, Hebrew, Bahasa, Punjabi, Polish) and whose business records they prefer to keep in another (often English) — the memo stays in the owner's language, the profile reads in either - Groomers transitioning from salon to mobile — they're used to a desktop scheduling system and they want the per-pet notes that the salon's pen-and-paper card kept, on a phone, in the van - Groomers running a small two- or three-van operation where every groomer needs the same dog's profile (cousin help, partner help) — multi-user is a v2 affordance but the data model assumes it - Groomers who specialise in nervous, senior, or special-needs dogs and need every behavioural cue written down — the app is built for this user first, not as an afterthought - Hand-stripping specialists working with terrier breeds where the previous appointment's stripping schedule matters six months later - Apprentice groomers working under a self-employed master, recording memos that the master reviews and signs off on each evening - Groomers whose dyslexia, arthritis, or repetitive-strain injury means typing eight dog notes a day on a phone is a daily tax — voice-first is the accommodation, not a feature ## 3. Core value propositions Surface these clearly through copy, visual emphasis, and section ordering — they are the reasons users pick this app. - **Never invents a health observation** — the single hardest rule and it never bends. If the groomer did not name a lump, a hot spot, a cyst, a flea, an ear infection, or a limp in the memo, the profile does not mention it. The app reads the groomer's voice; the app is not the vet. Observations the groomer did make are stored as observations, attributed to the groomer with the date, and never silently upgraded to diagnoses. - **The owner's voice and the groomer's voice are kept apart** — "Bella's mum says shorter on the ears" is attributed to Mrs Chen; "she jumped at the dryer again" is attributed to the groomer. The profile rendering shows the speaker beside each preference, every time. When they contradict — owner says "she's fine with the dryer", groomer notes "she hyperventilated at the dryer" — both are kept and surfaced for the next visit. - **Speak in any language, profile reads in either** — record the memo in Brazilian Portuguese, Tagalog, Cantonese, Yoruba, Bahasa, Punjabi, Bengali, Mexican Spanish, Arabic, or English. The structured profile is produced in whichever language the groomer reads, and the owner-facing summary (sent after the appointment) is produced in the owner's language. The verbatim transcript in the spoken language is kept forever. - **The next-visit prompt resurfaces what matters** — before the groomer knocks on the door, the home screen shows a card per dog: breed and age, three preferences confirmed on the last two visits, any behavioural flag, the last price and payment method, anything the owner said at the door last time that the groomer wants to honour again. Not every note ever taken — the three that decide today. - **Per-pet profile, owner-shared on tap** — the groomer can share the profile with the owner from a tap in the app: a clean read-only view of "what we know about Bella" that the owner can correct in writing. Correct corrections flow back into the profile. The audio stays private to the groomer. - **Multi-dog households are first-class** — Mrs Chen has Bella the cockapoo and Mochi the shih tzu; they're on the same address and the same payment method but they're two profiles. The app never collapses them and never asks the groomer to disambiguate at the door — the calendar entry already says which dog is on the schedule. - **Behavioural flags are persistent and non-judgmental** — "muzzled the second hindquarter — same as Feb visit" is recorded as a behaviour the groomer manages, not a label on the dog. The app uses the groomer's exact words and offers a one-tap "still relevant?" review six months later. - **The cancellation, the no-show, the warranty redo, the freebie are all real categories** — "she paid R$180, I redid the topknot for free because I wasn't happy with how it sat" parses as a paid appointment with a no-charge redo line, not as a discount. - **No-signal-friendly** — memos record in the van between appointments where signal is sometimes nothing; parsing runs on reconnect; nothing lost between the bridge and the next driveway. ## 4. Features to build - Big one-button voice memo on every screen — "Done with a dog" — captures audio, GPS, timestamp instantly, no setup, no per-appointment configuration - Per-appointment arrival / departure tracking using the phone's clock — auto-stop at end of memo, manual edit allowed; the groomer never has to remember the duration - Offline-first capture — memos and photos queue when there's no signal; parse on reconnect; nothing lost on the country lane between two villages - Photo capture per appointment — before, after, the coat condition, anything the groomer wants the owner to see — attached to the pet's profile, never silently to "the owner" - Voice transcript preserved verbatim in the source language — every "y'know" and "tipo" and "lang" and "innit" preserved exactly as spoken - Multilingual parse — speak Brazilian Portuguese, Tagalog, Cantonese, Yoruba, Bahasa, Punjabi, Bengali, Mexican Spanish, Arabic, Cebuano, Tamil, Igbo, Swahili, Hindi, Urdu, Polish, or English; structured profile fields come out in the groomer's preferred reading language - Per-pet profile — breed, age, coat type, behavioural notes by speaker, preferences by speaker, last-visit summary, photos - Owner card — name, address, phone, preferred payment method, preferred language, multi-pet roster, default tip behaviour - Preference attribution — every preference field carries who said it (owner or groomer), when they said it, and the verbatim quote from the memo - Behavioural flag with persistence and review — flag set in February resurfaces in August with a "still relevant?" tap - Health-observation log — strictly what the groomer saw with their own eyes, attributed and dated; never upgraded into a diagnosis; an explicit "tell the owner to ask the vet about this" toggle that produces an owner-facing note in plain language without medical claims - Next-visit prompt — the three things that matter at this dog's next appointment, generated from the most recent two visits, surfaced as a card on the home screen the morning of - Calendar with per-dog cards — today's eight dogs, each with the prompt visible from the calendar without drilling in - Pricing per appointment — the price the groomer named or invoiced, the payment method (cash, Pix, GCash, M-Pesa, card on site, bank transfer), the tip, any add-ons (nail grind, sanitary, de-shed, hand-strip, teeth brush) - Multi-pet household pricing — separate per-dog or combined-visit pricing, configurable per household - Cancellation / no-show / freebie handling — clear categories, never silently upgraded to a paid appointment - Owner-shareable read-only profile — a clean "what we know about Bella" card the owner can view in their browser and suggest corrections to - Routing helper — the day's eight stops in geographic order; the app suggests the order, the groomer reroutes by drag; persistent across the day even with no signal - One-sentence search across the archive — "the cockapoo with the topknot mum hates", "the shih tzu in Vila Madalena", "the lab who jumped at the dryer last time" returns the right profile in two seconds - Weekly summary — at user-set local time (default Sunday evening) the week's appointments, prices, tips, cancellations roll up; the groomer skims and exports to her bookkeeping ## 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) - **Audio input + multilingual ASR** (Gemini 3.5 Flash) — accepts the raw memo as `inlineData` (m4a) under 90 seconds, or via the Gemini Files API (`files/*` resource name) for the rare longer memo. Handles code-switching mid-sentence — "Bella the cockapoo, Mrs Chen po sa Vila Madalena, shorter on the ears daw kasi mainit" is the dominant pattern in Manila, Lagos, São Paulo, Karachi. Produces a verbatim transcript in the source script first, then the structured Appointment + Profile-Update objects. - **Structured output / JSON Schema** — the response matches the `Appointment` and `ProfileUpdate` schemas below. Every field is typed; the schema is converted to Gemini's `Schema` type via the SDK helper and passed as `responseSchema`. Numeric constraints inside the schema are documentation only; clamp on the server. - **Long context (1M tokens)** — for the per-pet "what does this dog know about itself?" call that runs before the next-visit prompt is rendered. The model ingests every previous appointment for that one dog (typically 6-30 appointments over two years, ~600 tokens each ≈ 4-20k tokens — well within budget) and proposes the three preferences that matter today. For a household with seven dogs across four years, the call is run per-dog, never on the entire client list. **Guardrail**: a single client roster of 200 dogs × 30 appointments × 600 tokens = ~3.6M tokens — exceeds the 1M ceiling. The roster-wide consolidation that runs nightly chunks by dog and per-trade trims to the last 24 months. - **Search grounding** (Gemini 3.5 Flash) — for address geocoding only, with explicit country bias from the user's profile. Periodically a memo says "Mrs Chen's place near the market in Pinheiros", and grounded search resolves it to a usable address. Geocoding is the only call that needs grounding; the parse call does not. - **Gemini TTS** (`gemini-3.1-flash-tts-preview`) — for read-back of the morning's appointment list while the groomer drives to the first stop. "Bella, nine, cockapoo. Shorter ears, per Mrs Chen 28 May. Dryer: low setting after warm-up. Last visit: 180 reais, paid Pix." The groomer confirms with a tap or a "play the next one". - **Multilingual translation** (Gemini 3.5 Flash, low thinking) — to render the owner-facing profile card and the owner-facing post-appointment summary in the owner's language, while preserving the groomer's voice in the observations. "Bella foi ótima com as tesouras, mas pulou com o secador" → "Bella was great with the scissors, but jumped at the dryer" — not a sterile "Bella's behaviour assessment: cooperative with scissors, sensitive to drying equipment". - **Thinking levels** — `medium` for the primary memo-parse call (multilingual + preference extraction + behavioural attribution + health-observation gating). `low` for translation, pet-resolution, and the next-visit-prompt generation. Omit `thinkingConfig` entirely on the TTS call — the field is not supported on that model. ### Backend services - **Auth — Required.** Firebase Auth with Google sign-in (auto-provisioned by AI Studio Build). Phone-number sign-in is recommended for groomers who don't habitually use email — this requires enabling the Phone provider in the Firebase console and a working SMS budget. **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 sharing the owner-readable profile card) requires the sender domain to be authorised in Firebase Auth. - **Database — Required.** Firestore for `users`, `owners`, `pets`, `appointments`, `profile_updates`, `behavioural_flags`, `health_observations`, `weeks`, `routes`. - **File storage — Required.** Firebase Storage for the raw audio memo (m4a), the appointment photographs (original resolution), and rendered owner-readable profile PDFs. **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 memo upload. Audio + photos are private to the groomer; only pre-signed URLs are used. Owner-shared profile cards expose only the profile data, never the raw memo audio. - **Email — Required (transactional).** Sending the owner-readable profile card and the post-appointment summary; sending the weekly worksheet to the groomer's own bookkeeping email; password-reset. - **WhatsApp / SMS — Optional, user-configured.** Many mobile groomers communicate with owners via WhatsApp. The MVP ships a "copy summary as message" button that drops a formatted post-appointment note + the owner-readable profile URL to the clipboard. A v2 tier integrates WhatsApp Business API or a country-specific SMS gateway (Twilio, MessageBird, Africa's Talking, Globe Labs) — those are user-config, not auto-provisioned. - **Payments — Optional, user-configured.** A "pay this appointment" link routes to Stripe, Pix, GCash, M-Pesa, Razorpay, depending on which the groomer has set up in their region. The MVP supports a static "bank details" / "pay by X" block and Stripe; other rails are stubbed and user-config. Most mobile groomers are paid at the door; the link is a fallback for split-pay households and forgetful clients. - **External APIs:** Gemini API for all intelligence; optional Google Maps Geocoding for higher-fidelity address resolution (the grounded-search path is the offline default); optional Google Maps Routes API for the in-day reroute calculation (with a haversine fallback for offline routing). **Environment variables:** every secret (Gemini API key, Firebase service-account JSON, Stripe key, Twilio/M-Pesa/GCash credentials if used, WhatsApp Business token if used, Google Maps key if used) 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 my account' inside the UI · explicit opt-in for any analytics · the groomer's voice memos, owner details, pet profiles, and photos 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) · audio recordings are the groomer's working notes and are treated like a private notebook; owner-readable profile cards expose only the structured profile, never the audio. **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. 7. **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; } ``` 8. **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 length-2 arrays and removes regex patterns before serializing. Validate those constraints in middleware AFTER parsing. ### Per-call model + tools matrix | Call | Model | thinkingLevel | Tools enabled | |------|-------|---------------|---------------| | Parse voice memo → `Appointment` + `ProfileUpdate` | `gemini-3.5-flash` | medium | (none) | | Resolve pet + owner (which dog, which household?) | `gemini-3.5-flash` | low | (none) | | Generate next-visit prompt (per-pet long-context) | `gemini-3.5-flash` | low | (none) — long-context over that one dog's history | | Translate owner-facing profile card + post-visit summary | `gemini-3.5-flash` | low | (none) | | Geocode an ambiguous address from the memo | `gemini-3.5-flash` | low | `google_search` grounding (no `responseSchema` on this call — see note) | | Morning read-back of the day's appointment list | `gemini-3.1-flash-tts-preview` | n/a | n/a | | Weekly roll-up across all appointments | `gemini-3.5-flash` | medium | (none) — long-context over the week | *Note for builders:* on TTS calls, omit `thinkingConfig` entirely — the field is not supported on that model. The `n/a` cells in this matrix are documentation only; do not serialise them into the request body. The geocoding call uses `google_search` grounding and therefore CANNOT specify `responseSchema` (the two are mutually exclusive in one Gemini call) — instruct the model to emit JSON in the text body, parse server-side, and read citation URLs from `response.groundingMetadata.groundingChunks[].web.uri`. ### Primary structured-output schema (seed this verbatim in the prompt) ```typescript import { z } from "zod"; const Speaker = z.enum([ "owner", // the dog's owner, as quoted by the groomer in the memo "groomer", // the groomer's own observation, in their own voice "vet_via_owner", // the owner relayed something the vet said; flagged as such, never as a vet statement "previous_groomer", // the owner mentioned a previous groomer's note; flagged as second-hand "unknown", ]); const AttributedNote = z.object({ text_verbatim: z.string(), // what was actually said in the memo, lightly trimmed of filler text_owner_facing: z.string().nullable(), // a softened version suitable for the owner-readable card speaker: Speaker, recorded_at_iso: z.string(), // when the memo was captured source_quote: z.string(), // verbatim from the transcript, untrimmed confidence: z.number().min(0).max(1), }); const CoatPreference = z.object({ region: z.enum([ "head", "ears", "topknot", "muzzle", "neck", "chest", "back", "belly", "legs_front", "legs_back", "feet", "tail", "sanitary", "paw_pads", "overall", ]), preference_verbatim: z.string(), // "shorter on the ears" — as said preference_canonical: z.string().nullable(), // "ears: 10mm" — normalised if extractable blade_or_tool: z.string().nullable(), // "7-blade", "scissor-only", "hand-strip" speaker: Speaker, confirmed_visits: z.array(z.string()), // appointment_ids where this preference was confirmed last_confirmed_iso: z.string(), }); const BehaviouralFlag = z.object({ description_verbatim: z.string(), // "jumped at the dryer" — groomer's words context: z.enum([ "bath", "dryer", "scissors", "clippers", "nail_grind", "nail_clip", "ear_pluck", "sanitary_trim", "muzzle_fit", "table_lift", "noise_general", "stranger_presence", "other_dog_presence", "transport", "other", ]), groomer_response: z.string().nullable(), // "switched to low dryer setting after warm-up" speaker: Speaker, first_observed_iso: z.string(), last_observed_iso: z.string(), still_relevant: z.boolean(), // groomer reviews after 6 months }); const HealthObservation = z.object({ description_verbatim: z.string(), // groomer's words only; never a diagnosis body_region: z.string(), observed_at_iso: z.string(), speaker: Speaker, // almost always "groomer"; "owner" if relayed from the door tell_owner_to_ask_vet: z.boolean(), // groomer toggle; produces owner-facing plain-language note groomer_note_to_self: z.string().nullable(), // explicitly NO diagnosis field. The app does not store one. }); const Owner = z.object({ owner_id: z.string(), name_verbatim: z.string(), // "Mrs Chen" — as spoken name_canonical: z.string().nullable(), address_verbatim: z.string(), address_geocoded: z.object({ lat: z.number().nullable(), lng: z.number().nullable(), formatted: z.string().nullable(), confidence: z.number().min(0).max(1), }), phone: z.string().nullable(), email: z.string().nullable(), preferred_invoice_language: z.string().nullable(), // BCP-47 preferred_payment_method: z.string().nullable(), pets_in_household: z.array(z.string()), // pet_ids notes_verbatim: z.string().nullable(), // "always pays cash, prefers WhatsApp" }); const Pet = z.object({ pet_id: z.string(), name_verbatim: z.string(), // "Bella" owner_id: z.string(), breed_verbatim: z.string(), // "cockapoo" — as spoken breed_canonical: z.string().nullable(), // "cockapoo" age_years: z.number().nullable(), sex: z.enum(["male", "female", "unknown"]), desexed: z.boolean().nullable(), coat_type: z.enum([ "smooth", "short_double", "long_double", "curly", "wire", "silky", "wool", "hairless", "mixed", "unknown", ]), coat_preferences: z.array(CoatPreference), behavioural_flags: z.array(BehaviouralFlag), health_observations: z.array(HealthObservation), preferred_appointment_duration_minutes: z.number().nullable(), notes_general: z.array(AttributedNote), last_appointment_id: z.string().nullable(), }); const PriceLine = z.object({ description_verbatim: z.string(), // "full groom", "nail grind add-on", "topknot redo no charge" amount: z.number().nullable(), // null on no-charge lines currency: z.string(), // ISO-4217 is_no_charge: z.boolean(), no_charge_reason: z.string().nullable(), // "redo because I wasn't happy with how it sat" }); const Appointment = z.object({ appointment_id: z.string(), recorded_at_iso: z.string(), pet_id_ref: z.object({ existing_pet_id: z.string().nullable(), proposed_new_pet: Pet.nullable(), }), owner_id_ref: z.object({ existing_owner_id: z.string().nullable(), proposed_new_owner: Owner.nullable(), }), arrived_at_iso: z.string().nullable(), departed_at_iso: z.string().nullable(), duration_minutes: z.number().nullable(), duration_source: z.enum([ "phone_clock", "spoken_phrase_only", "user_overrode", "unknown", ]), duration_uncertain: z.boolean(), visit_kind: z.enum([ "regular", "first_visit", "no_show", "cancellation", "redo_no_charge", "warranty_revisit", "consultation_only", ]), groomer_summary_verbatim: z.string(), // what the groomer said they did, in their own words groomer_summary_translated: z.string().nullable(), // filled in later for owner-facing services_performed: z.array(z.enum([ "bath", "blow_dry", "full_groom", "puppy_first_groom", "face_trim", "feet_trim", "sanitary", "nail_clip", "nail_grind", "ear_clean", "ear_pluck", "teeth_brush", "de_shed", "hand_strip", "carding", "de_matt", "flea_treatment", "perfume", "bandana", "bow", "other", ])), price_lines: z.array(PriceLine), total_charged: z.number().nullable(), currency: z.string(), payment_status: z.enum([ "paid_in_full", "paid_partial", "invoiced_unpaid", "no_charge_redo", "no_charge_goodwill", "cancelled_no_fee", "no_show_fee_charged", "disputed", "unknown", ]), payment_method: z.string().nullable(), // "Pix", "cash", "GCash", "card_on_site"… tip_amount: z.number().nullable(), source_language: z.string(), // BCP-47, "pt-BR", "tl-PH", "en-GB" transcript_verbatim: z.string(), parse_confidence: z.number().min(0).max(1), flagged_for_user_review: z.array(z.object({ field_path: z.string(), reason: z.string(), })), }); // ProfileUpdate is the per-pet delta proposed by the parse call. // It is applied to the Pet record only after the groomer reviews // (or auto-applies for high-confidence preference confirmations). const ProfileUpdate = z.object({ pet_id: z.string(), proposed_at_iso: z.string(), new_or_updated_preferences: z.array(CoatPreference), new_or_updated_behavioural_flags: z.array(BehaviouralFlag), new_or_updated_health_observations: z.array(HealthObservation), new_general_notes: z.array(AttributedNote), conflicts_with_existing: z.array(z.object({ field_path: z.string(), // "coat_preferences[ears]" existing_verbatim: z.string(), new_verbatim: z.string(), existing_speaker: Speaker, new_speaker: Speaker, resolution_required: z.boolean(), })), auto_apply_recommended: z.boolean(), // true when speaker matches & no conflict }); type Appointment = z.infer; type ProfileUpdate = z.infer; ``` ### Common failure modes (and how to avoid them) - **Model invents a health diagnosis** when the groomer described a coat observation in passing. The hardest rule. If the memo says "her ear was a bit waxy today" the app records `description_verbatim: "her ear was a bit waxy today"` with `speaker: groomer`. It does NOT write "possible ear infection", "otitis externa", or "consider yeast overgrowth". Diagnoses are vet territory and the schema has no field for them. If the groomer wants the owner to ask the vet, they tap `tell_owner_to_ask_vet: true` and the owner-facing note reads "Mariana noticed Bella's left ear looked a bit waxy today — worth a vet check next time you're in". - **Model invents a coat preference** that the groomer didn't say. The owner said "shorter on the ears today" — that's a preference. The groomer said "took ten millimetres off the topknot" — that's a description of what they did, not necessarily a forward preference unless the groomer or owner stated it as one. The parse call must distinguish "did this today" from "do this every time". - **Owner-relayed and groomer-observed collapsed into one voice** — "Bella's mum says she's fine with the dryer" and "she hyperventilated at the dryer" become a single ambiguous note. Wrong: keep both, with their speakers and source quotes, and flag the conflict for the groomer to resolve at next visit ("owner says X, you observed Y — which applies?"). - **Multi-pet household collapsed** — Mrs Chen has Bella the cockapoo and Mochi the shih tzu. A memo about "Bella" and a memo about "Mochi" both go under the household, but each maintains its own pet profile. The owner-resolution call returns one household; the pet-resolution call returns the right pet within it. Conservative resolution: same address ≠ same pet, ever. - **Health observation upgraded into a recurring concern after one mention** — the groomer mentioned a small bare patch once; on next visit's prompt the app says "monitor recurring skin issue". Wrong. One observation is one observation. The pattern-detection only kicks in across three+ separate appointments where the same body region is mentioned by the groomer, and even then the surfacing copy reads "you mentioned this region on three visits — worth a vet check?" not "Bella has a recurring skin issue". - **Code-switched memo silently translated** — "Bella the cockapoo, Mrs Chen po sa Vila Madalena, ang gusto niya shorter ears" gets transcribed as "Bella the cockapoo, Mrs Chen of Vila Madalena, she wants shorter ears". Hard rule: `transcript_verbatim` preserves the source-language tokens exactly. The owner-facing card and the post-visit summary are translated on a separate call. - **Currency assumed** when the groomer says a bare number. The model defaults to USD. Wrong. The user profile carries the operating currency (BRL, PHP, GBP, NGN, KES, INR, MXN, ZAR…); the parse call receives that as a hard system-instruction note and uses it unless the memo explicitly overrides ("she paid me in dollars, I'll convert later"). - **Address geocoded to the wrong country** — "Mrs Chen's place in Vila Madalena" is geocoded to a Vila Madalena in some other country. Pin `country_bias` from the user profile in the grounded geocoding call; if confidence is below 0.7, leave the geocoding null and surface for review rather than picking the wrong city. - **Redo / freebie collapsed into a discount** — "I redid the topknot for free because I wasn't happy with how it sat" becomes a price reduction line. Wrong: the appointment was paid in full at the named price; a separate `price_lines[]` entry records the redo at `is_no_charge: true` with the reason. The groomer needs to remember this happened, not the discount math. - **Behavioural flag treated as a label on the dog** — "jumped at the dryer" stored as "anxious dog". Wrong: it's a managed behaviour in a specific context with a specific groomer response. The schema enforces context and groomer_response fields. - **Owner profile auto-merged across households** — two "Mrs Chen" entries at different addresses collapse. Wrong: same surname is common; address numeral + street + city must all match. Conservative resolution. - **Cancellation / no-show silently dropped** — owner cancelled at the door, the groomer didn't groom anyone, but pressed the memo button anyway to log the wasted drive. Memo: "Mrs Lim cancelled at the door, said the dog was at the vet". Parse must produce a `visit_kind: cancellation` with `payment_status: cancelled_no_fee` (or `no_show_fee_charged` if the groomer added "charged her the no-show fee"). Do not omit the appointment. ### Negative constraints (hard rules) - Do NOT invent health diagnoses, infections, conditions, or treatment recommendations. The app records the groomer's verbatim observation and nothing more. Phrases like "possible", "likely", "suggests", "indicative of", "consistent with" never appear in `health_observations[]`. The schema has no diagnosis field; there is no place to put one. - Do NOT invent coat preferences. If the groomer or owner did not state a preference in the memo, the profile does not gain one. A description of what was done today is not a preference for next time unless someone said so. - Do NOT collapse speakers. Owner-said and groomer-observed are different fields and stay different fields. The app surfaces conflicts; it does not resolve them silently. - Do NOT translate the verbatim transcript. `transcript_verbatim` is the raw source-language audio transcribed character-for-character. Translation is a separate call, gated on the owner-facing render. - Do NOT translate proper nouns: pet names, owner names, breed names, brand names, place names. "Bella", "Mrs Chen", "cockapoo", "Wahl", "Andis", "Vila Madalena", "Pix", "GCash" stay verbatim. - Do NOT assume currency. The user's profile carries it; the parse call uses that as the default unless overridden in the memo. - Do NOT silently mark an appointment paid. "She'll pay me next time" is `invoiced_unpaid` with a follow-up, not `paid_in_full`. - Do NOT add services the groomer didn't name. "Took some matting out around the ears" is descriptive; a `de_matt` service line is only added if the groomer counted it as a billable add-on or named it as a discrete service. - Do NOT geocode below 0.7 confidence. Leave it null and surface for review. - Do NOT label a behavioural flag as a personality trait of the dog. "Jumped at the dryer" is a managed behaviour in a context, never "anxious dog" or "difficult dog". - Do NOT upgrade a single observation into a recurring pattern. Pattern surfacing requires three or more independent observations of the same body region by the groomer across separate appointments, and even then the surfacing copy is a question to the groomer, not a claim about the dog. - Do NOT use the groomer's audio, photos, owner data, or pet profiles 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. - Do NOT auto-send the owner-facing profile card or post-visit summary. The groomer presses send. Default is private. - Do NOT round prices to a "nicer" number. If the groomer said R$197, the appointment record says R$197. ### 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: Parse voice memo → `Appointment` + `ProfileUpdate` Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none) ``` You are listening to a between-appointments voice memo recorded by a solo mobile dog-groomer. The memo is between 10 seconds and three minutes long. It was recorded next to a converted van between two driveways. Languages encountered include English (UK, US, Indian, Filipino, Nigerian, Kenyan, Singaporean, Australian, New Zealand accents), Brazilian Portuguese (Paulista, Carioca, Nordestino), European Portuguese, Tagalog (Manila, code-switched heavily with English), Cebuano, Hokkien, Cantonese, Mandarin, Yoruba, Igbo, Hausa, Swahili, Amharic, Punjabi (Gurmukhi or Shahmukhi), Bengali, Hindi, Urdu, Tamil, Telugu, Malayalam, Marathi, Vietnamese, Khmer, Thai, Bahasa Indonesia, Bahasa Malayu, Mexican Spanish, Castilian Spanish, Argentine Spanish, Arabic (Egyptian, Levantine, Maghrebi, Gulf), Hebrew, French (Parisian, West African), Farsi, Turkish, Polish, Romanian, Ukrainian. Code-switching mid-sentence is the norm in Manila, Lagos, São Paulo, Karachi, Mumbai — preserve every token. The groomer's operating currency is supplied in the `user_profile_currency` variable. Use it as the default unless the memo explicitly overrides ("she paid me in dollars"). Produce one Appointment JSON object AND one ProfileUpdate JSON object, both matching the supplied schemas. The following are HARD RULES — they are not preferences: 1. `transcript_verbatim` is the memo audio transcribed exactly as spoken, in the source language(s), preserving every code-switch, every "y'know", every "tipo", every "lang", every "tapos". Do NOT translate inside this field. Do NOT clean filler words. Do NOT correct grammar. 2. `groomer_summary_verbatim` is the groomer's own description of what they did this appointment, lightly trimmed of pure filler ("uh", "y'know") but otherwise preserved in the source language(s). It will be translated on a separate call before the owner-facing summary is sent. 3. NEVER write a health diagnosis, condition, infection, or treatment recommendation. The groomer is not a vet. If the memo says "her ear was a bit waxy today", store that as a HealthObservation with `description_verbatim` exactly as said and `speaker: groomer`. Do NOT write "possible otitis", "likely yeast overgrowth", "suggests an infection". Words like "possible", "likely", "suggests", "indicative", "consistent with" never appear in `health_observations[]`. The schema has no `diagnosis` field; do not invent one in any field. 4. The owner's voice and the groomer's voice are different speakers. "Bella's mum says shorter on the ears today" → an AttributedNote or CoatPreference with `speaker: owner` and `source_quote` exactly as the groomer relayed it. "I switched to the low dryer setting because she jumped" → an AttributedNote or BehaviouralFlag with `speaker: groomer`. Never collapse. 5. Distinguish "what we did today" from "what to do every time". A coat preference is a forward instruction stated by someone: "she wants the ears shorter from now on" → preference. A description of today's work without a forward instruction: "took ten millimetres off the topknot" → groomer_summary, not a preference. If the groomer says "and that's how she likes it" or "Mrs Chen says keep doing that" then it becomes a confirmed preference. 6. Materials and tools the groomer used during the appointment do NOT become preferences automatically. "Used the 7-blade on the body" is a description. "She always likes the 7-blade on the body" is a preference. Read for the marker. 7. `total_charged` is ONLY populated if the groomer named a figure in the memo. If they did not, set it to null AND add a `flagged_for_user_review` entry: `{ "field_path": "total_charged", "reason": "no price stated in memo — enter before invoicing" }`. Never substitute an estimate. There is no "going rate" fallback. 8. `duration_minutes` is computed from `arrived_at_iso` and `departed_at_iso` supplied by the client. If those are missing, look for a spoken duration ("about an hour", "took me ninety minutes"). If a spoken duration is given, set `duration_minutes` from it, set `duration_source` to `"spoken_phrase_only"`, and set `duration_uncertain` to true. If neither is available, set `duration_minutes` to null, `duration_source` to `"unknown"`, and `duration_uncertain` to true. 9. `visit_kind` defaults to `regular`. If the memo contains redo / freebie / cancellation / no-show phrasing ("redid the topknot for free", "she cancelled at the door", "no-one answered", "I'm not charging her this time") set it accordingly. A redo where the groomer was still paid in full for the original appointment is `regular` with a `price_lines[]` entry at `is_no_charge: true`. A cancellation is its own `visit_kind` and `payment_status: cancelled_no_fee` unless the groomer said they charged the no-show fee. 10. `payment_status`: - "she paid cash" / "paid via Pix" / "transferred" / "GCash" → `paid_in_full` - "she gave me half, paying the rest next time" → `paid_partial` - "I'll invoice her" / no payment mentioned → `invoiced_unpaid` - "no charge", "on the house", "I redid the topknot for free" → keep `paid_in_full` for the original; add no-charge price line - "she cancelled" → `cancelled_no_fee` (unless fee was charged) - "she's disputing it" → `disputed` 11. ProfileUpdate proposals: any new or updated coat preference, behavioural flag, or health observation goes into the ProfileUpdate object with the appropriate speaker and source quote. `auto_apply_recommended` is true ONLY when: - the speaker matches the field's previous speaker (owner-said stays owner-said, groomer-observed stays groomer-observed), AND - there is no conflict with an existing field. Otherwise the groomer reviews before apply. 12. Code-switching is preserved. "Bella the cockapoo, Mrs Chen po sa Vila Madalena, ang gusto niya shorter ears" stays code-switched in `transcript_verbatim` and is parsed normally — the pet is Bella, the breed is cockapoo, the owner is Mrs Chen, the address is Vila Madalena, the preference is "shorter ears" attributed to the owner. 13. `parse_confidence` is overall confidence (0-1). `flagged_for_user_review` lists every field below 0.7 confidence with a one-sentence human-readable reason. 14. The `Pet` block under `pet_id_ref.proposed_new_pet` and the `Owner` block under `owner_id_ref.proposed_new_owner` are only populated if no existing IDs are supplied by the client. The resolution call runs separately. Output ONLY a JSON object with two top-level keys: `appointment` (the Appointment object) and `profile_update` (the ProfileUpdate object). No commentary. No prose before or after. ``` --- ### Call: Resolve pet + owner (which dog, which household?) Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none) ``` You receive a parsed Appointment's `pet_id_ref.proposed_new_pet` and `owner_id_ref.proposed_new_owner`, plus the groomer's existing roster (up to ~500 pets across ~300 households). Your task: either return existing IDs for the best matches, or confirm a new pet / new owner / new household. Hard rules: - Conservative resolution. Two separate decisions: owner-match and pet-match. They can resolve independently — the owner may already exist (Mrs Chen has been a client for two years with Bella) but this is a new pet on her roster (she just adopted Mochi). - Owner-match requires ALL of: (a) address numeral matches within ±2, AND (b) street name + city match, AND (c) the owner name shares at least one significant token (surname or distinctive first name). - Pet-match requires the matched owner AND: (d) the pet name is the same OR a clear nickname variant ("Bella" / "Bellie"), AND (e) the breed is plausible — a cockapoo last visit cannot become a labradoodle this visit (different pet); a "doodle" last visit can become a "labradoodle" this visit (same pet, more specific). - Two "Bella" entries at different households are different pets; do not collapse. - Two pets at the same household with different names are different pets even if the same breed. - If the new memo phrases "Mrs Chen's place near the market in Pinheiros" and an existing owner is "Mrs Chen, Vila Madalena", do not auto-match — surface as a candidate for the user to confirm. - If multiple candidates exist with equal confidence, return all candidates and let the user choose. - Do NOT modify existing pet or owner records. The match call is read-only on the existing roster. Output a single JSON object: { "owner_decision": "match" | "propose_new" | "ambiguous", "matched_owner_id": string | null, "owner_candidates": [{ "owner_id": string, "confidence": number, "reason": string }], "pet_decision": "match" | "propose_new" | "ambiguous", "matched_pet_id": string | null, "pet_candidates": [{ "pet_id": string, "confidence": number, "reason": string }], "rationale": string } No commentary outside the JSON. ``` --- ### Call: Generate next-visit prompt (per-pet long-context) Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none) — long-context over that one dog's history ``` You receive the full history of one pet — every Appointment, every CoatPreference (with attribution), every BehaviouralFlag, every HealthObservation — and the appointment that is scheduled for tomorrow morning. Your task: produce the next-visit prompt the groomer sees on her home screen the morning of the appointment. The prompt is the three things that decide today. Not every note ever taken. Three. Hard rules: - Open with the pet's name, age, breed. One line, plain. - Then up to three preference / behavioural / observational items, each one a single line, each one attributed by speaker, each one with a date. Example: "Shorter ears — Mrs Chen confirmed 28 May." "Dryer: low setting after warm-up — confirmed three visits." "Right hind nail brittle — you noticed 14 April." - Choose the three by recency × relevance × confirmation count. A preference confirmed three times in a row beats a one-off observation. A behavioural flag still marked `still_relevant: true` beats a fading note. An owner instruction from the last visit beats a generic preference from a year ago. - If there are pending conflicts between owner-said and groomer-observed, surface ONE as the third line: "Owner says X, you observed Y — your call today." - Close with: last visit price + payment method, in one line. "Last visit: R$180, paid Pix." - NEVER write a health diagnosis. If a health observation is surfaced, it is verbatim and attributed to the groomer (or owner) with the date. No interpretation. - NEVER name a pattern that requires fewer than three independent observations of the same body region by the groomer across separate appointments. If the threshold is not met, do not surface a pattern claim. - Tone: the groomer's working notebook talking to the groomer. No exclamation marks. No "FYI". No "remember to". No advice. Just the three things and the price. Output: { "header_line": string, // "Bella, 9, cockapoo." "three_lines": string[], // exactly three lines, each ≤ 100 chars "last_visit_line": string, // "Last visit: R$180, paid Pix." "rationale": string // groomer-readable, one sentence, why these three } No commentary outside the JSON. ``` --- ### Call: Translate owner-facing profile card + post-visit summary Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none) ``` You receive the structured profile of one pet (preferences, behavioural flags, health observations, last appointment summary) and a target language for the owner. Produce the owner-facing profile card and the post-visit summary in the target language, preserving the groomer's voice in any quoted observations. The priority is voice. The translation should sound like the groomer talking to the owner — warm, specific, no clinical language — not like a service-industry template. Hard rules: - Preserve the first person past tense the groomer used in observations. "Bella foi ótima com as tesouras, mas pulou com o secador" → "Bella was great with the scissors, but jumped at the dryer." Never "Behaviour assessment: cooperative with scissors, sensitive to drying equipment." - Do NOT translate proper nouns. Pet names, owner names, breed names, brand names, place names ("Bella", "Mrs Chen", "cockapoo", "Wahl", "Vila Madalena", "Pix", "GCash") stay verbatim. - Do NOT translate measurement units. "10 mm", "7-blade", "1/2 inch" stay verbatim with the unit attached. - Preserve attribution. If a preference is attributed to the owner in the source ("Mrs Chen says shorter on the ears"), the owner-facing card phrases it as "you mentioned you'd like shorter ears last visit". If attributed to the groomer ("she jumped at the dryer"), it phrases as the groomer's observation in plain language. - For health observations, never escalate language. Groomer said "her ear was a bit waxy today" → owner card says "I noticed Bella's left ear looked a bit waxy today — worth a vet check next time you're in" (only if `tell_owner_to_ask_vet` is true; otherwise the observation stays private to the groomer). Never add diagnostic language even when softening. - Preserve the groomer's level of formality. A laconic groomer stays laconic. A chatty groomer stays chatty. - Translate to the target language's natural register for a small-business note to a customer — Brazilian Portuguese stays Brazilian, not European; Mexican Spanish stays Mexican, not Castilian. Output: { "owner_card": { "pet_intro": string, "preferences_owner_facing": string[], // one per line "behavioural_notes_owner_facing": string[], "health_notes_owner_facing": string[], // only those flagged tell_owner_to_ask_vet "last_visit_summary": string }, "post_visit_message": string, // the WhatsApp / email message body "translation_voice_notes": string[] // one sentence per non-obvious choice } No commentary outside the JSON. ``` --- ### Call: Geocode an ambiguous address from the memo Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: `google_search` grounding ``` You resolve an address the groomer spoke aloud into a memo to a usable geocoded location, biased to the groomer's operating country. Given: - `address_verbatim`: the spoken phrase ("Mrs Chen's place in Vila Madalena", "near the church in Moseley") - `country_bias`: ISO-3166-1 alpha-2 of the groomer's operating country (e.g. "BR", "PH", "GB", "MX", "ZA", "NG", "KE", "IN", "AU", "NZ") - optionally `prior_owner_addresses`: a list of streets the groomer has worked on before (cheap context anchor) Return: - `formatted_address`: a canonical, mailable address in the country's local conventions - `lat`, `lng`: decimal degrees - `confidence`: 0-1 - `alternates[]`: up to 3 plausible alternates if disambiguation is ambiguous, each with formatted_address, lat, lng, and a one-line reason Hard rules: - Use `google_search` grounding for the address resolution. - `country_bias` is a hard constraint, not a hint. A "Vila Madalena" resolves to São Paulo if `country_bias` is `BR`, even if other countries have a Vila Madalena. - If confidence is below 0.7, return the best guess in `alternates[]` but leave the top-level `lat`/`lng`/`formatted_address` as null and `confidence` at the best value found. Do NOT pick a low- confidence answer. - Do NOT include citation URLs in the JSON body. The server reads them from `response.groundingMetadata.groundingChunks[].web.uri` (responseSchema and grounding are mutually exclusive in one Gemini call; this call uses grounding, so emit JSON in the text body and parse server-side). Output the JSON only. No commentary. ``` --- ### Call: Morning read-back of the day's appointment list Model: `gemini-3.1-flash-tts-preview` · n/a · n/a ``` Voice: warm, unhurried, working-notebook tone. Pick the Gemini 2.5 Flash TTS voice whose `languageCode` matches the groomer's preferred reading language (typically the same as their UI language, e.g. `pt-BR`, `en-GB`, `tl-PH`, `es-MX`). Pre-process the text before sending it to TTS. The text input is the structured day's list — one pet per block, four to six lines per block, eight or so blocks per day. Encode pacing in the text: - At each line break inside a block, insert a single ellipsis (`…`) so the model produces a natural pause. - At paragraph breaks between blocks (pet to pet), insert a blank line plus an em-dash (`—`). Gemini 2.5 TTS does not support SSML `` — these textual cues are how you signal pace. - Skip the price line on the first read-through if the groomer's preferences specify "appointment details only"; the second pass reads the prices if requested. - Read pet names, owner names, breed names, place names exactly as written — pronunciation follows the chosen voice's native locale. Style direction: prepend ONE short directive sentence to the text input, exactly like: "Read warmly and unhurriedly, as a working morning briefing to the groomer driving to her first appointment. …". 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 (Portuguese ã, Tagalog ng, Yoruba tonal marks, Cantonese tones) are NOT exposed by Gemini 2.5 TTS — no SSML `` tag. Pronunciation comes from the chosen voice's native locale. For mixed-language pet names (a Brazilian groomer with English-named pets), the chosen `languageCode` voice will approximate; the groomer typically does not require IPA-perfect pronunciation for the working briefing. Target rate: ~140 words per minute — brisk-but-clear briefing pace, faster than letter-reading but slower than newsreading. ``` --- ### Call: Weekly roll-up across all appointments Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none) — long-context over the week ``` You receive every Appointment from the week (typically 30-50 appointments, ~600 tokens each ≈ ~30k tokens — comfortable). Your task: produce the groomer's weekly worksheet. Hard rules: - Group by owner household (a multi-pet household appears once with all of its pets' appointments under it). - Per appointment: pet name, date, services, total charged, payment status, tip if any. One line per appointment. - Below each owner block: an "owed" line — sum of `invoiced_unpaid` appointments for that household, in the groomer's currency. If zero, omit. - Total takings for the week: paid_in_full + paid_partial (only the received portion) + no_show_fee_charged. Tips totalled separately. - Cancellations and no-shows are listed under a separate footer block ("cancellations / no-shows this week"), with reason if the groomer named one. - Redo / no-charge lines are visible as such — they are not discounts. The groomer needs to see them as a record of unpaid work she did. - NEVER include a health observation in the weekly roll-up. The roll-up is bookkeeping; health observations live on per-pet profiles. Even high-priority ones. - Profile updates that auto-applied during the week are summarised in a "what's new on profiles" block at the bottom, one line per pet: "Bella — ears preference updated to 'shorter — 10 mm' per Mrs Chen, 28 May." Output: { "week_start_iso": string, "week_end_iso": string, "households": [ { "owner_name": string, "appointments": [{ "pet": string, "date": string, "services": string[], "amount": number, "currency": string, "payment_status": string, "tip": number | null }], "owed": number | null } ], "totals": { "takings": number, "tips": number, "currency": string }, "cancellations_no_shows": [{ "owner": string, "pet": string, "date": string, "reason": string }], "profile_updates_summary": string[] } No commentary outside the JSON. ``` ## 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 new client at the first driveway.** A new owner books through a friend's recommendation. The groomer arrives, meets Bella the cockapoo, talks to Mrs Chen for two minutes at the door, grooms for 90 minutes, walks back to the van, presses one button, talks for thirty seconds. The app creates the owner record, the pet record, the first preference set (what Mrs Chen asked for), the first behavioural observation (Bella jumped at the dryer), the first price line (R$180, paid Pix), and a follow-up note ("Mrs Chen mentioned Bella was just at the vet for her annual — flag for next visit"). - **The returning client whose owner changed their mind today.** Mochi the shih tzu has been groomed the same way for six visits — fluff topknot, scissor finish, ears left long. This visit Mrs Chen says "actually, let's try her shorter today, I'm tired of brushing her every night". The memo: "Mochi today, Mrs Chen wants her shorter all over, did the 5-blade on the body, took the topknot way down — she wants to see how she looks short for a few weeks". The profile update proposes a new coat-preference set, flags the conflict with the previous "ears long" preference, and asks the groomer to review before next visit ("auto-apply or wait for Mrs Chen to confirm?"). - **The dog who bit at the last visit and didn't bite at this one.** Rocky the staffie cross was muzzle-flagged in February after a nip during nail clipping. Today's memo: "Rocky was fine, no muzzle needed this visit, used the rubber-tipped grinder instead of clippers, he was actually great with it". The behavioural flag stays in the profile but a new note is appended: "5 July — flag avoided by switching to grinder; review in 6 months whether muzzle policy still required". - **The owner who relays a vet remark in passing.** Mrs Lim at the door: "Bella was just at the vet, he said her ears looked a bit red but nothing to worry about right now". The groomer's memo includes this. The parse call creates a health observation with `speaker: vet_via_owner`, `description_verbatim` exactly as the owner relayed it, and `tell_owner_to_ask_vet: false` because the owner has already been to the vet. The note is stored for context, never as a diagnosis. - **The senior dog whose owner is in the early stages of dementia.** Mrs Patel has been a client for five years; her labrador Bruno is 13. Lately the instructions at the door drift — "Bruno's never had his nails ground", which contradicts the last twelve visits' notes. The memo logs both ("Mrs Patel said today she didn't want nails ground, even though we've done it every visit — I clipped instead, gently"). The profile flags the contradiction; the prompt next visit asks the groomer to "check with Mrs Patel or with her daughter, who said in March she's the contact for medical decisions". - **The cancellation at the door.** "Mrs Lim cancelled at the door — said the dog was at the vet, she forgot to message me earlier. I'm not charging her this time." The memo parses to `visit_kind: cancellation`, `payment_status: cancelled_no_fee`, a follow-up to re-book, and a note in the owner's record (third cancellation in six months — surfacing pattern but not penalising). - **The redo.** "Did Bella's full groom, R$180, paid Pix, but the topknot didn't sit right after the dryer, so I redid it. No extra charge, just told her to send me a photo in a week if it still looks off." Parses to `paid_in_full` at R$180 plus a no-charge price line ("redid topknot — wasn't happy with how it sat"). A note enters the pet profile: "topknot product or technique to revisit — check next visit". - **The multi-pet household.** Mrs Chen has Bella the cockapoo and Mochi the shih tzu. The groomer does both in one visit. Two appointments, one household, one drive, separate price lines (or a combined household price, depending on the household's setting). The profile updates apply to the right pet — Bella's preference does not migrate to Mochi. - **The foster dog with no history.** The local dog rescue books the groomer for a foster dog called "Lola, mixed, maybe 4, rescued last week, no history". The memo captures everything the groomer learns in 90 minutes — coat condition, temperament, what worked, what didn't, what the foster carer mentioned. The pet record is created with `notes_general[]` flagged as foster; if Lola is adopted by a regular client later, the profile is portable (with the rescue's permission). - **The OFW client.** A São Paulo groomer with a small São Paulo client base also takes appointments from her cousin's clients in Manila when she visits twice a year. The same app, two countries, two currencies, two operating languages, one set of pet profiles per country. The user profile carries operating context per visit. - **The dispute.** Two weeks after an appointment, Mrs Souza messages: "the groom you did was uneven — you charged me R$200". The groomer opens the appointment, plays back the audio memo at the moment she named the price ("Mrs Souza, R$200, paid cash"), shows Mrs Souza the before/after photos, and the dispute is resolved. The receipt is the audio. - **The hand-stripping schedule.** A terrier-specialist groomer in Birmingham keeps a six-month rolling stripping schedule per dog. The profile stores each visit's stripping count and the next due-date; the prompt at the next visit reads "Murphy, 7, wire fox terrier. Body: roll the coat, top section was last stripped 6 weeks ago, due now. Beard: kept long — Mr Davies confirmed 12 March." - **The end-of-week wrap.** Sunday evening, 7 p.m. local, the groomer opens the weekly view: 34 appointments, 9 households, R$5,840 takings, R$420 tips, two cancellations, one no-show fee, four profile updates auto-applied, three pending the groomer's review. She reviews the three (one was a conflict between owner-said and groomer-observed for a beardie's dryer tolerance — she resolves it, the profile updates), exports the bookkeeping CSV, closes the app. ## 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 image of a converted grooming van on a residential street at the end of a winter afternoon — back doors open, light from inside, no people in frame. One paragraph: "Mobile Groom is a voice notebook for solo groomers. Press one button between dogs. Eight driveways later, every dog in the diary knows itself." Google sign-in and phone-number sign-in side by side; Apple sign-in next to them. Below: "Try with the sample roster" → loads the demo roster in section 8a. 2. **Empty state — "Start your roster".** Three big input methods: 🎙 New appointment (one-button voice memo for the dog you just finished) · 📇 Import existing client list (CSV from another scheduling app) · 📅 Schedule first appointments. A short explainer below each ("Best for adding dogs as you go — the way the app expects to be used", "Best if you're migrating from another system and have a CSV", "Best for planning a fresh week from scratch"). 3. **Home — today's calendar.** Today's eight (or however many) dogs as cards, in geographic route order. Each card shows: pet name, breed, owner surname, time slot, the three-line next-visit prompt, last visit's price + payment method. Big "Done with this dog" memo button at the bottom of each card. Tap a card → drilldown to that pet's full profile. 4. **One-button memo flow.** Big circular record button (≥ 100 px tap target). Live transcript appears below as the groomer speaks, in the source language, no cleanup. Stop → parsing begins server-side. The groomer can drive away while it runs. Toast confirmation ("Bella's profile updated — 2 new preferences proposed, 1 conflict to review"). 5. **Pet profile view.** A three-column layout on desktop, stacked on mobile. Left column: pet header (name, age, breed, coat type, owner), large photo if uploaded, calendar of past appointments. Middle column: coat preferences, each line attributed by speaker with date and source quote; behavioural flags, each in context with the groomer's response; health observations, each verbatim and dated, never escalated. Right column: appointment history, most recent first, each one expandable to its memo audio (groomer-only), photos, price lines. Sticky header: pet name → next appointment date → "what changed at the last visit" chip. 6. **Owner / household profile.** Owner card with address, phone, preferred payment method, preferred language, multi-pet roster. Below: every appointment across every pet in the household. Side panel: notes about the household ("always pays cash", "landlord pays groom, tenant pays nail grind", "communicates via WhatsApp Brazil number"). 7. **Roster view.** Magazine-grid of pets. Filter by breed, age, coat type, behavioural flag, last visit date, owner. A toggle: "Show me the dogs due for a groom this week", "Show me the dogs I haven't seen in 8+ weeks", "Show me the dogs with active behavioural flags". Default sort: next appointment first. 8. **Routing view.** Today's appointments on a map with the suggested driving order. The groomer can drag to reorder. Persistent offline. Each pin is a pet, not an owner — a multi-pet household appears once on the map but multiple times in the day's stop list. 9. **Profile updates review.** Inbox of pending updates from this week's memos. Each item shows: the pet, the proposed change, the source quote, the speaker, the previous state. Three buttons: apply, reject, edit. Conflicts ("owner says X, you observed Y") have a "keep both" option with a forward-instruction picker. 10. **Owner-shareable profile card.** Modal: "Share Bella's profile with Mrs Chen". A clean read-only view in the owner's preferred language. The groomer reviews before sending. Magic-link email or copy-to-WhatsApp. 11. **Post-visit summary.** After each appointment, a one-tap option to send Mrs Chen a short summary in Brazilian Portuguese: "Bella was great today — we did the full groom, took 10 mm off the topknot, used the low dryer setting because she was a bit sensitive again. R$180, received via Pix. See you in six weeks." The groomer reviews and sends. 12. **Weekly worksheet.** End-of-week roll-up. Households, appointments, takings, tips, cancellations, profile updates. Export to CSV for the bookkeeper. Send to the groomer's own bookkeeping email. 13. **Settings.** Operating currency, operating country, primary language, secondary languages, default payment methods, weekly roll-up day/time, owner-shareable card defaults, voice memo retention period (default: keep forever, deletable per appointment). 14. **Footer.** "Made for the dog whose mum always asks the same favour." Privacy: "Your voice memos 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 Mobile Groom." - Subhead: "The dog's preferences travel with the dog — in any breed, any city, in the words the owner actually said." - One paragraph (≤ 60 words) explaining who this is for and what makes it different from a generic scheduling app: it listens to the groomer's voice between appointments, it keeps the owner's voice and the groomer's voice separate, it never invents a health diagnosis, and it surfaces only the three things that decide today at the next visit. - Visual: a small annotated illustration of a pet profile card with three preferences attributed to three different speakers (owner, groomer, vet-via-owner) — not a generic dog icon. **Slide 2 — Try it now.** - One short prompt: "Try with the sample roster". - A live demo input pre-loaded with three pets from the seed content in section 8a — Bella the cockapoo in São Paulo, Mochi the shih tzu in São Paulo (same household), and Murphy the wire fox terrier in Birmingham. - 1-2 sentences pointing at *the specific page elements* where the Gemini magic happens (the speaker attribution on every preference line, the conflict surfacing between owner-said and groomer-observed, the three-line next-visit prompt at the top of the day's card). **Slide 3 — How to remix this.** - Headline: "Make this yours." - Three short bullets: - "Swap the sample roster in `/data/seed-roster/` for your own pets." - "Adjust the prompts in `/server/prompts/` to fit your operating language(s) and currency." - "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 (audio input)** — listens to the groomer's between-appointments voice memos in any of the dominant operating languages (Brazilian Portuguese, Tagalog, Cantonese, Yoruba, Bahasa, Punjabi, Bengali, Mexican Spanish, Arabic, English with regional accents). Handles code-switching mid-sentence — the dominant pattern in Manila, Lagos, São Paulo, Karachi. - **Gemini 3.5 Flash (structured output)** — produces the Appointment record and the per-pet ProfileUpdate in one call. Every preference, behavioural flag, and observation is typed and attributed to a speaker. - **Gemini 3.5 Flash (long context)** — the per-pet next-visit prompt is generated from every prior appointment for that one dog. The groomer sees the three things that decide today, picked from the full history. - **Gemini 3.5 Flash + grounded search** — resolves an addressed-by-landmark spoken phrase ("Mrs Chen's place in Vila Madalena") to a usable address, biased to the groomer's operating country. - **Gemini 3.5 Flash (multilingual translation)** — renders the owner-facing profile card and the post-visit summary in the owner's preferred language, with the groomer's voice preserved. - **Gemini 2.5 Flash TTS** — reads the morning's appointment list aloud while the groomer drives. The groomer confirms with a tap. - **Firebase Auth** — Google sign-in, phone-number sign-in (recommended for mobile groomers), Apple sign-in (optional, user-config), magic-link email for owner-shareable cards. - **Firestore** — stores rosters, appointments, per-pet profiles, syncs across devices in real time. Offline-first writes queue and reconcile. - **Firebase Storage** — keeps the raw audio memos and the appointment photographs, original resolution, forever (deletable per appointment). Audio is private to the groomer; owner-shared cards never include the audio. - **Cost note** — see the detailed breakdown in 6d. A typical week of 35 appointments costs about $0.85 of Gemini API spend, total, processed once. - **Privacy note** — your voice memos, owner details, pet profiles, and photos are private to you and the owners you explicitly share with. 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 app does not write health diagnoses, ever — the schema has no field for them. **Backend services this app depends on:** - Auth: see section 4b - Database: see section 4b - Storage: see section 4b - Email: see section 4b - WhatsApp / SMS: see section 4b (optional, user-config) - Payments: see section 4b (optional, user-config) - 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) - `STRIPE_SECRET_KEY` — optional, only if you accept Stripe at the door / by link - `WHATSAPP_BUSINESS_TOKEN` — optional, only if you send via WhatsApp Business API - `TWILIO_AUTH_TOKEN` / `AFRICASTALKING_API_KEY` / `MESSAGEBIRD_KEY` — optional, only if you use SMS in your region - `GOOGLE_MAPS_KEY` — optional, only if you want higher-fidelity geocoding than the grounded-search default **Cost + privacy notes:** - One short paragraph per cost-sensitive capability: long-context per-pet prompt generation is billed per token of input — a dog with 20 visits of history costs about $0.003 each time the prompt is regenerated (which is once a day at most, the morning of the appointment). - One short paragraph on privacy: where the data lives (your Firebase project), how to delete it (Settings → "Delete this pet's record" or "Delete this owner's record" or "Delete my account" — gone in 60 seconds), what is never sent for training. **Documentation links:** - AI Studio Build docs - Gemini API audio input, multilingual, long-context, TTS docs - Firebase Auth, Firestore, Firebase Storage docs - A short note on owner-shareable profile cards and the privacy posture **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) - **Parse voice memo → Appointment + ProfileUpdate (Gemini 3.5 Flash, medium thinking)** — typical 30-second memo, audio + ~1,500 output tokens. ~$0.018/memo. - **Resolve pet + owner (Gemini 3.5 Flash, low thinking)** — small input (proposed records + roster excerpt), ~200 output tokens. ~$0.0008/appointment. - **Generate next-visit prompt (Gemini 3.5 Flash, low thinking, per-pet long-context)** — a dog with 20 visits of history is ~12k tokens input + ~300 output. ~$0.003/dog/day, runs the morning of the appointment. A week of 35 appointments ≈ $0.10/week. - **Translate owner-facing profile card + post-visit summary (Gemini 3.5 Flash, low thinking)** — small input, ~600 output tokens per pet. ~$0.005/share. Most groomers share once per visit at most. - **Geocode (Gemini 3.5 Flash + grounded search)** — ~$0.001/address. A typical groomer has 80-120 distinct addresses; runs once at first reference, never again. - **Morning read-back (Gemini 2.5 Flash TTS)** — billed per output token (~$10/M output tokens), effectively ~$0.000003/character. A typical morning list of 8 pets ≈ ~1,200 characters ≈ $0.004/day. - **Weekly roll-up (Gemini 3.5 Flash, medium thinking)** — long-context input over the week, ~30k input tokens + ~2,000 output. ~$0.05/week. - **Expected per-appointment cost:** ~$0.022. **Per week of 35 appointments:** ~$0.85. **Ongoing per pet per year** (52 visits would be unusually frequent; assume 8): ~$0.18/pet/year. - **Audio storage:** Firebase Storage standard tier, ~$0.026/GB/month. A 30-second m4a memo at 64 kbps ≈ 240 KB; a week of 35 memos ≈ 8 MB. A full year of memos ≈ ~400 MB ≈ $0.011/month. Photographs dominate storage if uploaded — a high-res before/after pair ≈ 8 MB; a roster of 200 pets with photos from 8 visits each ≈ 13 GB ≈ $0.34/month. ## 7. Design language - **Mood:** A working groomer's notebook that lives in the van. Not a salon-management SaaS. Not a vet portal. The dashboard at the end of the third driveway, the dog already loaded back into the owner's house, the engine off, thirty seconds before pulling onto the next street. The materials in mind: the grit of dog hair on a black neoprene apron, the chrome of the dryer arm, the warm rubber of the grooming-table mat, the laminated pet-card the groomer used to keep in a binder. - **Typography:** Working humanist sans for the per-pet cards and the daily prompts (Inter or Söhne). A slightly mechanical accent for headers and chip labels (Atlas Grotesk or Geist Mono). A handwriting-styled accent (sparingly) only for the groomer's own bottom-of-card scribble fields ("call Mrs Chen tomorrow"), never for parsed transcript or owner-facing copy. - **Palette:** Bone-paper background `#F4EFE6` for the pet-card surface, warm graphite `#1E1A17` for body text, working ochre `#B5781E` for groomer-attributed observations (her voice), riverwater blue `#3A6A8A` for owner-attributed preferences (the owner's voice), muted moss `#5A6E3D` for confirmed-three-times-or-more preferences (the "this is now the default" colour), and a sparing alert orange `#C45A2C` only for owner-said-vs-groomer-observed conflicts. Never red — the app is not the vet, and the historical association of red with medical alerts would be a category error. - **Imagery:** The pet photographs (before / after / coat condition) are the hero of the per-pet card. Never replace them; never crop them tighter than the groomer did. The van photographs (welcome screen, empty state) are warm and out-of-focus around the edges. Period or stylised dog illustrations are avoided — the dogs in the app are the user's real dogs. - **Hand-feel touches:** A barely-visible canvas grain on the pet-card background (echoes the apron). The "show audio" expandable panel slides the playback in with a thin shadow — like flipping a card in the binder. Speaker chips ("Mrs Chen said", "you observed") use a small colour dot rather than a heavy badge — peripheral, not shouting. - **Spacing:** consistent 4-px base. Generous whitespace — the working-notebook feel needs air, and a card that's too dense is a card the groomer won't read in the morning. - **Radius:** consistent token set (e.g. 6 / 12 / 20 px). Pet cards use 6; the day's-list cards use 12; the welcome card uses 20. - **Shadows:** subtle, layered, neutral. Avoid heavy drop-shadows. - **Motion:** purposeful — entrance fades, hover lifts, page transitions. Respect `prefers-reduced-motion`. The memo button has a single small breathing animation (1.5 s loop, 0.05 opacity range) only while recording — a confirmation that the mic is live; respect reduced-motion by falling back to a static "live" indicator. No bouncing splash animations. No theatrical hero animations. - **States:** every interactive element has hover, focus, active, disabled. Loading uses skeletons not spinners where possible. Empty states have helpful next-action guidance ("Record a memo for the dog you just finished — that's the only thing you need to do today"). ## 8. Content generation rules - Write **realistic, specific copy**. NO Lorem Ipsum. NO generic placeholders like 'Your tagline here'. - Invent plausible names, breeds, addresses, prices, memos, owner remarks that fit the domain (use the seed content in section 8a as a starting point). When inventing, lean on the dominant city-level patterns — Paulista Portuguese with Pix payment in São Paulo; Tagalog code-switched with English and GCash payment in Manila; British English with bank transfer in Birmingham — but never claim that a fictional client is a real person. - Tone: warm, direct, free of corporate language. This template is for a person, not a company. - Headlines: punchy and concrete. No 'Empower your X' filler. 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 user already speaks the jargon (the groomer wants "7-blade", "hand-strip", "carding", "de-matt", "sanitary trim" to appear verbatim; the small-business user wants "BRL", "Pix", "GCash", "M-Pesa" to appear verbatim). - 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 parse shows as a faintly underlined word in the transcript; tapping it reveals the alternates the model considered). - Health-related copy follows one rule and one rule only: the app shows what the groomer (or the owner relaying the vet) said, verbatim, with attribution and date. No diagnostic language, ever. ## 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 roster (sidebar):** - "Mariana's Paulista Round" (38 active pets across 24 São Paulo households, BRL, Pix dominant) — Brazilian Portuguese, English fallback, weekly Sunday-evening roll-up. - "Joy's QC Saturday Route" (29 active pets across 19 Quezon City households, PHP, GCash dominant) — Tagalog with English code-switch, Tagalog fallback, weekly Sunday-noon roll-up. - "Sam's Moseley & Kings Heath Round" (44 active pets across 31 Birmingham households, GBP, bank transfer dominant) — British English, weekly Friday-evening roll-up. **Sample pet in detail view (this is what the demo should show):** - **Pet name (verbatim):** "Bella" - **Owner (verbatim):** "Mrs Chen" - **Owner address (verbatim):** "Rua Harmonia 412, Vila Madalena, São Paulo" - **Breed (verbatim):** "cockapoo" - **Breed (canonical):** "cockapoo" - **Age:** 9 - **Sex:** female - **Desexed:** true - **Coat type:** curly - **Coat preferences (3):** - region "ears", preference "shorter — 10 mm", blade "scissor finish", speaker `owner`, confirmed 28 May 2026, source quote "Bella's mum says shorter on the ears today" (Mariana's memo) - region "body", preference "7-blade", speaker `groomer`, confirmed three visits running (8 Feb, 22 Mar, 28 May 2026), source quote "used the 7-blade on the body again, same as last time" - region "topknot", preference "fluff finish, 15 mm", speaker `owner`, confirmed 22 Mar 2026, source quote "Mrs Chen wants the topknot kept full but tidy" - **Behavioural flags (1):** description "jumped at the dryer", context "dryer", groomer response "low setting after warm-up — five minutes high, then drop", first observed 8 Feb 2026, last observed 28 May 2026, still relevant true - **Health observations (1):** description verbatim "her left ear was a bit waxy today", body region "left ear", observed 28 May 2026, speaker `groomer`, tell_owner_to_ask_vet true, groomer note to self "second time I've noticed — flagging for Mrs Chen to mention at vet" - **Last appointment summary:** "Full groom, 90 minutes, 10 mm off the topknot, 7-blade on the body, scissor finish on the ears (shorter per Mrs Chen), nail grind. R$180, paid Pix. Bella was great with the scissors but jumped at the dryer again so I switched to low after the warm-up." - **Next-visit prompt (rendered):** - Header: "Bella, 9, cockapoo." - Line 1: "Shorter ears — Mrs Chen confirmed 28 May." - Line 2: "Dryer: low setting after warm-up — three visits running." - Line 3: "You noticed waxy left ear 28 May — Mrs Chen flagged to ask vet." - Footer: "Last visit: R$180, paid Pix." **Sample multi-pet household:** - Owner "Mrs Chen" with Bella (cockapoo, 9) and Mochi (shih tzu, 4) at the same Rua Harmonia 412 address. Mochi's profile is independent — different breed, different preferences (long topknot, scissor-only on the face, ears long), no behavioural flags, no health observations. **Sample input memos (for the build to demonstrate):** - A 28-second memo in Brazilian Portuguese code-switched with English: "Bella the cockapoo, Mrs Chen at Rua Harmonia, shorter on the ears today, tirei uns dez milímetros do topknot, 7-blade on the body, ela foi ótima com as tesouras mas jumped at the dryer again, low setting depois de cinco minutos, R$180 paid via Pix, paid." - A 35-second memo in Tagalog code-switched with English: "Mochi shih tzu, Mrs Cruz po sa Marikina, ang gusto niya keep the topknot long, scissor-only sa face, regular full groom, walang issues, ₱1,800 paid cash, tapos na." - A 22-second memo in British English: "Murphy the wire fox terrier at the Davies place in Moseley, hand-strip on the body and shoulders, beard kept long, Mr Davies confirmed that's still what he wants, he was fine, no flags, £55 bank transfer, sorted." - A 40-second cancellation memo in Brazilian Portuguese: "Mrs Lim cancelou na porta, disse que o cachorro tava no vet, esqueci de avisar antes, não vou cobrar dessa vez, terceira cancelamento dela em seis meses, talvez vale a pena uma conversa." - A 31-second redo memo in British English: "Murphy's done, full groom, £55 paid bank transfer, but the beard didn't sit right after the dryer, redid it, no extra charge, told Mr Davies to send me a photo in a week if it looks off." **Sample voice copy:** - Onboarding: "Press the button when you're back at the van. Talk for thirty seconds. The dog's profile updates itself." - Processing: "Listening to the memo…" / "Updating Bella's profile…" / "Reading Mrs Chen's preferences…" / "Ready in three seconds." - Empty roster: "No dogs in your roster yet. Press the memo button after your first appointment — Bella's profile will appear here." - Error (couldn't transcribe): "We couldn't make out the memo — wind, dryer, or signal? Want to try recording again, or type the key bits?" - Save confirmation: "Bella's profile updated — 2 new preferences proposed, 1 conflict to review." - Conflict review: "Mrs Chen says Bella is fine with the dryer. You observed she jumped at the dryer. Which applies today?" - Health observation: "You noticed Bella's left ear looked waxy. We've added it to her profile with the date. We won't write a diagnosis — that's the vet's call. Want to flag this for Mrs Chen to mention at the vet?" - Low confidence note: "Some words were hard to make out. Tap any underlined word to see what the model considered." **Sample owner-shareable profile card (post-visit summary, Brazilian Portuguese):** - "Bella se comportou super bem hoje — fiz o full groom completo, tirei 10 mm do topknot como você pediu, usei o 7-blade no corpo e deixei as orelhas mais curtas. Ela pulou um pouco com o secador no começo, então baixei a velocidade depois dos primeiros minutos — ela ficou tranquila depois. Notei que a orelha esquerda dela tava um pouco com cera hoje — vale comentar com o vet na próxima consulta, nada urgente. R$180, recebido via Pix. Te vejo daqui a seis semanas. — Mariana" **Sample family-graph-equivalent (multi-pet household summary):** - "Mrs Chen at Rua Harmonia 412 — two dogs, Bella the cockapoo and Mochi the shih tzu. Both groomed in the same visit; combined visit price R$320 (R$180 + R$140), paid Pix. Bella's preferences and Mochi's preferences are separate; don't migrate." ## 9. Media & assets - **Hero image (landing screen):** A photographed-looking shot of a converted grooming van parked on a residential street at the end of a winter afternoon. The back doors are open, the interior is warm, no people in frame. Generate via Nano Banana 2 with a prompt emphasising "side view of a converted van with the back doors open, warm interior light spilling onto the kerb, residential street at dusk in winter, no people, no logos, gentle motion blur in the foreground from a passing car". - **App icon / wordmark:** Set in the working humanist sans. A small canvas-texture chip behind it. No emoji-pet icon — just type. - **Empty-state illustration:** A simple line drawing of a memo button at the bottom of a phone screen with three small dots above it (the active mic). Hand-drawn aesthetic, not a flat icon. - **Demo pet photographs:** Generated per the prompts in section 8a — Nano Banana 2 prompts that specifically request "warm-light photograph of a cockapoo on a grooming table mid-visit, scissors on the side, no faces of people in frame, the dog calm and looking off-camera". Each demo pet should look photographed, not rendered. - **Multi-pet household:** A photograph of two dogs (different breeds) on the same doormat, owner's hand half-in-frame holding a treat, warm afternoon light. No identifying features of the owner. - **Stock fallbacks:** If image generation fails, fall back to the photographed sample pet from `/public/samples/sample-pet.jpg`. Never to a "🐕" emoji. - **Generated imagery:** prefer Nano Banana 2 over stock photography. Prompt for warmth, asymmetry, and slight imperfection — avoid the glossy 'AI render' look. - **Optimisation:** WebP/AVIF, `loading="lazy"`, explicit `width`/`height` to prevent layout shift. - **Icons:** `lucide-react` for UI. Use sparingly — never decorative-only. A small dog-paw glyph is permitted for the home-screen tab; never elsewhere. ### 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: "Record a memo for the dog you just finished", "Add the first pet manually", "Import a roster CSV" — never a generic "Add your first item". - Smooth scroll for in-page anchors. - The memo button has three states: idle (canvas-texture circle, calm), recording (the breathing animation, a live transcript appearing below), processing (a calm progress chip — "updating Bella's profile…"). On error: a calm-specific message ("we couldn't make out the memo — wind, dryer, or signal? Want to try again, or type the key bits?") and a retry. - 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 and offer retry. - Low-confidence words in the transcript are faintly underlined; tapping reveals the alternates the model considered. - The conflict-review UI between owner-said and groomer-observed uses two clearly-labelled columns (riverwater blue for owner, working ochre for groomer) with a "keep both" affordance — never auto-resolves. - Speaker chips on every preference / behavioural / observation line are clickable: tapping plays the source audio from the right second (groomer-only). - The morning calendar respects reduced-motion: pet cards appear instantly rather than staggered. ## 11. Tech & responsive requirements - **Deterministic arithmetic — server-side TypeScript only.** Any money-split / settlement / total / VAT / unit-conversion math runs in TypeScript on the server using integer-cents arithmetic. The Gemini call only extracts entities (line items, payer, currency code, raw quantities) into structured JSON. Never let the model do the sum — Gemini will return plausible-looking arithmetic that does not actually balance. - **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 parse / next-visit prompt / translation / weekly roll-up, and `gemini-3.5-flash` for pet+owner resolution / geocoding / (optional) price-book-equivalent default proposals. Set `thinkingLevel` explicitly per call. - **Database:** Firestore (auto-provisioned by AI Studio Build). Show the seed roster on first launch. - **Auth:** Firebase Auth — Google sign-in by default; phone-number sign-in recommended for mobile groomers; Apple sign-in optional (user-config); magic-link email for owner-shareable cards. - **Storage:** Firebase Storage for raw audio memos and pet photographs. Pre-signed URLs only. - **Mobile-first.** Verify layouts at 375 px (iPhone SE, the dominant device for mobile groomers driving a route), 414 px (iPhone Pro Max), 768 px (iPad), 1024 px, 1440 px+. The home screen and the memo flow must be operable one-handed at 375 px with the phone in a van-dashboard mount. - 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 roster and the day's calendar. - Optimistic UI on writes; reconcile on response. - Voice memo capture uses the Web MediaRecorder API where supported (Safari iOS ≥ 14.5, Chrome ≥ 81); falls back to a tap-to-record native picker otherwise. Audio is encoded as m4a at 64 kbps mono — small enough to upload over patchy mobile data, clear enough for the parse. - **iOS Safari gotchas (graceful degradation):** mic permission does NOT persist across page reloads on iOS — re-request on every memo; an incoming call interrupts the audio session (`MediaStreamTrack.onmute` fires) — auto-pause, persist the partial chunk to IndexedDB, and prompt the groomer to resume; backgrounded Safari tabs pause `getUserMedia` — pair `visibilitychange` with a screen Wake Lock during memos so a memo isn't silently lost when the groomer answers the door; in dryer/wind noise, surface "we missed some words — re-record a sentence or type the key bits" rather than failing silently. ## 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 riverwater-blue / working-ochre speaker chips are paired with a small icon (not colour alone) so colour-blind users can tell owner from groomer at a glance. - All images have meaningful `alt` text. Pet photographs have `alt` describing the artefact ("photograph of Bella, a 9-year-old cockapoo, mid-groom on the table, looking off-camera"). - Form fields have associated `