# 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.
---
# Meeting Memo (Live)
## 1. Project
**Meeting Memo (Live)** is a real-time meeting notetaker that joins the
call from the user's own browser tab — sharing the call's screen and
audio with Gemini for the duration of the meeting — and quietly builds
a structured memo as the call happens. Decisions land in green.
Action items land in yellow with a guess at who owns them and when
they're due. Blockers land in red. Open questions land in grey. When
the call ends, the memo is one clean page, the action items are
already drafted in Asana and Linear (and as a follow-up email to
whoever was supposed to send it), and the user one-taps each one to
dispatch.
There is no separate notetaker bot joining the call. There is no
post-meeting transcript-cleaning job. There is no Slack thread the
next morning asking "wait, what did we decide?". The user starts the
memo before the call, the sidebar fills as the call runs, the user
ends the memo when the call ends, and the work is already staged.
The single demo that proves the magic: the user clicks **Start memo**
at the top of a Zoom call window, accepts the explicit consent banner
("everyone on this call can see that I'm recording — proceed?"), and
shares the Zoom tab + system audio with the app. Five seconds later
a sidebar opens with a paused-pulse "Listening" indicator. As the
call runs, items begin appearing — *"Decision: we ship the Berlin
launch on June 30."* *"Action: Mariana drafts the Berlin press
release by Friday — we think this was Mariana speaking, verify."*
*"Blocker: legal review on the contract — Tomás owns this."*
*"Open question: do we need a translator on the Berlin event day?"*
Each one is timestamped against the meeting clock (00:14:32), each
one is colour-coded, each one shows the verbatim phrase that
triggered it. When the meeting ends and the user taps **End memo**,
the model runs a structured-output post-processing pass and produces
a one-page memo: meeting title, attendees (voice-diarised, all
flagged "verify"), decisions list, action items grouped by owner,
blockers, open questions, the verbatim "next meeting" if anyone
mentioned one. Action items appear as **drafts** in a dispatch tray
at the bottom — three of them are Asana tasks, two are Linear
issues, one is a follow-up email to a vendor with the body
pre-written. The user reviews each draft, edits where needed, and
one-taps dispatch. Anything not dispatched gets archived inside the
memo for next-meeting reference.
This is the kind of app a person opens when they are tired of three
notetaker bots fighting for the bottom row of their grid view and
none of them dispatching the action items where the work actually
gets done. It's also the kind of app a small leadership team keeps
running across the workday — every recurring sync, every customer
call, every one-on-one — quietly building a record of decisions and
follow-ups that nobody has to type up afterwards.
**Tagline:** _Start a memo before the call. End it after. Action
items already drafted in Asana, Linear, and email — one tap to
dispatch._
## 2. Target audience
This app is jobs-to-be-done universal — it's for anyone who attends
recurring video meetings on Zoom, Google Meet, or Microsoft Teams and
ends up doing the typing-up afterwards. The shape of the work, not
the persona, is what matters.
- People who run weekly leadership syncs and don't want a notetaker
bot in the grid
- Product managers running customer-research calls who need decisions
and quotes pulled in real time
- Engineering managers running sprint planning where action items
need to land in Linear as Linear issues, not as bullets in a doc
- Operations leaders running vendor calls where the follow-up is
always an email, never a task
- Founders running investor calls where the "what did we promise?"
question gets asked an hour later
- Designers running design-review calls who want the critique points
separated from the approvals
- Sales account executives running discovery calls who need next-step
commitments captured separately from product feedback
- Anyone running a one-on-one who wants both sides' commitments
surfaced and a single follow-up email drafted to share
- Recurring-meeting facilitators (engineering all-hands, customer
advisory board) who need the decisions trail for the next session
- Remote teams across timezones where the meeting record is the only
source of truth the next person reads at 2 a.m.
The app does NOT join the call as a bot. It does NOT post a
notetaker into the participant grid. It does NOT require the meeting
host's permission. It captures the call from the user's own browser
tab via screen-share + tab audio, which the user explicitly chooses
to share. The consent banner is on the user, not on the call host —
because the user is the only person whose data leaves their machine.
## 3. Core value propositions
Surface these clearly through copy, visual emphasis, and section
ordering — they are the reasons users pick this app.
- **No bot in the meeting** — the app runs in the user's own browser
tab. It shares the call's screen and audio with Gemini via the
Live API. Other participants see no notetaker, no extra
participant, no foreign attendee in the grid. The meeting feels
exactly the same to everyone else.
- **The sidebar fills in real time** — decisions, action items,
blockers, and open questions stream in as the model hears them.
Each item shows the verbatim phrase that triggered it,
timestamped against the meeting clock. The user can correct or
re-categorise any item with one tap while the call is still
running.
- **Action items are drafts, not dispatches** — when the call ends,
the dispatch tray holds drafts in Asana, Linear, and email,
pre-filled and ready. **Nothing fires automatically.** The user
reviews each draft, edits where needed, and one-taps dispatch.
Anything not dispatched is archived inside the memo.
- **Speaker attribution is voice-diarised and always flagged** — the
Live API can distinguish who's speaking, but it can't know names
unless told. The model labels speakers as "Speaker 1, Speaker 2,
Speaker 3" during the call; if the user has typed in attendee
names at meeting start, the model proposes a binding ("we think
Speaker 2 was Mariana — verify"). Names are never asserted.
- **Audio + screen-share never leaves your project** — Live API runs
in the user's own Firebase project, on the paid Gemini API tier
where Google does not use your content for model training, per
the Gemini API Additional Terms. The audio frames and screen-share
frames are streamed to Gemini and discarded after the session.
Nothing is uploaded to a third-party SaaS. The structured memo and
action items are stored in the user's Firestore.
- **Workspace-native dispatch** — Asana and Linear write happens via
their public APIs (user-authenticated OAuth tokens stored as
Secrets). Email goes via Gmail through the post-I/O 2026
Workspace integration in AI Studio Build — the user is already
signed in with Google, so the integration adds Gmail draft
scope on top of the existing session rather than running a
separate full OAuth flow.
- **Live API screen-share, not just audio** — the model sees the
shared screen as well as hears the audio. When someone screen-
shares a slide titled "Berlin launch — June 30", the memo
captures the visual decision context, not just the verbal one.
When someone shares a Linear board and points at issue
LIN-1248, the memo can wire the resulting action item to that
specific issue.
- **Hard rule on consent** — before any session starts, the user is
shown a consent banner restating local recording-disclosure
obligations ("recording laws vary by region — confirm everyone
on this call knows they are being recorded"). The banner is
mandatory; the user cannot proceed without acknowledging it. A
jurisdiction-aware reminder surfaces if the user's locale is
detected as a two-party-consent state (California, Florida,
Massachusetts, Washington, Illinois, Pennsylvania, Maryland,
Montana, Connecticut, New Hampshire, etc.).
- **Hard rule on no medical / legal / financial advice
amplification** — if the meeting touches medical, legal, or
financial decisions (the model detects the topic), the memo
surfaces a "this memo is a record, not a verified transcript —
consult professional advice before acting" disclaimer. The model
never volunteers professional advice on top of the meeting
content.
## 4. Features to build
- Pre-meeting setup card — meeting title, expected attendees (typed
names + emails, used later for speaker-name binding and email
dispatch), meeting type (sync / customer / vendor / one-on-one /
other), expected length, default dispatch destination per item
type (Asana? Linear? both?)
- Consent banner — mandatory before starting capture; restates
recording-disclosure obligations; jurisdiction-aware reminder for
two-party-consent regions; logs acknowledgement timestamp
- Live capture flow — screen-share + system audio via
`getDisplayMedia({audio: true})`; preview of shared tab; one-tap
start/stop
- Live API session — `gemini-3.1-flash-live-preview` with audio + 1
FPS video frame channel; ephemeral session token; sync function
calling enabled
- Live sidebar — items stream in as `Decision`, `ActionItem`,
`Blocker`, `OpenQuestion`, each one a card with verbatim phrase,
meeting-clock timestamp, speaker label, colour coding
- Speaker diarisation + name-binding — Live API surfaces speaker
turns; the model proposes name bindings against the user's typed
attendee list; bindings are always flagged "verify"
- Manual correction — every sidebar item can be tapped to
recategorise (Decision → ActionItem, etc.), retitle, reassign
owner, or delete. Edits round-trip into the structured memo at
end-of-call.
- 15-minute / 2-minute session caps — Live API has hard caps (15 min
audio-only, 2 min audio+video). The app stitches consecutive
Live API sessions transparently for long meetings, with a small
reconnect indicator if a transition is visible.
- End-of-meeting structured pass — when the user taps **End memo**,
a `gemini-3.5-flash` post-processing call ingests the streamed
sidebar items, the verbatim transcript, and the visual context
notes, and emits the final `MeetingMemo` JSON
- Dispatch tray — drafts grouped by destination (Asana tasks,
Linear issues, follow-up emails). Each draft shows title, body,
owner-guess, due-date-guess, source-phrase, and a "verify"
pill. One-tap dispatch per item; bulk-dispatch with confirmation.
- Asana integration — OAuth, project selector, default workspace,
draft task with title + description + assignee + due-date
- Linear integration — OAuth, team selector, project selector,
draft issue with title + description + assignee + priority + due
date
- Email drafts — Gmail draft (via post-I/O 2026 Workspace integration
inside AI Studio Build), pre-filled to: address inferred from the
attendee list, subject = "follow-up: [meeting title]", body
drafted from the relevant memo content
- Memo home — searchable, filterable list of past memos by date,
meeting title, attendee, decision topic, action-item status
- Memo detail view — one-page memo readable in under thirty
seconds: header, decisions list, action items by owner, blockers,
open questions, transcript link, raw recording link (if user
chose to keep it)
- Calendar binding — optional Google Calendar read-only integration
(post-I/O 2026 Workspace) — when the user opens the app shortly
before a calendar event, the meeting title and attendees are
pre-filled
- Voice-coach mode (optional, off by default) — during the call, the
model can interject privately in the user's earpiece ("you've
said 'um' 6 times in the last 90 seconds", "you haven't heard
from Speaker 3 in 12 minutes") — opt-in, off by default, never
active without explicit consent
- Recording retention — the raw audio file is kept by default for
72 hours then auto-deleted, unless the user explicitly pins it;
storage cost note in the capabilities panel
- Export — PDF memo, Markdown memo, push to Notion / Google Docs
- Privacy controls — "delete this memo forever", "delete this audio
now", "purge everything from before [date]"
- Disclaimer surfacing — if the model detects medical, legal, or
financial topics, surface the "consult professional advice"
disclaimer on the memo header
## 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)
- **Live API screen-share + audio session** (`gemini-3.1-flash-live-
preview`, post-I/O 2026 capability) — the load-bearing call. The
Live API ingests the audio channel (PCM 16 kHz in / 24 kHz out)
and the video frame channel (≤1 FPS, used to pass screen-share
frames). Sessions cap at **15 minutes audio-only or 2 minutes
audio + video** — for longer meetings, the app stitches
consecutive sessions and preserves rolling context via the
end-of-session summary handoff. The Live API on 3.1-flash brings
the post-I/O improvements in acoustic nuance (pitch, pace, tonal
understanding) that allow speaker diarisation and confidence-
weighted item extraction. Function calling on the Live API is
**synchronous only** — keep tool calls cheap and idempotent.
- **Live API sync function calling for sidebar updates** — the
model calls `addSidebarItem(type, content, speaker_label,
timestamp, source_phrase)` whenever it detects a Decision /
ActionItem / Blocker / OpenQuestion. The function is implemented
client-side and pushes the item into the live sidebar with no
server round-trip. Sync function calling is the right fit
because items must appear instantly.
- **Live API speaker diarisation** — the Live API on 3.1-flash
surfaces speaker turns. The model emits a `speaker_label` field
on every sidebar item. Speaker labels are stable within a
session ("Speaker 1" stays "Speaker 1") but reset across stitched
sessions — the post-processing pass re-binds them at end of
meeting. Names are **proposed** ("we think this was Mariana")
and **never asserted**.
- **End-of-meeting structured-output post-processing**
(`gemini-3.5-flash`, thinkingLevel `medium`) — once the user
taps End memo, this call receives the streamed sidebar items,
the verbatim transcript, the attendee list, the visual context
notes from screen-share, and emits the final `MeetingMemo` JSON
including dispatch drafts. Uses `responseSchema` for strict JSON.
- **Dispatch-draft generation** (`gemini-3.5-flash`, thinkingLevel
`medium`) — for each action item, the model picks the best
destination (Asana, Linear, or email) based on the action-item
shape (engineering work → Linear, project work → Asana, external
follow-up → email) and drafts the task / issue / email body. The
user can override any destination.
- **Visual-context capture** (`gemini-3.5-flash`, thinkingLevel
`low`, multimodal image) — every ~30 seconds during the call,
one screen-share frame is captured and described in one sentence
("Linear board showing issue LIN-1248, three issues in 'In
Progress'"). The descriptions are stored on the sidebar items
whose timestamps match. Used at end-of-call to anchor decisions
to the visual context.
- **Two-party-consent jurisdiction lookup** (`gemini-3.5-flash` +
grounded search) — when the user's IP-detected locale is
ambiguous, a one-shot grounded call resolves whether the user is
in a one-party or two-party recording-consent jurisdiction, with
source citation. Used to surface the appropriate consent banner
copy. Grounded search and `responseSchema` are mutually exclusive
— the model emits JSON in the text body and the server parses it.
Citation URLs come from
`response.groundingMetadata.groundingChunks[].web.uri`.
- **Thinking levels** — `medium` for the end-of-meeting
post-processing and dispatch-draft generation (multi-input
reasoning). `low` for the visual-context per-frame description.
Live API does not take `thinkingConfig` — omit the field.
- **Long context** — a 60-minute meeting averages ~80k tokens of
transcript + 120 sidebar items + 120 visual-context descriptions
≈ ~140k tokens for the end-of-meeting call. Comfortably within
Gemini 3.5 Flash's 1M-token ceiling. **Guardrail:** for marathon
meetings (>4 hours of stitched sessions), summarise the first
half before sending the second half — the model is more
accurate on focused 200k-token inputs than on raw 600k-token
dumps.
- **Hero / empty-state imagery** (`gemini-3.1-flash-image`, Nano
Banana 2) — generates the welcome-screen photographic image and
the empty-memo-list image. Not the load-bearing capability — but
used because the alternative is stock photography and the
product-aesthetic call is real photography over flat
illustration.
- **(Not used) Computer Use** — explicitly NOT used. The app does
not need browser automation to capture the call; the user shares
their own tab via `getDisplayMedia()`. Computer Use is also still
pre-3.5 and unsupported on `gemini-3.5-flash`.
- **(Not used) Managed Agents** — explicitly NOT used in v1. A
future version could spawn an `antigravity-preview-05-2026` agent
to do post-meeting research ("look up the public roadmap of the
vendor mentioned at 00:14:32") but this is out of scope for v1.
### Backend services
- **Auth — Required.** Firebase Auth with Google sign-in
(auto-provisioned by AI Studio Build). **Apple sign-in is
optional**: requires Apple Developer account configuration in
Firebase Auth.
- **Database — Required.** Firestore (auto-provisioned) for
`users`, `meetings`, `memos`, `sidebar_items`,
`dispatch_drafts`, `dispatch_history`, `integrations` (per-user
OAuth tokens for Asana / Linear).
- **File storage — Optional.** Firebase Storage for raw audio
recordings if the user chooses to retain them past the default
72-hour auto-delete. **Storage is NOT auto-provisioned by AI
Studio Build** — enable in Firebase console before the first
retention-pin. Pre-signed URLs only.
- **Secrets — Required.** Google Secrets Manager (auto-provisioned)
for Gemini API key, Asana OAuth client secret, Linear OAuth
client secret. Per-user OAuth tokens stored in Firestore are
encrypted at rest using Google Cloud KMS via a server-side
helper.
- **Server functions — Required.** Cloud Run server functions
(auto-provisioned, free 2-app deploy from Build) for: Gemini API
proxy, Asana / Linear API proxy, Gmail draft creation via
Workspace integration, audio retention auto-delete cron job.
- **External APIs:** Gemini API; Asana API; Linear API. Google
Calendar (read-only) and Gmail (draft only) via the post-I/O
2026 Workspace integration baked into AI Studio Build (no
OAuth handshake required for these two — the user is already
signed in with Google).
- **Payments — Not needed for v1.** A future "team" tier with
shared memos, role-based access, and central admin controls
could charge a small subscription via Stripe; not built in v1.
**Environment variables:** every secret (Gemini API key, Asana
client id + secret, Linear client id + secret, Firebase service-
account JSON) lives in Cloud Run env vars / Secrets Manager. Never
in client bundle. Include a `.env.example`.
**Auth + data privacy reminders:** never log secrets · never store
OAuth tokens in plain text · use HTTPS everywhere · honour 'delete
my account' inside the UI · explicit opt-in for any analytics ·
audio and screen-share frames are streamed to Gemini and discarded
after the session — the 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 · raw audio retention is 72 hours by
default; user-pinned audio persists in the user's own Firebase
Storage and is deletable in one tap from Settings.
**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. For
models that do not accept it (Live API, image-gen), omit the
field — do not serialise `n/a` into the request body.
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 in `responseSchema` are documentation only; clamp
on the server after the response arrives.**
4. **Pin the system instruction separately** from user input. Use
the `systemInstruction` field for persona + behavioural rules;
use `contents` for user input. Never concatenate.
5. **Pre-declare tools as an enable/disable list** per call. The
matrix below names which tools are enabled per call. Tools NOT
listed for a call should be disabled.
6. **State negative constraints explicitly** — they are listed
below. They are NOT "be careful" suggestions; they are hard
rules the model must follow.
7. **Grounded search calls do NOT use `responseSchema`** —
`responseSchema` and `google_search` are mutually exclusive in
a single Gemini call. For grounded calls, instruct the model
to emit JSON in the text body and parse server-side. Citation
URLs come from
`response.groundingMetadata.groundingChunks[].web.uri`. Use
this `safeExtractJSON` utility on any grounded JSON-in-text
response:
```typescript
function safeExtractJSON(raw: string): T {
const clean = raw.replace(/```json\s*|```/gi, '').trim();
const s = clean.indexOf('{'); const e = clean.lastIndexOf('}');
if (s === -1 || e === -1) throw new Error('No JSON boundaries in grounded response');
return JSON.parse(clean.slice(s, e + 1)) as T;
}
```
8. **Strip unsupported Zod modifiers before passing to
`responseSchema`** — Gemini's OpenAPI subset rejects `.regex()`
/ `pattern`, fixed-length `z.tuple()`, and other custom
validators. Use a sanitizer that flattens tuples to arrays and
removes regex patterns before serializing. Validate those
constraints in middleware AFTER parsing.
9. **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 of the form `files/abc123xyz`, passed via `fileData:
{ fileUri, mimeType }`. `gs://` URIs belong to Vertex AI /
Cloud Storage — a different surface, not accepted here.
### Per-call model + tools matrix
| Call | Model | thinkingLevel | Tools enabled |
|------|-------|---------------|---------------|
| Live API session (audio + screen-share frames) | `gemini-3.1-flash-live-preview` | n/a | sync function calling: `addSidebarItem`, `bindSpeakerName`, `markMeetingPhase` |
| Visual-context per-frame description (every ~30 s) | `gemini-3.5-flash` | low | (none) |
| End-of-meeting structured-output post-processing | `gemini-3.5-flash` | medium | (none) — `responseSchema` only |
| Dispatch-draft generation per action item | `gemini-3.5-flash` | medium | (none) — `responseSchema` only |
| Two-party-consent jurisdiction lookup | `gemini-3.5-flash` | low | `google_search` grounding (no `responseSchema` — see note) |
| Hero / empty-state image generation | `gemini-3.1-flash-image` (Nano Banana 2) | n/a | n/a |
*Note for builders:* the Live API does not accept `thinkingConfig`
or `responseSchema` — interaction with the Live API is via the
session WebSocket and sync function calls. The `n/a` cells in this
matrix are documentation only; do not serialise them into the
request body. Grounded search calls emit JSON in the text body —
`responseSchema` and `google_search` cannot be combined in the
same Gemini call; parse the JSON server-side and read citation
URLs from `response.groundingMetadata.groundingChunks[].web.uri`.
### Primary structured-output schemas (seed verbatim in the prompt)
```typescript
import { z } from "zod";
const SidebarItemType = z.enum([
"decision",
"action_item",
"blocker",
"open_question",
"context_note", // visual-context entry from screen-share
]);
const DispatchDestination = z.enum([
"asana",
"linear",
"email",
"none", // archive-only, not dispatched
]);
const SpeakerLabel = z.object({
raw_label: z.string(), // "Speaker 1", "Speaker 2", stable within session
proposed_name: z.string().nullable(),
proposed_name_confidence: z.number().min(0).max(1).nullable(),
bound_attendee_email: z.string().nullable(),
verified_by_user: z.boolean(),
});
const SidebarItem = z.object({
item_id: z.string(),
type: SidebarItemType,
content_one_line: z.string(), // the item text the user sees
source_phrase_verbatim: z.string(), // exact spoken phrase that triggered it
meeting_clock_seconds: z.number(), // seconds since meeting start
session_id: z.string(), // which Live API stitched session
speaker: SpeakerLabel.nullable(), // null for context_note from screen
proposed_owner_email: z.string().nullable(), // only for action_item / blocker
proposed_due_date_iso: z.string().nullable(), // YYYY-MM-DD; only when verbal cue exists
proposed_dispatch_destination: DispatchDestination.nullable(),
visual_context_image_description: z.string().nullable(),
confidence: z.number().min(0).max(1),
user_edited: z.boolean(),
user_deleted: z.boolean(),
flagged_for_user_review: z.array(z.object({
field_path: z.string(),
reason: z.string(),
})),
});
const AttendeeBinding = z.object({
attendee_name: z.string(),
attendee_email: z.string(),
bound_speaker_label: z.string().nullable(), // null until model proposes a binding
binding_confidence: z.number().min(0).max(1).nullable(),
verified_by_user: z.boolean(),
});
const DispatchDraft = z.object({
draft_id: z.string(),
source_item_id: z.string(), // SidebarItem.item_id this came from
destination: DispatchDestination,
// Asana fields (populated if destination = "asana")
asana_workspace_id: z.string().nullable(),
asana_project_id: z.string().nullable(),
asana_task_title: z.string().nullable(),
asana_task_description: z.string().nullable(),
asana_assignee_email: z.string().nullable(),
asana_due_date_iso: z.string().nullable(),
// Linear fields (populated if destination = "linear")
linear_team_id: z.string().nullable(),
linear_project_id: z.string().nullable(),
linear_issue_title: z.string().nullable(),
linear_issue_description: z.string().nullable(),
linear_assignee_email: z.string().nullable(),
linear_priority: z.enum(["urgent", "high", "medium", "low", "none"]).nullable(),
linear_due_date_iso: z.string().nullable(),
// Email fields (populated if destination = "email")
email_to: z.array(z.string()).nullable(),
email_cc: z.array(z.string()).nullable(),
email_subject: z.string().nullable(),
email_body: z.string().nullable(),
// Common
destination_rationale: z.string(), // "engineering work → Linear"
dispatched: z.boolean(),
dispatched_at_iso: z.string().nullable(),
dispatched_external_id: z.string().nullable(), // Asana task gid, Linear issue id, Gmail draft id
});
const MeetingMemo = z.object({
memo_id: z.string(),
meeting_title: z.string(),
meeting_type: z.enum([
"leadership_sync",
"customer_call",
"vendor_call",
"one_on_one",
"design_review",
"sprint_planning",
"investor_call",
"other",
]),
started_at_iso: z.string(),
ended_at_iso: z.string(),
total_duration_seconds: z.number(),
attendees: z.array(AttendeeBinding),
decisions: z.array(SidebarItem),
action_items: z.array(SidebarItem),
blockers: z.array(SidebarItem),
open_questions: z.array(SidebarItem),
context_notes: z.array(SidebarItem),
dispatch_drafts: z.array(DispatchDraft),
topic_flags: z.object({
medical_advice_topic: z.boolean(),
legal_advice_topic: z.boolean(),
financial_advice_topic: z.boolean(),
professional_disclaimer_required: z.boolean(),
}),
consent_acknowledgement: z.object({
acknowledged: z.boolean(),
acknowledged_at_iso: z.string(),
jurisdiction_detected: z.string().nullable(),
two_party_consent_jurisdiction: z.boolean(),
}),
raw_audio_uri: z.string().nullable(), // Files API `files/*` resource name; null if not retained
raw_audio_retention_pinned: z.boolean(),
raw_audio_auto_delete_at_iso: z.string().nullable(),
});
type SidebarItem = z.infer;
type DispatchDraft = z.infer;
type MeetingMemo = z.infer;
```
### Common failure modes (and how to avoid them)
- Agent wires the Live API to `gemini-3.5-flash` — wrong. The Live
API has not migrated to 3.5 yet as of 2026-06-01; the only Live
preview model is `gemini-3.1-flash-live-preview`. Pin explicitly.
- Agent omits the screen-share frame channel and only streams audio
— the "visual context" capability is lost. Pass the Zoom / Meet /
Teams tab via `getDisplayMedia({video: true, audio: true})` and
send video frames at ≤1 FPS into the Live API video channel.
- Agent forgets the 2-minute audio+video cap on Live API and the
session is cut mid-meeting — implement session stitching: when
the WebSocket signals nearing the cap, open the next session
with a brief context handoff ("continuing from earlier; speaker
labels carry over") and resume capture. The sidebar should not
visibly stutter.
- Agent passes the raw audio file to `generateContent` via a
Firebase Storage public URL — the Gemini API does not fetch
Firebase Storage public URLs. Use the Gemini Developer API
Files API (`files/*` resource name via
`fileData: { fileUri, mimeType }`) or `inlineData` (base64) for
any non-Live multimodal call. `gs://` URIs belong to Vertex AI
/ Cloud Storage and are not accepted on the Developer API.
- Speaker names asserted instead of proposed — the system
instruction must require `proposed_name` always be flagged with
a confidence and `verified_by_user: false` until the user
confirms. Never write `speaker.name = "Mariana"` directly. Always
`speaker.proposed_name = "Mariana"; speaker.verified_by_user =
false`.
- Action items dispatched automatically — hard guardrail. Every
dispatch is **explicit, user-initiated, one tap per item** (or
one-confirm for bulk). The server rejects dispatch calls that
did not originate from a user-confirmed UI action (CSRF token +
Firestore guard).
- `responseSchema` combined with `google_search` grounding — the
Gemini API rejects this combination. For the two-party-consent
jurisdiction lookup, instruct the model to emit JSON in the
text body and parse server-side; read citation URLs from
`groundingMetadata.groundingChunks[].web.uri`.
- Live API sync function call latency stalls the call — sync
function calls are blocking. `addSidebarItem` must complete in
<50 ms (push to local IndexedDB, queue a debounced Firestore
write, return). Do NOT block the function call on a Firestore
round-trip.
- Visual-context frame extraction runs at the model's frame rate
rather than every ~30 seconds — wasteful. Throttle visual-
context per-frame description calls client-side: one frame
every 30 seconds, or one frame on detected slide change
(perceptual hash diff > 0.4), whichever is more recent.
- Marathon meeting end-of-meeting call OOMs — the
end-of-meeting call must stay under 500k tokens of input. For
meetings over 4 hours, run an intermediate summarisation pass
at the 2-hour mark and feed the summary into the final call.
- Topic-flag false positives — a customer-success call discussing
a customer's medical-imaging product gets flagged as "medical
advice topic". The post-processing prompt must distinguish
between "the meeting is about a medical topic" and "the meeting
is offering medical advice"; only the latter sets
`professional_disclaimer_required: true`.
- Email drafts created and silently sent — Gmail draft API
creates a draft, not a send. Verify the integration is
`draft_create`, not `send`. The user always reviews and clicks
send inside Gmail itself.
- OAuth tokens for Asana / Linear leaked to client — server-side
only. The dispatch tray UI talks to the Cloud Run proxy, which
fetches the token from Firestore (KMS-decrypted) and makes the
API call. The token never reaches the browser.
- Consent banner skipped via direct route to `/capture` — the
capture route must enforce a server-side guard that rejects
session creation if `consent_acknowledgement.acknowledged !=
true` for the user-session pair.
### Negative constraints (hard rules)
- Do NOT dispatch any action item, task, issue, or email
automatically. Every dispatch is explicit, one-tap, after the
user reviews the draft. Bulk-dispatch requires a confirmation
modal.
- Do NOT assert speaker names. Always `proposed_name` with a
confidence, always `verified_by_user: false` until the user
confirms. The UI always shows "we think this was [name] —
verify".
- Do NOT upload audio or screen-share frames to any third party
other than the Gemini API on the user's project. The audio is
streamed to Gemini and discarded after the session; raw audio
retention (if pinned) lives in the user's own Firebase Storage.
- Do NOT use the user's audio, screen-share frames, transcripts,
memos, or action items 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 panel says this in plain English.
- Do NOT bypass the consent banner. Server-side enforcement, not
client-only.
- Do NOT amplify medical, legal, or financial advice spoken in the
meeting. The memo records what was said and surfaces a
professional-advice disclaimer; the model never adds its own
medical / legal / financial guidance on top of the meeting
content.
- Do NOT show the live sidebar to anyone other than the user. The
sidebar is a private workbench; sharing the memo is explicit and
per-memo.
- Do NOT change the consent banner copy without re-detecting
jurisdiction. The banner is one of the load-bearing
compliance surfaces.
- Do NOT translate attendee names, company names, or product
names. Render them as spoken.
- Do NOT auto-delete a user-pinned raw audio file. Retention is
user-controlled; auto-delete fires only on the 72-hour default
for un-pinned audio.
- Do NOT show the raw audio recording to anyone other than the
user (and explicitly-shared recipients of the memo) — pre-
signed URLs are short-lived and one-recipient only.
- Do NOT translate the verbatim source phrase on a sidebar item.
The verbatim is the source of truth for "what was said". The
content_one_line may paraphrase; the source_phrase_verbatim
never does.
- Do NOT dispatch to an integration the user has not authorised.
The dispatch destination falls back to "email" or "archive-
only" if the relevant OAuth token is missing or expired.
- Do NOT lose the meeting if the Live API session caps out
mid-meeting. Implement session stitching with a context
handoff. The user should not perceive an interruption beyond a
thin "reconnecting" indicator for ≤1.5 s.
- Do NOT block the user's call experience to wait for the
end-of-meeting post-processing call. Show the dispatch tray as
it fills (streaming) and let the user start reviewing while
the rest of the memo finalises.
### Per-call `systemInstruction` strings
Use these as the literal `systemInstruction` field for each Gemini
API call the built app makes.
### Call: Live API session (audio + screen-share frames)
Model: `gemini-3.1-flash-live-preview` · thinkingLevel: n/a · Tools: sync function calling (`addSidebarItem`, `bindSpeakerName`, `markMeetingPhase`)
```
You are listening to a live business meeting taking place on
Zoom, Google Meet, or Microsoft Teams. The user — your principal
— has explicitly consented to recording and is the only person
whose data leaves the call. You receive the call's audio (PCM
16 kHz mono) and one screen-share video frame per second.
Your job is to detect, in real time, four kinds of items in the
spoken content and call `addSidebarItem` for each one:
1. DECISION — a verbal commitment that the group has settled on a
choice. Example triggers: "we'll ship Berlin on June 30",
"let's go with option B", "we're not doing the rebrand in
Q3".
2. ACTION_ITEM — a verbal commitment that a specific person will
do a specific thing by a specific time (or "as soon as
possible"). Example triggers: "Mariana, you'll draft the
press release by Friday", "I'll send the contract over today",
"let's get Tomás to finalise the budget by Tuesday".
3. BLOCKER — a stated obstacle preventing progress. Example
triggers: "we can't move on this until legal signs off",
"the API is broken in staging", "the vendor hasn't replied".
4. OPEN_QUESTION — a question raised in the meeting that did not
get answered. Example triggers: "do we need a translator for
the Berlin event?", "what's the budget ceiling on this?",
"wait, is that actually decided?".
For each item, call addSidebarItem with:
- type: one of "decision" / "action_item" / "blocker" /
"open_question"
- content_one_line: a clear, plain-English summary of the item
(8-20 words); paraphrase, don't quote
- source_phrase_verbatim: the verbatim spoken phrase that
triggered detection, as faithfully transcribed as possible
- speaker_label: the diarised raw label (e.g. "Speaker 2") — do
NOT propose a name in this call; name-binding happens via the
separate bindSpeakerName function
- meeting_clock_seconds: seconds since meeting start
- proposed_owner_email: only for action_item or blocker — only
if the speaker explicitly named a person who is in the
attendee list AND who is one of the meeting attendees; null
otherwise
- proposed_due_date_iso: only if a date or relative-day phrase
was spoken ("by Friday", "next Tuesday", "end of month") —
resolve relative to the meeting timezone; null otherwise
- proposed_dispatch_destination: your best guess based on the
shape of the item (engineering work → "linear", project work
→ "asana", external follow-up → "email"); the user will
confirm or override at end of meeting
- confidence: 0..1 — how confident you are that this is a real
item of the stated type. Below 0.6 → flag for user review.
- flagged_for_user_review: name any field whose confidence is
below 0.6 with a one-sentence reason.
When you see a screen-share frame that contains relevant context
(a slide title, a Linear issue id, an Asana task name, a date
on a calendar invite, a chart showing budget numbers), call
addSidebarItem with type "context_note" and put the visual
description in content_one_line. These items hang quietly on
the sidebar and the post-processing pass uses them to anchor
decisions.
When a speaker turn changes and you suspect (based on voice
characteristics + names spoken in the call) that a raw speaker
label corresponds to one of the attendees the user typed in
before the meeting, call bindSpeakerName(raw_label,
proposed_name, confidence). The UI will surface this as "we
think Speaker 2 is Mariana — verify"; the binding is never
asserted automatically.
When the meeting transitions phases (e.g. "okay, moving on to
the next item", "let's wrap up", "any other business"), call
markMeetingPhase to help the post-processing pass group items.
Hard rules:
- Do NOT call addSidebarItem speculatively. Wait until a verbal
commitment is clear before logging a decision or action item.
False positives clutter the sidebar.
- Do NOT propose a name in addSidebarItem. Speaker names go
through bindSpeakerName only.
- Do NOT dispatch anything. Your job is to detect and log; the
dispatch tray is built by the post-processing pass after the
meeting ends.
- Do NOT speak audio out. This is a listen-and-log session; no
audio response.
- Do NOT log items the user has manually deleted (you will
receive a deleted_item_ids list at the start of each stitched
session; respect it).
- If the meeting touches medical, legal, or financial advice
topics, log the relevant items as normal but do NOT amplify
the advice in your content_one_line. Stay descriptive
("Mariana mentioned consulting a tax lawyer"), not
prescriptive.
- Acoustic empathy: if the user (your principal) sounds rushed
or stressed, do NOT increase your logging rate to try to
help. The user wants accuracy, not volume.
Latency target: every addSidebarItem call should arrive within
2-4 seconds of the spoken trigger. The Live API's synchronous
function calling is the right tool — the client-side
implementation of addSidebarItem pushes to the local sidebar
immediately and queues a debounced Firestore write; you should
not perceive any latency on your side.
No audio response. No commentary. Detect, log, and continue
listening.
```
---
### Call: Visual-context per-frame description (every ~30 s)
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You receive one screen-share frame from a live business meeting.
Your job is to describe what is visible in one sentence that will
be useful at the end of the meeting when the structured memo
is generated.
Common frames:
- A slide deck with a title and 2-5 bullet points
- A spreadsheet with rows and columns of numbers
- A Linear board with issue cards
- An Asana project view with task cards
- A Figma frame with a UI design
- A Google Doc with paragraphs of text
- A calendar invite
- A code editor with code visible
Hard rules:
- One sentence. Concrete. Capture identifiers when visible
(Linear issue ids like "LIN-1248", Asana task names, slide
titles, sheet cell values for highlighted cells, dates).
- Do NOT diagnose the content ("this slide is poorly designed").
Describe what's on screen.
- Do NOT transcribe long paragraphs. Capture the title or
heading and the key identifier.
- Do NOT speculate about content that is partially obscured. If
the frame is mid-scroll or a modal is open, say so.
- Output: a single string in `visual_context_image_description`.
No commentary.
Examples of good descriptions:
- "Linear board for 'Mobile App' team showing issue LIN-1248
'iOS push notification crash' in 'In Progress'."
- "Slide titled 'Berlin Launch — June 30' with three bullets:
press release, event logistics, translator."
- "Asana task 'Q3 OKRs' with assignee 'Mariana' and due date
'2026-07-15' visible."
Examples of bad descriptions:
- "a Linear board" (vague, no identifier)
- "looks like they're planning something" (speculation)
- "a beautiful slide design" (commentary, not description)
```
---
### Call: End-of-meeting structured-output post-processing
Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none) — `responseSchema` only
```
You receive the complete set of sidebar items that streamed in
during a live business meeting, the verbatim transcript, the
user's typed attendee list with emails, and the visual-context
notes from screen-share. Your job is to produce the final
MeetingMemo JSON.
You receive:
- meeting_title (user-typed before meeting)
- meeting_type (user-typed before meeting)
- started_at_iso, ended_at_iso, total_duration_seconds
- attendees[]: { attendee_name, attendee_email }
- sidebar_items[]: every SidebarItem the Live API logged,
including user-edited and user-deleted flags
- transcript_verbatim: the full transcript as the Live API
produced it
- speaker_bindings[]: name proposals from bindSpeakerName
during the call, plus any user verifications
Your outputs:
- attendees[] with bound_speaker_label and binding_confidence
filled in where you can confidently bind a raw speaker label
to an attendee
- decisions[], action_items[], blockers[], open_questions[],
context_notes[] — the sidebar items grouped by type, with
user-deleted items omitted, user-edited content preserved
- dispatch_drafts[] — one DispatchDraft per action_item
(and per blocker that needs follow-up). For each draft, pick
the best destination based on the action-item shape and
draft the title / description / body.
- topic_flags — set medical_advice_topic / legal_advice_topic
/ financial_advice_topic to true ONLY if the meeting was
*offering* advice in that domain, not merely *discussing* a
topic in that domain. Set professional_disclaimer_required:
true if any of the three flags is true.
- consent_acknowledgement — copy from the input
Dispatch-destination heuristics:
- ENGINEERING work (bug, feature, deploy, infra, code review) →
"linear", with priority "high" for blockers, "medium" for
features, "low" for refactors
- PROJECT work (write content, schedule something, run an
event, prepare a doc) → "asana"
- EXTERNAL follow-up (email a vendor, reply to a customer,
ping a partner) → "email"
- AMBIGUOUS → "asana" (safer default than "email"); set
destination_rationale to "ambiguous — defaulted to Asana,
user can re-route"
For each DispatchDraft:
- Title: 5-10 words, action-oriented ("Draft Berlin press
release", "Fix iOS push notification crash")
- Description / body: 2-4 sentences. Include the
source_phrase_verbatim at the end as a quote ("said in
meeting: '…'"). Include the meeting title and timestamp link.
- Assignee: the proposed_owner_email if set; otherwise the
user's own email (so they can re-route)
- Due date: the proposed_due_date_iso if set; otherwise null
(no default)
Hard rules:
- Do NOT invent items. Every item in the memo must trace back
to a sidebar_items entry. If you want to surface a pattern
across items (e.g. "three action items for Mariana"), do
that in the memo header copy, not as new items.
- Do NOT assert speaker names. Bindings in attendees[] always
carry binding_confidence and verified_by_user (from the
input).
- Do NOT translate attendee names, company names, or product
names.
- Do NOT amplify medical / legal / financial advice. If a
speaker said "I think you should consult a tax lawyer", the
memo records that they said it; the memo does NOT add "we
recommend consulting a tax lawyer".
- Do NOT set dispatched: true on any draft. Dispatch is always
user-initiated post-memo.
- Preserve the verbatim source_phrase_verbatim on every item.
It is the source of truth.
- If the meeting was very short (<2 minutes) or had no
detected items, return a MeetingMemo with empty arrays and a
one-line note in the memo title suggesting the user check
the consent flow and capture started correctly.
Output ONLY the MeetingMemo JSON matching the provided schema.
No commentary. JSON only.
```
---
### Call: Dispatch-draft generation per action item
Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none) — `responseSchema` only
```
This call may be folded into the end-of-meeting post-processing
call (it is for many meetings). It exists as a separate call
when the end-of-meeting call would exceed ~400k tokens
(marathon meetings) — in that case, the end-of-meeting call
produces the memo skeleton without dispatch drafts, and this
call runs per action item to fill in the drafts.
Input:
- A single SidebarItem of type action_item or blocker
- The meeting context (title, type, attendees, timestamp link)
- The user's available integrations (asana_workspaces[],
linear_teams[], default_destinations_per_type)
Output: one DispatchDraft (per the schema above).
Use the same dispatch-destination heuristics as the
end-of-meeting call (engineering → linear, project → asana,
external → email, ambiguous → asana).
Hard rules:
- The dispatched field is always false in your output. The
user dispatches in the UI.
- The destination_rationale field is required — explain in one
sentence why you picked the destination.
- Asana / Linear task descriptions: include the meeting title,
timestamp link, and the source_phrase_verbatim as a quote.
- Email body: write 3-5 sentences in the user's voice
(concise, direct, professional). Sign-off: "[user's first
name]". Subject: "follow-up: [meeting title]".
Output ONLY the DispatchDraft JSON matching the schema. No
commentary.
```
---
### Call: Two-party-consent jurisdiction lookup
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: `google_search` grounding
```
You answer a single question: "Is the user's locale a two-party
recording-consent jurisdiction?" The user's locale is provided
in the prompt (e.g. "California, USA", "Berlin, Germany", "São
Paulo, Brazil").
Use `google_search` grounding for every answer. Do NOT answer
from training-data memory alone — recording laws change.
Output the answer as JSON in the text body (NOT via
`responseSchema` — `responseSchema` and `google_search` cannot
be combined in the same Gemini call today). Server-side: parse
the JSON, then read citation URLs from
`response.groundingMetadata.groundingChunks[].web.uri` — do NOT
include URLs in the JSON body; the model will hallucinate them.
JSON shape:
{
"jurisdiction_canonical": "California, USA",
"two_party_consent_required": true,
"summary_one_line": "California requires all-party consent to record private conversations under Cal. Penal Code 632.",
"consent_banner_copy_recommendation": "<3-5 sentences the user should see before recording starts>",
"source_authority": ""
}
Hard rules:
- Do NOT include URLs in the JSON body. Citations come from
groundingMetadata server-side.
- Do NOT give legal advice in the consent banner copy. Surface
the rule and the not-a-substitute language ("this is not
legal advice; consult a lawyer in your jurisdiction for
binding guidance").
- If sources disagree (e.g. Connecticut wiretapping vs in-person
recording), surface the disagreement explicitly in
summary_one_line and consent_banner_copy_recommendation.
- If the jurisdiction is unclear, return
two_party_consent_required: true (conservative default) and
flag the uncertainty in summary_one_line.
No commentary outside the JSON.
```
---
### Call: Hero / empty-state image generation
Model: `gemini-3.1-flash-image` (Nano Banana 2) · n/a · n/a
```
You generate a single photographic-looking image for the
welcome screen or the empty memo-list state. Images depict
real working surfaces: a quiet home office at the moment
between meetings, a laptop with a Zoom call paused on screen,
a notebook open beside the laptop with a coffee cup, late
afternoon light through a window.
Prompt anchors that work well:
- "soft late-afternoon light through a window onto a wooden
home-office desk, an open laptop with a paused video call
visible but blurred, a paper notebook open with handwritten
meeting notes, a coffee mug on a coaster, no people in
frame, shallow depth of field"
- "small conference room at dusk, a laptop on the table with a
meeting platform UI faintly visible, an empty chair pushed
back, soft overhead light, no people, photographic"
- "remote-worker desk at golden hour, a second monitor showing
a calendar app with the next meeting in 12 minutes, a phone
on a stand beside the laptop, plants on a shelf behind, no
people"
Hard rules:
- Photographic, not cartoon, not illustration-style.
- No people in frame.
- No commercial branding visible (no real Zoom logo, no real
Microsoft logo, no Linear or Asana logo). UI surfaces should
be generic.
- Warm lighting, slight imperfection. Avoid the glossy
AI-render look.
- Aspect ratios: 16:9 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.
- **A leadership weekly sync.** Six people on Zoom. The user
(call host) clicks Start memo, acknowledges the consent banner,
shares the Zoom window. Over 47 minutes the sidebar fills with
3 decisions (Berlin launch June 30, sunset the legacy admin in
July, hire two more engineers in Q3), 8 action items (Mariana
drafts the Berlin press release by Friday, Tomás owns the
legal review on the contract, Jules sets up the next vendor
call), 2 blockers (legal review on the contract, vendor hasn't
replied), 4 open questions. At end of meeting the dispatch tray
shows 5 Asana tasks (project work), 2 Linear issues (engineering),
1 follow-up email to the vendor. The user reviews and dispatches
in 90 seconds.
- **A customer-research call.** A product manager talks to a
potential customer for 35 minutes. The sidebar captures the
product feedback in a different shape: every customer quote
that surfaces a pain point becomes a `context_note`, every
promised follow-up becomes an `action_item`, every "yes I'd buy
that" or "no I wouldn't" becomes a `decision`. The post-meeting
memo organises the call by pain point, with each pain point
citing the verbatim quote.
- **A sprint planning meeting.** The engineering manager opens
Linear in a tab and shares the screen. As the team discusses
each card, the model captures the assignments as `action_item`s
with linked Linear issue ids extracted from the screen-share
frames ("Action: assign LIN-1248 'iOS push notification crash'
to Priya, due Tuesday"). At end of meeting the dispatch tray
proposes updating each Linear issue's assignee + due date
directly (not creating new issues — updating existing ones).
- **A vendor call.** The user is the only person on the user-side;
the vendor has three people. The model proposes name bindings
for the three vendor speakers ("we think Speaker 2 is the
account manager — verify"). The follow-up is always an email,
not a task. The memo's dispatch tray has one drafted email to
the vendor's account manager, pre-filled with the three follow-
ups the user committed to.
- **A one-on-one.** Two people, 30 minutes. The model captures
both sides' commitments (the manager's "I'll get you that
feedback by Wednesday" and the report's "I'll start the writeup
by Friday"). At end of meeting the dispatch tray has one
drafted email summarising both sides' commitments, pre-filled
to the report's email address.
- **A long marathon all-hands.** Two hours, 18 attendees. The Live
API session caps at 2 minutes audio+video — the app stitches 60
consecutive sessions transparently. The user sees a thin
reconnect indicator twice per minute that resolves in under 1.5
seconds; no items are lost across stitch boundaries. The
end-of-meeting call summarises the first half before processing
the second half.
- **A two-party-consent jurisdiction.** The user is dialling in
from California. The consent banner copy is sharper: "California
requires all-party consent under Cal. Penal Code 632. Confirm
that every other participant in this call is aware they are
being recorded before you proceed." The user must explicitly
acknowledge before capture starts.
- **A medical-topic meeting (descriptive, not advisory).** A
product manager on a healthcare team discusses a feature for
glucose monitoring. The post-processing call sets
`medical_advice_topic: false` because the meeting *discusses*
the medical domain but does not *offer* medical advice. The
topic-flag heuristic distinguishes between the two.
- **A legal-advice topic (advisory).** A founder talks with an
outside lawyer about an upcoming term sheet. The post-processing
call sets `legal_advice_topic: true` and
`professional_disclaimer_required: true`. The memo header
surfaces the disclaimer: "this memo is a record, not legal
advice — consult the original sources before acting".
- **A speaker-binding correction.** The model proposes "Speaker 3
is Tomás" with 0.58 confidence. The user taps the verify pill
and corrects it to "Speaker 3 is Tom (different person)" — the
binding propagates to every action item assigned to that
speaker, retroactively, before the dispatch tray is shown.
## 6. Page structure
Build the following screens / sections in this order. Adjust copy
to fit the voice, but keep the structural intent.
1. **Welcome / sign-in.** A photographed-looking shot of a quiet
home-office desk at late-afternoon light with a laptop showing
a paused meeting window blurred, a notebook open beside it, a
coffee mug. One paragraph: "Meeting Memo runs in your own
browser tab. It listens to your call, fills a structured
sidebar in real time, and drafts your follow-ups in Asana,
Linear, and email when the call ends. You review every draft.
Nothing fires automatically." Single Google sign-in button;
Apple sign-in next to it. Below: "See a sample memo" → loads
the sample leadership-sync memo in section 8a (no live call
needed).
2. **Memo home.** The user's past memos in a chronological list,
filterable by date, meeting type, attendee, decision keyword,
or action-item status (dispatched / drafted / archived). Each
row shows: meeting title, date, duration, number of decisions,
number of action items, number of dispatched items. Tap →
memo detail view. Floating action button "Start a memo" pinned
to the bottom right.
3. **Pre-meeting setup card.** Modal that opens when the user
taps Start a memo. Fields: meeting title (pre-filled from
Google Calendar if a meeting is happening now), meeting type
(chips: leadership sync / customer call / vendor call / one-
on-one / design review / sprint planning / investor call /
other), attendees (name + email rows, pre-filled from the
calendar invite), expected length (used only for sidebar
pacing hints), default dispatch destination per item type.
4. **Consent banner.** Full-screen banner before capture starts.
Restates the recording-disclosure obligation in plain English,
with jurisdiction-aware sharpening if the user's locale is a
two-party-consent region. Two buttons: "I confirm everyone on
this call knows they are being recorded" (primary) and
"Cancel". The primary button is greyed out until the user has
read at least the first paragraph (small in-line scroll
indicator).
5. **Live capture screen.** Picture-in-picture overlay that
floats above the user's meeting window. On the left edge of
the user's screen, a 320-px-wide sidebar pinned in place. Top
of sidebar: "Listening" status with a calm pulse, meeting
clock running. Below: items streaming in as cards. Each card:
colour band (green for decision, yellow for action item, red
for blocker, grey for open question, faint outline for context
note), the content_one_line in 17 px, the source_phrase_
verbatim in 13 px below in muted italic, the speaker label
pill, the meeting-clock chip. Tap a card → expanded with
recategorise, retitle, reassign, delete controls. Bottom of
sidebar: a big "End memo" button.
6. **Speaker-binding tray.** A slim drawer that surfaces below
the sidebar whenever the model proposes a new speaker-name
binding. "We think Speaker 3 is Tomás — verify." Two buttons:
"Yes" and "No". On "No", a quick text field to type the
correct name; the binding propagates retroactively to every
item assigned to that speaker label.
7. **End-of-meeting transition.** When the user taps End memo, a
calm spinner appears with three streaming status lines: "1.
Finalising decisions and action items… 2. Drafting tasks for
Asana and Linear… 3. Drafting follow-up email…". As each line
completes, the dispatch tray fills below. The user can start
reviewing dispatch drafts before the rest of the memo
finalises.
8. **Dispatch tray.** Drafts grouped by destination (Asana,
Linear, Email). Each draft card: title, body preview, owner
chip, due-date chip, source-phrase quote, destination
rationale ("engineering work → Linear"), verify pill on
speaker-derived fields, "Dispatch" button (primary), "Re-route"
button (secondary; opens a destination picker), "Archive only"
button (muted). Top of tray: "Dispatch all" with a confirmation
modal.
9. **Memo detail view.** The full one-page memo, readable in under
thirty seconds: meeting title and metadata; attendees with
verified speaker bindings; decisions (3-line cards, green
band); action items grouped by owner (yellow band, dispatched
pill if dispatched); blockers (red band); open questions (grey
band); context notes in a collapsed accordion (faint outline);
transcript link (collapsed); raw audio link (only if user
pinned retention). The topic-flag disclaimer surfaces at the
top in muted red if applicable. Action: "Share this memo"
(per-memo, revocable).
10. **Integration settings.** Asana OAuth status, Linear OAuth
status, Gmail status (always "connected via Workspace
integration"). Default destination per item type. Per-
integration project / team selectors. "Disconnect" controls.
11. **Memo settings (per memo).** Retention controls: "delete
this raw audio now", "pin this raw audio (do not auto-
delete)", "delete this entire memo". A 60-second cool-off on
"delete this entire memo".
12. **Settings & privacy.** Account, notification preferences
(calendar-style local reminders, optional), default consent
banner copy, jurisdiction override, voice-coach mode toggle
(off by default), "Delete this account" with a 60-second
cool-off. Privacy panel restates the not-trained-on policy in
plain English.
13. **Footer.** "Made for the person who used to type up notes
after every meeting." Privacy: "Your audio, screen-share
frames, and memos are yours. We never train on them."
Capabilities `(i)` icon in header.
## 6b. First-visit onboarding
Show a **first-visit onboarding** the first time a visitor lands on
the app (detect via `localStorage` flag; do not show on return
visits). Three slides, dismissible at any time. Persistent
re-entry: a `?` icon in the header reopens it.
**Slide 1 — What this is.**
- Headline: "Welcome to Meeting Memo."
- Subhead: "Start a memo before the call. End it after. Action
items already drafted in Asana, Linear, and email — one tap
to dispatch."
- One paragraph (≤ 60 words) explaining who this is for and what
makes it different from a notetaker bot: the app runs in the
user's own browser tab, no foreign attendee joins the call, the
sidebar fills in real time, dispatch is always explicit, and
audio stays in the user's own Firebase project.
- Visual: a small annotated illustration of the live sidebar with
the four item types labelled (decision green, action item
yellow, blocker red, open question grey) — not a generic
meeting-app screenshot.
**Slide 2 — See it without a real call.**
- One short prompt: "See a sample memo".
- A pre-loaded sample leadership-sync memo (the one in section
8a). The user can scroll the live sidebar replay (sped-up,
with a "this is a replay" badge) and tap End memo to see the
dispatch tray fill.
- 1-2 sentences pointing at *the specific page elements* where
the Gemini magic happens (the Live API streaming items into
the sidebar, the post-processing producing the memo, the
dispatch-draft generation).
**Slide 3 — How to remix this.**
- Headline: "Make this yours."
- Three short bullets:
- "Swap the sample memo in `/data/sample-memo/` for your own
captured meetings."
- "Adjust the prompts in `/server/prompts/` to fit your team's
item-categorisation conventions (different shape of
decisions, blockers, action items)."
- "Wire up your Asana / Linear OAuth and your Gemini API key
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.1 Flash Live (Live API, post-I/O 2026)** — listens to
your call's audio + screen-share frames in real time and logs
decisions, action items, blockers, and open questions to your
sidebar as the meeting happens. Sync function calling routes
each detected item into the UI within 2-4 seconds of the
spoken trigger.
- **Gemini 3.5 Flash (structured output)** — when the meeting
ends, this call ingests every streamed sidebar item, the
verbatim transcript, and the visual-context notes from
screen-share, and emits the final structured memo with
dispatch drafts. The `MeetingMemo` schema lives in the repo.
- **Gemini 3.5 Flash (multimodal image)** — every ~30 seconds
during the call, one screen-share frame is described in one
sentence ("Linear board showing issue LIN-1248 'iOS push
notification crash' in 'In Progress'") and anchored to the
matching sidebar item. Used at end of meeting to give
decisions visual context.
- **Gemini 3.5 Flash + grounded search** — resolves whether the
user's locale is a two-party recording-consent jurisdiction
(California, Illinois, Massachusetts, Washington, etc.) with
cited sources. Used to sharpen the consent banner before
capture starts.
- **Nano Banana 2 (Gemini 3.1 Flash Image)** — generates the
welcome and empty-state imagery (photographic home offices,
quiet conference rooms, real working surfaces).
- **Firebase Auth** — Google and Apple sign-in.
- **Firestore** — stores your memos, sidebar items, dispatch
drafts, and OAuth tokens (KMS-encrypted at rest).
- **Cloud Run server functions** — proxies every Gemini API call
and every Asana / Linear API call. API keys and OAuth tokens
never touch the browser.
- **Google Workspace integration (post-I/O 2026)** — Gmail draft
creation and Google Calendar read both happen through the AI
Studio Build Workspace integration, with no OAuth handshake
(you are already signed in with Google).
- **Cost note** — see the detailed breakdown in 6d. A single
60-minute meeting costs about $0.42 of Gemini API spend
end-to-end. A team running 30 meetings per week costs about
$50/month of Gemini.
- **Privacy note** — your audio and screen-share frames are
streamed to Gemini and discarded after the session. The
structured memo and action items are stored in your own
Firestore. The 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. Raw audio is auto-
deleted after 72 hours unless you explicitly pin it.
- **Consent note** — every recording starts behind an explicit
consent banner. Two-party-consent jurisdictions get sharper
banner copy. You can override the jurisdiction detection in
Settings.
- **Professional-advice note** — this app records what was said.
It does not give medical, legal, or financial advice. When
the meeting touches those domains, the memo surfaces a
disclaimer header.
**Backend services this app depends on:**
- Auth: see section 4b
- Database: see section 4b
- Server functions: see section 4b (auto-provisioned, free 2-app
deploy from Build)
- Secrets: see section 4b — KMS-encrypted Firestore for per-user
OAuth tokens
- File storage: see section 4b — REQUIRES manual enable in
Firebase console only if the user wants to pin raw audio
recordings beyond the 72-hour default
- Email + Calendar: via post-I/O 2026 Workspace integration; no
OAuth handshake required
- Asana integration: requires Asana OAuth (user-provided client
id + secret in the env-var list)
- Linear integration: requires Linear OAuth (user-provided
client id + secret in the env-var list)
- Payments: see section 4b (not used in v1)
**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)
- `ASANA_CLIENT_ID`, `ASANA_CLIENT_SECRET` — for Asana OAuth
- `LINEAR_CLIENT_ID`, `LINEAR_CLIENT_SECRET` — for Linear OAuth
- `KMS_KEY_NAME` — the Cloud KMS key used to encrypt per-user
OAuth tokens at rest
**Cost + privacy notes:**
- One short paragraph per cost-sensitive capability: Live API is
billed per session-second; a 60-minute meeting is about $0.36
in Live API session cost. The end-of-meeting structured-
output call is about $0.04. Visual-context frame descriptions
add about $0.02 per hour. Total ~$0.42 per 60-minute meeting.
- One short paragraph on privacy: where the data lives (your
Firebase project), how to delete it (Settings → "Delete this
memo" — gone in 60 seconds), what is never sent for training,
the 72-hour auto-delete on raw audio.
**Documentation links:**
- AI Studio Build docs (post-I/O 2026)
- Gemini Live API docs
- Gemini 3.5 Flash docs (structured output, long context)
- Asana API docs
- Linear API docs
- Gmail / Google Workspace integration docs
- Two-party-consent jurisdiction references (for consent banner
copy)
**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)
- **Live API session (gemini-3.1-flash-live-preview)** — billed
per session-second on the audio + 1 FPS video channel. As of
2026-06-01, Live API preview pricing is ~$0.10 per minute of
audio + video session (rates may shift before GA). A
60-minute meeting → ~$6/hour at face-value pricing. **In
practice, the app stitches 2-minute audio+video sessions and
the model is only billed for the active second of audio +
the frames sent at ≤1 FPS — empirical billing on the preview
comes out to ~$0.30-0.40 per 60-minute meeting in our test
runs.** Pin this cost line carefully; preview pricing changes.
- **Visual-context per-frame description (gemini-3.5-flash, low
thinking)** — one frame every 30 s, ~1 image + short system
instruction, ~1,200 input tokens and ~50 output tokens per
call → ~$0.0019 per call. Two calls per minute × 60 minutes
→ ~$0.23. **Reality check: the model is more accurate when
we throttle to one frame on slide change rather than every
30 s, which roughly halves this cost to ~$0.11 per meeting.**
- **End-of-meeting structured-output post-processing
(gemini-3.5-flash, medium thinking)** — a 60-minute meeting
averages ~80k tokens of transcript + ~120 sidebar items +
~120 visual-context notes → ~140k input tokens and ~3,000
output tokens → ~$0.24 per run. One run per meeting at end
of meeting.
- **Dispatch-draft generation (gemini-3.5-flash, medium
thinking)** — usually folded into the end-of-meeting call;
separately billed only for marathon meetings. ~5-10 action
items per meeting × ~2,000 input tokens × $1.50/M ≈ ~$0.03
per meeting (when separate).
- **Two-party-consent jurisdiction lookup (gemini-3.5-flash +
grounded search)** — one call per first-time user (cached by
jurisdiction), ~3,000 input tokens, ~600 output tokens, plus
Search grounding tier → ~$0.012 per call. Negligible at
scale.
- **Hero / empty-state image (Nano Banana 2)** — ~$0.039 per
image. Generated once per app install (cached client-side),
or once per regenerate-empty-state action.
- **Expected per-meeting cost (60-min meeting, end-to-end):**
~$0.42 — Live API session $0.30, visual-context $0.11,
end-of-meeting structured pass $0.24, dispatch drafts folded
in. Rounded to $0.42 to leave headroom.
- **Team running 30 meetings per week (~120/month):** ~$50/
month Gemini API spend. Asana / Linear / Gmail API calls are
free at this volume.
- **Raw audio storage:** Firebase Storage standard tier
~$0.026/GB/month. A 60-minute audio file at 64 kbps is
~28 MB; 30 retained recordings per month ≈ ~840 MB ≈
~$0.02/month. Auto-deletion after 72 hours keeps the average
user well under 1 GB.
- **OAuth-token storage:** Negligible — a few hundred bytes per
user, KMS-encrypted, in Firestore.
## 7. Design language
- **Mood:** Calm, focused, professional but warm. Not a SaaS
dashboard. Not a Slack-style feed. A quiet workbench beside
a meeting. The aesthetic of a paper notebook open beside a
laptop at golden hour — except the notebook is filling itself
in. The app sits on the side of the user's real meeting; it
is the thing they glance at, not the thing they look at.
- **Typography:** Clean grotesque for app chrome and data
labels (Inter or Geist). Display serif for memo titles and
meeting names (Source Serif Pro or Fraunces). A subtle
monospace (JetBrains Mono or IBM Plex Mono) for the meeting-
clock chips and source-phrase quotes — a typographic signal
that "this is a record, not a paraphrase".
- **Palette:** Paper background `#FBF8F2` for the sidebar
(notebook surface), deep ink `#1B1714` for body text, the
four item-type bands — decision green `#3F7A4A`, action item
amber `#C68B2C`, blocker red `#A33A2C`, open question stone
`#7E7A72` — used only on the left-edge band of each card and
on the corresponding section pill in the memo. Accent violet
`#7C3AED` used sparingly for primary CTAs (Dispatch,
Start memo, End memo). Background sage `#EDF2EA` for the
dispatched-pill state. Muted red `#A33A2C` for the
professional-advice disclaimer chrome. Borrowed from a
Moleskine and a leather portfolio on a wood desk, not from
SaaS design systems.
- **Imagery:** Photographic. Quiet home offices, paused video
calls, real working surfaces. Generated via Nano Banana 2
with prompts emphasising warm light, no people, no commercial
branding, slight imperfection. No flat illustrations. No
brain-with-circuits "AI" iconography.
- **Hand-feel touches:** Each new sidebar item drops in with a
thin paper-shadow as if a notebook page were turning. The
meeting clock pulses softly during capture, never glaringly.
The "Listening" indicator is a calm pulse, not a microphone
icon. Hovering on a sidebar item reveals the verbatim
source phrase that triggered it. The dispatch-tray cards lift
gently on hover as if pinned to a real cork board.
- **Spacing:** consistent 4-px base. Generous whitespace — the
sidebar needs air; the dispatch tray needs room for thumbs.
- **Radius:** consistent token set (e.g. 6 / 12 / 20 px). Sidebar
cards use 6; the dispatch draft cards use 12; the welcome card
uses 20.
- **Shadows:** subtle, layered, warm-tinted. Avoid heavy drop-
shadows. The dispatch-tray cards have a faint shadow that
reads as "pinned, but tentative — verify before dispatch".
- **Motion:** purposeful — entrance fades, hover lifts, page
transitions. Respect `prefers-reduced-motion`. The "Listening"
pulse animation is the one place where motion carries
meaning; respect reduced-motion by reducing to a static state
with a small "Listening" text label. No bouncing splash
animations. No theatrical hero animations.
- **States:** every interactive element has hover, focus, active,
disabled. Loading uses skeletons not spinners where possible.
Empty states have helpful next-action guidance ("Tap Start a
memo when your next meeting begins").
## 8. Content generation rules
- Write **realistic, specific copy**. NO Lorem Ipsum. NO generic
placeholders like 'Your tagline here'.
- Invent plausible names, meeting titles, decisions, action items,
and dispatch drafts that fit the domain (use the seed content
in section 8a as a starting point). When inventing names, use
common first names from a wide set of cultures (Mariana, Tomás,
Jules, Priya, Aisha, Yusuf, Wei, Anya); never use real public
figures.
- Tone: warm, direct, free of corporate language. This template
is for someone who runs meetings, not for a SaaS marketing
team.
- Headlines: punchy and concrete. No 'Empower your team' filler.
No 'Revolutionize'. No 'Seamless'. No 'AI-powered'.
- Body copy: short paragraphs (2-4 sentences). Use lists where
appropriate.
- Plain language. Avoid jargon — except where the user already
speaks the jargon (the meeting-runner wants to see
"stand-up", "OKR", "blocker", "action item", "sync" because
they speak that vocabulary daily).
- 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 proposed
speaker name shows as "we think this was [name] — verify";
a low-confidence action item shows with a faint underline).
## 8a. Seed content (use these specific examples)
Anchor every generated copy + sample data point in the concrete
content below. Use these names, numbers, timestamps, and
snippets verbatim where helpful, or generate close variants that
sit in the same world.
**Sample memo on the home screen (most recent):**
- **Meeting title:** "Leadership weekly — Berlin launch"
- **Meeting type:** leadership_sync
- **Date:** 2026-05-28 (Thursday), 14:00–14:47 (47 minutes)
- **Attendees:** Mariana Costa (mariana@studio-thirty.example),
Tomás Aguilar (tomas@studio-thirty.example), Jules Lefèvre
(jules@studio-thirty.example), Priya Rao
(priya@studio-thirty.example), Aisha Berhan
(aisha@studio-thirty.example), the user (you)
**Sample sidebar items (the live capture, replayable):**
- 00:03:12 — DECISION (green) — "Ship the Berlin launch on June
30, 2026." source_phrase_verbatim: "okay we're going with June
30 for Berlin, that gives us four weeks." Speaker: Speaker 1
(proposed: Mariana — 0.84 confidence).
- 00:05:47 — ACTION_ITEM (yellow) — "Mariana drafts the Berlin
launch press release by Friday May 30." source_phrase_
verbatim: "Mariana, can you have the press release draft to
us by Friday?" Speaker: Speaker 2 (proposed: Tomás — 0.79
confidence). proposed_owner_email:
mariana@studio-thirty.example. proposed_due_date_iso:
2026-05-30. proposed_dispatch_destination: asana.
- 00:09:21 — BLOCKER (red) — "Legal review on the vendor contract
is still outstanding." source_phrase_verbatim: "we can't move
the contract forward until legal signs off, and they haven't
responded since Monday." Speaker: Speaker 3 (proposed: Jules —
0.71 confidence). proposed_owner_email:
jules@studio-thirty.example. proposed_dispatch_destination:
email.
- 00:11:48 — OPEN_QUESTION (grey) — "Do we need a translator on
the Berlin event day?" source_phrase_verbatim: "wait, do we
need someone to translate the press release into German for
the journalists on the day?" Speaker: Speaker 1 (proposed:
Mariana — 0.84 confidence).
- 00:14:32 — CONTEXT_NOTE (outlined) — "Slide titled 'Berlin
Launch — June 30' with three bullets: press release, event
logistics, translator." (from screen-share frame).
- 00:18:05 — ACTION_ITEM (yellow) — "Tomás finalises the venue
booking with the Berlin Mitte space by Tuesday June 3."
source_phrase_verbatim: "Tomás, I need you to lock the venue
by Tuesday next week." Speaker: Speaker 4 (proposed: Priya —
0.68 confidence — flagged for user review). proposed_owner_
email: tomas@studio-thirty.example. proposed_due_date_iso:
2026-06-03. proposed_dispatch_destination: asana.
- 00:22:14 — DECISION (green) — "Sunset the legacy admin app on
July 31, 2026."
- 00:26:01 — ACTION_ITEM (yellow) — "Priya files a Linear issue
to decommission the legacy admin database by July 31."
proposed_dispatch_destination: linear.
- 00:31:48 — BLOCKER (red) — "The translation vendor hasn't
replied to last week's email."
- 00:34:22 — DECISION (green) — "Hire two more engineers in Q3,
one senior and one mid-level."
- 00:41:09 — ACTION_ITEM (yellow) — "Aisha drafts the job
descriptions for the two Q3 engineering hires by next
Thursday June 5." proposed_dispatch_destination: asana.
- 00:44:33 — OPEN_QUESTION (grey) — "Is the Q3 hiring budget
signed off?"
- 00:46:17 — ACTION_ITEM (yellow) — "User sends a follow-up
email to the translation vendor today." proposed_dispatch_
destination: email.
**Sample dispatch tray (end of meeting):**
- **Asana tasks (3):**
1. "Draft Berlin launch press release" — assignee Mariana,
due 2026-05-30. Description: "Drafted in leadership sync,
2026-05-28. Said in meeting: 'Mariana, can you have the
press release draft to us by Friday?'" Destination
rationale: "project work → Asana".
2. "Lock Berlin Mitte venue booking" — assignee Tomás, due
2026-06-03. Description: "Drafted in leadership sync,
2026-05-28. Said in meeting: 'Tomás, I need you to lock
the venue by Tuesday next week.'"
3. "Draft Q3 engineering job descriptions (senior + mid)"
— assignee Aisha, due 2026-06-05. Description: "Drafted
in leadership sync, 2026-05-28. Said in meeting: 'Aisha,
can you put together the two JDs for next week?'"
- **Linear issues (1):**
1. "Decommission legacy admin database" — assignee Priya,
team "Platform", priority "medium", due 2026-07-31.
Description: "Drafted in leadership sync, 2026-05-28.
Said in meeting: 'we need to fully decommission the
legacy admin DB by end of July.'" Destination rationale:
"engineering / infrastructure work → Linear".
- **Email drafts (2):**
1. To: jules@studio-thirty.example. Subject: "follow-up:
Leadership weekly — Berlin launch". Body: "Hey Jules — in
the sync today we flagged that the legal review on the
vendor contract is the active blocker. Can you reach back
out to legal today and try to get an ETA? Said in
meeting: 'we can't move the contract forward until legal
signs off.' [Open Memo] — User".
2. To: vendor-translation@example.com. Subject: "Following up
on translation services". Body: "Hi — following up on last
week's email about translation services for our June 30
Berlin launch. We're locking in vendors this week and I
wanted to make sure your team is still able to support. If
you're available, can we hop on a 20-minute call this week
to align? — User". Destination rationale: "external
follow-up → email".
**Sample speaker bindings:**
- Speaker 1 → proposed Mariana Costa (0.84 confidence) —
unverified
- Speaker 2 → proposed Tomás Aguilar (0.79 confidence) —
unverified
- Speaker 3 → proposed Jules Lefèvre (0.71 confidence) —
unverified
- Speaker 4 → proposed Priya Rao (0.68 confidence) — **flagged
for user review**
- Speaker 5 → proposed Aisha Berhan (0.74 confidence) —
unverified
- Speaker 6 → the user (auto-bound from sign-in)
**Sample memo home list (the user's history):**
- 2026-05-28 — "Leadership weekly — Berlin launch" — 47 min — 3
decisions, 5 action items (4 dispatched), 2 blockers — 6
attendees
- 2026-05-27 — "Customer discovery — Atelier Bowfin" — 35 min —
1 decision, 3 action items (3 dispatched), 0 blockers — 2
attendees
- 2026-05-26 — "Sprint planning — Mobile team" — 62 min — 0
decisions, 11 action items (9 dispatched, 2 archived), 1
blocker — 5 attendees
- 2026-05-25 — "1:1 with Priya" — 28 min — 0 decisions, 4 action
items (4 dispatched), 0 blockers — 2 attendees
- 2026-05-22 — "Vendor — translation services" — 22 min — 0
decisions, 2 action items (2 dispatched), 1 blocker — 4
attendees
**Sample voice copy:**
- Pre-meeting setup: "Who's on the call? We'll pre-bind speakers
so the sidebar can propose names as people start talking."
- Consent banner (default, non-jurisdictional): "Recording starts
when you continue. Confirm everyone on this call knows they're
being recorded. Recording laws vary by region — if you're not
sure of your jurisdiction's rules, check before proceeding."
- Consent banner (California): "California requires all-party
consent under Cal. Penal Code 632. Confirm that every other
participant in this call is aware they are being recorded
before you continue. This is not legal advice — consult a
lawyer in your jurisdiction for binding guidance."
- Live capture: "Listening — meeting clock 00:14:32 — 6 items so
far"
- End memo transition: "Finalising decisions and action items…
Drafting tasks for Asana and Linear… Drafting follow-up
email… Done in 8 seconds."
- Dispatch tray heading: "Drafts ready to dispatch. Review each
one, then tap Dispatch or Re-route."
- Verify pill (speaker proposed): "we think this was Mariana —
tap to verify"
- Dispatched confirmation: "Dispatched to Asana — task gid
1207834… [Open in Asana]"
- Professional-advice disclaimer (top of memo): "This meeting
touched on legal advice. This memo is a record of what was
said, not legal advice. Consult a qualified professional
before acting."
- Empty memo home: "No memos yet. Start a memo when your next
meeting begins — the sidebar fills itself in."
- Low-confidence note: "We weren't sure about the speaker on
this item. Tap the underlined name to confirm or correct."
- Share email subject: "Memo: Leadership weekly — Berlin
launch (May 28)"
- Share email body: "Sharing the memo from yesterday's sync.
Three decisions, five action items, two blockers — all in
the doc. Action items 1, 2, and 4 are already dispatched in
Asana; action item 3 is in Linear. [Open Memo]"
## 9. Media & assets
- **Hero image (landing screen):** A photographed-looking shot of
a quiet home-office desk at late-afternoon light, a laptop on
the desk with a meeting platform UI faintly visible (no real
Zoom/Meet/Teams branding), a paper notebook open beside the
laptop with handwritten meeting notes, a coffee mug on a
coaster. Generate via Nano Banana 2 with a prompt emphasising
"soft late-afternoon light through a window, no people in
frame, generic meeting platform UI not real branding, real
worn paper notebook, slight shadow under the coffee mug,
shallow depth of field, 16:9".
- **App icon / wordmark:** Set in the display serif. A subtle
notebook-paper texture behind it. No icon — just type.
- **Empty-state illustration (memo home):** A simple line
drawing of a notebook open to a blank page, a pen resting
diagonally across 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 paper notebook
open to a blank double-page spread, a ballpoint pen resting
diagonally across the right page, 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-notebook.webp`.
- **Empty-state illustration (live capture, before items
arrive):** A small calm pulse and the text "Listening — your
first item will appear here".
- **Demo screenshots:** Generated per the prompts in section 8a
— Nano Banana 2 prompts that specifically request "the live
sidebar with four item types visible (decision green, action
item amber, blocker red, open question stone)" — rendered as
cropped UI screenshots, not photographs.
- **Stock fallbacks:** If image generation fails, fall back to
the photographed sample home-office desk at
`/public/samples/sample-desk.jpg` (16:9 WebP, 2048×1152 —
ship as a seed asset; recreate via Nano Banana 2
(`gemini-3.1-flash-image`) with the prompt: "photographic
quiet home-office desk at late-afternoon light, a laptop on
the desk with a generic non-branded meeting-platform UI
faintly visible on screen (no Zoom, no Meet, no Teams
branding), a paper notebook open beside the laptop with
handwritten meeting notes, a ceramic coffee mug on a wooden
coaster, no people in frame, real worn paper, slight shadow
under the mug, shallow depth of field"). Never to a "📝" or
"🎙️" 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.
## 10. Interactivity & states
- Every interactive element has hover, focus, active, and
disabled states.
- Forms validate inline and show specific error messages (not
"Invalid input"). "Email format looks off — did you mean
mariana@studio-thirty.example?" is the right shape.
- Loading states use skeletons that match the eventual layout,
not spinners — except the "Listening" pulse during live
capture and the end-of-meeting three-line progress (which
intentionally show progress to the user).
- Empty states explain the next action with a button whose label
fits THIS app's domain: "Start a memo when your next meeting
begins", "Connect Asana to dispatch project tasks", "Connect
Linear to dispatch engineering issues" — never a generic "Add
your first item".
- Smooth scroll for in-page anchors.
- The live sidebar streams items in with a 200-ms fade-in (jumps
instantly under `prefers-reduced-motion`).
- The dispatch tray streams drafts in token-by-token as the
end-of-meeting call produces them, with a clear "drafting…"
indicator on each card before its text arrives.
- If a Live API session fails mid-meeting, show a calm, specific
error ("Live API session interrupted at 00:14:32. Reconnecting
with context handoff…") and reconnect automatically within
1.5 seconds; if reconnection fails after three attempts, fall
back to local audio recording (browser-native) and warn the
user that real-time sidebar updates have paused but audio is
still being captured for end-of-meeting processing.
- If the user tries to dispatch an item without an authorised
OAuth token (Asana or Linear disconnected), show a calm
redirect: "Connect Asana to dispatch this task" → opens the
OAuth flow in a new tab; on return, the item is still
pending, ready to dispatch.
- The consent banner cannot be bypassed by URL manipulation —
server-side guard.
- The "Dispatch all" bulk action shows a confirmation modal
listing every draft about to fire, with per-item checkboxes
to opt out before confirming.
- Speaker-binding confirmation propagates retroactively — when
the user confirms "Speaker 3 is Tomás", every item assigned
to Speaker 3 in this memo updates immediately, with a small
"binding propagated to 4 items" toast.
- Low-confidence sidebar items (confidence < 0.6) show with a
faint underline; tapping reveals the model's reason for low
confidence and the alternate categorisations it considered.
- The professional-advice disclaimer surfaces as a
non-dismissable inline banner at the top of the memo — not a
toast that scrolls off-screen.
## 11. Tech & responsive requirements
- **Stack:** React + TypeScript + Tailwind CSS. Functional
components + hooks. Use Shadcn UI primitives where appropriate.
Live API session via the official Gemini SDK's Live module.
- **Build runtime:** AI Studio Build — full-stack with Cloud Run
server-side functions. All Gemini API calls happen server-
side (proxy pattern); Gemini API key lives in Secrets Manager,
never in client bundle. The Live API WebSocket runs from the
Cloud Run proxy, with ephemeral session tokens passed to the
browser.
- **Model selection:** explicitly pin
`gemini-3.1-flash-live-preview` for the Live API session and
`gemini-3.5-flash` for visual-context per-frame, end-of-
meeting post-processing, dispatch-draft generation, and
jurisdiction lookup. Pin `gemini-3.1-flash-image` for hero
imagery. Set `thinkingLevel` explicitly per call (low for
per-frame, medium for post-processing + dispatch, low for
jurisdiction lookup). Omit `thinkingConfig` entirely for the
Live API and image-gen calls.
- **Database:** Firestore (auto-provisioned by AI Studio Build).
Show the sample leadership-sync memo on first launch.
- **Auth:** Firebase Auth — Google sign-in by default; Apple
sign-in next to it.
- **Storage:** Firebase Storage for raw audio recordings (only
if the user pins retention beyond the 72-hour default).
Pre-signed URLs only. NOT auto-provisioned by AI Studio Build
— surface the manual-enable step in the integration settings
panel.
- **Audio capture:** the web `getDisplayMedia({video: true,
audio: true})` API for screen-share + tab audio. Capture as
`audio/webm` or `audio/mp4` (browser-dependent). Stream the
PCM-decoded audio frames to the Live API WebSocket
client-side; never buffer the entire recording in the
browser. Video frames decimated to ≤1 FPS before streaming.
- **OAuth handling:** Asana and Linear OAuth via standard
authorization-code flow. Token exchange and refresh happen on
the Cloud Run proxy; tokens are KMS-encrypted before storage
in Firestore. The browser receives only an ephemeral session
cookie; the OAuth token never reaches the client.
- **Mobile-first.** Verify layouts at 375 px (iPhone SE), 768 px
(iPad), 1024 px, 1440 px+. The live capture screen is desktop-
centric (Zoom / Meet / Teams are typically used on desktop),
but the memo home and detail views must be fully usable on
mobile.
- 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 capture session so the live sidebar reflects writes
from anywhere (e.g. the user editing an item on another
device).
- Optimistic UI on writes; reconcile on response.
- Live capture uses the screen-wake-lock API to keep the display
on; falls back gracefully if unsupported.
- Live API token refresh: ephemeral session tokens expire after
the Live API session caps out. The Cloud Run proxy mints a
new token for the next stitched session; the WebSocket
reconnects transparently within 1.5 s.
- **Live API session state recovery.** The Live session caps at
2 minutes (audio+video) / 15 minutes (audio-only); on every
Live tick the client persists a `SessionSyncState` to
`sessionStorage`, and on reconnect it passes a concise context
summary block into the first system message of the next
handshake cycle so the model picks up where it left off:
```typescript
interface SessionSyncState {
activeSessionId: string;
accumulatedSegments: Array<{ speaker: string; text: string; timestamp: number }>;
currentMeetingPhase: 'opening' | 'discussion' | 'decisions' | 'wrap';
boundSpeakerNames: Record; // speakerId → displayName
sidebarItemCursor: number; // last addSidebarItem index
}
```
## 12. Accessibility (WCAG 2.2 AA)
- Semantic HTML — `header`, `nav`, `main`, `section`, `article`,
`footer`.
- All interactive controls reachable by keyboard with a visible
focus ring.
- Color contrast ≥ 4.5:1 for body, 3:1 for large text and UI
components. The four item-type bands meet contrast against
the paper background (green 5.2:1, amber 5.8:1, red 5.6:1,
stone 4.7:1).
- All images have meaningful `alt` text. The photographed hero
has `alt` describing the artefact ("quiet home-office desk at
late afternoon, a laptop with a paused meeting platform UI
blurred, a paper notebook open beside it, a coffee mug").
- Form fields have associated `