# 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 `