# 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.
---
# Spreadsheet Whisperer
## 1. Project
**Spreadsheet Whisperer** turns a messy Google Sheet into a live
dashboard in twenty-five seconds. Paste the Sheets URL, sign in,
and the app reads the sheet directly — no OAuth dance, no API key
to wire up, no Looker Studio setup — using AI Studio Build's
post-I/O 2026 Workspace integration. Gemini 3.5 Flash infers what
the data is about, picks the three numbers that matter most,
renders two charts that suit the shape of the data, computes the
month-over-month delta, and proposes the next question to ask.
The single demo that proves the magic: a user pastes a Google
Sheets URL to her expense log — three columns, six months of
rows, dates in two different formats, amounts in mixed currencies,
"Category" written sometimes lowercase, sometimes title-case,
sometimes blank. In under thirty seconds the dashboard renders:
**three KPI cards** ("$4,213 spent in May", "Top category:
Groceries — $1,184", "Subscriptions up 27 % vs April"); **two
visualisations** (a stacked-bar of category spend per month, and
a sparkline of weekly total spend); **one month-over-month delta
callout** ("You spent $740 more in May than in April — the gap is
mostly Subscriptions and Dining"); and **one suggested next
question** ("Show me by week instead of month, only Subscriptions.").
She taps the suggestion. The dashboard rebuilds in five seconds.
Tap any number → the source cells highlight in the embedded sheet
view. The dashboard refreshes when the underlying sheet changes —
the user adds a row in Google Sheets, the dashboard updates in
the next minute without a reload. The source sheet is **never
mutated**. Every number is traceable. Ambiguous columns are
flagged once, asked about once, remembered forever.
This works on any sheet that has shape: a subscriber list, an
expense log, an inventory, a time-tracking log, a survey-results
export, a CRM mini-pipeline, a sports scoring table. The app
does not require the user to know the words "pivot table",
"VLOOKUP", or "XLOOKUP". The user pastes a URL.
**Tagline:** *Paste a Google Sheets URL. Get a live dashboard in
twenty-five seconds — without writing a single pivot table.*
## 2. Target audience
The job is universal — anyone with a sheet that has outgrown
"just look at the rows" but hasn't earned a Looker Studio setup.
- The freelancer with an expense sheet who needs to know where
the money is going before the quarterly tax estimate
- The newsletter author with a subscriber export who wants to
know which referral source actually converts to paid
- The small e-commerce operator with an inventory sheet who
wants to know which SKU runs out fastest
- The hobby project lead tracking volunteer hours across a team
spreadsheet
- The teacher with a survey-results export who needs the top
three patterns by Monday morning
- The board treasurer of a small non-profit who has six months
of donations in a sheet and needs a one-page picture
- The startup operator pasting a CRM export from a tool that
doesn't have a dashboard
- The student with a Google Form's response sheet who wants
charts for the project deck on Thursday
- The household with a shared budget sheet — "where did May go?"
- The product manager with a sheet of NPS survey responses
- The book club running a reading log across twenty members
- The amateur sports league with weekly scoring data
- Anyone who looked at Looker Studio for ten minutes, closed
the tab, and went back to scrolling rows
## 3. Core value propositions
- **Paste a URL — that's it.** No OAuth handshake, no service-
account JSON, no "share with this email". Post-I/O 2026 AI
Studio Build apps can read a signed-in user's Google Sheets
directly via the new Workspace integration. The user signs in
once with Google, pastes a sheet URL, and the app reads.
- **Twenty-five seconds to a dashboard.** Gemini 3.5 Flash with
`medium` thinking is fast enough that the user does not get a
coffee — they watch the dashboard appear. Three KPI cards, two
charts, one delta callout, one next-question prompt.
- **The dashboard answers the question the user came with.** Not
every column gets a chart. The model infers the most-likely
question the user has ("where is the money going?", "which
source converts?", "which SKU is selling?") and picks the
three metrics that answer it.
- **Every number is traceable.** Tap any KPI, any bar, any
sparkline point — the corresponding source cells highlight in
the embedded sheet view. No black-box numbers. The model is
required to emit a `source_range` (A1 notation) for every
metric in the structured response; the server validates that
the range exists in the parsed sheet.
- **Month-over-month, not just "totals".** A flat "$4,213 in
May" tells you nothing on its own. The dashboard surfaces the
delta against the previous comparable period — last month for
monthly data, last week for weekly, last quarter for
quarterly. The model chooses the comparable period from the
data's natural cadence; if there isn't enough history to
compare, it says so.
- **Iterate in plain English.** "Show me by week instead of
month." "Only Subscriptions." "Group by referral source." The
user types a follow-up; the dashboard rebuilds. No formula
syntax, no SQL, no UI for filters.
- **The sheet is read-only.** The app never writes to the
source. Every dashboard is materialised in the app's own
Firestore; the sheet is the source of truth and is preserved.
- **Ambiguity surfaced once, not every refresh.** "Date"
appearing in three formats is asked about exactly once — the
user picks the format, the choice is remembered for that sheet
forever. Same for currency symbols, category casing, blank
cells, header row position.
- **Live, not snapshotted.** A change in the source sheet
appears in the dashboard within ~60 seconds, via Drive change
notifications. The user does not need to "refresh" anything.
## 4. Features to build
- One-line URL paste with the Google sign-in button right next
to it — the "30-second demo" entry point
- Workspace-integrated sheet read (post-I/O 2026 OAuth-free
read) — fetch a range, fetch a tab, list tabs, list named
ranges, get sheet metadata (last edit time, owner)
- Sheet schema inference — column header detection, type
inference (date / number / currency / category / free text /
boolean), per-column ambiguity flag
- "Ask once" prompt for ambiguous columns — date format picker,
currency picker, header-row picker, blank-cell handling rule
- Dashboard generator: 3 KPI cards + 2 visualisations + 1
month-over-month callout + 1 next-question prompt
- Chart picker — the model chooses the chart type (bar, stacked
bar, line, sparkline, donut, scatter) from a fixed set based
on the data's shape; the chart type is part of the structured
response, not the model's free text
- Source-cell traceback — every metric stores its
`source_range`; tapping the KPI / bar / point highlights it
in the embedded sheet view
- Natural-language follow-up box — "show me by week",
"group by source", "exclude Costco" — rebuilds the dashboard
using the same structured response shape
- Auto-refresh — Drive `changes.watch` push notification (or
60-second polling fallback) triggers a re-read of the sheet
and a delta-only dashboard refresh
- Saved dashboards — every sheet the user pastes becomes a
named dashboard the user can return to; each dashboard
remembers the user's "ask once" answers and the follow-up
history
- Multi-tab support — if a sheet has more than one tab, the user
picks which tab to dashboard
- "Show me the data" toggle — flip from chart view to a clean
paginated table of the underlying rows (the parsed view, not
the raw sheet)
- Embedded sheet panel — a read-only inline iframe of the source
Google Sheet, scrolled and highlighted by the traceback action
- Export — the rendered dashboard as a PDF or as a PNG of the
three KPI cards plus the two charts (Nano Banana Pro composes
the export layout with crisp typography)
- Share — read-only link to a dashboard, optional passcode, the
source sheet permissions are NOT changed (the share link shows
the dashboard, not the underlying sheet)
- Refresh history — the last ten refreshes of the dashboard,
each one labelled with what changed in the source sheet
- "Why this number?" — tap a KPI's info icon to see the
one-paragraph rationale the model wrote for choosing this
metric
- Honest fallbacks — if the sheet has fewer than 10 rows, the
dashboard says "this is a small sample; here are the rows" and
shows the table only; if there's no time column, no MoM
callout appears
- Data type override — per column the user can correct the
model's type inference ("treat this as currency, not
number")
- The "next question" prompt — exactly one suggested follow-up,
worded as a question the user might actually type
- A privacy panel showing exactly what the app reads, where it
is stored, and a one-tap "forget this sheet" that wipes the
cached parse and dashboard
## 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)
- **Sheet shape inference + dashboard plan (Gemini 3.5 Flash,
`medium` thinking)** — receives the parsed sheet (first 200
rows of every tab, all column headers, type-inference results,
ambiguity flags, last edit time) and emits a `DashboardPlan`
JSON object: three `KpiCard`s, two `Visualisation`s, one
`MonthOverMonthCallout`, one `NextQuestion`. Every metric
carries its `source_range` (A1 notation) so the server can
validate the range exists in the sheet.
- **Column ambiguity resolver (Gemini 3.5 Flash, `low` thinking)**
— given a column the model flagged as ambiguous ("Date" with
three formats), emit a `ColumnAmbiguity` object listing the
format candidates ranked by frequency, with one example value
per candidate the UI can show the user as a chooser.
- **Natural-language follow-up parser (Gemini 3.5 Flash,
`medium` thinking)** — takes the user's plain-English question
("show me by week instead of month, only Subscriptions") plus
the current `DashboardPlan` and the parsed sheet, emits a
revised `DashboardPlan`. The follow-up is interpreted in the
context of the current view, not the original sheet.
- **Why-this-number rationale (Gemini 3.5 Flash, `low` thinking)**
— one paragraph (≤ 60 words) explaining why the model chose
this specific metric, citing the column names and the row
count it considered. Called on-demand when the user taps the
info icon on a KPI.
- **PDF / PNG export composition (Nano Banana Pro,
`gemini-3-pro-image`)** — given the three KPI numbers, the two
chart SVGs (rendered server-side from the structured
response), and the sheet's display title, generate a 4K-legible
one-page poster suitable for emailing. Nano Banana Pro's
post-I/O typography rendering means the numbers are crisp at
print size.
- **Structured output / JSON Schema** — every Gemini call in
this template returns structured JSON. The schemas are seeded
verbatim in the system instruction and as `responseSchema`. No
free-text outputs that the app has to parse heuristically.
- **Thinking levels** — `medium` for the dashboard plan and the
follow-up parser (they pick metrics and chart types, which is
the load-bearing reasoning). `low` for ambiguity resolver and
rationale generation. `high` is not used; the dashboards do
not require chain-of-thought reasoning depth beyond `medium`.
- **Long context (Gemini 3.5 Flash, 1M tokens)** — used when a
sheet has more than 200 rows. Send the first 50 rows, the last
50 rows, plus 50 random samples, plus the full type-inference
result. **Guardrail:** never exceed 500k tokens of payload per
call — chunk and summarise older rows for very large sheets
(>50k rows). The shape of the data, not every row, is what
the dashboard plan needs.
### Backend services
- **Auth — Required.** Firebase Auth with Google sign-in (auto-
provisioned by AI Studio Build). Google sign-in is the only
supported auth path for v1, because the Workspace integration
reads the user's sheets and needs the same identity. Apple
sign-in is intentionally **not** offered in v1 — the user
cannot read a Google Sheet from an Apple identity without an
additional Google sign-in step, so the simpler model is
Google-only.
- **Database — Required.** Firestore for `users`,
`connected_sheets`, `dashboards`, `dashboard_versions`,
`column_ambiguity_resolutions`, `follow_up_history`,
`refresh_history`, `shared_links`.
- **File storage — Optional.** Firebase Storage for the exported
PDF / PNG. NOT auto-provisioned by AIS Build today — enable
it before exporting. The cached parse of a sheet lives in
Firestore, not Storage.
- **Workspace integration — Required (the hero capability).**
Post-I/O 2026 AI Studio Build apps can read a signed-in user's
Google Sheets directly via the new Workspace tool, without an
OAuth handshake. **Status: preview, still rolling out to AIS Build
accounts as of 2026-06-01** — surface a clear error and a CSV-paste
fallback if the capability isn't enabled on the user's account
yet. Wire it via the `workspace.sheets` capability
declared in the AIS Build config; AIS Build issues the
short-lived per-call credential, never exposes a refresh
token, and scopes the read to a single sheet at a time. **The
app never asks for Drive-wide scope.** A sheet's URL must be
pasted by the signed-in user; the app does not list all
sheets in the user's Drive.
- **Drive change watch — Required for live refresh.** A Cloud
Run function subscribes to `drive.changes.watch` for each
connected sheet's file ID. On change, push a re-read job;
enqueue a dashboard refresh. **Fallback:** if the watch
channel cannot be established (some Workspace tenants restrict
it), poll the sheet's `modifiedTime` every 60 seconds while
the dashboard is open in the browser.
- **Email — Required (transactional).** Magic-link share emails
for read-only dashboard sharing. Sender domain must be
authorised in Firebase Auth.
- **Payments — Not needed for v1.** Free for personal use. A
future "team" tier (shared dashboards across an organisation
with role-based access) might charge via Stripe; not built in
v1.
- **Free Cloud Run deploy** — post-I/O 2026 AIS Build deploys
the first 2 apps to Cloud Run with no credit card. This
template is one of those 2; the deploy step ships the Cloud
Run functions for the Workspace reader, the dashboard
generator, and the Drive change-watch handler.
**Environment variables:** every secret (Gemini API key,
Firebase service-account JSON, Drive change-watch webhook
verification token, Stripe key if team tier added) lives in
environment variables — never in the 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 sheet contents are never sent to Gemini
for model training (use the Gemini API on the paid tier, where
Google does not use your content for model training, per the
Gemini API Additional Terms) · the cached parse is per-user,
per-sheet, and wipeable with one tap.
**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
schemas are below. **Convert the Zod schema to Gemini's
`Schema` type via the SDK helper** before passing to
`responseSchema` — do NOT pass raw Zod. **Numeric `min`/`max`
constraints are documentation only inside `responseSchema`;
clamp on the server after the response arrives.**
4. **Pin the system instruction separately** from user input.
Use the `systemInstruction` field for persona + behavioural
rules; use `contents` for the parsed sheet and the user's
question. Never concatenate.
5. **Pre-declare tools as an enable/disable list** per call. The
matrix below names which tools are enabled per call. Tools
NOT listed for a call should be disabled.
6. **State negative constraints explicitly** — they are listed
below. They are NOT "be careful" suggestions; they are hard
rules the model must follow.
7. **Strip unsupported Zod modifiers before passing to
`responseSchema`** — Gemini's OpenAPI subset rejects `.regex()`
/ `pattern`, fixed-length `z.tuple()`, and other custom
validators. Use a sanitizer that flattens tuples to arrays and
removes regex patterns before serializing. Validate those
constraints in middleware AFTER parsing.
8. **Files API uses `files/*` resource names, not `gs://` URIs.**
The AI Studio Build runtime uses the Gemini Developer API
(`@google/genai` SDK). Files API `upload` returns a resource
name like `files/abc123xyz`, passed via `fileData: { fileUri,
mimeType }`. `gs://` URIs belong to Vertex AI / Cloud Storage.
### Per-call model + tools matrix
| Call | Model | thinkingLevel | Tools enabled |
|------|-------|---------------|---------------|
| Sheet shape inference + DashboardPlan | `gemini-3.5-flash` | medium | (none) |
| Column ambiguity resolver → ColumnAmbiguity | `gemini-3.5-flash` | low | (none) |
| Natural-language follow-up → revised DashboardPlan | `gemini-3.5-flash` | medium | (none) |
| Why-this-number rationale (one paragraph) | `gemini-3.5-flash` | low | (none) |
| PDF / PNG dashboard export composition | `gemini-3-pro-image` | n/a | n/a |
| Onboarding empty-state illustration | `gemini-3.1-flash-image` | n/a | n/a |
*Note for builders:* on image-generation calls, omit
`thinkingConfig` entirely — the field is not supported on those
models. The `n/a` cells in this matrix are documentation only;
do not serialise them into the request body. No call in this
template uses `google_search` grounding — every answer is
grounded in the user's own sheet, not the public web.
### Primary structured-output schemas (seed verbatim in the prompt)
```typescript
import { z } from "zod";
const ColumnType = z.enum([
"date", "datetime", "number", "currency", "percent",
"category", "free_text", "boolean", "url", "email", "unknown",
]);
const ColumnSchema = z.object({
header: z.string(), // verbatim header text
header_normalised: z.string(), // lowercased, trimmed
letter: z.string(), // "A", "B", ..., "AA"
inferred_type: ColumnType,
ambiguity_flag: z.boolean(), // true if the resolver should ask
ambiguity_reason: z.string().nullable(), // e.g. "3 date formats observed"
example_values: z.array(z.string()), // up to 5, verbatim from sheet
non_blank_row_count: z.number(),
total_row_count: z.number(),
});
const ColumnAmbiguity = z.object({
column_letter: z.string(),
column_header: z.string(),
candidates: z.array(z.object({
candidate_label: z.string(), // "DD/MM/YYYY", "MM/DD/YYYY"
example_value_from_sheet: z.string(), // verbatim row value
frequency_in_sample: z.number(), // 0..1
})),
recommended_candidate_index: z.number(), // model's best guess
user_must_confirm: z.boolean(), // true unless one candidate >= 0.95
});
const ChartType = z.enum([
"single_bar", "stacked_bar", "grouped_bar",
"line", "sparkline", "donut", "scatter", "table_only",
]);
const Visualisation = z.object({
chart_type: ChartType,
title: z.string(), // "Spend by category, last 6 months"
x_axis_label: z.string(),
y_axis_label: z.string(),
source_range: z.string(), // A1 notation, e.g. "Sheet1!A2:D200"
group_by_column_letter: z.string().nullable(),
measure_column_letter: z.string(),
measure_aggregation: z.enum([
"sum", "count", "avg", "min", "max", "median",
]),
filter_clauses: z.array(z.object({
column_letter: z.string(),
operator: z.enum([
"equals", "not_equals", "contains", "not_contains",
"greater_than", "less_than", "in_range",
]),
value: z.string(),
})),
rationale_one_line: z.string(), // why this chart, ≤ 24 words
});
const KpiCard = z.object({
label: z.string(), // "Spent in May"
value_display: z.string(), // "$4,213"
value_numeric: z.number(), // 4213.00
unit_or_currency: z.string(), // "USD", "subs", "hours"
source_range: z.string(), // A1 notation
comparison_label: z.string().nullable(), // "vs April"
comparison_delta_pct: z.number().nullable(),
comparison_delta_direction: z.enum([
"up", "down", "flat", "not_applicable",
]),
rationale_one_line: z.string(), // ≤ 24 words
});
const MonthOverMonthCallout = z.object({
applicable: z.boolean(), // false if no time column
not_applicable_reason: z.string().nullable(),
comparison_period_label: z.string(), // "May vs April"
headline_one_line: z.string(), // "May was $740 heavier than April"
detail_two_lines: z.string(), // the two-line explanation
top_contributor_label: z.string().nullable(),
top_contributor_delta_pct: z.number().nullable(),
source_range: z.string(),
});
const NextQuestion = z.object({
question_text: z.string(), // worded as a question
reasoning_short: z.string(), // why this is a good next question
estimated_seconds_to_answer: z.number(), // server's estimate
});
const DashboardPlan = z.object({
dashboard_id: z.string(),
sheet_file_id: z.string(),
sheet_tab_name: z.string(),
generated_at_iso: z.string(),
inferred_user_intent: z.string(), // model's read on what the user wants
kpi_cards: z.array(KpiCard).length(3),
visualisations: z.array(Visualisation).length(2),
month_over_month: MonthOverMonthCallout,
next_question: NextQuestion,
columns_used: z.array(z.string()), // column letters touched
rows_considered: z.number(),
small_sample_warning: z.boolean(), // true if < 10 rows
confidence: z.number().min(0).max(1),
read_only_notice: z.literal(
"The source sheet was not modified."
),
});
type DashboardPlan = z.infer;
type ColumnSchema = z.infer;
type ColumnAmbiguity = z.infer;
```
### Common failure modes (and how to avoid them)
- Agent picks `gemini-3.5-flash` for the dashboard plan call —
that model is **not GA** until June 2026 and will resolve to a
deprecated endpoint. Pin `gemini-3.5-flash` explicitly. The
Flash-tier model in 3.5 beats the prior Pro tier on the
reasoning benchmarks this template needs.
- Model invents a `source_range` that does not exist in the
sheet — the server validates every `source_range` against the
parsed sheet's actual extent (last non-blank row, last
non-blank column per tab); on mismatch, the call is rejected
and re-requested with an error message listing the valid
extent. **Do not silently render an invalid range.**
- Model returns more than 3 KPI cards or fewer than 2 charts —
the schema's `.length(3)` and `.length(2)` clauses are
documentation only inside `responseSchema`. Enforce on the
server: drop extras, reject and re-request if there are too
few.
- Date format silently chosen — if a "Date" column has three
formats, the ambiguity resolver MUST set
`user_must_confirm: true` unless one format dominates
≥ 95 %. If the model sets it to false on a 60/40 split, the
server overrides.
- Currency conflation — if the "Amount" column has "$12.00" and
"€8.50" and "£5", flag as ambiguity. Do not silently sum them
as if they were the same currency.
- Header row at row 3 — many real sheets have a title row, a
blank row, then headers at row 3. The shape-inference call
must detect this and emit `header_row_letter: "3"`. The server
re-parses from the detected header row before generating the
dashboard plan.
- Long sheet truncation — the model receives only the first 200
rows by default. If the sheet has 5,000 rows, the server
augments the payload with the last 50 rows plus 50 random
samples and an explicit row-count in the system instruction
so the model does not assume the sample is the whole sheet.
- Free-text "Category" column with 800 distinct values — the
model should not group by it without clustering. Detect
cardinality; if `distinct_count / total_count > 0.5`, treat
as `free_text`, not `category`.
- Month-over-month called on a sheet with one month of data —
the model must set `month_over_month.applicable: false` and
populate `not_applicable_reason` honestly.
- Workspace integration short-lived credential expired mid-call
— wrap the sheet-read in a retry that requests a fresh
per-call credential from AIS Build's Workspace API; do not
cache the credential beyond its expiry.
- Sheet read times out on a sheet with 1M cells — degrade
gracefully: read the first tab, the first 5000 rows, surface
a "we sampled the first 5,000 of 47,210 rows" notice in the
dashboard footer.
- Drive change-watch webhook arrives for a sheet the user has
unconnected — silently drop with `200 OK` to keep Drive from
retrying; do not 404.
### Negative constraints (hard rules)
- Do NOT mutate the source sheet. The Workspace integration's
read-only mode is the only mode used. Any attempt by the model
to suggest a write (e.g. "I'll add a Totals column") is
rejected at the server boundary; the response is re-requested
with a stricter system instruction.
- Do NOT invent source ranges. Every `source_range` in the
structured response must reference a range that exists in the
parsed sheet's actual extent. The server validates and
rejects.
- Do NOT exceed three KPI cards, two visualisations, one MoM
callout, one next-question. The dashboard's discipline is
load-bearing; more numbers do not help.
- Do NOT silently convert units. Currencies are not coerced to
USD. Dates are not coerced to a single format. Mixed units in
a column raise an ambiguity flag.
- Do NOT request sensitive fields the dashboard does not need.
If a sheet has columns "Email", "SSN", "DOB", the dashboard
plan must not pull them unless they are the load-bearing
signal; PII columns get a "PII detected — confirm you want
this on the dashboard" prompt.
- Do NOT cache the sheet's full contents server-side beyond the
active session. The cached parse lives in Firestore scoped to
the user, and is wiped on one tap of "forget this sheet".
- Do NOT use the user's sheet data to train 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 label uncertain inferences as certain. A 60/40 date
format split is uncertain; the UI surfaces a chooser, not a
silent guess.
- Do NOT propose follow-ups that require writing to the sheet
("Add a column for category totals"). The next-question is
always a question, not an action.
- Do NOT show "Powered by AI" badges. The intelligence is in
the dashboard, not in the marketing chrome. The capabilities-
info panel is honest about what runs where.
### 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: Sheet shape inference → DashboardPlan
Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none)
```
You receive a parsed Google Sheet and must emit a DashboardPlan
JSON object that answers the most likely question the sheet's
owner has about their own data.
You receive (in the user contents):
- sheet_metadata: file_id, file_title, tab_name, owner_email,
last_modified_iso, total_row_count, total_column_count
- columns[]: an array of ColumnSchema objects with header,
letter, inferred_type, ambiguity_flag, example_values,
non_blank_row_count
- rows_sample[]: a sampled array of rows (first 50 + last 50 +
50 random samples; up to 200 rows total). Each row is an
array of cell strings indexed by column letter.
- user_resolutions: any prior ambiguity resolutions the user has
confirmed for this sheet (date format, currency, header row).
Your task:
1. Infer the most likely question the sheet's owner has. Write
it as one short sentence in `inferred_user_intent`. Examples:
"Where is the money going?", "Which referral source converts
to paid?", "Which SKU is running low?".
2. Choose EXACTLY THREE KPI cards. Each KPI is a single number
that answers part of the question. Pick KPIs that:
- Are headline-worthy (the user would say "ah" when seeing
them)
- Use different columns or aggregations (don't show three
variants of the same total)
- Each carry a source_range pointing at the source cells
- Each carry a one-line rationale ≤ 24 words explaining the
choice
3. Choose EXACTLY TWO visualisations. Each chart is a
Visualisation object with chart_type, source_range,
group_by_column_letter, measure_column_letter,
measure_aggregation, filter_clauses. Pick chart types from
the ChartType enum based on the data shape:
- time series with one measure → line or sparkline
- categorical breakdown of one measure → single_bar or donut
- categorical breakdown over time → stacked_bar or
grouped_bar
- two numeric columns → scatter
- if no chart makes sense, set chart_type to "table_only"
for that slot
4. Compute the month-over-month callout. If the sheet has a
time column with at least two distinct months of data, fill
the MonthOverMonthCallout with the comparison. Identify the
single largest delta contributor (category, source, SKU,
whatever the breakdown column is). If the sheet has no time
column or fewer than two periods, set
month_over_month.applicable: false and explain why.
5. Propose EXACTLY ONE next question worth asking. Phrase it as
a question the user might actually type ("Show me by week
instead of month", "Which source has the highest LTV?",
"Exclude the test rows from January"). The question must be
answerable from the same sheet — do not propose questions
that would require pulling another data source.
Hard rules:
- Every source_range you emit must be a real A1-notation range
inside the parsed sheet's actual extent. The server will
validate. Do not invent ranges.
- Do NOT propose any action that would write to the sheet. The
app is read-only.
- If the sheet has fewer than 10 non-blank rows, set
small_sample_warning: true. Pick KPIs that make sense for a
small sample (don't compute a "median" of 4 numbers).
- If a column is flagged ambiguity_flag: true in the columns
array, do NOT use it in a KPI or chart's measure_column. You
can use it in a filter only if the user has already resolved
the ambiguity (look in user_resolutions).
- If the sheet has PII columns (Email, SSN, DOB, Phone, full
Name combined with other identifying fields), do NOT include
them in KPI values, axis labels, or filters unless the user
has explicitly confirmed in user_resolutions.
- read_only_notice MUST be the literal string "The source sheet
was not modified." — the server checks this.
- Set confidence honestly. 0.9+ when the data shape is obvious
(an expense log with date + amount + category columns).
0.5-0.7 when the data could plausibly be about several things.
Below 0.5 when you are guessing.
Output ONLY the DashboardPlan JSON matching the provided schema.
No commentary. JSON only.
```
---
### Call: Column ambiguity resolver → ColumnAmbiguity
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You receive one column that the shape-inference call flagged as
ambiguous, along with up to 30 example values from that column.
Your job is to enumerate the candidate interpretations and rank
them.
Common ambiguity types:
- date formats: DD/MM/YYYY vs MM/DD/YYYY vs YYYY-MM-DD vs
"May 12, 2026"
- currencies: "$12.00" (USD), "€8.50" (EUR), "£5" (GBP), "12,00"
(locale comma-as-decimal)
- casing in a category column: "Groceries", "groceries",
"GROCERIES" all meaning the same value
- boolean: "Yes/No", "Y/N", "TRUE/FALSE", "1/0"
- percentage: "27%", "0.27", "27"
For the column you receive, emit a ColumnAmbiguity object:
- column_letter, column_header (copy from input)
- candidates: an array, each with candidate_label,
example_value_from_sheet (verbatim from the sample),
frequency_in_sample (0..1)
- recommended_candidate_index: the index in candidates of the
one you think is most likely correct
- user_must_confirm: true unless one candidate has
frequency_in_sample >= 0.95 AND is unambiguous in isolation
Hard rules:
- Every example_value_from_sheet must be copied verbatim from
the input. Do not invent example values.
- Do NOT auto-resolve a 60/40 split. user_must_confirm: true
when the top candidate is below 0.95.
- If the column is genuinely free_text (a "Notes" column),
return a single candidate with label "free_text" and
user_must_confirm: false.
Output ONLY the ColumnAmbiguity JSON. No commentary.
```
---
### Call: Natural-language follow-up → revised DashboardPlan
Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none)
```
You receive the current DashboardPlan, the user's plain-English
follow-up question, and the parsed sheet. Your job is to emit a
revised DashboardPlan that answers the follow-up.
Examples of follow-ups you will receive:
- "Show me by week instead of month."
- "Group by referral source."
- "Only Subscriptions."
- "Exclude Costco from the breakdown."
- "What's the trend on the smallest category?"
- "Compare Q1 to Q2 instead of May to April."
- "Show me just the top 5 customers."
How to interpret follow-ups:
- Time bucket changes ("by week", "by quarter") → adjust the
visualisations' group_by + the MonthOverMonthCallout's
comparison_period_label.
- Filters ("only X", "exclude Y") → add to every relevant
Visualisation's filter_clauses and recompute the KPIs to
match.
- New grouping ("group by source") → swap the breakdown column
on the visualisations.
- A new question entirely ("what's the trend on X") → propose
three new KPIs and two new charts focused on X; preserve
resolutions the user has already confirmed.
- "Show me the rows" / "I want to see the data" → set both
visualisations' chart_type to "table_only".
Hard rules:
- The revised DashboardPlan keeps the same shape: 3 KPI cards,
2 visualisations, 1 MoM callout, 1 next-question. The
follow-up does not change the dashboard's discipline.
- The next_question MUST be different from the user's
just-typed follow-up (do not echo it back).
- If the follow-up requires data not in the sheet, return the
unchanged plan with inferred_user_intent appended:
"(follow-up could not be answered from this sheet alone)".
- If the follow-up asks for a write or a sheet edit, refuse —
return the unchanged plan with inferred_user_intent appended:
"(this app is read-only)".
Output ONLY the revised DashboardPlan. No commentary.
```
---
### Call: Why-this-number rationale
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You receive one KPI card (label, value_display, source_range,
columns used, aggregation) plus the parsed sheet's metadata.
Write one paragraph (≤ 60 words) explaining why this metric was
chosen.
Voice: direct, specific, plain English. No marketing language.
Examples of good rationales:
- "May spend topped the chart for the year. Sum of column D
across rows 81-114 (May 2026 entries). Up from April mainly
because of two new annual subscription charges."
- "Top category by spend is the headline because Groceries
outpaces every other category by more than 2×. Sum of column D
where column C equals 'Groceries'."
Hard rules:
- ≤ 60 words. One paragraph.
- Cite the columns by header name (not just letter) and the row
count if relevant.
- Do not editorialise ("which is impressive", "as expected").
- Do not recommend an action ("you should cut subscriptions").
Output: a single string. No JSON wrapper. No commentary.
```
---
### Call: PDF / PNG dashboard export composition
Model: `gemini-3-pro-image` · n/a · n/a
```
You generate a single 4K-legible one-page poster summarising a
dashboard. The poster is suitable for emailing or printing on
A4.
You receive:
- sheet_display_title (for the poster header)
- the three KPI numbers + labels
- the two chart SVGs (server-rendered from the structured
response — you do NOT redraw them, you compose them into the
layout)
- the MoM callout text
- the next-question text (set as a subtle footer aside)
- the generation timestamp
Composition rules:
- A4 landscape, 4K resolution, white background, generous
margins.
- Header: sheet_display_title in a clean grotesque (Inter or
similar), 36pt. Generation date in 14pt muted-grey beside
it.
- KPI cards: three columns across the top third. Each card has
the value in 56pt, the label in 18pt, the comparison delta in
16pt with an up/down arrow.
- Charts: two side-by-side in the middle third, embedded as the
provided SVGs (you compose them, not redraw them; Nano Banana
Pro renders the surrounding typography crisply at 4K).
- MoM callout: bottom third, single sentence in 22pt, with the
detail in 16pt below.
- Footer: the next-question text in 12pt muted-grey, italicised.
- Typography is the load-bearing element. Use Nano Banana Pro's
post-I/O 4K-legible-text capability — the numbers must be
crisp at print size.
- No decorative imagery. No background gradients. No emoji. The
data is the design.
Hard rules:
- Do not invent numbers. Render exactly what you were given.
- Do not add a "Powered by" footer or any marketing chrome.
- Aspect ratio: 16:9 for landscape A4-like output.
```
---
### Call: Onboarding empty-state illustration
Model: `gemini-3.1-flash-image` · n/a · n/a
```
You generate one photographic-looking image for the welcome
screen. Subject: a desk at work hours with a laptop showing a
Google Sheet, a coffee, a notebook with handwritten figures
beside it. Warm afternoon light, no people in frame, no
recognisable brand logos on the laptop or notebook.
Prompt anchors that work well:
- "warm afternoon light through a window, a laptop on a wooden
desk showing a spreadsheet with rows of numbers, a small
ceramic coffee cup beside it, a paper notebook open with
handwritten figures, real worn paper texture, no people in
frame, no brand logos visible"
Hard rules:
- Photographic, not cartoon, not illustration-style.
- No people in frame.
- No commercial branding visible.
- Warm lighting, slight imperfection, real paper texture for
the notebook; avoid the glossy AI-render look.
- Aspect ratios: 3:2 for hero, 1:1 for empty states.
```
## 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 expense log.** Three columns — Date, Amount, Category.
Six months of rows. Dates in DD/MM/YYYY and MM/DD/YYYY
mixed. Amounts in USD. Categories with inconsistent casing.
The dashboard generator infers "where is the money going?"
and surfaces: total spend this month, top category, biggest
month-over-month mover. The chart picker chooses a stacked-
bar of category × month and a sparkline of weekly total.
- **The subscriber list.** Six columns — Email, Signup Date,
Source, Plan, Country, Lifetime Value. Two thousand rows.
The dashboard infers "which source converts to paid?" and
surfaces: total subscribers, paid conversion rate, top
referral source by paid count. Chart picker chooses a donut
of source share + a grouped bar of plan × source. PII
detection flags the Email column; the user confirms once that
Email is fine to use as a count (not as a display value).
- **The inventory sheet.** Five columns — SKU, Name, Units in
Stock, Reorder Threshold, Last Sold. Two hundred rows. The
dashboard infers "which SKU is running low?" and surfaces:
count of SKUs below threshold, units-sold-per-day on the
fastest mover, days-of-cover for the closest-to-out SKU.
- **The time-tracking log.** Four columns — Date, Project,
Hours, Notes. Three months. The dashboard infers "where is
the time going?" and surfaces: total hours this week, top
project, biggest week-over-week mover.
- **The survey-results export.** Twelve columns of mixed
question types. The dashboard infers "what did people
actually say?" — picks the top three rated dimensions, shows
a donut of overall NPS distribution, surfaces the comment
column's most common keywords as a small ranked list.
- **The donations sheet.** Three columns — Date, Donor, Amount.
Eighteen months. Dashboard infers "is giving growing?" and
surfaces: total this quarter, repeat-donor share, biggest
quarter-over-quarter mover.
- **The book-club reading log.** Four columns — Member, Book,
Pages Read, Date. Six months across twenty members. The
dashboard infers "who's reading the most?" and surfaces: top
reader, average pages per member per month, biggest month-
over-month jump.
- **The CRM mini-pipeline.** Columns — Deal, Stage, Value,
Owner, Close Date. The dashboard infers "what's stuck?" and
surfaces: pipeline-weighted value, win rate, longest-stuck
deal.
- **The follow-up flow.** User asks "show me by week instead of
month". Dashboard rebuilds with weekly buckets. User asks
"only Subscriptions". Dashboard rebuilds with the filter
applied. User asks "what's the trend on Dining?". A new view
appears with Dining-focused KPIs.
- **The ambiguity flow.** A "Date" column has three formats —
the resolver surfaces a chooser with one example per format
("2026-05-21", "21/05/2026", "May 21, 2026"). The user picks
one. The choice is remembered.
- **The "tap a number" traceback.** The user taps "$1,184" in
the Top Category KPI. The embedded sheet panel scrolls to
the relevant rows; the cells highlight in soft yellow for 4
seconds.
- **The auto-refresh.** The user adds a new row in Google
Sheets in another tab of the browser. Within 60 seconds the
dashboard's KPI cards animate to their new values and the
charts redraw. A small "refreshed at 14:02" line appears in
the footer.
- **The small-sample fallback.** A sheet with 6 rows triggers
`small_sample_warning: true`. The dashboard renders a clean
table view with a one-line callout: "Six rows is a small
sample. Here's what's in the sheet." No chart is forced.
- **The PII confirmation.** A sheet with an "Email" column
surfaces a one-time prompt: "This sheet has PII. We'll use
Email for counting only, not as a displayed value. Confirm to
continue." The user confirms; the choice is remembered.
- **The share link.** The user shares a read-only dashboard
with a colleague via magic link. The colleague sees the
dashboard but cannot access the underlying sheet unless the
sheet's owner has separately shared it in Google Sheets.
## 6. Page structure
Build the following screens / sections in this order. Adjust
copy to fit the voice, but keep the structural intent.
1. **Welcome / paste-URL hero.** A photographed-looking shot of
a desk with a laptop showing a Google Sheet. One paragraph:
"Paste a Google Sheets URL. We'll build the dashboard in
twenty-five seconds — without writing a single pivot table."
The URL input is the centerpiece. Google sign-in button
next to it. Below: "Try with a sample sheet" → loads the
demo expense log in section 8a.
2. **Reading-your-sheet state.** A live progress card with five
sub-steps: "Connecting to your sheet…", "Reading the first
200 rows…", "Inferring column types…", "Picking the three
numbers that matter…", "Drawing the charts…". Each step
completes with a check; the whole flow takes ≤ 25 seconds.
3. **Ambiguity flow (one-time).** If any columns are flagged
ambiguous, a single modal asks: "Before we draw, two quick
questions." Each question shows one example per candidate
with a radio selector. "Remember for this sheet" is checked
by default.
4. **Dashboard.** Three KPI cards across the top, two
visualisations in the middle, the MoM callout below them,
the next-question prompt as a single tappable chip at the
bottom. Each KPI has an info `(i)` icon → why-this-number
rationale.
5. **Follow-up input.** A persistent input at the bottom of the
dashboard: "Ask anything about this sheet." Examples cycle
in the placeholder ("Show me by week instead of month", "Only
Subscriptions", "What's the trend on the smallest
category?").
6. **Embedded sheet panel.** A read-only inline iframe of the
source Google Sheet on the right (desktop) or bottom (mobile),
toggleable. Tapping a KPI / bar / point highlights the
corresponding cells.
7. **Saved dashboards.** Sidebar listing every sheet the user
has connected. Each entry shows the sheet title, the tab
name, last-refreshed timestamp, and a small sparkline of the
first KPI.
8. **Refresh history.** A timeline of the last ten refreshes
for the active dashboard. Each entry names what changed in
the source sheet ("3 new rows added", "Amount in row 47
edited from $12 to $120", "no detected changes").
9. **Export view.** Modal with three options: "Download PDF",
"Download PNG", "Copy share link". The PDF is composed by
Nano Banana Pro for crisp 4K typography.
10. **Settings & privacy.** A panel with: "Connected sheets"
(list with one-tap forget); "Ambiguity resolutions"
(per-sheet remembered choices, individually editable);
"Delete this account" with a 60-second cool-off. Privacy
panel restates the not-trained-on policy in plain English.
11. **Footer.** "Read-only. We never modify your sheet."
Privacy: "Your sheet data is yours. We never train on it."
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 Spreadsheet Whisperer."
- Subhead: "Paste a Google Sheets URL. Get a live dashboard in
twenty-five seconds — without writing a single pivot table."
- One paragraph (≤ 60 words) explaining the workflow: sign in
with Google once, paste a sheet URL, watch the dashboard
appear. Three KPIs, two charts, a month-over-month callout, a
next question to ask. The sheet is never modified.
- Visual: a small annotated illustration of a dashboard with
the three sections labelled (KPI cards, visualisations, MoM
callout) — not a generic spreadsheet icon.
**Slide 2 — Try it now.**
- One short prompt: "Try with the sample expense log".
- A live demo input pre-loaded with the seed sheet URL from
section 8a (a public sample sheet the user can read without
signing in).
- 1-2 sentences pointing at *the specific page elements* where
the Gemini magic happens (the 25-second dashboard build, the
follow-up input, the source-cell traceback).
**Slide 3 — How to remix this.**
- Headline: "Make this yours."
- Three short bullets:
- "Connect your own Google Sheet — paste any URL after
signing in."
- "Adjust the prompts in `/server/prompts/` to fit the kind
of dashboards your domain needs."
- "Wire up your Gemini API key and Firebase project via the
env-var list in the capabilities panel."
- Primary CTA: "Use this template" → links to AI Studio Build
remix entry point.
- Secondary: "Just exploring — close" (sets localStorage flag,
never auto-shows again).
**Accessibility:** focus trap, `Esc` closes, `role="dialog"`,
`aria-modal="true"`, `aria-labelledby`, focus restored to
trigger on close. Respect `prefers-reduced-motion`.
**Don't:**
- Don't gate content behind the modal. The page beneath must
be fully usable.
- Don't auto-reshow on return visits. Use
`localStorage['onboarding-seen-v1']`.
- Don't include unrelated CTAs (newsletter signup, social
follow). Keep it about the template only.
## 6c. Capabilities info button (persistent in header)
Add a persistent `(i)` icon in the top-right of the header
(next to the primary nav). Click → opens a modal/panel titled
**"What powers this app"**.
**Panel contents (in this order):**
**Gemini capabilities used (the hero list):**
- **Gemini 3.5 Flash (post-I/O 2026 default flagship)** — does
the load-bearing work: infers the question your sheet is
about, picks the three KPIs, picks the chart types, computes
the month-over-month callout, suggests the next question.
The Flash-tier model in 3.5 beats the previous-generation
Pro tier on the benchmarks this template cares about, at ~4×
the speed.
- **Gemini 3.5 Flash (structured output)** — every Gemini call
in this template returns typed JSON conforming to the
`DashboardPlan`, `ColumnAmbiguity`, or revised-`DashboardPlan`
schemas. The schemas live in the repo.
- **Gemini 3.5 Flash (long context, 1M tokens)** — handles
sheets up to ~50,000 rows by sampling intelligently. For
very large sheets the server samples and summarises older
rows to stay under 500k tokens of payload.
- **Nano Banana Pro (`gemini-3-pro-image`)** — composes the
exported PDF / PNG with 4K-legible typography. The post-I/O
Pro image model renders numbers crisply at print size, which
the previous-generation image models could not.
- **Nano Banana 2 (`gemini-3.1-flash-image`)** — generates the
welcome and empty-state imagery.
- **AI Studio Build Workspace integration (new at I/O 2026)** —
reads your Google Sheets without an OAuth handshake. AI
Studio Build issues a short-lived per-call credential when
you paste a sheet URL; the app never holds a Drive-wide
refresh token, and the scope is one sheet at a time.
- **Drive change-watch** — subscribes to file change events on
the sheets you connect, so the dashboard refreshes within
60 seconds of an edit. Falls back to 60-second polling on
tenants that restrict the watch channel.
- **Firebase Auth** — Google sign-in; magic-link email for
read-only dashboard share links.
- **Firestore** — stores your connected sheets, dashboards,
remembered ambiguity resolutions, and follow-up history.
Syncs across devices in real time.
- **Cost note** — see the detailed breakdown in 6d. A typical
dashboard build costs about $0.011 of Gemini API spend; a
follow-up costs about $0.009. A heavy-use month (50
dashboards, 200 follow-ups) is well under $2.
- **Privacy note** — your sheet contents are private to you
and any user you explicitly share a dashboard with. This app
uses the Gemini API on the paid tier, where Google does not
use your content for model training, per the Gemini API
Additional Terms. The cached parse lives in your Firestore
project and is wiped on one tap of "Forget this sheet".
**Backend services this app depends on:**
- Auth: see section 4b (Google sign-in only in v1)
- Database: see section 4b
- Storage: see section 4b (optional, for PDF / PNG exports)
- Workspace integration: see section 4b — the post-I/O 2026
hero capability
- Email: see section 4b — magic-link sharing requires the
sender domain to be authorised in Firebase Auth.
- 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)
- `DRIVE_WATCH_WEBHOOK_TOKEN` — a random secret used to verify
Drive change-watch callbacks
- `MAGIC_LINK_SENDER_DOMAIN` — the authorised domain for share
emails
**Cost + privacy notes:**
- Dashboard build calls average ~7,000 input tokens and ~900
output tokens on Gemini 3.5 Flash → ~$0.019 per build (at
$1.50/M input, $9.00/M output). Cached input ($0.15/M) brings
refreshes after the first build down to ~$0.011.
- Follow-up calls average ~4,500 input tokens and ~700 output
tokens → ~$0.013 per follow-up; ~$0.009 with cached input.
- One short paragraph on privacy: where the data lives (your
Firebase project), how to delete it (Settings → "Forget this
sheet" — gone in 60 seconds; "Delete this account" — gone in
60 seconds), what is never sent for training.
**Documentation links:**
- AI Studio Build docs
- Gemini 3.5 Flash multimodal + structured output + long-
context docs
- Nano Banana Pro (Gemini 3 Pro Image) docs
- Workspace integration without OAuth (AIS Build, I/O 2026)
- Drive `changes.watch` docs
- Firebase Auth, Firestore 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)
- **Sheet shape inference → DashboardPlan (Gemini 3.5 Flash,
medium thinking)** — a 200-row sheet sample plus the column
schema plus the system instruction averages ~7,000 input
tokens and ~900 output tokens. At Gemini 3.5 Flash pricing
($1.50/M input, $9.00/M output) that is ~$0.019 per first
build. With cached input ($0.15/M), refreshes after the
first build come in around ~$0.011 each.
- **Column ambiguity resolver (Gemini 3.5 Flash, low
thinking)** — small input (one column's example values),
~1,200 input tokens and ~300 output tokens → ~$0.0049 per
resolution. Called only once per ambiguous column, then
remembered.
- **Natural-language follow-up parser (Gemini 3.5 Flash,
medium thinking)** — ~4,500 input tokens (sheet sample +
current DashboardPlan + follow-up) and ~700 output tokens →
~$0.013 per follow-up. ~$0.009 with cached input.
- **Why-this-number rationale (Gemini 3.5 Flash, low
thinking)** — small input, ~800 input + ~150 output → ~$0.002
per call. User-driven; called only when the info icon is
tapped.
- **PDF / PNG export (Nano Banana Pro,
`gemini-3-pro-image`)** — ~$2 input / $12 output per 1M
tokens; one A4-landscape export ≈ ~$0.04 per file.
- **Welcome / empty-state image (Nano Banana 2)** —
~$0.025/image. Generated once per app install (cached
client-side), or once per regenerate-empty-state action.
- **Expected per-dashboard cost (across full lifecycle):**
~$0.05 — one initial build at ~$0.019, two ambiguity
resolutions at ~$0.010, three follow-ups at ~$0.027, two
why-this-number taps at ~$0.004, one PDF export at ~$0.04 if
exported.
- **Heavy-use month (50 dashboards, 200 follow-ups, 20 PDF
exports):** ~$0.95 + ~$2.60 + ~$0.80 ≈ ~$4.40 total.
- **Firestore parse storage:** ~$0.18/GB/month for stored docs;
a parsed sheet (first 200 rows + column schema + plan
history) averages ~25 KB ≈ ~$0.0000045/sheet/month —
effectively free for personal use.
- **Drive change-watch:** free; subject to Drive's quota of
10,000 push notifications per user per day, which is far in
excess of what this app needs.
## 7. Design language
- **Mood:** A clean, fast working surface. Not a spreadsheet
clone. Not a SaaS dashboard with "by the numbers" tiles. The
desk at 14:02 on a Wednesday when the user just wants to
know where the money went in May before getting back to the
rest of the afternoon.
- **Typography:** A clean grotesque for app chrome and data
labels (Inter or Geist). A display grotesque slightly heavier
for KPI numbers (Geist Display or Inter Display) — the
numbers are the headline, and they should be readable at
small sizes. A monospace (Geist Mono or JetBrains Mono) only
for the embedded sheet panel's cell values.
- **Palette:** Paper-white background `#FAFAF8` for the
dashboard surface, deep ink `#0F0F12` for body text, KPI-
accent violet `#5B4FD9` for the primary KPI value, soft mint
`#3CA17A` for "up" deltas, soft coral `#D04A5A` for "down"
deltas, neutral grey `#8C8C94` for muted labels, highlight
yellow `#FFF1B8` for the source-cell flash. Avoid bright
primary blues that pull the eye away from the data.
- **Imagery:** Photographic for the welcome screen only.
Inside the app, no decorative imagery — the data is the
design.
- **Hand-feel touches:** The dashboard appears one element at
a time during the build (KPI 1, KPI 2, KPI 3, chart 1, chart
2, MoM callout, next-question chip) with a 100 ms stagger —
the user reads the numbers as they arrive, like a Polaroid
developing. The source-cell highlight fades in over 200 ms
and fades out after 4 seconds with a soft yellow wash.
- **Spacing:** consistent 4-px base. Generous whitespace — the
KPI cards need air to feel like headlines.
- **Radius:** consistent token set (e.g. 8 / 14 / 22 px). KPI
cards use 14; the source-cell highlight uses 6; the welcome
card uses 22.
- **Shadows:** subtle, layered. Avoid heavy drop shadows.
- **Motion:** purposeful — entrance fades, hover lifts, the
dashboard build stagger. Respect `prefers-reduced-motion`.
The "numbers animate to new values" on auto-refresh is the
canonical reduced-motion check — verify it jumps instead of
tweening.
- **States:** every interactive element has hover, focus,
active, disabled. Loading uses skeletons that match the
eventual layout, not spinners. Empty states have helpful
next-action guidance ("Paste a Google Sheets URL above to
begin").
## 8. Content generation rules
- Write **realistic, specific copy**. NO Lorem Ipsum. NO
generic placeholders like 'Your tagline here'.
- Invent plausible sample sheets and KPI values that fit the
job (use the seed content in section 8a as a starting point).
When inventing, lean on realistic spreadsheet patterns —
expense categories, signup sources, SKU codes — but never
invent fake brand names or fictional companies that suggest
endorsement.
- Tone: direct, useful, free of corporate language. This
template is for a working person, not a SaaS prospect.
- Headlines: punchy and concrete. No 'Empower your X' filler.
No 'Revolutionize'. No 'Seamless'. No 'AI-powered'.
- Body copy: short paragraphs (2-4 sentences). Use lists where
appropriate.
- Plain language. Avoid jargon — except where the user already
speaks the jargon ("MoM", "YoY", "NPS", "LTV" in the right
contexts).
- 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 small-
sample warning, a low-confidence chip on a chart).
## 8a. Seed content (use these specific examples)
Anchor every generated copy + sample data point in the
concrete content below.
**Sample sheets the demo lineage exposes:**
- **"Personal Expenses 2026"** — Date, Amount, Category. 187
rows across Jan–May 2026. Categories: Groceries, Dining,
Subscriptions, Transport, Utilities, Health, Misc. Dates in
mixed DD/MM/YYYY and MM/DD/YYYY (this triggers the
ambiguity flow). Amounts in USD.
- **"Newsletter Signups Q1"** — Email, Signup Date, Source,
Plan, Country. 1,420 rows. Sources include Twitter,
Newsletter Referral, Direct, Google, ProductHunt, Reddit.
- **"Workshop Inventory"** — SKU, Name, Units in Stock,
Reorder Threshold, Last Sold. 142 rows of small-batch
workshop items.
- **"Volunteer Hours — Spring"** — Date, Volunteer, Project,
Hours. 412 rows from a community garden's spring season.
- **"Donations 2024-2026"** — Date, Donor, Amount,
Recurring Y/N. 720 rows across two and a half years.
**Sample dashboard in detail view (the demo expense log):**
- Sheet display title: "Personal Expenses 2026"
- Inferred user intent: "Where is the money going this year?"
- **KPI 1:** "May spend — $4,213" · comparison "vs April"
delta "+$740" direction "up" · source_range "Sheet1!B81:B114"
· rationale "Sum of column B for rows where column A is in
May 2026."
- **KPI 2:** "Top category — Groceries — $1,184" ·
comparison "vs April" delta "+$112" direction "up" ·
source_range "Sheet1!A2:C187" · rationale "Sum of column B
where column C equals 'Groceries' across the full sheet."
- **KPI 3:** "Subscriptions up 27 % vs April — $612" ·
source_range "Sheet1!A2:C187" · rationale "Largest month-
over-month mover by percent change among the seven
categories."
- **Chart 1:** Stacked bar — "Spend by category, last 6
months" · x = month · y = sum of Amount · group_by = Category
· source_range "Sheet1!A2:C187" · rationale "Category share
per month surfaces where the budget actually goes."
- **Chart 2:** Sparkline — "Weekly spend, last 12 weeks" · x =
week · y = sum of Amount · source_range "Sheet1!A2:C187" ·
rationale "Pace check — are weekly spending bursts speeding
up or slowing down?"
- **MoM callout:** "May was $740 heavier than April. The gap is
mostly Subscriptions and Dining — two new annual subscription
charges and four extra dinners out." · top contributor
"Subscriptions" · delta_pct "+27 %"
- **Next question:** "Show me by week instead of month, only
Subscriptions."
**Sample ambiguity resolution (the date column flow):**
- Column letter: "A" · header: "Date" · ambiguity_reason: "two
date formats observed (DD/MM/YYYY and MM/DD/YYYY)"
- Candidates: "DD/MM/YYYY" (example "21/05/2026", frequency
0.62) · "MM/DD/YYYY" (example "05/21/2026", frequency 0.38)
- recommended_candidate_index: 0 · user_must_confirm: true
**Sample follow-up flow (rebuild after follow-up):**
- User types: "show me by week instead of month, only
Subscriptions"
- Revised inferred_user_intent: "How is Subscriptions spend
trending week over week?"
- New KPI 1: "This week — $148"; KPI 2: "Last 4 weeks —
$612"; KPI 3: "Active subscriptions this month — 9".
- New Chart 1: Line of weekly Subscriptions spend, last 12
weeks. New Chart 2: Donut of which specific subscriptions
contributed in the latest week.
- New MoM callout: "This week was $42 heavier than last week.
Two annual charges landed on the same day."
- New next_question: "What's the trend on the smallest
subscription line?"
**Sample voice copy:**
- Welcome: "Paste a Google Sheets URL. We'll build the
dashboard in twenty-five seconds — without writing a single
pivot table."
- URL placeholder: "https://docs.google.com/spreadsheets/d/…"
- Reading-your-sheet steps: "Connecting to your sheet…" /
"Reading the first 200 rows…" / "Inferring column types…" /
"Picking the three numbers that matter…" / "Drawing the
charts…"
- Ambiguity prompt heading: "Before we draw, two quick
questions."
- Ambiguity prompt body: "Your 'Date' column has two formats.
Which one is yours?"
- KPI info tooltip placeholder: "Why this number?"
- Source-cell flash caption: "Cells in your sheet that
contributed to this number."
- Follow-up placeholder: "Ask anything about this sheet."
- Small-sample warning: "Six rows is a small sample. Here's
what's in your sheet."
- PII confirmation: "This sheet has an Email column. We'll use
it for counting only, not as a displayed value. Confirm to
continue."
- Read-only assurance (in the footer): "Read-only. We never
modify your sheet."
- Auto-refresh toast: "Refreshed — 3 new rows added in your
sheet."
## 9. Media & assets
- **Hero image (landing screen):** A photographed-looking shot
of a wooden desk at golden hour, a laptop in the centre
showing a Google Sheet with rows of numbers, a small ceramic
coffee cup beside it, a paper notebook open with handwritten
figures. Generate via Nano Banana 2 with a prompt emphasising
"warm afternoon light through a window, a laptop on a wooden
desk showing a spreadsheet with rows of numbers, no people in
frame, no brand logos visible, real worn paper texture, soft
shadow under the laptop".
- **App icon / wordmark:** Set in the display grotesque.
Slightly worn paper texture behind it. No icon — just type.
- **Empty-state illustration:** A simple line drawing of a
spreadsheet grid with a sparkline emerging from it. Hand-
drawn aesthetic, not a flat icon. Generate once at build
time via Nano Banana 2 (`gemini-3.1-flash-image`), 1:1 WebP
at 1024×1024, prompt: "single hand-drawn ink line
illustration of a small grid of empty rectangular cells (six
by four), with a thin hand-drawn sparkline curve rising out
of the top-right corner of the grid, off-white paper
background, slight pen imperfection, no shading, no colour
fill, no text, no commercial branding". Ship as a seed
asset at `/public/samples/empty-state-grid.webp`.
- **Chart visuals:** Server-rendered SVGs from the structured
response, NOT model-generated images. The chart_type field
drives a deterministic renderer.
- **Exported PDF / PNG:** Composed by Nano Banana Pro per the
system instruction. A4 landscape, 4K-legible typography.
- **Stock fallbacks:** If image generation fails, fall back to
the photographed sample desk from `/public/samples/sample-
desk.jpg` (3:2 WebP, 2048×1365 — ship as a seed asset;
recreate via Nano Banana 2 (`gemini-3.1-flash-image`) with
the prompt: "photographic wooden desk at golden hour, a
laptop in the centre showing a generic spreadsheet UI with
rows of numbers — no real brand logos, no real company name
— a small ceramic coffee cup beside it, a paper notebook
open with handwritten figures, soft warm window light, no
people, real worn paper texture, soft shadow under the
laptop"). Never to a generic spreadsheet emoji.
- **Generated imagery:** prefer Nano Banana 2 over stock
photography. Prompt for warmth 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.
## 10. Interactivity & states
- Every interactive element has hover, focus, active, and
disabled states.
- Forms validate inline and show specific error messages (not
"Invalid input"). "That URL doesn't look like a Google Sheets
link — paste the URL from your browser's address bar when the
sheet is open." is the right shape.
- Loading states use skeletons that match the eventual layout,
not spinners. The dashboard's KPI cards and chart frames
appear as skeletons during the 25-second build.
- Empty states explain the next action with a button whose
label fits THIS app's domain: "Paste a Google Sheets URL",
"Connect a sheet", "Ask a follow-up" — never a generic "Get
started".
- Smooth scroll for in-page anchors.
- All AI-generated content streams in element-by-element where
supported (the KPI cards arrive one at a time as the
DashboardPlan parses), with a clear "thinking…" indicator
before content starts arriving.
- If an AI call fails, show a calm, specific error ("The
sheet's first column has values we couldn't classify. Want
to pick a column type, or send us the sheet ID for a closer
look?") and offer retry.
- Source-cell traceback: tapping a KPI / bar / point fades a
soft yellow wash onto the corresponding cells in the embedded
sheet panel for 4 seconds, then fades out.
- Auto-refresh: when a Drive change arrives, the KPI numbers
animate to their new values over 400 ms (or jump under
`prefers-reduced-motion`). The chart redraws with a fade.
- A small inline notice appears for any small-sample,
insufficient-history, or PII-detected case, not a modal that
blocks the dashboard.
- The follow-up input is debounced — typing does not fire a
call; pressing Enter does.
## 11. Tech & responsive requirements
- **Stack:** React + TypeScript + Tailwind CSS. Functional
components + hooks. Use Shadcn UI primitives where
appropriate. Chart rendering via Recharts or Visx (both
work; Visx gives more control over the source-cell traceback
highlight animation).
- **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 the
client bundle. Free Cloud Run deploy from AIS Build (one of
the two free deploys allotted per account post-I/O 2026).
- **Model selection:** explicitly pin `gemini-3.5-flash` for
dashboard plan / follow-up / ambiguity / rationale calls;
`gemini-3-pro-image` (Nano Banana Pro) for the PDF / PNG
export; `gemini-3.1-flash-image` (Nano Banana 2) for the
welcome / empty-state imagery. Set `thinkingLevel`
explicitly per call.
- **Database:** Firestore (auto-provisioned by AI Studio
Build). Show the seed lineage on first launch.
- **Auth:** Firebase Auth — Google sign-in only in v1, because
the Workspace integration uses the same identity to read
sheets.
- **Workspace integration:** Wire via the AIS Build Workspace
Sheets capability (declared in the AIS Build config; the
exact capability identifier — e.g. `workspace.sheets` —
should be verified against the live AIS Build reference,
as the integration is still rolling out post-I/O 2026). AIS
Build issues a short-lived per-call credential when the user
pastes a sheet URL; the app never holds a Drive-wide refresh
token. The scope is one sheet at a time.
- **Drive change-watch:** A Cloud Run function subscribes to
`drive.changes.watch` for each connected sheet's file ID. The
function validates incoming push notifications against
`DRIVE_WATCH_WEBHOOK_TOKEN`. Falls back to 60-second polling
on tenants that restrict the watch channel.
- **Auto-refresh transport:** Firestore real-time listeners on
the active dashboard view, so when the Cloud Run refresh job
writes new KPI values, the browser updates without a manual
refresh.
- **Embedded sheet panel:** Google Sheets's official embed
iframe URL, scoped to a single tab. Source-cell highlighting
uses Google Sheets's `range` parameter to scroll to the
highlighted range; the highlight wash is overlaid by the
app's own SVG since the embed doesn't expose cell-level DOM.
- **Mobile-first.** Verify layouts at 375 px (iPhone SE), 768
px (iPad), 1024 px, 1440 px+. On mobile, the embedded sheet
panel becomes a bottom sheet that the user can drag up.
- 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 active dashboard view (so a refresh in the Cloud Run job
appears in the browser instantly).
- Optimistic UI on follow-ups; reconcile on response.
## 12. Accessibility (WCAG 2.2 AA)
- Semantic HTML — `header`, `nav`, `main`, `section`,
`article`, `footer`.
- All interactive controls reachable by keyboard with a
visible focus ring.
- Color contrast ≥ 4.5:1 for body, 3:1 for large text and UI
components. The mint "up" and coral "down" delta chips both
exceed 4.5:1 on the paper-white background.
- All images have meaningful `alt` text. The welcome image
alt: "A wooden desk at golden hour with a laptop showing a
spreadsheet and a small ceramic coffee cup beside it."
- Form fields have associated `