================ 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. --- # Van Quote ## 1. Project **Van Quote** is a quoting app for a one-person trade. The user — an electrician, a plumber, a roofer, a heating engineer — walks a customer's house with their phone, points the camera at every fuse board, socket, light fitting, and run of cable that the job touches, and the app produces an itemised quote in the user's own branding, priced from the user's own rate card, and emailed to the customer as a PDF before the user has even pulled out of the driveway. The materials list is structured. The labour estimate is broken down room by room. Every line that involved guesswork is flagged as a guess so the sparky can confirm or correct it before sending. This is the kind of app a sole-trader sparky builds for himself on a quiet Sunday because he is tired of going home at 8pm, eating cold food, and then writing quotes on a knackered laptop until midnight — quotes that the customer sometimes never even replies to because three other sparkies got their numbers in first. It is also the kind of app a Nigerian electrician in Surulere, Lagos uses to quote a domestic rewire on a Saturday afternoon in 38°C heat; a Filipino contractor in Quezon City uses to quote a new aircon circuit before a typhoon forecast cancels the rest of the day; and a second-generation Punjabi-British sparky in Sparkbrook, Birmingham uses to quote a landlord's HMO rewire between two earlier jobs, in the half-hour the customer has between school pickup and his evening shift. Same shape of moment, different continent, different rate card, different currency, same trade. The single demo that proves the magic: the sparky stands in the customer's hallway, taps the big red record button, and walks the property talking to himself the way he already does — "right, this fuse board is a Wylex with rewireable fuses, that's coming out… sockets in here are single, customer wants doubles… two pendants want swapping for downlights, six in the kitchen ceiling, MR16s… bathroom's a zone 1, needs an IP-rated extractor and a shaver socket…" — and five minutes later, back at his van, he opens the app and the quote is already drafted. Every fitting he pointed at is in the materials list. Every room he walked is a labour line. Total at the bottom in his currency. His logo in the header. His VAT number in the footer. Customer's name and address pre-filled from the calendar appointment. He scans for guess flags, fixes two prices the model wasn't sure about, taps Send, and the PDF is in the customer's inbox by the time he turns the key in the ignition. And in the harder cases — the older properties, the dodgy installs, the "can you also just look at" cases that double the scope of a quote halfway through — the app keeps up. The 1930s council flat in Glasgow with cloth-insulated wiring and no earth on the lighting circuit. The rural longhouse in Donegal with three separate consumer units, two of them illegal. The bungalow in Christchurch whose previous owner DIYed the kitchen and left a death-trap behind a fridge. The app does not pretend to be a Part-P inspector; it captures what the sparky saw, flags what he flagged with his voice, and lets him annotate the quote PDF with the words "subject to test" against any circuit he hasn't put a meter on yet. **Tagline:** _A quote in the customer's inbox before they wave goodbye — in any trade, any country, any currency, priced from your own rate card._ ## 2. Target audience - Self-employed electricians, plumbers, gas-safe engineers, heating engineers, roofers, general builders, carpenters, decorators — one-person operations or small two-three person crews - Trades who work across many small domestic jobs a week and lose evenings to admin — quotes, invoices, parts lists - Trades whose first language is not the customer's language — Polish sparkies in London, Romanian builders in Italy, Filipino contractors in Saudi Arabia, Mexican electricians in Texas — and who want quotes that read like a native speaker wrote them - Trades in regions where the rate card is in a soft currency or shifts weekly — Lagos, Buenos Aires, Istanbul, Cairo — and who need the app to respect their saved per-day labour rate, not invent one - Apprentices and second-year improvers running their first solo quotes who need the scaffolding of an itemised template to learn what a good quote looks like - Estimators inside small family firms (a dad-and-son electrical contractor; a small wedding-photography studio that occasionally subs in a second shooter) who run the same workflow at slightly bigger scale - Trade-school instructors using the app as a teaching tool — show a learner the inside of a real domestic job and watch the structured quote build - Coordinators inside community-housing co-ops (Manila, Mumbai, Mexico City) running scheduled maintenance across many small units who need consistent, auditable estimates ## 3. Core value propositions Surface these clearly through copy, visual emphasis, and section ordering — they are the reasons users pick this app. - **Walk the job, get the quote** — the input is the trade's existing motion: walking the property, looking at fittings, talking out loud. Gemini 3.5 Flash reads the video frames AND the voice memo in one multimodal pass; the structured quote is the output. No tap-tap-tap on a tiny keyboard between fuse boards. - **Priced from YOUR rate card, never invented** — the app refuses to make up prices. Every line item is priced against the user's own saved per-unit and per-day rates; anything not in the rate card is flagged as a guess with the model's best estimate alongside a "your rate?" prompt. The customer never sees a number the sparky didn't confirm. - **Itemised, not "lump sum"** — the quote breaks down by room, by circuit, by fitting. "Kitchen — 1× new 32A radial for hob, 4× double sockets, 6× MR16 downlights, 1× extractor isolator." Customers stop asking "what does this £4,200 include" because they can see exactly what. - **Branded PDF, ready to send** — the user's logo, name, trading address, VAT or GST or NTN registration number, payment terms, and bank details are baked into the template once. Every quote inherits them. The PDF that hits the customer's inbox looks like it came from a printer-shop, not a free Word template. - **Guess flags are honest** — every line the model wasn't sure about is marked `guess` in the structured output and rendered with a small amber dot in the quote view. The sparky reviews these before sending; the customer never sees them. - **Materials list separately** — the structured output produces TWO documents: the quote PDF for the customer (totals in plain language) and a materials picklist for the wholesaler (part numbers, quantities, brand preferences from the rate card). The sparky emails one to the customer and forwards the other to City Electrical Factors or Edmundson's or the local merchant. - **Works in any currency, any tax regime** — £ GBP with 20% VAT, € EUR with reverse-charge B2B, ₦ NGN with 7.5% VAT, ₱ PHP with 12% VAT, $ USD with state-by-state sales tax, ₨ PKR with no tax registration. The user configures their tax setup once; the app does the maths. - **Tradies do not need to type long sentences** — the app is built for thick fingers, dust on the screen, and bad reception. Voice memo input is first-class; typing is a fallback. ## 4. Features to build - Voice memo + camera capture (mobile-first): one big record button; user walks the property, talks, and pans the camera over fittings as they go - Per-room mode (optional): user taps "next room" between rooms so the structured quote can attribute labour cleanly - Multi-photo upload fallback: when the customer asked the sparky to "just send me a price for the kitchen", the sparky can capture three or four photos with one short audio note instead of a full walk-through - Multimodal parse — voice transcript + video frame extraction + scene understanding (count of sockets, type of consumer unit, age of cabling visible) in a single Gemini 3.5 Flash call per room or per artefact - Trade-aware ontology — fuse board, RCBO, MCB, single socket, double socket, pendant, downlight, LED panel, extractor fan, shaver socket, towel rail, immersion heater, combi boiler, isolator, junction box — across electrical, plumbing, heating, roofing, decorating, carpentry vocabularies - Rate-card management — per-item rates, per-day labour rates, mark-up rules, currency, tax regime, preferred wholesalers; editable in a clean settings screen - Quote draft — structured line items with room attribution, materials sub-list, labour estimate, day count, tax line, total - Guess flags — every line the model wasn't sure about is flagged; the sparky confirms or overrides before sending - Materials picklist export — a second document with manufacturer part numbers, quantities, and the sparky's preferred wholesaler name, ready to email to the trade counter - Branded PDF generation — user's logo, business name, VAT/GST/NTN, payment terms, bank details rendered into a clean serif PDF, two pages or one - Email send — the PDF is attached and a short cover note is generated in the customer's language, signed off in the sparky's name - Customer record — name, address, phone, last quote sent, follow-up flag for "no reply after 5 days"; lightweight, not a CRM - History — every quote sent, with status (sent, viewed, accepted, expired), filterable by week or month - Re-quote — clone a previous quote, edit, re-send; saves the sparky 20 minutes when the customer comes back asking for two extra sockets - Subject-to-test flag — the sparky can mark any circuit or fitting "subject to test", which renders a small footnote in the customer-facing PDF - Offline-first draft — if the property has no signal, the camera + voice memo are captured locally; the Gemini call fires the moment the van rolls back into 4G - TTS read-back (optional) — the sparky can ask the phone to read the draft quote back to him while he drives to the next job, so he can confirm or flag a re-edit - Multi-language quote output — the user is a Polish sparky in London; his customer is English; his rate card is in £; the cover note and the PDF body are in English; the materials picklist (for his Polish-speaking wholesaler) is in Polish. The app handles this routing per-quote. - Currency awareness — the app reads which currency the rate card is in and renders totals accordingly. The sparky in Lagos sees ₦; the sparky in Manila sees ₱; the sparky in Birmingham sees £. - Per-trade templates — electrician, plumber, gas engineer, roofer, builder, decorator, carpenter — each with the right ontology and the right default rate-card seed ## 4b. Required Gemini capabilities + backend services **This template's intelligence comes from the Gemini capabilities below. Wire them up explicitly — don't substitute generic LLM calls.** ### Gemini capabilities (the load-bearing intelligence) - **Multimodal video + audio input** (Gemini 3.5 Flash) — reads the sparky's walk-through video and his voice memo in one pass. Recognises fittings on screen (consumer unit make, socket type, cable visible behind a removed faceplate), parses the spoken commentary, and ties the two together. One API call per room (or one per artefact when the user is in "photo + voice note" mode). - **Structured output / JSON Schema** — the response matches the `Quote` schema below. Every line item is typed; every guess flag is explicit; the schema is included verbatim in the system instruction and as `responseSchema`. - **Multilingual transcription + translation** (built into Gemini 3.5 Flash) — handles English, Spanish, Portuguese, Polish, Romanian, Filipino (Tagalog), Vietnamese, Mandarin, Cantonese, Hindi, Urdu, Bengali, Punjabi (Gurmukhi and Shahmukhi), Tamil, Korean, Japanese, Amharic, Swahili, Yoruba, Igbo, Hausa, Arabic, Farsi, Turkish, Greek with trade-specific vocabulary preserved (the sparky says "Wylex board"; the model keeps "Wylex" verbatim). - **Long context (1M tokens)** — once the customer record grows, the re-quote flow needs to see the previous quote AND the new walk-through together. A typical Quote object is ~3,000 tokens; a history of 20 prior quotes ≈ ~60k tokens (comfortable). For trades with very long customer histories (>200 quotes against one landlord with many properties), chunk by property address before the re-quote call. - **Search grounding** — for materials-pricing sanity-check ONLY. The grounded call asks "what is the typical UK trade price for a Wylex Amendment 2 18-module consumer unit, May 2026" and surfaces a citation. The grounded number is NEVER used in the customer quote unless the sparky's rate card has no rate AND the sparky explicitly accepts the grounded price as a one-time guess. - **Gemini TTS** (`gemini-3.1-flash-tts-preview`) — reads the draft quote aloud while the sparky drives to the next job. Voice is unhurried, in the user's preferred language, at the trade's reading pace ("a sparky listening, not a podcast listener"). - **Thinking levels** — `medium` for the primary walk-through parse (multimodal video + audio + structured output). `low` for cover-note generation, materials picklist generation, and TTS prep. `high` is reserved for the re-quote conflict resolution (this room's previous quote vs this walk-through's findings) when the user opts in. ### Backend services - **Auth — Required.** Firebase Auth with Google sign-in (auto-provisioned by AI Studio Build). **Apple sign-in is optional but user-configured**: it requires an Apple Developer account, Service ID, Key ID, and private key wired into the Firebase Auth console. **Magic-link email** (used for "let an apprentice draft a quote on your behalf") also requires the sender domain to be authorised in Firebase Auth. Each rate card and quote history is private to the owner. - **Database — Required.** Firestore for `users`, `rate_cards`, `customers`, `quotes`, `quote_versions`, `materials_master`, `team_members` (for two-person crews). - **File storage — Required.** Firebase Storage for the original walk-through videos, the voice memos, the room photos, and the generated 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 capture. Videos are kept for 30 days then archived to cold storage; PDFs are kept indefinitely (they're tax records). - **Email — Required (transactional).** Quote PDFs to customers; materials picklists to wholesalers. Use Firebase Auth's transactional sender by default; allow the user to wire in their own SMTP (their business address: `quotes@mike-electrical.co.uk`) so the customer doesn't see a generic `noreply@`. - **Payments — Not needed for v1.** Free for personal use; a future "team" tier (≥3 trades sharing one rate-card) could charge per seat. Payments via Stripe Connect if added; never store card details in the app. - **External APIs:** Gemini API for all intelligence; optional integration with the user's existing accounting tool (Xero, QuickBooks, FreshBooks, Wave) via OAuth — but the v1 ships without this; the v1 emails PDFs. **Environment variables:** every secret (Gemini API key, Firebase service-account JSON, Stripe key if payments added, SMTP credentials if BYO sender added) 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 · customer records 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) · video walk-throughs of customer interiors are particularly sensitive (you can see inside someone's home) and are stored only as long as the sparky needs them. **Read this first — prompt-craft rules that apply to every call in this template:** 1. **Name the model variant explicitly** in every Gemini API call. Do not let the agent pick the model. See the per-call matrix below. 2. **Pin `thinkingLevel` explicitly** per call. See the matrix. 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 walk-through (video + audio) → room-level `RoomCapture` | `gemini-3.5-flash` | medium | (none) | | Assemble rooms into final `Quote` priced against rate card | `gemini-3.5-flash` | medium | (none) | | Generate customer-facing cover note (target language) | `gemini-3.5-flash` | low | (none) | | Generate materials picklist (wholesaler-facing) | `gemini-3.5-flash` | low | (none) | | Materials-price sanity check (only when rate card missing) | `gemini-3.5-flash` | low | `google_search` grounding (no `responseSchema` on this call — see note) | | Re-quote conflict resolution (previous quote vs new walk-through) | `gemini-3.5-flash` | high | (none) — long-context over prior quotes | | Generate TTS read-back of draft quote | `gemini-3.1-flash-tts-preview` | n/a | n/a | *Note for builders:* on TTS and image-generation calls, omit `thinkingConfig` entirely — the field is not supported on those models. The `n/a` cells in this matrix are documentation only; do not serialise them into the request body. ### Primary structured-output schema (seed this verbatim in the prompt) ```typescript import { z } from "zod"; const FittingObserved = z.object({ category: z.enum([ "consumer_unit", "socket_single", "socket_double", "socket_usb", "light_pendant", "light_downlight", "light_panel", "light_strip", "switch_single", "switch_double", "switch_dimmer", "switch_pir", "fan_extractor", "fan_ceiling", "isolator", "junction_box", "smoke_alarm", "co_alarm", "ev_charger", "immersion_heater", "shower", "radiator", "towel_rail", "boiler_combi", "boiler_system", "tap_basin", "tap_kitchen", "wc_close_coupled", "wc_concealed", "tile_wall_sqm", "tile_floor_sqm", "roof_tile", "gutter_run_m", "flashing_m", "decorating_room_emulsion", "decorating_room_satinwood", "other", ]), description_verbatim: z.string(), // "Wylex Amendment 2 18-module dual RCD" quantity_observed: z.number().min(0), visible_age_estimate: z.string().nullable(), // "1970s rewireable, asbestos-lagged backbox" condition_note: z.string().nullable(), // "cloth-insulated tails, no main earth visible" customer_action_requested: z.string().nullable(), // "wants doubles where currently single" voice_quote_verbatim: z.string().nullable(), // exact sparky sentence }); const LabourLine = z.object({ description: z.string(), // "first fix kitchen — 4× new double socket back-boxes, chase + plaster make-good" estimated_hours: z.number().min(0), estimated_days: z.number().min(0), // computed at hours / 8 day_rate_applied: z.number().min(0), // from rate card hourly_rate_applied: z.number().min(0), // from rate card subtotal: z.number().min(0), rate_source: z.enum(["rate_card", "guess_from_model", "manual_override"]), guess_flag: z.boolean(), guess_reason: z.string().nullable(), }); const MaterialLine = z.object({ description: z.string(), // "Wylex NM618 18-module dual RCD consumer unit" manufacturer_part_number: z.string().nullable(), // "NM618" if known quantity: z.number().min(0), unit_price: z.number().min(0), markup_pct: z.number().min(0).max(100), // sparky's saved markup line_total_inc_markup: z.number().min(0), preferred_wholesaler: z.string().nullable(), // "City Electrical Factors, Sparkbrook branch" price_source: z.enum([ "rate_card_user", // sparky's saved unit price "rate_card_wholesaler_feed", // optional wholesaler API "grounded_search_guess", // last resort "manual_override", ]), guess_flag: z.boolean(), guess_reason: z.string().nullable(), }); const RoomCapture = z.object({ room_label: z.string(), // "Kitchen", "Master bedroom", "Hallway" capture_started_iso: z.string(), capture_ended_iso: z.string(), video_uri: z.string().nullable(), // gs:// URI for the room's video audio_uri: z.string().nullable(), // gs:// URI for the room's voice memo (if separate) transcript_voice_memo: z.string(), // verbatim, source language transcript_voice_memo_language: z.string(), // BCP-47, "en-GB" fittings_observed: z.array(FittingObserved), customer_requests_in_room: z.array(z.string()), // "wants downlights, no pendants" hazards_noted: z.array(z.string()), // "no earth on lighting circuit" subject_to_test_circuits: z.array(z.string()), // "ring final in this kitchen" reading_confidence: z.number().min(0).max(1), flagged_for_user_review: z.array(z.object({ field_path: z.string(), reason: z.string(), })), }); const Quote = z.object({ quote_id: z.string(), quote_number_human: z.string(), // "MIKE-2026-0184" issued_date_iso: z.string(), // "2026-05-28" valid_until_iso: z.string(), // "2026-06-27", 30-day default // user (the sparky) business_name: z.string(), // "Mike Singh Electrical Ltd" business_address: z.string(), business_phone: z.string(), business_email: z.string(), tax_registration_label: z.string(), // "VAT", "GST", "NTN", "RFC" tax_registration_number: z.string().nullable(), trade: z.enum([ "electrician", "plumber", "gas_engineer", "heating_engineer", "roofer", "builder", "carpenter", "decorator", "general", ]), // customer customer_name: z.string(), customer_address: z.string(), customer_phone: z.string().nullable(), customer_email: z.string().nullable(), customer_preferred_language: z.string(), // BCP-47, "en-GB" // job job_title: z.string(), // "Full domestic rewire, 3-bed semi" job_description: z.string(), // 2-3 sentences for the cover note rooms: z.array(RoomCapture), // priced output materials: z.array(MaterialLine), labour: z.array(LabourLine), subtotal_materials: z.number().min(0), subtotal_labour: z.number().min(0), subtotal_before_tax: z.number().min(0), tax_label: z.string(), // "VAT 20%" tax_rate_pct: z.number().min(0).max(100), tax_amount: z.number().min(0), total_inc_tax: z.number().min(0), currency: z.string(), // ISO 4217, "GBP" // honesty guess_flag_count: z.number().min(0), any_subject_to_test: z.boolean(), capture_confidence: z.number().min(0).max(1), flagged_for_user_review: z.array(z.object({ field_path: z.string(), reason: z.string(), })), // payment terms payment_terms: z.string(), // "50% deposit on acceptance, balance on completion" bank_details_blob: z.string().nullable(), // sparky's saved sort code + account number }); type Quote = z.infer; ``` ### Common failure modes (and how to avoid them) - Agent silently downgrades `thinkingLevel` on the the walk-through parse call to save quota — pin `gemini-3.5-flash` with the matrix-specified `thinkingLevel` explicitly. Flash misses fitting counts, confuses single and double sockets, and conflates the sparky's voice memo with the customer's interjections. - Model invents a price when the rate card has no rate for the fitting — pin the negative constraint: `price_source` must be `rate_card_user` OR `guess_flag: true`, never silently `guess_from_model` without the flag. Add a unit test that a deliberately-missing item (e.g. "smart doorbell, brand X") comes back with `guess_flag: true`. - Sparky's trade slang dropped or "corrected" — "Wylex board with old rewireables" comes back as "fuse box, 1970s". Hard rule: preserve the verbatim trade slang in `description_verbatim` and in `voice_quote_verbatim`; the cleaned label goes in `category`. - Customer's name spelled wrong because the sparky said it out loud once in the video — pin the customer name from the calendar appointment record OR from the explicit "this quote is for…" field, never from the voice memo's audio recognition alone. - Labour estimates wildly off because the model assumed a clean install — surface the hazards (cloth insulation, no earth) into the labour line's `description` so the day-count reflects the make-good work. - Customer-facing cover note translated into the WRONG language — pin the target language from the customer's `customer_preferred_language` field, not from the language of the walk-through (the sparky speaks Polish to himself but writes English to the customer). - Quote total in the wrong currency — pin currency from the user's `rate_card.currency`, not from the model's inference of "the country in the video". A sparky in Birmingham quoting a French landlord's UK rental is in £, not €. - Tax line doubled or missed — the model adds VAT inside line items AND adds another VAT line at the bottom. Hard rule: line items are ex-VAT; the tax line is computed server-side from the materials + labour subtotal. - Materials picklist sent to the customer instead of the wholesaler — keep the two documents structurally separate; the email-send screen names them explicitly. - Voice memo containing the customer's interjections (the customer talking over the sparky) treated as the sparky's own observations — system instruction tells the model that the sparky's voice is the dominant track; any second voice is the customer and only their direct requests are captured in `customer_requests_in_room`. - TTS reads "MR16 downlights" as "M R sixteen down lights" — fine and expected; sparkies know what their kit sounds like and this is not a podcast. ### Negative constraints (hard rules) - Do NOT invent prices. If the rate card has no rate for a line, set `price_source` accordingly and `guess_flag: true`, and prompt the user before the quote is sent. - Do NOT translate brand names or part numbers. "Wylex", "Hager", "British General", "Vaillant", "Worcester Bosch", "Grohe", "Geberit", "Ariston", "Marley" stay verbatim in every language. - Do NOT translate proper nouns in the customer's name or address. - Do NOT modify the rate card or save new prices to it unless the user explicitly taps "add this to my rate card" on a confirmed line. - Do NOT auto-send. Every quote requires a human tap on "send" after a review screen that surfaces guess flags. - Do NOT auto-claim a fitting is unsafe. The model can note `condition_note: "cloth-insulated tails, no main earth visible"` based on what it saw; it must not write "this installation is dangerous" in customer-facing copy. The sparky decides the wording. - Do NOT use the sparky's walk-through video to train or fine-tune any model. Use the Gemini API on the paid tier, where Google does not use your content for model training, per the Gemini API Additional Terms. The capabilities-info panel says this in plain English. - Do NOT auto-publish quotes to any public surface. Quotes are private between the sparky and the customer. - Do NOT extrapolate to scope the customer didn't ask about. If the sparky walks past a dodgy boiler in the airing cupboard and says nothing, the boiler does NOT enter the quote. The app captures what the sparky drew attention to, not what the model thinks "should" be done. - Do NOT pad the quote with markup beyond the sparky's saved `markup_pct`. The mark-up is a stored rule, not a model decision. ### 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 walk-through (video + audio) → room-level `RoomCapture` Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none) ``` You are reading a walk-through of a domestic property captured by a self-employed tradesperson — typically an electrician, but possibly a plumber, heating engineer, gas-safe engineer, roofer, builder, decorator, or carpenter. The user has tapped a record button on their phone and walked the property, pointing the camera at fittings and talking out loud about what they see and what the customer has asked for. They speak the trade's existing vocabulary, in whatever language they think in. They do NOT slow down for the camera; this is the same walk-through they would do alone in their head. Languages encountered include English (British, Irish, American, Australian, South African, Nigerian, Indian, Filipino), Spanish (Castilian and Latin American variants), Portuguese (Brazilian and European), Polish, Romanian, Hungarian, Czech, Russian, Ukrainian, Tagalog (Filipino, often with English code-switching), Vietnamese (chữ Quốc ngữ), Mandarin and Cantonese (simplified and traditional), Korean, Japanese, Tamil, Hindi, Urdu, Bengali, Punjabi (Gurmukhi and Shahmukhi), Sinhala, Amharic, Swahili, Yoruba, Igbo, Hausa, Arabic (Egyptian, Gulf, Levantine, Maghrebi), Farsi, Turkish, Greek, Khmer, and Malay. Code-switching mid-sentence is normal and expected: a Polish sparky in London will say a sentence in Polish then switch to English for "RCBO" and "consumer unit"; a Filipino contractor in Manila will switch to English for "MCB" and back to Tagalog. Your task: parse THIS room's capture into a RoomCapture object. Do NOT price anything yet. Pricing happens in a separate call against the user's rate card. Hard rules: - Identify every fitting visible on camera. Count them. Do not invent fittings the user did not point the camera at and did not mention in the voice memo. - Preserve trade slang verbatim in description_verbatim. "Wylex board with rewireables" stays exactly that; the cleaned category goes in `category`. - Preserve the user's exact spoken sentence in voice_quote_verbatim for any fitting the user spoke about by name. If the model is uncertain which fitting a sentence referred to, leave voice_quote_verbatim null and lower reading_confidence. - Capture the customer's requested action separately from the current state. "Currently single sockets; customer wants doubles" is two facts: the current state (`quantity_observed: 4` of `socket_single`) AND the customer's request (`customer_action_requested: "upgrade to double"`). - Note visible hazards FACTUALLY. "Cloth-insulated tails visible under the consumer unit"; "no main earth observed at the tails"; "asbestos-lagged backbox suspected behind socket". Do NOT editorialise. Do NOT write "this is dangerous"; let the sparky decide the customer-facing language. - Distinguish the sparky's voice from the customer's voice in the audio. The sparky is the dominant track. The customer's voice appears as interjections; capture their direct requests in `customer_requests_in_room` and ignore their other commentary. - If a circuit cannot be confirmed without a meter, name it in `subject_to_test_circuits` so the final quote can render the "subject to test" footnote. - Use the trade's correct units. A run of cable is in metres. A tile area is in square metres. A pipe run is in metres. Time is in hours and days. Currency is NOT in this output (pricing is a separate call). - reading_confidence is 0.95+ for a clean walk-through in good light with a clear voice memo. Drop below 0.8 when the camera is poorly lit, the audio is muffled, or the sparky talked over themselves. - flagged_for_user_review names any specific field where confidence is below 0.7 with a one-sentence reason. No commentary. JSON only. ``` --- ### Call: Assemble rooms into final `Quote` priced against rate card Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none) ``` You receive: an array of RoomCapture objects (this walk-through), the user's saved rate card (per-item unit prices, per-day labour rates, mark-up rules, currency, tax regime, preferred wholesalers), and the customer record from the calendar appointment. Your task: assemble the final Quote object. Price every line. Hard rules: - Price every material line against the user's rate card. If the user has a saved unit price for "Wylex NM618 consumer unit", use it; set `price_source: "rate_card_user"`; set `guess_flag: false`. - If the user's rate card has NO entry for a line, set `price_source: "grounded_search_guess"` ONLY if the materials- price grounded call has been run for this item; otherwise set `price_source: "manual_override"` with `unit_price: 0` and `guess_flag: true` so the sparky knows to fill it in. - Apply the user's saved markup_pct to materials. Do NOT add a silent markup the user did not configure. - Group labour by room. One LabourLine per room minimum; multiple lines per room if the work splits cleanly ("first fix" vs "second fix" vs "test and certify"). - Day rate vs hourly rate: if the user's rate card lists a day rate, prefer it for any job ≥ 4 hours. Below 4 hours, use the hourly rate. Render both day_rate_applied and hourly_rate_applied even if only one is the source; set the unused one to 0. - Render hazards into labour line descriptions. "First fix kitchen — INCLUDES safe disposal of asbestos-lagged backboxes (4×) by licensed contractor" if the sparky noted asbestos. - Subject-to-test: if any circuit was named subject_to_test in any RoomCapture, set `any_subject_to_test: true`; the PDF will render the footnote. - Tax: compute the tax line server-side from `subtotal_before_tax` × `tax_rate_pct / 100`. Do NOT include tax in any individual line. - Currency: pin to the rate card's currency. Do NOT convert. Do NOT assume the customer's country sets the currency. - Customer-preferred-language: pin the value from the customer record; the cover-note generation call (separate) uses this. - Quote number: human-readable, format `{business_short}-{year}- {4-digit sequence}`. If the user's last quote was `MIKE-2026-0183`, this one is `MIKE-2026-0184`. - Issued date: today, ISO 8601. Valid-until: today + 30 days unless the rate card specifies otherwise. - flagged_for_user_review surfaces any line where guess_flag is true OR where the day-count seems to disagree with the hazards noted in the room captures (e.g. asbestos noted, but no disposal allowance in the labour line). No commentary. JSON only. ``` --- ### Call: Generate customer-facing cover note (target language) Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none) ``` You write a short cover note in the customer's preferred language to accompany a quote PDF. The cover note is one short paragraph (40-80 words) attached to the email body or to the PDF's opening page. The voice is professional, friendly, and unmistakably from a one-person tradesperson — not from a marketing department. Hard rules: - The customer's preferred language is pinned in `customer_preferred_language` (BCP-47). Write in that language. If the customer's language is the SAME as the sparky's, the note is still short. - Reference the customer by their first name only ("Hi Sarah —"), with the appropriate culturally-correct greeting form (Mr/Mrs/ surname for some Asian and African contexts, first-name in British/Irish/Australian/American/South African contexts). - Reference the job title verbatim from `job_title`. Do not paraphrase. - State the total inc tax in the rate card's currency, with the ISO code. "Total: £4,820 inc VAT." - State validity: "Valid for 30 days." in the appropriate translation. - Reference the deposit / payment terms from the user's saved payment_terms. Do not invent payment terms. - Sign off in the sparky's first name only ("All the best, Mike"); do not invent a job title. - Do NOT translate brand names, part numbers, "RCBO", "MCB", "MR16", "GU10". The customer expects to see them as is. - Do NOT use marketing language. No "We pride ourselves". No "Industry-leading". No "Bespoke solution". The sparky talks like a person, not a brochure. - Do NOT promise scope outside what is in the Quote. No "we will also take a look at the boiler while we are there" unless the Quote has a line item for it. Output: a single string. No JSON. No commentary. ``` --- ### Call: Generate materials picklist (wholesaler-facing) Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none) ``` You generate a materials picklist to be emailed to the user's preferred wholesaler trade counter. The picklist is a clean, tabular plain-text body (the wholesaler counter reads emails on a small screen) listing every material line with manufacturer part numbers, quantities, and the sparky's trade account reference. Hard rules: - Output language: the language the user has configured for their trade-counter communications. This may differ from the customer's language. A Polish sparky in London emails his preferred Polish-speaking wholesaler in Polish even though the customer's PDF is in English. - Use brand names and part numbers verbatim from the Quote's materials array. Do not substitute, translate, or "improve" them. - Quantities are integer counts where applicable, decimal where the unit is metres / square metres / litres. - Do not include prices. The wholesaler's trade-counter prices may differ; the picklist is a parts request, not a price check. - Include the user's trade-account reference at the top. - Include the job reference (the human-readable quote number) so the wholesaler can label the picked materials. - One short line of plain English (or the trade-counter language) asking for any out-of-stock substitutions to be confirmed by phone before picking. Output: a single plain-text string formatted as a tidy table. No commentary, no JSON. ``` --- ### Call: Materials-price sanity check (rate-card miss) Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: search grounding ``` You receive a single material description and the user's currency and country. The user's rate card does not have a price for this item. Your task: find a typical trade-counter price for this item in the user's country, in their currency, and return it with a citation. Hard rules: - Use `google_search` grounding. Search for the manufacturer part number AND the country's trade-counter wholesaler names (City Electrical Factors, Edmundson, CEF, Rexel for UK; Home Depot Pro, Graybar for US; Ningbo / Honghai-style suppliers for China; Ace Hardware / Wilcon for the Philippines; Coscharis / Slot for Nigeria; Reece, Bunnings Trade for Australia). - Return the modal trade price you find, not the cheapest, not the dearest. Round to a reasonable trade-counter number. - Output the response as JSON in the text body (NOT via `responseSchema` — `responseSchema` and `google_search` cannot be combined in the same Gemini call today). Server-side: parse the JSON, then read citation URLs from the response's `groundingMetadata.groundingChunks[].web.uri` — do NOT ask the model to include URLs in the JSON body; it will hallucinate them. - If the prices found vary by more than 30%, set `confidence: low` and surface multiple candidates. - If you cannot find a credible trade price, set `unit_price_guess: null` and `confidence: too_low_to_quote`. - This is a GUESS for the sparky to confirm. It is NEVER silently inserted into the customer-facing quote. Output JSON shape (in text body): { "item_description": "...", "unit_price_guess": 86.40, "currency": "GBP", "confidence": "high" | "medium" | "low" | "too_low_to_quote", "notes": "..." } No commentary outside the JSON. ``` --- ### Call: Re-quote conflict resolution (previous vs new) Model: `gemini-3.5-flash` · thinkingLevel: high · Tools: (none) — long-context ``` You receive: the customer's previous Quote object (or several) AND this walk-through's array of RoomCapture objects. Your task: produce a structured diff naming what has changed. Hard rules: - For each room, name fittings that are NEW since last quote, CHANGED in count, REMOVED from this walk-through, or CONFIRMED unchanged. - For each labour line, name whether the day-count changed and why. ("Master bedroom: now 1.5 days, was 1 day; reason: customer added two USB sockets and one PIR detector.") - For each material price: if the rate card has changed since the last quote, flag the delta with `price_delta_pct` and the date the rate-card line was updated. - Do NOT silently update the rate card. Surface the deltas to the user for review. - If the customer's preferred language has changed (rare but possible — landlord changed property manager), flag it. - If the previous quote was accepted and partially completed, the user will tell you which lines are now "done" (out of scope of this re-quote); honour that. Output: a Quote-shaped JSON object with an additional `diff_notes` array describing each meaningful change in one sentence each. No commentary outside the JSON. ``` --- ### Call: Generate TTS read-back of draft quote Model: `gemini-3.1-flash-tts-preview` · n/a · n/a ``` Voice: warm, brisk, unhurried. Pick the Gemini 2.5 Flash TTS voice whose `languageCode` matches the user's preferred read-back language (which may be the user's language, NOT the customer's) — pronunciation will follow that locale automatically. Prefer the gender the user has selected in settings; fall back to whichever is available rather than blocking. Pre-process the text before sending it to TTS: - Read from the Quote object: business name, customer name, job title, then each line item (description, quantity, line total, guess-flag note when applicable), then subtotals, tax line, total inc tax, payment terms. - At each line break, insert a single ellipsis (`…`) so the TTS model produces a natural pause. At paragraph breaks, insert a blank line plus an em-dash (`—`). Gemini 2.5 TTS does not support SSML `` — these textual cues are how you signal pace. - Spell out part numbers letter-by-letter where they are short ("N M six one eight" for NM618). Read long codes naturally; sparkies recognise their kit. - Insert a clear "GUESS" prefix before any line where guess_flag is true, so the sparky catches it while driving. - Skip the customer's address (the sparky knows where they were) and skip the bank details (private; the sparky doesn't need them aloud). - Target rate: ~130 words per minute — driving pace, not podcast pace. Style direction: prepend ONE short directive sentence to the text input, exactly like: "Read this quote draft to the user as they drive between jobs — brisk, clear, with a slight pause between rooms and a clear GUESS prefix on any uncertain line. …" 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 for part numbers are NOT exposed by Gemini 2.5 TTS — no SSML `` tag. Pronunciation comes from the chosen voice's native locale. ``` ## 5. Use cases & content to include Build dedicated UI sections or flows for each of these — they tell you what content the app must support. - **The Saturday rewire in Lagos.** A self-employed electrician in Surulere quotes a domestic rewire for a 4-bedroom flat. He walks the property in 22 minutes, in English with occasional Yoruba code-switching for fittings ("orí àárín" for the ceiling rose). The rate card is in ₦ NGN; the materials picklist names Slot and Coscharis trade-counter prices. The customer gets a one-page PDF in English with a total in naira and a 50% deposit term. - **The HMO rewire in Birmingham.** A second-generation Punjabi-British sparky quotes a landlord's HMO. He works through six bedsits in 40 minutes, switching between English (for the customer in the doorway) and Punjabi (to himself, for the trade slang). The quote PDF goes to the landlord in English, in £ with 20% VAT. The materials picklist goes to his preferred Punjabi-speaking wholesaler in Sparkbrook, in Punjabi. - **The Manila aircon job.** A Filipino electrical contractor quotes a 2.5HP aircon installation in a Quezon City condo. He walks the room in Tagalog with English code-switching for "MCB" and "RCD". The quote PDF goes to the unit owner in English, in ₱ with 12% VAT. The job is small enough that he prices it as an hourly rate, not a day rate, and the app prefers the hourly figure from his rate card. - **The 1930s council flat in Glasgow.** A sparky walks a tenement flat with cloth-insulated wiring and no main earth on the lighting. He notes the hazards factually in his voice memo ("no earth on the lights, cloth tails behind the board, asbestos-lagged backboxes likely behind sockets"). The app captures these as hazards, includes asbestos-disposal allowance in the labour lines, marks every circuit subject-to-test, and adds the "subject to test" footnote to the PDF. - **The rural longhouse in Donegal.** Three separate consumer units, two of them unlicensed. The sparky photographs each consumer unit, names what is wrong in plain Irish-English ("this one is a Schneider, no problem; this one I have never seen and it is wired backwards"). The quote separates the work by consumer unit and surfaces the "I would need to test before I can confirm" notes prominently. - **The Christchurch DIY death-trap.** A sparky in suburban Christchurch quotes the rectification of a previous owner's kitchen DIY. He walks the kitchen, photographs the buried junction box behind the fridge, and dictates his findings. The app produces a quote and a separate hazard summary the customer can show to her insurer. - **The plumber in Cairo.** A plumber quotes a bathroom retrofit in Heliopolis. He walks the bathroom in Egyptian Arabic, code-switching to English for "Grohe" and "Geberit". The rate card is in EGP. The quote PDF is in Arabic; the materials picklist is in Arabic to his preferred Cairo wholesaler. The right-to-left rendering on the PDF works correctly. - **The wedding photographer who is also a sole trader.** A photographer in Cebu uses Van Quote to send wedding-package quotes to brides who message her on Instagram. The "trade" is set to "general"; her rate card is "1× full-day coverage, 1× pre-wedding shoot, 1× album". She walks the venue in advance (or skips the walk-through entirely and uses photo + voice note mode), captures the bride's brief in 90 seconds of voice memo, and sends the quote PDF that evening. - **The re-quote two weeks later.** The customer in Birmingham has come back asking for two extra USB sockets in the master bedroom and a PIR detector in the hallway. The sparky reopens the customer record, taps "re-quote", does a 4-minute mini-walk-through covering only the changes, and the app produces a clean delta-aware quote with `diff_notes` naming exactly what shifted. - **The apprentice draft.** A senior sparky lets his second-year apprentice run the walk-through alone on a small job. The apprentice records, the app drafts. The senior sparky reviews the draft on his phone over lunch, sees three guess flags, corrects the prices, and sends. The apprentice learns; the customer gets a quote within the day. ## 6. Page structure Build the following screens / sections in this order. Adjust copy to fit the voice, but keep the structural intent. 1. **Welcome / sign-in.** A photographed-looking shot of a tradesperson's hand opening the rear doors of a van at first light, tools visible in racking, a phone held in the other hand. One paragraph: "Van Quote turns a walk-through into an itemised quote in your customer's inbox — priced from your own rate card, in your own branding, before you wave goodbye." Single Google sign-in button; Apple sign-in next to it. Below: "Try with the sample rate card" → loads the demo electrician's setup in section 8a. 2. **First-run rate-card setup.** Three short steps: pick your trade (electrician / plumber / etc.); pick your country and currency; paste or photograph your existing price list (the app's structured-extraction call reads a photographed Excel sheet or a typed paragraph and seeds the rate card). The sparky can skip this and ride the seed defaults; the guess flags will appear more often until they fill in their real rates. 3. **Home — your jobs today.** A list of today's calendar appointments (customer name, address, time slot, job-title preview). Each row has a big "Quote this job" button. Empty state for the day off: "No appointments today. Want to quote a phantom job for practice? Try the demo." A second tab: "Recent quotes" with status pills (Drafted, Sent, Viewed, Accepted, Expired). 4. **Walk-through capture flow** (mobile-first). One big red record button at the bottom. Top-left shows the room label ("Hallway") with a tap to change. Top-right shows a small "Next room" button. The viewfinder is full-screen. While recording, a subtle waveform shows the audio. After tapping stop, an "Add another room" button or "Done — draft my quote" button. 5. **Quote draft review.** Three columns on desktop, stacked on mobile. Left: the rooms (each as a collapsible card with the fittings, hazards, customer requests, and the audio snippet that captured them). Middle: the priced quote (materials grouped by room, labour grouped by room, subtotals, tax, total). Right: the issues panel — every guess flag, every subject-to-test, every low-confidence reading, with one-tap "confirm" or "edit" buttons. Sticky header: customer name → job title → total inc tax → "Review & send". 6. **Send screen.** Two big cards side by side: "Send to customer" (shows the PDF preview, recipient email, cover note in the customer's language) and "Forward materials picklist" (shows the picklist preview, wholesaler email, trade-account reference). Each has its own "Send" button so the sparky can fire one without the other. 7. **Customer record.** Name, address, phone, email, preferred language, every quote sent to this customer in chronological order. From here the sparky can re-quote, mark a quote accepted, or add a free-text note ("Customer's husband is the decision-maker; do not chase her, chase him."). 8. **Rate card.** A clean editable table per category — materials and labour. Sparky can add a line, edit a price, change the markup percentage, set a preferred wholesaler. A "What changed?" mini-log shows the last 10 edits with timestamps. 9. **Templates.** Pre-built per-trade scaffolds the sparky can clone (e.g. "single fuse-board swap", "kitchen rewire", "bathroom retrofit", "EV charger installation"). Each template seeds the labour structure; pricing is pulled live from the user's rate card. 10. **Settings.** Business profile (name, address, VAT/GST/NTN, logo upload), payment terms, bank details, preferred TTS read-back voice + language, default currency, default tax regime, customer-language detection rule (auto from phone contact / always English / always native). 11. **Footer.** "Built for the sparkies, plumbers, and trades who go home at 8pm." 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 Van Quote." - Subhead: "A quote in the customer's inbox before they wave goodbye — in any trade, any country, any currency, priced from your own rate card." - One paragraph (≤ 60 words) explaining who this is for and what makes it different from a generic invoice app: it walks the property WITH you, it reads what you see and what you say, and it refuses to invent prices that aren't in your rate card. - Visual: a small annotated illustration of a phone in a tradesperson's hand, with the record button highlighted, a waveform across the bottom, and labels pointing at "video", "voice memo", "structured quote out". **Slide 2 — Try it now.** - One short prompt: "Try with the sample rate card". - A live demo input pre-loaded with one of the three seed walk-throughs from section 8a (default: the Birmingham HMO). - 1-2 sentences pointing at *the specific page elements* where the Gemini magic happens (the multimodal frame-by-frame parse, the rate-card lookup that returned `guess_flag: true` on the unknown brand, the auto-generated cover note in the customer's preferred language). **Slide 3 — How to remix this.** - Headline: "Make this yours." - Three short bullets: - "Swap the seed rate card in `/data/seed-rate-card/` for your own price list." - "Adjust the prompts in `/server/prompts/` to fit your trade's vocabulary and your country's tax regime." - "Wire up your Gemini API key and Firebase project via the env-var list in the capabilities panel." - Primary CTA: "Use this template" → links to AI Studio Build remix entry point. - Secondary: "Just exploring — close" (sets localStorage flag, never auto-shows again). **Accessibility:** focus trap, `Esc` closes, `role="dialog"`, `aria-modal="true"`, `aria-labelledby`, focus restored to trigger on close. Respect `prefers-reduced-motion`. **Don't:** - Don't gate content behind the modal. The page beneath must be fully usable. - Don't auto-reshow on return visits. Use `localStorage['onboarding-seen-v1']`. - Don't include unrelated CTAs (newsletter signup, social follow). Keep it about the template only. ## 6c. Capabilities info button (persistent in header) Add a persistent `(i)` icon in the top-right of the header (next to the primary nav). Click → opens a modal/panel titled **"What powers this app"**. **Panel contents (in this order):** **Gemini capabilities used (the hero list):** - **Gemini 3.5 Flash (multimodal video + audio)** — reads the sparky's walk-through video and voice memo together. Counts fittings on screen, identifies consumer-unit makes and visible cable conditions, parses the spoken commentary, and ties everything to the room it was captured in. One call per room. - **Gemini 3.5 Flash (structured output)** — every quote is a typed JSON object. Materials, labour, tax, totals, guess flags — all schema-enforced. No free-text drift. - **Gemini 3.5 Flash (multilingual)** — handles 30+ languages including English, Spanish, Polish, Tagalog, Vietnamese, Mandarin, Hindi, Urdu, Punjabi, Tamil, Arabic, Farsi, Yoruba, Swahili. Code-switching mid-sentence (English trade terms inside a Polish or Tagalog voice memo) is recognised. - **Gemini 3.5 Flash (long context)** — for re-quote flows: the previous quote AND the new walk-through fit in one call, so the diff is clean. - **Gemini 3.5 Flash + grounded search** — used ONLY when the rate card has no price for a line. Returns a typical trade-counter price with a citation, flagged as a guess for the sparky to confirm. Never silently inserted into a customer quote. - **Gemini TTS** — reads the draft quote aloud to the sparky while they drive to the next job, with a clear "GUESS" prefix on any uncertain line. - **Firebase Auth** — Google and Apple sign-in, team-member invites via magic links. - **Firestore** — stores your rate cards, customer records, and quote history; syncs across devices in real time. - **Firebase Storage** — keeps the walk-through videos (30 days, then archived) and the generated PDFs (indefinitely; they are tax records). - **Cost note** — see the detailed breakdown in 6d. A typical 8-room domestic rewire quote costs about $0.12 of Gemini API spend, end to end. - **Privacy note** — your customer records, your rate card, and your walk-through videos are private to you. This app uses the Gemini API on the paid tier, where Google does not use your content for model training, per the Gemini API Additional Terms. Walk-through videos can see inside customers' homes; we delete them after 30 days unless you pin specific ones. **Backend services this app depends on:** - Auth: see section 4b - Database: see section 4b - Storage: see section 4b - Email: see section 4b - Payments: see section 4b (not used in v1) - External APIs: see section 4b **Environment variables you'll need to configure:** - `GEMINI_API_KEY` — your Google AI Studio API key - `FIREBASE_PROJECT_ID` — your Firebase project id - `FIREBASE_SERVICE_ACCOUNT` — service-account JSON (server-side only) - `SMTP_USERNAME` / `SMTP_PASSWORD` — optional, only if you want quote PDFs to arrive from your own business email rather than the default sender - `STRIPE_SECRET_KEY` — optional, only if the future team tier is enabled **Cost + privacy notes:** - One short paragraph per cost-sensitive capability: the walk-through parse is the dominant cost; a long video with poor lighting can cost ~$0.04 for a single room because of the higher token count on video frames. Budget conservatively at $0.15-0.25 per quote until you have data. - One short paragraph on privacy: where the data lives (your Firebase project), how to delete it (Settings → "Delete this account forever" — gone in 60 seconds), what is never sent for training. Walk-through videos can see inside customers' homes; the auto-delete after 30 days is a deliberate default for that reason. **Documentation links:** - AI Studio Build docs - Gemini API multimodal, multilingual, long-context, TTS docs - Firebase Auth, Firestore, Firebase Storage docs - A short note on country-by-country tax regimes for the seed rate cards (UK VAT, EU VAT + reverse charge, US sales tax, Nigerian VAT, Philippine VAT, Indian GST) **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) - **Walk-through parse per room (Gemini 3.5 Flash, medium thinking, multimodal video + audio)** — typical 3-minute room video ≈ ~25k input tokens (mostly video frames), ~1.5k output tokens. ~$0.04/room. An 8-room domestic ≈ ~$0.30 for capture; most jobs are 3-5 rooms ≈ ~$0.15. - **Quote assembly (Gemini 3.5 Flash, medium thinking)** — long input (all RoomCaptures + rate card), ~30k input + ~3k output. ~$0.05/quote. - **Cover note (Gemini 3.5 Flash, low thinking)** — short input, ~200 output tokens. ~$0.0001/quote. - **Materials picklist (Gemini 3.5 Flash, low thinking)** — short input, ~500 output tokens. ~$0.0002/quote. - **Materials-price grounded search (Gemini 3.5 Flash, low thinking)** — runs ONLY when rate card has no price. ~$0.001/lookup. A typical sparky's first month: 20-30 lookups while the rate card fills out; near-zero by month three. - **Re-quote conflict resolution (Gemini 3.5 Flash, high thinking, long-context)** — ~$0.10/run, runs on demand only. - **TTS read-back (Gemini 2.5 Flash TTS)** — billed per output token (~$10/M output tokens), effectively ~$0.000003/character. A 2-minute read-back of a 5-room quote ≈ ~$0.01. Optional and on-demand. - **Expected per-quote cost on a normal 4-room job:** ~$0.22 end to end. **Per-month cost for a busy sparky doing 30 quotes/week:** ~$26. - **Video storage:** Firebase Storage standard tier, ~$0.026/GB/month. A 3-minute 1080p video is ~150 MB; an 8-room walk-through is ~1.2 GB. Auto-archive after 30 days drops this to ~$0.005/GB/month. ## 7. Design language - **Mood:** A tradesperson's tool. Not a SaaS product. Not a consumer app. The phone screen visible through a sweaty palm at 2pm on a hot day, the user squinting at the sun, the customer waiting in the doorway. The app must look like something a sparky would not be embarrassed to whip out in front of a customer. - **Typography:** A sturdy grotesque for app chrome and structured content (Inter, Geist, or IBM Plex Sans). A clean serif for the customer-facing PDF — never for the in-app UI; the PDF and the app are two different documents. Monospace for part numbers and quote numbers (IBM Plex Mono). - **Palette:** Hi-vis amber `#F4B400` reserved for guess flags and the record button — the two things a sparky most needs to see. Slate `#0F172A` for body text on white. Off-white `#F8FAFC` for the app background. Trade-counter green `#15803D` for "confirmed against rate card". Brick red `#B91C1C` for hazards and subject-to-test markers. Not a SaaS palette; closer to a hardware-store aisle. - **Imagery:** Photographed-looking, not rendered. A sparky's hand. A consumer unit with its faceplate off. A van's rear doors at first light. Always real-trade specificity — never a generic "happy person with phone" stock shot. - **Hand-feel touches:** A subtle status dot at the top of each room card (green = confirmed, amber = guess flag, red = hazard). A barely-audible click on the record button. The "Send" button has a slight resistance: a 200ms hold before it commits, so the sparky doesn't fat-finger it in the van. - **Spacing:** consistent 4-px base. Generous tap targets — at least 48px on touch. - **Radius:** consistent token set (e.g. 6 / 12 / 20 px). Quote cards use 12; the record button is a perfect circle; settings panels use 6. - **Shadows:** subtle, layered. Avoid heavy drop-shadows; the sparky is reading this in bright daylight. - **Motion:** purposeful — entrance fades, button-press depressions, room-card collapses. Respect `prefers-reduced-motion`. The record button's pulsing waveform is the one place where motion carries meaning; respect reduced-motion by showing a static "REC" indicator instead. - **States:** every interactive element has hover, focus, active, and disabled. Loading uses skeletons matching the eventual layout, not spinners. Empty states have helpful next-action guidance ("Tap a job to start a walk-through" / "Add your first rate-card line"). ## 8. Content generation rules - Write **realistic, specific copy**. NO Lorem Ipsum. NO generic placeholders like 'Your tagline here'. - Invent plausible names, addresses, fittings, brand names, part numbers that fit the trade and the country (use the seed content in section 8a). When inventing, lean on real-world trade vocabularies — UK CEF-style merchant patterns, Nigerian Slot/Coscharis patterns, Filipino Wilcon/Ace Hardware patterns — but never claim a fictional customer is a real person. - Tone: warm, direct, free of corporate language. This template is for a person who does manual work, not for a CRM buyer. - Headlines: punchy and concrete. No 'Empower your trade' filler. No 'Revolutionize your quoting'. No 'Seamless'. - Body copy: short paragraphs (2-4 sentences). Use lists where appropriate. - Plain language. Avoid jargon — except where the user already speaks the jargon (the sparky wants to see "RCBO" and "MR16" in their rate card, not "residual-current circuit breaker with overload protection"). - 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 (guess flags as amber dots; subject-to-test as a small footnote indicator). ## 8a. Seed content (use these specific examples) Anchor every generated copy + sample data point in the concrete content below. Use these names, numbers, dates, currencies, and snippets verbatim where helpful, or generate close variants that sit in the same world. **Sample users (sidebar):** - "Mike Singh Electrical Ltd" (Birmingham, UK; £ GBP with 20% VAT; trade: electrician; preferred wholesaler: City Electrical Factors, Sparkbrook branch). Rate card seeded with 64 common line items. - "Adebayo Electrical Services" (Lagos, Nigeria; ₦ NGN with 7.5% VAT; trade: electrician; preferred wholesaler: Slot Systems Ltd, Computer Village branch). Rate card seeded with 48 common line items priced in naira. - "Marivic Aircon & Electrical" (Quezon City, Philippines; ₱ PHP with 12% VAT; trade: general; preferred wholesaler: Wilcon Depot, Commonwealth branch). Rate card seeded with 38 common line items. - "O'Shea Plumbing" (Galway, Ireland; € EUR with 13.5% VAT; trade: plumber; preferred wholesaler: Heatmerchants Galway). Rate card seeded with 52 common line items. **Sample walk-through in detail view (this is what the demo should show):** - **Sparky:** Mike Singh Electrical Ltd - **Customer (verbatim):** "Sarah Patel, 47 Coventry Road, Birmingham B10 0RX" - **Customer preferred language:** en-GB - **Job title:** "Full domestic rewire, 3-bed Victorian terrace" - **Quote number:** "MIKE-2026-0184" - **Issued date ISO:** "2026-05-28" - **Valid until ISO:** "2026-06-27" - **Currency:** GBP - **Tax label:** "VAT 20%" - **Rooms captured (5):** - "Hallway / consumer unit" (3 minutes, transcript in English with one Punjabi aside): one Wylex NB rewireable consumer unit observed (1970s, asbestos-lagged backbox suspected), customer wants a modern Amendment 2 board; subject-to-test on the existing tails. - "Kitchen" (4 minutes, transcript in English): 4× single sockets, customer wants doubles; 1× single pendant, customer wants 6× MR16 downlights; no extractor, customer wants one. Hazard noted: visible cloth-insulated cable running across the kitchen ceiling. - "Lounge" (3 minutes): 4× single sockets, customer wants doubles; 1× pendant, no change; 1× TV aerial point, no change. - "Master bedroom" (3 minutes): 3× single sockets, customer wants doubles + 2× USB sockets bedside; 1× pendant, customer wants a dimmable LED panel; customer also asked for a smart thermostat — Mike flagged this as "out of scope, recommend specialist" in his voice memo. - "Bathroom" (2 minutes): zone-1 environment, no extractor currently, customer wants an IP-rated extractor + a shaver socket; existing lighting is one ceiling pendant in a non-IP fitting (hazard, called out). - **Materials (8 sample lines):** - 1× Wylex NM618 18-module dual-RCD consumer unit @ £142.00 (rate_source: rate_card_user) - 14× British General 933 double socket white moulded @ £4.20 (rate_source: rate_card_user) - 6× MR16 GU10 LED 4W warm-white downlight @ £6.80 (rate_source: rate_card_user) - 1× Vent-Axia Lo-Carbon Silent IP45 100mm bathroom extractor @ £58.00 (rate_source: rate_card_user) - 1× MK 1041WHI shaver socket @ £24.50 (rate_source: rate_card_user) - 2× British General USB-A + USB-C twin module @ £19.40 (rate_source: rate_card_user) - 1× Aurora EnFiniti 6W dimmable LED panel @ £42.00 (rate_source: grounded_search_guess, guess_flag: true, guess_reason: "not in your rate card; trade price typical at City Electrical Factors") - 1× licensed asbestos-disposal allowance @ £85.00 (rate_source: rate_card_user) - **Labour (5 sample lines, one per room):** - Hallway / consumer unit — 1 day @ £320/day = £320; includes safe removal of existing board - Kitchen first + second fix — 1.5 days @ £320/day = £480; includes chase + plaster make-good - Lounge — 0.5 day @ £320/day = £160 - Master bedroom — 1 day @ £320/day = £320; includes USB + LED panel install - Bathroom — 0.5 day @ £320/day = £160; zone-1 install + test - **Subtotal materials:** £612.20 - **Subtotal labour:** £1,440.00 - **Subtotal before tax:** £2,052.20 - **Tax (VAT 20%):** £410.44 - **Total inc tax:** £2,462.64 - **Guess flag count:** 1 (the Aurora panel) - **Any subject to test:** true (the existing tails noted in the hallway) - **Payment terms:** "50% deposit on acceptance, balance on practical completion. BACS or cheque, no card." - **Capture confidence:** 0.94 **Sample customer-facing cover note (English):** "Hi Sarah — thanks for showing me around this morning. Here's the quote for the full rewire as we discussed. Total comes to £2,462.64 inc VAT. There's one item (the dimmable LED panel) I've flagged with my own price guess because I haven't sourced that exact model before; happy to confirm before we order. Valid for 30 days. 50% deposit on acceptance, balance on completion. Any questions, give me a ring. All the best, Mike" **Sample materials picklist (to City Electrical Factors, Sparkbrook):** ``` Trade account: MIKE-EL-4421 Job reference: MIKE-2026-0184 1 × Wylex NM618 18-module dual-RCD consumer unit 14 × British General 933 double socket white moulded 6 × MR16 GU10 LED 4W warm-white downlight 1 × Vent-Axia Lo-Carbon Silent IP45 100mm bathroom extractor 1 × MK 1041WHI shaver socket 2 × British General USB-A + USB-C twin module 1 × Aurora EnFiniti 6W dimmable LED panel (confirm stock please) Please call 0121-xxx-xxxx with any out-of-stock substitutions before picking. Pickup tomorrow AM if all available. Cheers, Mike ``` **Sample input artefacts (for the build to demonstrate):** - A 3-minute walk-through video of a kitchen with the sparky talking through socket counts and pointing the camera at a consumer unit. - A 90-second photo + voice-note capture of a bathroom retrofit where the sparky did not have time for a full walk-through. - A photographed paper rate-card sheet (handwritten on a yellowed page from a sparky's notebook) that the structured-extraction call seeds into the in-app rate card. - A re-quote scenario where the customer has come back asking for two extra USB sockets and a hallway PIR detector; the sparky does a 4-minute mini-walk-through covering only the changes. - A second-language scenario: a Polish sparky in London quoting a job for an English customer, where the voice memo is in Polish and the customer-facing PDF is in English. **Sample voice copy:** - Onboarding: "Walk the property, talk like you always do. We'll write the quote." - Recording: "Recording the hallway — 0:42" / "Recording the kitchen — 1:18" - Processing: "Reading the walk-through…" / "Counting the fittings…" / "Pricing from your rate card…" / "Drafting the cover note in English…" - Empty rate card: "Your rate card has 0 line items. The app will guess prices and flag every one of them until you fill it in. Want to start from the seed electrician card?" - Guess flag detected: "1 guess flag. We weren't sure about: Aurora dimmable LED panel. Confirm or set your price." - Hazard noted: "Hazard noted in the bathroom: existing ceiling fitting is not zone-1 rated. We've flagged this in the quote." - Subject-to-test detected: "Hallway consumer unit tails: subject to test. Footnote added to the PDF." - Send confirmation: "Sent to Sarah Patel at sarah.p@… Picklist sent to City Electrical Factors, Sparkbrook." - Error (couldn't read): "We couldn't make out the audio in the lounge. Want to re-record just that room, or type a quick note instead?" **Sample family / team invitation email subject + body:** - Subject: "Mike — I'm setting up Van Quote for the firm. Will you join?" - Body: "Hi Dave — I've set up Van Quote on my phone. It's been doing the quotes for me end-to-end this week. I've shared our rate card with you. Tap to join the firm's account and you can use it too. Cheers, Mike" [Open Firm Account] ## 9. Media & assets - **Hero image (landing screen):** A photographed-looking shot of a tradesperson's hand opening the rear doors of a van at first light, tools visible in racking, a phone held in the other hand. Generate via Nano Banana 2 with a prompt emphasising "tradesperson's hand, real van, real toolbox, first-light morning, condensation on the back doors, no faces, no logos, soft natural light". - **App icon / wordmark:** Set in the grotesque, on a hi-vis amber tile. Slightly worn paint texture behind it. The icon is a stylised socket outline; the wordmark is "Van Quote" in the grotesque. - **Empty-state illustration:** A simple line drawing of a phone with a record button in the middle. Hand-drawn aesthetic, not a flat icon. - **Demo walk-through stills:** Generated per the prompts in section 8a — Nano Banana 2 prompts that specifically request "kitchen interior, real consumer unit visible, no people in frame, soft afternoon window light, realistic British / Nigerian / Filipino domestic interior depending on seed user, no marketing perfection". Each demo still should look photographed, not rendered. - **PDF logo defaults:** Three sample logos for the seed users (Mike Singh Electrical Ltd, Adebayo Electrical Services, Marivic Aircon & Electrical), each set in the grotesque with a small trade-specific glyph. Never use a stock "electrical bolt" emoji. - **Stock fallbacks:** If image generation fails, fall back to the photographed sample van shot from `/public/samples/sample-van.jpg`. Never to a "🔧" emoji. - **Generated imagery:** prefer Nano Banana 2 over stock photography. Prompt for honest grit — dust, wear, daylight — and 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. ### 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: "Start a walk-through", "Add a rate-card line", "Send to wholesaler" — never a generic "Add your first item". - Smooth scroll for in-page anchors. - All AI-generated content streams in token-by-token where supported, with a clear "thinking…" indicator before content starts arriving. - If an AI call fails, show a calm, specific error ("We couldn't make out the audio in the kitchen. Want to re-record just that room, or type a quick note instead?") and offer retry. - Guess-flagged lines show an amber dot on the left; tapping reveals the model's reasoning and the alternates considered. - Subject-to-test circuits show a small red footnote-marker; the customer-facing PDF renders a matching footnote at the bottom of the page. - The record button's pulsing waveform is the one motion that carries meaning; with `prefers-reduced-motion`, replace it with a static "REC" indicator. - The "Send" button has a 200ms hold-to-confirm interaction so the sparky doesn't fat-finger it. ## 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 / assemble / re-quote and `gemini-3.5-flash` for cover note / picklist / grounded materials check. Set `thinkingLevel` explicitly per call. - **Database:** Firestore (auto-provisioned by AI Studio Build). Show the seed rate card and three sample customer records on first launch. - **Auth:** Firebase Auth — Google sign-in by default; Apple sign-in next to it; magic-link email as fallback. - **Storage:** Firebase Storage for walk-through videos, voice memos, and generated PDFs. Pre-signed URLs only. - **Mobile-first.** Verify layouts at 375 px (iPhone SE), 768 px (iPad), 1024 px, 1440 px+. The walk-through capture flow is portrait-only on phones; rotate-lock the camera screen. - Use `clamp()` for fluid typography. Prefer container queries over media queries for component-level responsiveness. - Use `dvh` / `svh` instead of `vh`. Respect safe-area insets on iOS (the record button must not collide with the home-indicator strip). - Zero horizontal overflow at any width. Zero layout shift on load. - Persist user data in Firestore. Use real-time listeners on the quote draft (the sparky can leave the screen and come back without losing state). - Optimistic UI on writes; reconcile on response. - Walk-through capture uses the Web Camera API + MediaRecorder for combined video + audio; falls back to the native camera on browsers without support. - **iOS Safari gotchas (graceful degradation):** Safari `MediaRecorder` only supports `video/mp4` (H.264 + AAC) — feature-detect and persist as MP4; an incoming call interrupts the audio session (`MediaStreamTrack.onmute` fires) — auto-pause the walk-through, save the partial clip to IndexedDB tagged "interrupted", and prompt the sparky to resume that room; backgrounded Safari tabs pause `getUserMedia` — checkpoint each chunk every 5s and use a screen Wake Lock during a walk-through so a long property capture is not silently dropped; camera permission does NOT persist across reloads on iOS — re-request on every walk-through; rotation drops the camera track — re-bind on `orientationchange`; always offer `` as the hard fallback when WebRTC is denied. - Offline-first: if the property has no signal, the camera + voice memo are captured locally to IndexedDB; the Gemini call fires the moment 4G returns. ## 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 hi-vis amber is paired with slate text on white, never with white text — verified at 4.5:1 minimum. - All images have meaningful `alt` text. The walk-through stills have `alt` describing the artefact ("photograph of a kitchen during a sparky's walk-through, consumer unit visible top-left, sparky's hand pointing at a single socket"). - Form fields have associated `