================ 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.
---
# Quote Follow-Up
## 1. Project
**Quote Follow-Up** is a personalised follow-up assistant for a
one-person fitting trade — kitchen fitters, bathroom fitters, joiners,
tilers, bespoke-furniture makers, conservatory installers. The user
sent out a stack of quotes last week, heard back from a handful, and
is now staring at the silent ones on a Sunday night wondering which
ones are still alive. The app keeps a lightweight CRM record of every
quote the user has sent — who it was for, what the job was, what the
price was, what was specifically discussed at the kitchen table, what
the customer's worry was, when the customer said they'd decide by —
and drafts a personalised follow-up email or WhatsApp message for
each silent quote in the user's own voice. The user reviews each
draft, edits anything that doesn't sound right, and sends.
This is the kind of app a Filipino bespoke-kitchen contractor in
Quezon City builds for himself on a Sunday afternoon between
typhoon-cancelled visits because he sent twelve quotes last week —
six in English to expat clients in Bonifacio Global City, six in
Taglish to families in Mandaluyong and Marikina — and only three
customers have come back. He cannot bring himself to send the same
`Hi, just following up on the quote I sent` email twelve times in a
row because the one about Mrs Aquino's worry over the typhoon-season
humidity buckling the larder doors is not the same conversation as
the one about the bespoke walnut island for the BGC expat who asked
specifically about Carrara-style quartz. It is also the kind of app
a Polish-Irish kitchen fitter in Drumcondra, Dublin builds for
himself on a Sunday night with a cup of tea — twelve detailed quotes
out, three back, nine silent, and one of the silent ones is the
couple in Sandymount whose Polish-speaking grandmother sat through
the visit and whose worry about lead time on the worktop the fitter
still remembers exactly. And it is the kind of app a
Mexican-American bespoke-cabinetry contractor in East Austin builds
for herself on a Sunday morning over coffee, because Familia Reyes
in South Lamar specifically asked her to quote in Spanish and asked
about butcher-block walnut for their daughter's quinceañera in
October, and Familia Reyes has been silent for nine days, and she
will not send Familia Reyes the same English follow-up template she
sends her Anglo clients. Same shape of moment, different language,
different customer, same trade.
The single demo that proves the magic: the contractor opens the app
on Sunday night and sees a tile labelled "9 quotes waiting for an
answer". He taps it. In under eight seconds the app produces nine
drafted follow-up messages, one per silent quote, each one
personalised to the conversation he had at the kitchen table —
referencing the customer's specific worry ("you'd mentioned you were
comparing quotes from two other fitters before deciding"), the
specific spec detail ("the bespoke walnut larder with the pull-out
spice rack"), the price ("the ₱520,000 figure I quoted you on the
18th"), and the deadline the customer set themselves ("you mentioned
wanting it done before the rainy season starts"). Each draft is in
the right language for the customer — English for the BGC expat,
Taglish for the Mandaluyong family. The user reads them, edits two,
deletes a sentence in a third, and taps Send All. Nine personalised
follow-ups are out before he's finished his cup of coffee.
And in the harder cases — the customer who went quiet because their
remortgage fell through, the customer who's juggling three quotes
and keeps asking for "just one more change", the customer who's
actually sitting on a competitor's lower number and weighing the
risk — the app keeps up. It will draft a softer follow-up for the
customer who told the user "things are a bit up in the air at the
moment" ("happy to hold the quote for another month if it helps").
It will draft a confidence-builder for the customer comparing three
fitters ("I've attached a couple of recent kitchens of mine for the
neighbourhood you're in"). It will draft an honest, no-pressure
note for the customer the user privately suspects has gone with
someone else ("if you've gone in a different direction that's
absolutely fine — just let me know so I can close the file"). Every
draft cites the original quote PDF so the user can re-attach it in
one tap, and every draft passes through the user's eyes before
anything goes out.
**Tagline:** _Nine silent quotes, nine personalised follow-ups, in the time it takes to drink a cup of tea — in any trade, any voice, any customer._
## 2. Target audience
- Kitchen, bathroom, and bedroom fitters running as a one-person business or a two-person crew
- Tilers, joiners, and bespoke-furniture makers who quote bespoke jobs at the customer's home
- Conservatory and orangery installers, garden-room builders, log-cabin makers
- Mobile mechanics, classic-car restorers, and boat-yard fitters who quote bespoke restoration jobs over multiple visits
- Wedding photographers, videographers, celebrants, and event florists juggling 12-20 simultaneous proposals at any moment
- Filipino contractor sending bespoke-kitchen quotes in Metro Manila in a mix of Tagalog and English
- Polish-Irish fitter in Dublin sending quotes in English with occasional Polish endearments to Polish-speaking customers
- Mexican-American fitter in Austin sending quotes in English to one customer and Spanish to the next within the same morning
- Independent VA, agency owner, or freelancer who has stopped chasing because chasing makes them feel like a debt collector and the unchased money is now their biggest line-item miss
## 3. Core value propositions
Surface these clearly through copy, visual emphasis, and section ordering — they are the reasons users pick this app.
- **Reads the original quote PDF and remembers everything you said** — Gemini 3.5 Flash reads the PDF the user sent the customer (the price, the spec, the line items) AND the kitchen-table notes the user dictated after the visit (the customer's worry, the deadline they set themselves, the partner's job, the dog's name). One follow-up email per quote, each grounded in the actual conversation, never a generic ping.
- **Drafts in the user's voice, not the app's voice** — the user records two or three sentences in their own voice on first launch, the app extracts a voice profile, and every follow-up sounds like the user wrote it. No corporate sign-off. No "I hope this email finds you well." If the user normally signs off "Cheers, Conor", that's how every draft signs off.
- **Function calling drives every action** — the app exposes a small set of declared functions (`get_silent_quotes`, `draft_follow_up`, `attach_original_pdf`, `schedule_send`, `mark_quote_lost`, `move_to_pipeline_stage`). The agent picks the right function for the right quote — soft tone for the wobbly customer, confidence-builder for the comparison customer, polite close-out for the silent-for-three-weeks customer. The user sees the agent's reasoning; nothing happens server-side that the user can't see and reverse.
- **CRM-lite, not CRM-heavy** — there is exactly one screen of pipeline: Quoted · Following Up · Won · Lost · On Hold. Drag a card across. That is the entire CRM. No custom fields. No pipeline stages the user has to invent. The user is a tradesperson, not a HubSpot administrator.
- **Reviews everything before anything sends** — there is no "auto-send on Tuesday at 9am" mode in v1. The hard rule is: every draft passes through the user's eyes. The big green button is "Send" and it is only ever pressed by the user. The capabilities panel says this in the same words.
- **Every draft cites the original quote** — the email body opens by referencing the price, the date, and the headline spec of the original quote ("the £14,250 figure I quoted you on the 18th for the bespoke walnut larder"). The original quote PDF is attached, or a one-tap "re-send the quote" link is included. The customer never has to dig through their inbox for the document the user is talking about.
- **WhatsApp and SMS as first-class channels** — for the customers who don't really do email. The app drafts the same personalised content into the channel the customer used during the visit ("she WhatsApped you the kitchen photo, follow up on WhatsApp"). The send happens via the user's own number through the operating-system share sheet — not via a third-party gateway.
- **Multi-language fluency** — Polish endearment for the Polish-Irish customer, a Tagalog-English code-switch for the Manila customer, Spanish for the customer who specifically requested it. The user toggles the draft language per customer; the model does the right thing with the formal/informal register without being asked.
## 4. Features to build
- Quote-PDF upload — drag-drop or share-sheet from the user's existing quote tool (Joist, Tradify, Xero, manually-made PDFs). Multi-page PDFs supported; one PDF per quote
- Post-visit voice memo capture — the user records 30-90 seconds of "what we talked about at the kitchen table" right after the visit (in the van, before driving home), the app parses it into structured notes (customer worry, deadline, partner, decision-making context)
- Auto-extraction of every line on the quote — price, line items, materials, labour, VAT, total, deposit terms — into a structured `Quote` record matched to a `Customer` record
- One-screen pipeline — Quoted · Following Up · Won · Lost · On Hold, drag-and-drop between columns, count of days-since-sent visible on every card
- "Follow up the silent ones" big-button action — one tap drafts a personalised follow-up for every quote in Quoted column that has been silent for ≥ the user's chosen interval (default 5 working days, configurable per trade)
- Per-draft tone hint — the user can override the default ("softer", "warmer", "more direct", "shorter") and the agent re-drafts in seconds without re-running the whole pipeline
- User-voice profile — record 2-3 sentences on first launch, extract sign-off style, opening style, formality, common phrases the user uses ("no worries", "absolutely", "ta")
- Multi-channel drafts — email, WhatsApp, SMS. The agent picks the channel that matches how the customer first contacted the user (configurable); the user can override
- Re-attach original PDF in one tap — the email draft includes the original quote PDF; the WhatsApp draft includes a one-tap "re-send the quote" deep link
- Customer worry surfacing — the agent always pulls the customer's specific worry from the post-visit notes and references it in the draft ("I remember you'd mentioned you were worried about the lead time on the worktop")
- Deadline-aware drafts — if the customer said "we want to be cooking in there by June", the agent references that deadline in the follow-up
- Competition-aware drafts — if the customer mentioned getting two other quotes, the agent does NOT lower the price; it draws confidence from referenced past jobs (photos in the user's library) tagged to a similar postcode or job type
- Quote-history grounding — when the same customer has come back to the user before (a returning customer asking about a second job), the agent grounds the draft in the prior job ("hope the kitchen we did last year is still serving you well")
- "Mark as Lost" with one-tap honesty draft — the user marks a quote as Lost; the agent drafts a graceful "no worries at all, do let me know if your circumstances change" close-out the user can send before closing the file
- Schedule-send (review-first) — the user can review a draft now and queue it to actually go out at 9:07am Tuesday morning, which performs better than Sunday-night sends in most trades — but the draft has been seen by the user before it leaves
- Bulk-review screen — when the user runs "follow up the silent ones", all nine drafts appear in one vertical list, each editable inline, each with its own Send button; a single "Send all reviewed" at the bottom only fires the ones the user has approved
- Reply-aware re-prompting — when a customer replies (forward the reply into the app), the agent reads the reply and updates the customer's pipeline stage + drafts the appropriate next-step
- Privacy switch — "this customer asked me not to keep their data" → the user can scrub a customer record in one tap; the original PDF stays in the user's email; the app keeps only the redacted metadata needed for pipeline analytics
## 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)
- **Function calling** (Gemini 3.5 Flash) — the heart of the app. A small declared toolbelt: `get_silent_quotes`, `draft_follow_up`, `attach_original_pdf`, `schedule_send`, `mark_quote_lost`, `move_to_pipeline_stage`, `summarise_voice_memo`, `extract_quote_lines_from_pdf`. The agent picks the right function for the right quote. The user sees every tool call in a "what the agent did" trail; nothing fires silently.
- **Multimodal PDF input** (Gemini 3.5 Flash) — reads the original quote PDF the user sent the customer, even if it was made in Joist or Tradify or by hand in Pages. Extracts line items, totals, VAT, deposit terms, the headline job description. PDFs are submitted via the Gemini Files API (`files/*` resource name) — do NOT pass Firebase Storage public URLs directly.
- **Audio input** (Gemini 3.5 Flash) — reads the user's post-visit voice memo, in the user's own accent, code-switching where the user code-switches (Tagalog-English, Polish-English, Spanish-English). Extracts the customer's worry, the deadline they set themselves, the partner's job, anything the user wants the app to remember.
- **Structured output / JSON Schema** — the `Quote`, `Customer`, `VisitNotes`, and `DraftedFollowUp` schemas below. The schema is included verbatim in the system instruction and as `responseSchema` on every structured call. Numeric min/max constraints inside `responseSchema` are documentation only; clamp server-side.
- **Multilingual generation** (built into Gemini 3.5 Flash) — drafts in English, Spanish, Polish, Tagalog, Filipino-English code-switch, Hindi-English, Tamil, Mandarin, Vietnamese, Portuguese, Arabic, French — and matches the formal/informal register the user toggles per customer.
- **Thinking levels** — `medium` for the initial "decide which follow-up tone fits this customer" call (the model has to reason across the customer worry, the days silent, the price tier, the competition context). `low` for the actual draft generation (faster, the structure is well-defined). Surface `thoughtSummary` next to each draft only when the user clicks the small "(i) why this tone" icon.
- **Long context (1M tokens)** — once the user has 12-18 months of quotes, the "what works for me" weekly summary call sees the whole history: which kinds of follow-up tones converted into Won, which converted into Lost, which the customer replied to within 24 hours. **Guardrail**: a single Quote+VisitNotes record averages ~600 tokens; an 18-month archive of 250 quotes ≈ 150k tokens (comfortable). For archives larger than 1,200 quotes, chunk by quarter before the weekly review call.
- **Gemini 3.5 Flash** — fast tonal re-draft when the user taps "softer", "warmer", "shorter". Low thinking; the structure already exists, the model is rewriting prose.
### 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 when the user logs in from a second device — e.g. the laptop at home, having signed in on the phone in the van) also requires the sender domain to be authorised in Firebase Auth. The user's customer list is private to the user; no team-sharing in v1.
- **Database — Required.** Firestore for `users`, `customers`, `quotes`, `visit_notes`, `drafted_follow_ups`, `sent_messages`, `voice_profile`, `pipeline_events`.
- **File storage — Required.** Firebase Storage for original quote PDFs and voice-memo audio. **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 PDF upload. Pre-signed URLs only; quotes are never publicly addressable. Voice memos are deletable per-memo from the UI.
- **Email — Required.** The actual send happens via the user's existing email account (Gmail, Outlook, iCloud) through Gmail/Outlook OAuth — the app composes the draft and hands it to the user's email account, so the message arrives in the user's Sent folder and threads correctly with the customer's reply. Magic-link login also uses Firebase Auth's transactional email. The app does NOT have its own SMTP — sends never originate from a `noreply@` address.
- **WhatsApp / SMS — User-side share sheet only.** v1 does not integrate the WhatsApp Business API or Twilio. Instead, the user taps "Open in WhatsApp" and the draft text is pre-filled into a WhatsApp share-sheet conversation with the customer's number. The send happens from the user's own WhatsApp account on the user's own phone. This is a deliberate choice — it keeps the conversation in the user's number, threads correctly, and avoids a separate compliance regime.
- **Payments — Not needed for v1.** The app helps the user win paid work; the app itself is free for personal use. A future "team" tier (one fitter + one office partner) could add Stripe.
- **External APIs:** Gemini API for all intelligence; Gmail / Outlook OAuth for email send-on-behalf; no Twilio, no third-party CRM integrations in v1.
**Environment variables:** every secret (Gemini API key, Firebase service-account JSON, Gmail OAuth client ID/secret, Outlook OAuth client ID/secret) 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 user's customer data is 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) · a customer who has asked to be forgotten can be scrubbed by the user in one tap, and the scrub propagates to all subsequent agent runs.
**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. **Function declarations are pinned per call.** When function calling is enabled, the declared function list is sent in `tools[]`; the model must not invoke any function not on that list. Server-side: validate the function name before executing.
7. **State negative constraints explicitly** — they are listed below. They are NOT "be careful" suggestions; they are hard rules the model must follow.
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 |
|------|-------|---------------|---------------|
| Extract quote lines from uploaded PDF → `Quote` schema | `gemini-3.5-flash` | low | (none) |
| Transcribe + parse post-visit voice memo → `VisitNotes` schema | `gemini-3.5-flash` | low | (none) |
| Decide pipeline tone for one silent quote (function-calling agent) | `gemini-3.5-flash` | medium | function calling: `get_silent_quotes`, `draft_follow_up`, `attach_original_pdf`, `schedule_send`, `mark_quote_lost`, `move_to_pipeline_stage` |
| Draft personalised follow-up text → `DraftedFollowUp` schema | `gemini-3.5-flash` | low | (none) |
| Tonal re-draft when user taps "softer" / "warmer" / "shorter" | `gemini-3.5-flash` | low | (none) |
| Extract user-voice profile from the 30s voice sample | `gemini-3.5-flash` | low | (none) |
| Read incoming customer reply → update pipeline + draft next-step | `gemini-3.5-flash` | medium | function calling: `move_to_pipeline_stage`, `draft_follow_up`, `mark_quote_lost` |
| Weekly "what's working for me" review across full history | `gemini-3.5-flash` | medium | (none) — long-context |
*Note for builders:* function-calling calls use `tools: [{ functionDeclarations: [...] }]`. The agent's tool choice is exposed in `response.candidates[0].content.parts[].functionCall` — your server executes the function, then sends the function's output back into the conversation as a `functionResponse` part, and the model continues with the result. Do NOT let the model invoke arbitrary HTTP endpoints; only the declared function names are routed.
### Primary structured-output schema (seed this verbatim in the prompt)
```typescript
import { z } from "zod";
const QuoteLine = z.object({
description: z.string(), // "Bespoke walnut larder, pull-out spice rack"
qty: z.number().nullable(),
unit_price: z.number().nullable(), // in the quote's currency, ex-tax
line_total: z.number().nullable(),
category: z.enum([
"materials", "labour", "subcontractor",
"appliances", "delivery", "vat", "discount", "other",
]),
});
const Quote = z.object({
quote_id: z.string(),
customer_id: z.string(),
pdf_uri: z.string(), // gs:// URI in Firebase Storage
quote_number: z.string().nullable(), // "Q-2026-038"
job_headline: z.string(), // "Full kitchen refit, bespoke walnut larder"
quote_date_iso: z.string(), // when the quote was sent to customer
currency: z.string(), // ISO 4217: "EUR", "USD", "GBP", "PHP"
subtotal_ex_tax: z.number().nullable(),
tax_total: z.number().nullable(),
total_inc_tax: z.number().nullable(),
deposit_terms_verbatim: z.string().nullable(), // "30% on order, 60% on delivery, 10% on completion"
lead_time_weeks_verbatim: z.string().nullable(), // "8-10 weeks from deposit"
lines: z.array(QuoteLine),
notes_to_customer_verbatim: z.string().nullable(), // the closing paragraph of the quote PDF
});
const VisitNotes = z.object({
customer_id: z.string(),
recorded_at_iso: z.string(),
audio_uri: z.string().nullable(), // gs:// URI; nullable if user typed instead
transcript_verbatim: z.string(), // verbatim from Gemini audio transcription
customer_primary_worry: z.string().nullable(), // "lead time on the worktop"
customer_deadline_verbatim: z.string().nullable(), // "wants to be cooking in there by end of June"
customer_decision_context: z.string().nullable(), // "comparing three fitters; partner makes the call"
partner_name_verbatim: z.string().nullable(),
household_context: z.string().nullable(), // "two kids under 5, dog named Bru"
channel_preference_observed: z.enum([
"email", "whatsapp", "sms", "phone_call", "unclear",
]),
user_gut_feel: z.enum([
"warm", "neutral", "cool", "wobbly", "already_committed_elsewhere",
]).nullable(),
multilingual_inserts: z.array(z.object({
language: z.string(), // BCP-47, "tl", "pl-PL", "es-MX"
phrase_verbatim: z.string(),
translation_for_user_reference: z.string(),
})),
});
const Customer = z.object({
customer_id: z.string(),
name_verbatim: z.string(), // "Mr & Mrs Lim"; "Aoife & Krzysiek"; "Familia Reyes"
partner_name_verbatim: z.string().nullable(),
address_verbatim: z.string().nullable(),
email: z.string().nullable(),
phone_e164: z.string().nullable(),
preferred_channel: z.enum(["email", "whatsapp", "sms", "phone_call"]),
preferred_language_bcp47: z.string(), // "en-IE", "es-MX", "tl-PH", "en-GB"
preferred_formality: z.enum(["formal", "neutral", "informal"]),
notes_freeform: z.string().nullable(),
do_not_contact: z.boolean(),
scrub_pii_after_iso: z.string().nullable(),
});
const DraftedFollowUp = z.object({
follow_up_id: z.string(),
quote_id: z.string(),
customer_id: z.string(),
channel: z.enum(["email", "whatsapp", "sms"]),
language_bcp47: z.string(),
tone: z.enum([
"warm-friendly", // default, most quotes
"softer-no-pressure", // customer told user "things are up in the air"
"confidence-builder", // customer comparing 2-3 fitters
"deadline-reminder", // customer set a deadline that is approaching
"polite-close-out", // 3+ weeks silent, time to close the file
"returning-customer-warmth", // user has worked for this customer before
"honest-no-pressure-final", // user's gut says they've gone elsewhere
]),
subject_line: z.string().nullable(), // null for whatsapp/sms
body_text: z.string(), // the full draft, in the customer's language
cites_original_quote: z.object({
references_price: z.boolean(),
references_quote_date: z.boolean(),
references_headline_spec: z.boolean(),
references_customer_worry: z.boolean(),
references_deadline: z.boolean(),
}),
attach_original_pdf: z.boolean(),
schedule_send_at_iso: z.string().nullable(), // null = send-now after user review
agent_reasoning_one_sentence: z.string(), // shown behind "(i) why this tone"
flagged_for_user_review: z.array(z.object({
field_path: z.string(), // "body_text paragraph 2"
reason: z.string(), // "references customer's worry — confirm wording"
})),
});
type Quote = z.infer;
type VisitNotes = z.infer;
type Customer = z.infer;
type DraftedFollowUp = z.infer;
```
### Common failure modes (and how to avoid them)
- Agent silently downgrades `thinkingLevel` on the the tone-decision call call to save quota — pin `gemini-3.5-flash` with the matrix-specified `thinkingLevel` explicitly. Flash skips the reasoning step and writes the same warm-friendly tone for every silent quote, including the one where the user's gut says they've gone elsewhere.
- Generic follow-up that doesn't cite the original quote — the cardinal failure. Every draft must reference the price, the date, OR the headline spec from the `Quote` record. Set `cites_original_quote.references_price = true` and validate server-side that the price string actually appears in `body_text`.
- The model translates the customer's first name — "Krzysiek" comes back "Chris". Hard rule: first names never translated, even when the rest of the draft is in a different language.
- Voice-memo transcription smooths over code-switches — the user said "she wants the larder to be parang ganito" mid-sentence (Tagalog "like this"); the transcription drops the Tagalog. Pin in the system instruction: "preserve code-switches verbatim; do not normalise to one language".
- The agent invents a customer detail not in `VisitNotes` — a hallucinated "your daughter's birthday is coming up". Hard rule: every personal detail in the draft must trace back to a verbatim string in `VisitNotes`. The validator checks this server-side and flags violations.
- The agent calls a function that wasn't declared — for example tries to call `send_email_directly` instead of returning a draft. Server-side: reject any function name not in the declared `tools[]` list and re-prompt with "only the declared tools are available".
- "Send all reviewed" sends the un-reviewed ones too — the UI must track per-draft "reviewed" state and the send action must filter on it. A draft that was edited but not explicitly marked reviewed still counts as un-reviewed.
- The schedule-send time is in the wrong timezone — the user's quote was sent from London but the model schedules in UTC. Always carry the user's IANA timezone in the `Customer` record and resolve `schedule_send_at_iso` against it server-side.
- The model softens the price down to win the customer — "actually, I could do it for £13,800". Hard rule: the agent never modifies the price. The price in the draft must equal the price on the original `Quote` record, exact to the unit.
- The model addresses the partner instead of the customer — confused names in `VisitNotes.partner_name_verbatim`. Pin in the system instruction: the addressee is always `Customer.name_verbatim`; the partner is named in passing if at all.
- The agent picks the wrong channel — drafts an email when the customer only WhatsApped. Use `Customer.preferred_channel` as the default; the model can suggest a different channel but must surface the override visibly.
### Negative constraints (hard rules)
- Do NOT send any message without user review. There is no auto-send mode in v1. The big green button is pressed by a human, every time. The capabilities panel says this in the same words.
- Do NOT modify the price. The price in every follow-up matches the price in the original `Quote` record, byte-for-byte. Discounts, price reductions, or "let me see what I can do" offers are user-initiated only and the user types them.
- Do NOT invent customer details. Every personal reference in the body — the partner's name, the dog's name, the deadline, the worry — must come from a verbatim string in `VisitNotes`. If `VisitNotes` is empty or doesn't mention a detail, the draft must not include it.
- Do NOT translate proper nouns. First names, surnames, place names, brand names stay verbatim. Add a parenthetical gloss only when the user has explicitly asked for one.
- Do NOT switch language without explicit signal. If `Customer.preferred_language_bcp47` is `en-IE`, the draft is in Irish-English regardless of the user's other Polish-speaking customers. The customer chose the language; respect it.
- Do NOT use generic openers ("Hope you're well!", "Hope this email finds you well", "Just touching base", "Just circling back"). These are banned strings; reject the draft server-side if any appear.
- Do NOT pressure the customer with urgency framing the customer didn't already set. The model never invents a deadline. If the customer didn't say "we want to be in by June", the draft does not say "I'd hate for you to miss the window".
- Do NOT use the user's customer data 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 call a function that is not declared in the `tools[]` list for the current call. Server-side: validate the function name and reject otherwise.
- Do NOT remember a customer who has been marked `do_not_contact`. The agent must not draft for a customer with `do_not_contact: true`; the validator rejects any draft generated for such a customer.
### 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: Extract quote lines from uploaded PDF → `Quote` schema
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You are reading a PDF that a one-person trade has already sent to a
customer as a formal quote. PDFs come from a wide range of tools:
Joist, Tradify, Xero, FreshBooks, QuickBooks, Wave, Apple Pages,
Microsoft Word exported to PDF, or hand-rolled HTML-to-PDF. Some
are beautifully laid out; some are functional. Some have a logo in
the header; some are plain. Layouts vary. Currencies vary.
The PDF is provided via the Gemini Files API (`files/*` resource name). Do NOT
pass Firebase Storage public URLs directly to `generateContent` —
the API does not fetch them server-side.
Your task: read the PDF and produce a single JSON object matching
the `Quote` schema below.
Hard rules:
- Preserve currency symbols and ISO codes exactly. Do not convert.
£14,250 stays £14,250; €18,400 stays €18,400; ₱520,000 stays
₱520,000; $14,250 stays $14,250.
- Preserve customer-facing strings verbatim — the job headline,
the deposit terms, the lead-time string, the closing paragraph
to the customer. The user will reference these in follow-ups
and they must match the customer's copy of the PDF exactly.
- Quote dates: parse the date the quote was sent to the customer.
This is usually labelled "Date", "Quote Date", "Issued", or in
the header. If multiple dates appear (e.g. "Date" and "Valid
until"), use the "Date" / "Issued" field for `quote_date_iso`.
- Line categories: assign each line to the closest enum value. A
line that says "Sub-total" or "Total ex VAT" goes into
`subtotal_ex_tax`, not into `lines[]`. A line that says "VAT
20%" goes into `tax_total`, not into `lines[]`.
- If the quote contains multiple discrete jobs (e.g. "Phase 1:
Kitchen", "Phase 2: Utility"), treat the whole document as one
Quote and put each phase as a parent line; do NOT split into
multiple `Quote` records.
- Do NOT extrapolate or fix what the user wrote. If a line says
"Bespoke walnut larder — see drawing", that is the verbatim
description. Do not expand it to "Bespoke walnut larder with
pull-out spice rack and soft-close drawers".
- If the PDF is illegible (a scan of a hand-written quote), set
every numeric field to null and put the verbatim text you DID
read into `job_headline` and `notes_to_customer_verbatim`.
The user will fill in numbers manually.
Output: a single JSON object matching the Quote schema. No
commentary.
```
---
### Call: Transcribe + parse post-visit voice memo → `VisitNotes` schema
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You are listening to a voice memo recorded by a one-person trade
(kitchen fitter, bathroom fitter, joiner, tiler, bespoke-furniture
maker) in their van immediately after a customer visit. The memo
is typically 30-90 seconds, in the user's own accent. The user is
talking to themselves — there is no listener — so the memo is
candid, sometimes fragmentary, sometimes profane, sometimes
code-switching mid-sentence between English and Tagalog, Polish,
Spanish, Hindi, Mandarin, Vietnamese, or Arabic depending on the
user.
Your task: produce a structured `VisitNotes` JSON object capturing
what the user wants the app to remember about this customer.
Hard rules:
- transcript_verbatim is the literal transcription, including
code-switches, hesitations, and the user's own voice. Do not
smooth, do not translate, do not omit profanity. If the user
says "she wants the larder parang ganito" mid-sentence, you
write it parang ganito, not "like this".
- customer_primary_worry is the single thing the customer told
the user they were worried about. If the user reported it
("she's worried about the lead time on the worktop"), that
is the worry. If the user didn't mention a worry, leave it
null.
- customer_deadline_verbatim is the customer's own language
about when they want the job done, as the user reported it.
"Wants to be cooking in there by end of June" — verbatim
from the user's memo.
- customer_decision_context is the user's read of how the
decision will be made. Are they comparing fitters? Is one
partner the decision-maker? Did they say "we'll talk it over
this weekend"?
- channel_preference_observed: pick the closest enum. If the
customer first contacted the user by WhatsApp, that is the
observed preference even if the quote was emailed.
- user_gut_feel is the user's own read, not yours. Only
populate if the user explicitly said something like "I
think they're going with someone else" or "this one feels
good to me". Otherwise null.
- multilingual_inserts: every phrase the user used in a
language other than the user's default. Include the
verbatim phrase + a one-line translation FOR THE USER'S
REFERENCE (not for the customer-facing draft).
- partner_name_verbatim: only populate if the user said the
partner's name on the memo. Do not infer ("her husband" is
not a name).
- household_context: things like "two kids under 5", "dog
named Bru", "they're expecting in October". The user's
read; verbatim if possible.
Output: a single JSON object matching the VisitNotes schema. No
commentary.
```
---
### Call: Decide pipeline tone for one silent quote (function-calling agent)
Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: function calling
```
You are the agent that handles silent quotes for a one-person
trade. You receive: one `Quote`, one `Customer`, one `VisitNotes`,
and the silence-duration in working days. Your task: decide which
pipeline action to take, then call the appropriate function. You
do not draft prose in this call — that is a separate call invoked
by `draft_follow_up`.
Declared functions:
- get_silent_quotes(silence_days_min: number): returns Quote[]
- draft_follow_up(quote_id, tone, channel, language, schedule_send_at_iso?): returns DraftedFollowUp
- attach_original_pdf(follow_up_id, attach: boolean): returns DraftedFollowUp
- schedule_send(follow_up_id, send_at_iso: string): returns DraftedFollowUp
- mark_quote_lost(quote_id, reason: string): returns Quote
- move_to_pipeline_stage(quote_id, stage: "Quoted" | "FollowingUp" | "Won" | "Lost" | "OnHold"): returns Quote
- summarise_voice_memo(visit_notes_id): returns VisitNotes
- extract_quote_lines_from_pdf(quote_pdf_uri: string): returns Quote
Decision rules:
- Default tone is "warm-friendly". Use it unless one of the
rules below overrides.
- If VisitNotes.user_gut_feel is "wobbly" or the customer told
the user "things are up in the air at the moment" (look in
transcript_verbatim and customer_decision_context), use tone
"softer-no-pressure". Do NOT use a deadline reference.
- If VisitNotes.customer_decision_context mentions comparing
fitters (2 or more, named or implied), use tone
"confidence-builder". Suggest attaching photos of one or two
recent similar jobs.
- If VisitNotes.customer_deadline_verbatim is populated AND the
deadline is within 30 days of today, use tone
"deadline-reminder".
- If the quote has been silent for ≥ 21 working days AND the
user has already sent at least one follow-up, use tone
"polite-close-out".
- If VisitNotes.user_gut_feel is "already_committed_elsewhere",
use tone "honest-no-pressure-final" and propose moving the
quote to "Lost" after sending.
- If the Customer is a returning customer (has another Quote
in stage "Won" with quote_date_iso > 6 months ago), prepend
"returning-customer-warmth" tone framing.
- The channel is Customer.preferred_channel by default. The
language is Customer.preferred_language_bcp47.
- If Customer.do_not_contact is true, do NOT call
draft_follow_up. Instead call move_to_pipeline_stage with
stage "OnHold" and surface a note for the user.
After choosing the tone, call `draft_follow_up` with the chosen
tone, channel, and language. Do NOT call any function not
declared above. Do NOT invent function names.
Surface your reasoning in `agent_reasoning_one_sentence` on the
returned DraftedFollowUp — one sentence, plain English, that the
user can read and override.
No commentary outside function calls and the structured output.
```
---
### Call: Draft personalised follow-up text → `DraftedFollowUp` schema
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You draft a single follow-up message for one silent quote. You
receive: one `Quote`, one `Customer`, one `VisitNotes`, the
chosen tone, the chosen channel, and the chosen language. You
do NOT pick the tone — that was decided by the prior call.
The user is a one-person trade who sent this quote at the
kitchen table after a real conversation with a real customer.
The user wants a follow-up that sounds like the user wrote it,
not like a CRM autoresponder. The user will read the draft
before sending. The user is not embarrassed by good prose; the
user IS embarrassed by "Hope this email finds you well".
Hard rules:
- The opening sentence must reference a specific detail from the
Quote — the price, OR the quote date, OR the job headline.
Pick whichever feels most natural for the tone.
- The body must reference at least ONE specific detail from
VisitNotes (the customer's worry, the deadline, the
household context). The body must not invent details not in
VisitNotes. If VisitNotes is sparse, keep the body short.
- The price in the draft equals Quote.total_inc_tax with the
Quote.currency symbol, formatted as in the original PDF. Do
NOT round. Do NOT discount. Do NOT add taxes.
- The customer is addressed by Customer.name_verbatim. The
partner is mentioned in passing if at all, by
VisitNotes.partner_name_verbatim.
- First names are never translated. "Krzysiek" stays
"Krzysiek" even when the rest of the draft is in English.
"María José" stays "María José" even when the draft is in
Tagalog.
- Tone realisation:
* warm-friendly: 3-5 sentences, references the price and one
VisitNotes detail, ends "no rush — just wanted to check
in" or equivalent in the target language.
* softer-no-pressure: 3-4 sentences, no deadline, no price
re-statement, ends "happy to hold the quote for another
month if that helps".
* confidence-builder: 4-6 sentences, references the price
and the customer's worry, offers to send 1-2 photos of
recent similar jobs, ends with "happy to answer any
questions" or equivalent.
* deadline-reminder: 3-4 sentences, references the price
AND the deadline verbatim ("you'd mentioned wanting to be
cooking in there by end of June"), notes the lead time
from the original Quote, ends "let me know whenever
works".
* polite-close-out: 2-3 sentences, references the quote
date, offers to close the file if the customer has
decided otherwise, ends "either way, all the best".
* returning-customer-warmth: prepend one sentence
referencing the prior job ("hope the kitchen we did last
year is still serving you well"), then follow the chosen
secondary tone.
* honest-no-pressure-final: 2-3 sentences, plainly
acknowledges they may have chosen someone else, asks for
a one-line confirmation so the user can close the file,
no follow-up planned after this one.
- Banned openers (reject the draft if any appear):
"Hope you're well", "Hope this email finds you well",
"Just touching base", "Just circling back", "I wanted to
reach out", "Per my last email", "As discussed". The user
has explicitly asked for these to never appear.
- Channel constraints:
* email: include a subject_line. Body can be 4-6 short
paragraphs. Sign-off matches the user's voice profile.
* whatsapp: subject_line is null. Body is 1-3 short
paragraphs, no formal opener, no formal sign-off.
* sms: subject_line is null. Body is ≤ 240 characters.
Treats the customer's name with informal address even
in a formal locale.
- Language: write the entire draft in the chosen
language_bcp47. Do not include parenthetical translations.
The user's customer reads one language at a time.
- Sign-off: match the user's voice profile (extracted in a
separate call). If no voice profile exists yet, use a neutral
warm sign-off in the target language ("All the best, [user
first name]") and flag for user review.
- attach_original_pdf: true by default for email; false for
WhatsApp/SMS (the user will paste a link instead).
- agent_reasoning_one_sentence: ONE sentence, plain English,
explaining why this tone was chosen for this customer.
Example: "She told you she's comparing two other fitters
and is worried about the worktop lead time, so this is a
confidence-builder that names that worry and offers photos."
- flagged_for_user_review: any sentence where the draft
references a VisitNotes detail and you want the user to
double-check the wording.
Output: a single JSON object matching the DraftedFollowUp
schema. No commentary.
```
---
### Call: Tonal re-draft when user taps "softer" / "warmer" / "shorter"
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You receive an existing DraftedFollowUp and a single user-issued
tone hint ("softer", "warmer", "shorter", "more direct", "less
formal", "more formal"). Rewrite the body_text and (if email) the
subject_line accordingly. Do NOT change the price, the quote date,
the customer name, the channel, or the language. Do NOT remove
references to the customer's worry or deadline — those were
intentional. Preserve agent_reasoning_one_sentence unchanged.
Banned openers list still applies. If "shorter" is requested,
target ≤ 60% of the original word count.
Output: an updated DraftedFollowUp JSON object. No commentary.
```
---
### Call: Extract user-voice profile from the 30s voice sample
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You receive a 20-40 second audio recording of the user speaking
naturally — typically describing themselves and their trade in
their own words. The user is being asked to "talk for thirty
seconds the way you'd talk to a customer at the kitchen table".
Extract:
- preferred_greeting_examples: 2-3 examples ("Hi Mrs Lim", "Hi
there Aoife", "Hi Familia Reyes")
- preferred_signoff_verbatim: the user's typical sign-off, in
the user's own first name and casing. "Cheers, Conor" or
"All the best, Marisol" or "Maraming salamat, Lorena".
- formality_default: one of formal | neutral | informal
- common_phrases: 3-5 phrases the user uses naturally ("no
worries", "absolutely", "give us a shout", "ta",
"ahorita", "kuya")
- language_default_bcp47: the user's default sending language
- code_switch_languages: any second language the user
comfortably switches into mid-message (e.g. "en-IE" + "pl"
for the Polish-Irish fitter)
- accent_locale: one BCP-47 string for the user's accent — used
later to bias TTS read-aloud voices if the user enables
"read my draft aloud before I send it"
Output: a single JSON object with the fields above. No
commentary.
```
---
### Call: Read incoming customer reply → update pipeline + draft next-step
Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: function calling
```
You receive the verbatim text of a customer's reply to a prior
follow-up. The user has forwarded the reply into the app. You
also receive the prior Quote, the prior VisitNotes, and the
DraftedFollowUp the customer is replying to.
Your task: classify the reply, update the pipeline stage via
move_to_pipeline_stage, and (if appropriate) call draft_follow_up
for the next-step message.
Reply classifications (you decide which best fits, in order of
priority):
- "Won" — customer accepts: words like "yes please", "let's go
ahead", "I'd like to proceed", "send me the contract", the
Tagalog "sige", the Spanish "dale", the Polish "tak,
jedziemy".
- "Lost" — customer declines: "we've gone with someone else",
"we're not proceeding", "not for us, thanks". Move to Lost
AND call draft_follow_up with tone "polite-close-out" (a
gracious thank-you note, not a save-the-deal pitch).
- "OnHold" — customer asks to pause: "things are up in the
air", "we're delaying until autumn", "we need to wait until
the remortgage clears". Move to OnHold; do NOT draft a
follow-up immediately.
- "FollowingUp / Has Question" — customer asks a question
("can you do walnut instead of oak?", "how long if we
delay the start?"). Stay in FollowingUp. Call
draft_follow_up with tone "warm-friendly" and surface the
question in agent_reasoning_one_sentence so the user
reviewing the draft knows to answer it concretely.
- "Unclear" — the reply is ambiguous. Stay in FollowingUp.
Do NOT draft anything; surface a flag asking the user to
read it themselves.
Declared functions (same as the pipeline-tone agent):
move_to_pipeline_stage, draft_follow_up, mark_quote_lost.
No commentary outside function calls.
```
---
### Call: Weekly "what's working for me" review across full history
Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none) — long-context
```
You receive every Quote + VisitNotes + DraftedFollowUp + outcome
for the past 6-18 months for a single user. Your task: produce a
short, honest, plainly-written review of what is working for this
user's follow-up strategy and what is not.
Hard rules:
- The user is a one-person trade. Do NOT write in marketing
voice. Do NOT write "leverage", "optimise", "conversion
rate". Write as a colleague would, on the back of a
receipt at the pub.
- Be specific. Not "warm-friendly tones perform best" — but
"of the 18 warm-friendly follow-ups you sent in the last
6 months, 11 turned into Won, 4 into Lost, 3 still
silent. Your average days-to-Won after a warm-friendly
follow-up is 9 days."
- Surface patterns the user might not have noticed: "every
customer who told you they were comparing 3 fitters and
who you sent a confidence-builder follow-up to with
photos attached came back. The ones who got a
warm-friendly follow-up without photos didn't."
- Surface the bad pattern honestly: "the polite-close-out
drafts you've sent on Sundays at 11pm have a 0%
response rate. The ones you sent on Tuesday mornings
had a 40% response rate."
- Do NOT extrapolate to a general theory of follow-ups.
This is YOUR review of YOUR data. n is small. Surface
small-n caveats in plain language.
- Do NOT recommend a paid feature, a third-party tool, or
an upgrade. The output is informational, not
promotional.
- If the user has fewer than 25 quotes in history, output
a short note saying so and offer to run the review again
in a month.
Output: 4-8 short paragraphs of plain text, in the user's
preferred language. No bullet lists. No headings. The user
reads this once a week with a cup of tea.
```
## 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 Sunday-night sweep.** A kitchen fitter sat down at 9pm on Sunday, having sent 12 quotes the previous week and heard back from 3. He taps "9 quotes waiting for an answer" and the app drafts 9 personalised follow-ups in 8 seconds. He reviews them at the rate of one every 30 seconds — adds a sentence to the one for the Polish-Irish couple about the Polish builder the wife had recommended, deletes a sentence in the one for the customer he's privately given up on, ticks Send on the other seven. By 9:18pm he is in bed.
- **The Filipino contractor with mixed-language customers.** A bespoke-kitchen contractor in Quezon City sends quotes in English to expat clients in Bonifacio Global City and in Tagalog-English code-switch to Manila clients. The agent picks up `Customer.preferred_language_bcp47` and writes the right register for each — formal English for the BGC client, warm Taglish for the Manila client ("sa makakaya po naming i-deliver yung kitchen by August").
- **The Polish-Irish fitter in Dublin.** Aoife & Krzysiek in Drumcondra are weighing two fitters; Krzysiek has been "comparing notes with his cousin in Poznań who is also a fitter". The post-visit memo captures both worries. The agent writes a confidence-builder follow-up in English, references the lead-time concern Krzysiek raised, offers two photos of recent kitchens in nearby D9, and signs off the way the user always signs off ("Cheers, Conor").
- **The Mexican-American customer in East Austin.** Familia Reyes specifically asked for the quote in Spanish, asked about butcher-block walnut, and mentioned their daughter's quinceañera in October as the soft deadline. The agent writes the follow-up in Spanish — addresses Sra. Reyes directly, mentions the butcher-block walnut by name, references the October deadline ("ya falta poco para octubre"), and proposes a soft schedule-send for Tuesday morning.
- **The customer who went quiet because of a remortgage.** The post-visit memo says "she mentioned the remortgage hasn't gone through, might be a few weeks". The agent picks tone "softer-no-pressure". The draft offers to hold the quote for another month without any price change. The user reads it, nods, sends.
- **The returning customer.** A customer the user fitted a kitchen for in 2024 is asking about a utility-room conversion in 2026. The agent prepends a returning-customer-warmth sentence ("hope the kitchen is still serving you well — Mrs Lim said hers still gets compliments two years on"), then follows the warm-friendly tone for the new quote.
- **The honest close-out.** A quote has been silent for 28 working days; the user has already sent one follow-up. The agent picks tone "polite-close-out". The draft asks for a one-line yes-or-no so the user can close the file, says "all the best either way", and proposes marking the quote as Lost on send. The user reads it, agrees, sends, and Lost-stages the quote.
- **The reply that's actually a yes.** A customer replies "sige, let's go ahead". The agent reads the Tagalog, classifies the reply as Won, moves the quote to Won, and drafts a warm short reply with deposit instructions extracted from the original Quote's `deposit_terms_verbatim`.
- **The weekly review.** Sunday morning over coffee, the user taps "what's working for me this month". The agent produces a 4-paragraph plain-English summary: of the 22 follow-ups sent this month, 9 turned into Won, 3 into Lost, 10 still silent; confidence-builder tones with photos had a 75% Won rate; Sunday-night sends had a 12% reply rate vs Tuesday-morning sends at 48%.
## 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 fitter's van at dusk with the back doors open and a half-tiled splashback strapped to the bulkhead. One paragraph: "Quote Follow-Up keeps every quote you've sent, and helps you follow up the silent ones — in your voice, with the original quote attached." Single Google sign-in button; Apple sign-in next to it. Below: "Try with the sample pipeline" → loads the sample 12-quote pipeline in section 8a.
2. **Voice profile capture (first launch only).** "Talk to me for thirty seconds the way you'd talk to a customer at the kitchen table." A big record button. The user records 20-40 seconds, the app extracts the voice profile, the user confirms the sign-off ("Cheers, Conor — looks right?").
3. **Empty state — "Add your first quote".** Three big input methods: 📄 Upload a quote PDF · 🎙 Record post-visit notes · ✍️ Type a quote manually. Short explainer below each.
4. **Quote intake flow.** The user drops a PDF; the model extracts the lines and shows them for confirmation. If a line was illegible, it's flagged for the user to fix. Customer details are pre-filled from the PDF if present; the user can add the customer's preferred channel and language with a two-tap selector.
5. **Post-visit voice memo flow.** Big red record button. The user talks for 30-90 seconds. The model transcribes, extracts the structured VisitNotes, and shows them in editable form. The user can edit any field, add detail, or re-record.
6. **Pipeline view (the home screen).** Five columns: Quoted · Following Up · Won · Lost · On Hold. Each card shows the customer name, the job headline, the price, the days since the quote was sent. Drag-and-drop between columns. A red dot on cards that are silent past the user's chosen interval.
7. **"Follow up the silent ones" big-button screen.** A single big card at the top of the pipeline: "9 quotes waiting for an answer." Tap → the bulk-review screen.
8. **Bulk-review screen.** A vertical list of 9 drafted follow-ups, each in its own card. Each card shows: customer name, job headline, price, tone chip (warm-friendly / softer / confidence-builder / etc.), the draft body editable inline, the subject line editable inline, a "(i) why this tone" link that reveals `agent_reasoning_one_sentence`, a "softer / warmer / shorter" trio of one-tap re-draft buttons, a "channel: email / whatsapp / sms" toggle, an "attach quote PDF" checkbox, a "schedule send" picker, a per-card Send button. At the bottom of the page: "Send all reviewed (4 of 9)" — only fires the cards explicitly marked reviewed.
9. **Quote detail view.** The original PDF on the left; the structured Quote lines in the middle; the VisitNotes on the right; the history of drafted/sent follow-ups and any customer replies at the bottom in chronological order, threaded.
10. **Customer detail view.** The customer's name, contact details, preferred channel/language/formality, all the quotes they've ever received, the full history of every message exchanged. A "scrub this customer" button with a confirmation step.
11. **Weekly review screen.** The plain-English summary from the long-context review call. A "send this to me by email every Sunday at 8am" toggle.
12. **Settings.** Email account (Gmail/Outlook OAuth status), voice profile editor, default silence-interval-before-follow-up (default 5 working days), banned-openers list (read-only), data export, data delete.
13. **Footer.** "Built for the trades where every customer is a real person." Privacy: "Your customer data is yours. We never train on it. Every message is reviewed by you before it sends." 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 Quote Follow-Up."
- Subhead: "Nine silent quotes, nine personalised follow-ups, in the time it takes to drink a cup of tea — in your voice, with the original quote attached."
- One paragraph (≤ 60 words) explaining who this is for and what makes it different from a generic CRM: it reads the original quote PDF you sent, it remembers what you and the customer talked about at the kitchen table, it drafts in your voice, and you press Send — never the app.
- Visual: a small annotated illustration of a single pipeline card with the relevant fields labelled (customer name, job headline, price, days silent, tone chip) — not a generic dashboard.
**Slide 2 — Try it now.**
- One short prompt: "Try with the sample pipeline".
- A live demo pipeline pre-loaded with 12 sample quotes — including the kitchen fitter from East Austin, the Polish-Irish couple in Drumcondra, and the Manila contractor's expat client.
- 1-2 sentences pointing at *the specific page elements* where the Gemini magic happens (the structured VisitNotes parse from the voice memo, the tone-chip on each draft, the "(i) why this tone" reasoning, the banned-opener list working in real time).
**Slide 3 — How to remix this.**
- Headline: "Make this yours."
- Three short bullets:
- "Swap the sample pipeline in `/data/seed-pipeline/` for your own quotes."
- "Adjust the prompts in `/server/prompts/` to fit your trade's tone and language(s)."
- "Wire up your Gemini API key and your Gmail/Outlook OAuth client 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 (function calling)** — the agent picks which action to take per quote: which tone to draft in, which channel to send via, whether to mark the quote as Lost. You see every function call in plain English; nothing happens silently.
- **Gemini 3.5 Flash (multimodal PDF)** — reads the quote PDFs you've already sent customers — from Joist, Tradify, Xero, or hand-rolled — and pulls out the line items, the total, the deposit terms, the lead time.
- **Gemini 3.5 Flash (audio)** — listens to your post-visit voice memo in your own accent and parses the structured notes that drive every later follow-up.
- **Gemini 3.5 Flash (multilingual)** — drafts in English, Spanish, Polish, Tagalog, Hindi, Tamil, Mandarin, Vietnamese, Portuguese, Arabic, French — and matches the formal/informal register the customer chose.
- **Gemini 3.5 Flash (long context)** — once you have a year of quotes, the weekly review call sees the whole pipeline at once to tell you what is working for you.
- **Gemini 3.5 Flash** — fast tonal re-drafts when you tap "softer" or "warmer" or "shorter" on a draft.
- **Firebase Auth** — Google and Apple sign-in.
- **Firestore** — stores your pipeline, syncs across your van and your kitchen-table laptop in real time.
- **Firebase Storage** — keeps the original quote PDFs and voice memos at upload resolution.
- **Gmail / Outlook OAuth** — the actual email send happens from your own email account, so threading and Sent-folder behaviour work normally. The app never sends from a `noreply@` address.
- **Cost note** — see the detailed breakdown in 6d. A typical week of 12 quotes plus 9 follow-up drafts costs about $0.45 of Gemini API spend.
- **Privacy note** — your customer data is yours. 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. Every message passes through your eyes before it sends — there is no auto-send in v1.
**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)
- `GMAIL_OAUTH_CLIENT_ID` and `GMAIL_OAUTH_CLIENT_SECRET` — for the Gmail send-on-behalf flow
- `OUTLOOK_OAUTH_CLIENT_ID` and `OUTLOOK_OAUTH_CLIENT_SECRET` — for the Outlook send-on-behalf flow
**Cost + privacy notes:**
- One short paragraph per cost-sensitive capability: long-context calls are billed per token of input — a 250-quote weekly review costs about $0.20 each Sunday.
- One short paragraph on privacy: where the data lives (your Firebase project), how to delete it (Settings → "Delete this customer forever" — gone in 60 seconds), what is never sent for training, and the explicit hard rule that nothing sends without your review.
**Documentation links:**
- AI Studio Build docs
- Gemini API function calling, multimodal, multilingual, long-context docs
- Firebase Auth, Firestore, Firebase Storage docs
- Gmail and Outlook OAuth send-on-behalf docs
**Accessibility:** same standards as the onboarding modal — focus trap, `Esc`, ARIA, restored focus.
**Behaviour:**
- Always available — single click from anywhere in the app.
- Tooltip on the `(i)` icon: "How this app is built".
- Mobile: opens as a full-screen sheet that slides up.
- Should be the most honest part of the app — never hand-wave service requirements; never say "AI" without naming the specific Gemini model and capability.
## 6d. Detailed cost breakdown (deployer reads this BEFORE shipping)
- **Extract quote lines from PDF (Gemini 3.5 Flash, low thinking)** — typical 2-page quote PDF ≈ 2 images, ~600 output tokens. ~$0.008/quote.
- **Transcribe + parse voice memo (Gemini 3.5 Flash, low thinking, audio input)** — typical 60-second memo. ~$0.007/memo.
- **Decide pipeline tone (Gemini 3.5 Flash, medium thinking, function calling)** — one call per silent quote. ~$0.006/quote.
- **Draft personalised follow-up (Gemini 3.5 Flash, low thinking)** — one call per drafted follow-up. ~$0.005/draft.
- **Tonal re-draft (Gemini 3.5 Flash, low thinking)** — fast and cheap. ~$0.0005/re-draft.
- **User-voice profile extraction (Gemini 3.5 Flash, low thinking, audio input)** — runs once at signup. ~$0.005 lifetime.
- **Read incoming reply + update pipeline (Gemini 3.5 Flash, medium thinking, function calling)** — one call per customer reply. ~$0.004/reply.
- **Weekly "what's working" review (Gemini 3.5 Flash, medium thinking, long-context)** — runs Sunday morning. ~$0.20 per archive of 250 quotes per run.
- **Expected per-quote cost in steady state** (one quote intake + one voice memo + two follow-up drafts over its lifetime): ~$0.030. **Typical week** (12 new quotes + 9 follow-up drafts + 1 weekly review): ~$0.45.
- **Storage:** Firebase Storage standard tier, ~$0.026/GB/month. A typical quote PDF is ~250 KB; a 60-second voice memo is ~500 KB. A year of 250 quotes + 250 memos uses ~190 MB ≈ ~$0.005/month.
## 7. Design language
- **Mood:** A trade journal kept by someone who takes pride in their work — but a paper trade journal, with a bit of dust on the spine. Not a CRM. Not a marketing tool. The kitchen-table laptop at 9pm on Sunday with a cup of tea, the kid asleep upstairs, the quotes window open, the customer's number on a Post-it.
- **Typography:** A confident humanist sans for body text (Inter, Söhne, or IBM Plex Sans). A slightly warmer serif for the draft body itself (Source Serif Pro, Charter) — so that when the user is reading the drafted follow-up to a real customer, the typography signals "this is a piece of correspondence" rather than "this is a UI element". A faint monospaced (JetBrains Mono, IBM Plex Mono) for quote IDs, dates, and currency formatting in the structured-data views.
- **Palette:** Bone background `#F8F5EF` for the pipeline view, deep ink `#1F1B16` for body text, a single accent of working-blue `#2E5B83` for primary actions (the Send button, the "Follow up the silent ones" CTA), a muted terracotta `#A55A3E` for the tone chips on softer-no-pressure and polite-close-out drafts (because those tones need a visual differentiator), a warm green `#3F6B43` only for Won states. No red for Lost — Lost gets a quiet grey `#7A7570` so the user is not punished for closing a file.
- **Imagery:** Photographed-looking, not rendered. A van at dusk; a kitchen mid-fit with the worktop on trestles; a hand holding a quote PDF and a mug of tea. The empty-state illustration is a single line drawing of a kitchen-table notebook open to a column of customer names. Never stock-photo handshakes. Never CRM-dashboard renderings.
- **Hand-feel touches:** The pipeline cards have a barely-visible paper grain. The "(i) why this tone" reveal slides in from below with a thin shadow, like turning over a small index card. The Send button has a satisfying micro-press; the "Send all reviewed" button reveals a per-card check-summary before firing.
- **Spacing:** consistent 4-px base. Generous whitespace — the user reads at speed, and reads better with air.
- **Radius:** consistent token set (e.g. 6 / 12 / 20 px). Pipeline cards use 6; the bulk-review draft cards use 12; the welcome card uses 20.
- **Shadows:** subtle, layered, warm-grey-tinted. Avoid heavy drop-shadows.
- **Motion:** purposeful — entrance fades, hover lifts, page transitions. Respect `prefers-reduced-motion`. No bouncing splash animations. No theatrical hero animations. The drag-and-drop between pipeline columns has a gentle lift-and-drop; respect reduced-motion by snapping rather than animating.
- **States:** every interactive element has hover, focus, active, and disabled. Loading uses skeletons not spinners where possible. Empty states have helpful next-action guidance ("Drop a quote PDF to start your pipeline", "Record 30 seconds of post-visit notes — your van is the perfect studio") — never a generic "Add your first item".
## 8. Content generation rules
- Write **realistic, specific copy**. NO Lorem Ipsum. NO generic placeholders like 'Your tagline here'.
- Invent plausible names, dates, customers, quote totals, and post-visit notes that fit the domain (use the seed content in section 8a as a starting point). When inventing, lean on real trade patterns — bespoke walnut larders, butcher-block worktops, induction hobs vs gas, lead-time worries, deposit terms — but never claim that a fictional quote is a real customer document.
- Tone: warm, direct, free of corporate language. This template is for a tradesperson, not a sales-ops manager.
- Headlines: punchy and concrete. No 'Empower your X' filler. No 'Revolutionize'. No 'Seamless'. No 'Conversion'.
- Body copy: short paragraphs (2-4 sentences). Use lists where appropriate.
- Plain language. Avoid jargon — except where the user already speaks the jargon (kitchen-fitters know "carcass", "plinth", "PIR-rated", and "soft-close"; tilers know "anti-fracture membrane"; the app should not dumb these down).
- 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 draft sentence shows as faintly underlined; tapping reveals the alternates the model considered).
## 8a. Seed content (use these specific examples)
Anchor every generated copy + sample data point in the concrete content below. Use these names, numbers, dates, and snippets verbatim where helpful, or generate close variants that sit in the same world.
**Sample pipeline (sidebar):**
- **Conor's Kitchens — Dublin** (the demo user; 22 quotes this quarter; trades in EUR; sends in English with occasional Polish endearments to Polish-speaking customers).
- **Lorena Estrada — East Austin Custom Cabinetry** (alt demo user; trades in USD; sends in English and Spanish; specialises in butcher-block walnut and clay-tile splashbacks).
- **Jojo Fernandez — Bespoke BGC** (alt demo user; Quezon City; trades in PHP; sends in English to expat clients in Bonifacio Global City and Taglish to Manila clients; specialises in tropical-hardwood kitchens with humidity-resistant carcasses).
**Sample silent-quote queue (12 quotes, sent last week, 9 silent):**
1. **Aoife & Krzysiek Nowak** — Drumcondra, D9, Dublin. Full kitchen refit + bespoke walnut larder with pull-out spice rack. €14,250 inc VAT. Quote sent 18 May. Post-visit memo: "Aoife's a teacher, Krzysiek works for An Post; they're comparing two other fitters; Krzysiek's cousin in Poznań is a fitter and they're checking in with him; they're worried about the lead time on the worktop because the existing kitchen is unusable; wants to be cooking in there by end of June." Channel preference: email. Language: en-IE. Tone the agent should pick: confidence-builder.
2. **Mr & Mrs Lim — Sandymount, D4** — Galley kitchen with under-counter ice-maker (Mrs Lim was very specific about this), induction hob, dark walnut finish. €18,400 inc VAT. Quote sent 19 May. Post-visit memo: "Mrs Lim worried about the ice-maker — has a brand in mind (Hoshizaki), wants to know if the cabinet is wide enough to fit it; Mr Lim mostly nods; they have a son at TUD; dog called Bru; gut feel: warm." Channel preference: email. Language: en-IE. Tone: warm-friendly.
3. **Familia Reyes — East Austin (USD demo user's pipeline)** — Open-plan kitchen with butcher-block walnut island, clay-tile splashback, induction range. $23,800 inc tax. Quote sent 17 May. Post-visit memo: "Sra. Reyes wants the kitchen ready for her daughter Camila's quinceañera in October; budget is a stretch but real; comparing with one other fitter; preferred language for all comms: Spanish; partner Don Hector defers to her on design; gut feel: warm but stretched." Channel preference: email. Language: es-MX. Tone: deadline-reminder.
4. **Bernadette Ortega — BGC, Manila (PHP demo user's pipeline)** — Compact wet-and-dry kitchen for a 65 sqm condo; humidity-resistant carcasses; built-in espresso machine. ₱520,000 inc VAT. Quote sent 16 May. Post-visit memo: "Bernadette flies between Manila and Singapore for work; wants this done while she's away in July; comparing two other fitters; partner Marc is in Singapore and only on video; wants comms in English with the formal-ish Filipino register." Channel preference: email. Language: en-PH. Tone: confidence-builder.
5. **Anita & Niranjan Patel — Smithfield, Dublin** — Two-bed apartment kitchen, IKEA carcasses with bespoke walnut doors. €8,900 inc VAT. Quote sent 20 May. Post-visit memo: "Anita is on parental leave; Niranjan works in fintech; baby due in October; soft deadline October but flexible; they specifically mentioned wanting to support a small local fitter rather than a chain." Channel preference: WhatsApp. Language: en-IE. Tone: warm-friendly.
6. **Catalina Morales — Hyde Park, Austin** — Galley kitchen refresh, no walls moving, white shaker doors, quartz worktop. $11,400 inc tax. Quote sent 15 May. Post-visit memo: "Catalina runs her own clinic in Austin; wants the kitchen done before her in-laws visit in late July; comparing three fitters; partner Diego is happy with whatever she chooses; gut feel: she's going with someone else, but be polite about it." Channel preference: email. Language: en-US. Tone: honest-no-pressure-final.
7. **Tom Hennessey — Stoneybatter, D7** — Single-bed apartment, painted MDF doors, simple layout, electric hob. €6,200 inc VAT. Quote sent 14 May. Post-visit memo: "Tom is a teacher, single dad of two boys, on a budget but real; wants to be in by August before school starts; comparing one other fitter; quite open about the budget being tight." Channel preference: WhatsApp. Language: en-IE. Tone: deadline-reminder.
8. **Familia Hernández — South Lamar, Austin** — Full kitchen refit with the wall coming down to open into the living room, butcher-block walnut, dark green doors. $31,200 inc tax. Quote sent 13 May. Post-visit memo: "Hernández family said the remortgage hasn't gone through yet; might be a few weeks; they really like the design; preferred language Spanish; partner makes the final call but they decide together; gut feel: wobbly on timing, not on the fitter choice." Channel preference: email. Language: es-MX. Tone: softer-no-pressure.
9. **Sinéad O'Brien — Phibsborough, D7** — Tiny galley kitchen, wood worktop, panelled white cupboards, vintage look. €7,400 inc VAT. Quote sent 21 May (3 days ago — not yet silent past the 5-day threshold; do NOT include in this week's follow-up batch). Channel preference: WhatsApp. Language: en-IE.
10. **Mrs Beatriz Reyes** (returning customer; not Familia Reyes above) — utility room conversion, follow-up job after a 2024 kitchen. $6,800 inc tax. Quote sent 19 May. Post-visit memo: "Returning customer; we did her kitchen in February 2024; she asked specifically about whether the same walnut would still be available; wants this done before Thanksgiving." Channel preference: email. Language: en-US. Tone: returning-customer-warmth + warm-friendly.
11. **Maciek & Karolina Wójcik — Lucan, Dublin** — Open-plan kitchen, all-Polish-build crew preference but happy to take a quote from Conor on his cousin's recommendation. €19,500 inc VAT. Quote sent 18 May. Post-visit memo: "Maciek is the decision-maker; spoke half-Polish half-English at the visit; Karolina is on maternity; wants the kitchen done before the baby turns 1; gut feel: warm but they're really weighing the all-Polish-crew option for cost reasons." Channel preference: email. Language: en-IE with a Polish endearment in the sign-off if natural. Tone: confidence-builder.
12. **Padraig Geraghty — Greystones, Wicklow** — Country kitchen, exposed beams, butler-style sink, Aga-style range cooker. €27,400 inc VAT. Quote sent 12 May (12 working days ago — silent past the user's threshold, this is the candidate for polite-close-out tone). Post-visit memo: "Padraig is retired, his daughter Aoife was at the visit and was the more engaged one; haven't heard back in nearly two weeks; gut feel: they might have gone with someone else, or it might be on hold." Channel preference: email. Language: en-IE. Tone: polite-close-out.
**Sample draft (this is what the bulk-review screen should show for #1):**
> **To:** Aoife & Krzysiek Nowak · **Channel:** Email · **Tone:** Confidence-builder · **Language:** English
>
> **Subject:** Following up on the Drumcondra kitchen quote
>
> Hi Aoife and Krzysiek,
>
> Just a quick follow-up on the €14,250 quote I sent you on the 18th for the full kitchen refit and the bespoke walnut larder.
>
> I know you'd mentioned you were comparing a couple of other fitters and that Krzysiek's cousin in Poznań is a fitter himself — totally fair, that's the right way to do it. I've attached two photos of recent kitchens I've fitted in D9 (one in Drumcondra, one in Glasnevin) in case it helps to see the larder finish and worktop edge up close.
>
> On the lead time worry — the worktop suppliers I use have been quoting 4 weeks from deposit, and I can start the strip-out in 2 weeks if you give the green light by next Friday. That puts us cooking by mid-to-late June which lines up with what you mentioned wanting.
>
> Happy to answer any questions or pop back round for a chat if you'd like to walk through it again with the photos.
>
> Cheers, Conor
>
> *(i) why this tone: Aoife & Krzysiek told me they're comparing two other fitters including Krzysiek's cousin in Poznań, and that the lead time on the worktop is their main worry. Confidence-builder with referenced past jobs and a concrete lead-time answer.*
**Sample voice copy:**
- Onboarding: "Drop the quote you already sent. Record thirty seconds about the kitchen table. We'll handle the awkward part."
- Processing: "Reading the quote PDF…" / "Parsing your post-visit notes…" / "Drafting in your voice…" / "Choosing the tone…"
- Empty pipeline: "Drop a quote PDF to start. Or skip ahead and try the sample pipeline."
- Error (couldn't read PDF): "We couldn't make out the quote in this PDF — want to try a clearer scan, or type the headline price and job in by hand?"
- Save confirmation: "Added — Aoife & Krzysiek Nowak, Drumcondra. €14,250. 5 working days since you sent it."
- Banned-opener detected: "This draft started with 'Hope this email finds you well' — that's on your banned list, so we re-drafted. Want to see the change?"
- Low confidence note: "We've flagged the sentence about the worktop lead time — double-check the wording before sending."
- Send confirmation: "Sent — to Aoife & Krzysiek. The quote PDF was attached. We'll let you know when they reply."
**Sample WhatsApp draft (this is what the bulk-review screen should show for #5):**
> **To:** Anita & Niranjan Patel · **Channel:** WhatsApp · **Tone:** Warm-friendly · **Language:** English
>
> Hi Anita and Niranjan — just a quick one to check in on the €8,900 quote I sent through on the 20th for the Smithfield kitchen.
>
> I know you'd mentioned wanting the new kitchen in before October when the wee one arrives — happy to hold the deposit window open if you're still mulling it over. Any questions, give us a shout.
>
> Cheers, Conor
**Sample Spanish draft (this is what the bulk-review screen should show for #3):**
> **Para:** Familia Reyes · **Canal:** Email · **Tono:** Recordatorio de fecha · **Idioma:** Spanish (es-MX)
>
> **Asunto:** Seguimiento sobre el quote para la cocina abierta
>
> Hola Sra. Reyes,
>
> Quería darle seguimiento al quote de $23,800 que le mandé el 17 sobre la cocina abierta con la isla de butcher-block walnut y el splashback de azulejo de barro.
>
> Sé que la idea es tener la cocina lista para la quinceañera de Camila en octubre — ya falta poco, y para alcanzar esa fecha necesitaría confirmar el depósito hacia mediados de junio para reservar el material y empezar a mediados de julio.
>
> Si hay alguna pregunta sobre el diseño, el material o las fechas, con gusto la llamo o paso de nuevo. Sin prisa.
>
> Saludos cordiales, Lorena
**Sample customer reply that becomes "Won":**
> *Hi Conor — sige, let's go ahead. Send through the deposit details whenever you can. Thanks for the photos, the Glasnevin one is exactly the worktop edge we wanted. — Krzysiek*
The agent should classify this as **Won**, move the quote to the Won column, and draft a short warm reply with the deposit terms extracted from the original Quote's `deposit_terms_verbatim`.
## 9. Media & assets
- **Hero image (landing screen):** A photographed-looking shot of a fitter's van at dusk with the back doors open, a half-tiled splashback strapped to the bulkhead, a clipboard with a curling-edge quote on the dash, a mug of tea on the wheel arch. Generate via Nano Banana 2 with a prompt emphasising "trade van, evening light, real worn tools, hand of a fitter in his thirties or forties, soft shadow under the van, no people centered in frame, no logos on the van".
- **App icon / wordmark:** Set in the humanist sans. Quietly confident. No icon — just type with a single accent dot on the period after "Follow-Up".
- **Empty-state illustration:** A simple line drawing of a kitchen-table notebook open to a column of customer names with a pencil resting across it. Hand-drawn aesthetic, not a flat icon.
- **Sample quote PDFs:** Generate three sample PDFs (one Joist-style, one Tradify-style, one hand-rolled-Pages-style) for the demo pipeline. Each in a different currency (EUR, USD, PHP). Each must have realistic line items in the trade's actual vocabulary.
- **Sample post-visit voice memos:** Record-quality sample audio for the demo pipeline — 30-90 seconds each, in the user's accent, with at least one code-switch example. For the build, these can be TTS-generated using `gemini-3.1-flash-tts-preview` with a warm voice, as a stand-in for a real fitter's recording.
- **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 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.
### 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: "Drop a quote PDF to start", "Record post-visit notes — your van is the perfect studio", "Follow up the silent ones" — 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 read this PDF — want to try a clearer scan, or type the headline price and job in by hand?") and offer retry.
- The banned-opener check runs server-side in real time as a draft is generated; if a banned phrase appears, the draft is regenerated automatically and a one-line note is shown to the user.
- Drafts stream in token-by-token in the bulk-review screen — the user sees the prose appear, which both speeds perceived response time and lets them start editing as the draft completes.
- Drag-and-drop between pipeline columns is keyboard-accessible (Space to pick up, arrow keys to move, Space to drop) and respects `prefers-reduced-motion`.
- "Send all reviewed" only fires drafts explicitly marked reviewed; un-reviewed drafts are left in place with a "needs your eyes" badge.
- The "(i) why this tone" reveal slides up from below the card with a thin shadow; `prefers-reduced-motion` shows it instantly with a fade.
- A live count of "X of Y reviewed" sits next to the Send All button so the user always knows how many drafts they've vetted.
## 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 PDF extraction, voice-memo parse, tone-decision, draft, reply-read, weekly review, voice-profile extraction; and `gemini-3.5-flash` for tonal re-draft. Set `thinkingLevel` explicitly per call.
- **Function calling:** the tone-decision and reply-read calls use Gemini function calling. Declared functions are validated server-side before execution; unknown function names are rejected.
- **Database:** Firestore (auto-provisioned by AI Studio Build). Show the sample pipeline 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 original quote PDFs and voice memos. Pre-signed URLs only.
- **Mobile-first.** Verify layouts at 375 px (iPhone SE), 768 px (iPad), 1024 px, 1440 px+.
- Use `clamp()` for fluid typography. Prefer container queries over media queries for component-level responsiveness.
- Use `dvh` / `svh` instead of `vh`. Respect safe-area insets on iOS.
- Zero horizontal overflow at any width. Zero layout shift on load.
- Persist user data in Firestore. Use real-time listeners on the pipeline view.
- Optimistic UI on writes (e.g. drag-and-drop between pipeline columns); reconcile on response.
- Voice-memo capture uses the Web Audio API with `MediaRecorder`; falls back to native voice-memo on iOS Safari where needed.
- **iOS Safari gotchas (graceful degradation):** Safari `MediaRecorder` only supports `audio/mp4` (AAC) — feature-detect and persist as AAC mono; mic permission does NOT persist across page reloads on iOS — re-request on every memo and surface a one-line "tap to allow mic" hint; an incoming call interrupts the audio session (`MediaStreamTrack.onmute` fires) — auto-pause and prompt resume; backgrounded Safari tabs pause `getUserMedia` — combine `visibilitychange` with a screen Wake Lock during memos.
## 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.
- All images have meaningful `alt` text. The sample-van hero shot has `alt` describing the artefact ("photograph of a fitter's van at dusk, back doors open, a half-tiled splashback strapped to the bulkhead").
- Form fields have associated `