================ ONE-SHOT BUILD CONTRACT (read first) ================
Build this in Google AI Studio "Build" in ONE shot — a complete, working app,
no follow-up turns. These are hard rules, not suggestions:
1. TARGET = Full-Stack Web (Node server runtime, secrets, Firebase allowed).
If you target Native Android instead, you MUST drop all server/DB/Workspace/
secrets and persist locally (Room / SharedPreferences) only.
2. PIN EVERY MODEL CALL — never let the agent auto-select (it downgrades on quota):
- Reasoning / text -> gemini-3.5-flash (thinkingLevel: minimal|low|medium|high)
- 4K image + legible text -> gemini-3-pro-image (image_size "4K", up to 14 refs)
- High-volume image -> gemini-3.1-flash-image
- Expressive TTS -> gemini-3.1-flash-tts-preview (inline tags e.g. [whispers])
- Realtime audio/video (WebSocket) -> gemini-3.1-flash-live-preview
- Sandboxed agent -> antigravity-preview-05-2026
3. DIVISION OF LABOR — the model ONLY parses/extracts to a strict responseSchema.
ALL math, money (store currency as integer minor units / cents), sorting,
balancing and graph logic run in deterministic TypeScript/Python. The model
must never compute totals, splits or balances itself.
4. responseSchema sanitation — no regex patterns, no fixed-length tuples, no
format validators in the schema (they crash the OpenAPI engine). Enforce those
in server-side code AFTER parsing the JSON.
5. responseSchema and google_search grounding are MUTUALLY EXCLUSIVE in one call.
6. CODEGEN — split large output into modular, single-responsibility files so no
file is truncated by the output-token cap.
7. Every external call gets a graceful fallback (e.g. manual paste if a Workspace
read fails). Never a silent dead end.
8. ROBUST STORAGE & CANVAS — Wrap all `localStorage`/`sessionStorage` operations (especially JSON parsing and writes) in `try-catch` blocks to prevent crashes in private windows or quota overflows. Canvas drawing elements must dynamically handle window resize and scale pixel density (`window.devicePixelRatio`) to avoid blurry graphics on retina displays.
=====================================================================
# MUST OBEY — Mobile-first build requirements
This app's PRIMARY surface is a mobile phone. Build it impeccably on mobile FIRST, then verify on tablet and desktop. Treat the rules below as non-negotiable hard constraints, not suggestions.
## Viewports to verify (every screen, every state)
- 320 px, 360 px, 375 px, 390 px, 414 px, 480 px
- 768 px, 834 px (iPad portrait / Pro 11)
- 1024 px, 1280 px, 1440 px, 1920 px, 2560 px
- Plus: 200% browser zoom, landscape orientation on every mobile width, iPhone with safe-area insets visible
## Hard layout rules
- Mobile-first CSS. Default styles target mobile; `@media (min-width: ...)` for larger viewports.
- Use `dvh` and `svh` instead of `vh` for full-height surfaces (iOS Safari URL-bar bug).
- Use `clamp()` for fluid typography across all viewports.
- Prefer container queries (`@container`) over media queries for component-level responsiveness.
- Use `min(100%, ...)` widths so content never overflows. Zero horizontal overflow at any viewport.
- Add `` to every page.
- Apply `padding: max(safe-area-inset-X, fallback)` on every edge-bleeding container so notched iPhones in landscape never clip content.
- Wide tables and code blocks scroll INSIDE their container (`overflow-x: auto`), never push the body.
- Use `background-attachment: scroll` on mobile, not `fixed` (iOS Safari repaint bug).
- Avoid `backdrop-filter` on animated elements. Use it sparingly on static surfaces only.
- **Canvas Scaling**: Canvases must dynamically scale with window resize events and properly handle high-DPI screens (`window.devicePixelRatio`). Set physical dimensions (`canvas.width`/`canvas.height`) using pixel ratio and render relative to this grid, using CSS to control responsive viewport scaling.
- **Robust Storage**: Every access to `localStorage`/`sessionStorage` (especially `JSON.parse` of loaded state or writes) MUST be wrapped in a `try-catch` block to handle disabled storage, private browsing mode, quota limits, or corrupted JSON gracefully. Fall back to a robust in-memory object store.
## Touch & accessibility
- Tap targets ≥ 44 × 44 px on touch (Apple HIG). Increase to 48 px under `@media (hover: none) and (pointer: coarse)`.
- All interactive controls reachable by keyboard with a visible focus ring; respect `:focus-visible`.
- Color contrast ≥ 4.5:1 for body text, 3:1 for UI components.
- All images have meaningful `alt`. Decorative images use `alt=""`.
- Respect `prefers-reduced-motion: reduce` — zero animation durations under that query.
- Forms validate inline; error messages are specific, not "Invalid input".
- Modals: focus trap, `Esc` closes, `role="dialog"`, `aria-modal="true"`, focus restored on close.
## Performance bar (Lighthouse mobile, throttled 3G/4G)
- LCP < 2.5 s · INP < 200 ms · CLS < 0.1
- JS bundle gzip < 200 KB mobile-first; lazy-load non-critical screens via `React.lazy` / dynamic imports.
- No render-blocking resources above the fold.
- Images: WebP/AVIF preferred, `loading="lazy"`, explicit `width`/`height` attributes (zero CLS), `srcset` for retina.
- Videos: `preload="metadata"`, low-resolution poster, max 720p mobile fallback. Never autoplay with audio.
- Fonts: `font-display: swap`; preload only the one used above the fold.
- Smooth scroll honoured via CSS `scroll-behavior: smooth` with reduced-motion fallback.
## Pre-ship mobile checklist (the deployer MUST verify before declaring done)
1. Open at 375 px in DevTools — every screen scrolls vertically only; zero horizontal scroll.
2. Browser zoom 200% — layout reflows without overlap.
3. iPhone Safari with the URL bar visible AND landscape — no content under the home indicator; no notch clipping.
4. iPad portrait (768 px) and landscape (1024 px) — no awkward gaps; tablet-specific breakpoints land cleanly.
5. Tap every interactive element with a thumb at real-device size — every target is easy to hit.
6. `prefers-reduced-motion: reduce` — every transition / animation skips cleanly, scroll-behavior becomes instant.
7. Lighthouse mobile score ≥ 90 across all 4 categories.
8. Zero `console.error` and zero CLS shift in real-device testing on a mid-tier Android (e.g. Pixel 6a) and an iPhone SE.
---
The original template starts below. All rules above apply on TOP of whatever this template specifies.
---
# The Commute Tutor
## 1. Project
**The Commute Tutor** is a spoken-language coach for adults who study a
second language in the cracks of an ordinary working day — twelve
minutes on the bus, eighteen minutes on the train, the four minutes
between dropping a child at school and arriving at the office.
The user opens the app, taps once, and is in a live conversation that
already knows what she got wrong last Tuesday and quietly works those
exact phrases back into today's talk. At the end of the session the
three sentences she stumbled on are scheduled for the next time she
opens the app — not as a flashcard pile, but as living material the
tutor will weave into a later conversation.
This is the kind of app a thirty-two-year-old payroll analyst in
Querétaro builds in her commute to study Korean before she meets her
boyfriend's parents at Chuseok — Spanish is her first language and she
has six weeks. It is also the kind of app a forty-one-year-old
Korean-American software engineer in San Jose uses to learn his
fiancée's Tagalog before their wedding in Cebu; the language reverses,
the muscle memory reverses, the shape of the moment is the same. Same
calibrated coach, different language pair, different stakes.
This is the kind of app you build because you are tired of decks
that ask "which is correct?" when what you actually need is someone
who heard you say *eoje* yesterday with the wrong vowel, did not
correct you in the moment, and brings the same word back today inside
a different sentence so you can hear it correctly and choose. Calibrated.
Patient. Never condescending.
**Tagline:** _A twelve-minute live conversation in any language — calibrated to last week's mistakes, in any direction, with every stumble scheduled to come back._
## 2. Target audience
- Adult professionals studying a second language in commute-sized windows — bus, train, walking, between meetings — 8 to 20 minutes at a time
- Heritage learners reclaiming a parental or grandparental language they understood as a child but never spoke aloud — Tagalog, Vietnamese, Cantonese, Punjabi, Amharic, Farsi, Yoruba, Korean
- People learning a partner's or in-laws' language ahead of a specific family event — a wedding, a Chuseok, a Tết, a Diwali, an Eid gathering, a baptism, an *anniversaire*
- Long-distance-relationship students preparing to meet a partner's family for the first time
- Immigrants studying the local language of their adopted country — Spanish-speakers in the US studying English, English-speakers in Tokyo studying Japanese, Eritreans in Berlin studying German, Filipinos in Doha studying Arabic
- Returning expats studying the language they should have learned as a child — second-generation diaspora picking up Khmer, Tamil, Igbo, Tigrinya, Tigre, Quechua
- Older adults (50+) who want a patient conversational partner — never condescended to, never rushed, never told their accent is wrong
- Travellers preparing for a specific trip with specific dialogue needs ("I need to argue politely about a hotel charge in Seoul next month")
- Caregivers and healthcare workers learning the languages of the patients they serve — Spanish for nurses in Texas, Mandarin for paediatricians in Auckland, Somali for midwives in Minneapolis
## 3. Core value propositions
Surface these clearly through copy, visual emphasis, and section ordering — they are the reasons users pick this app.
- **A real twelve-minute conversation, not a quiz.** Gemini Live API drives a continuous spoken exchange — turns measured in milliseconds, interruptions handled, your "ah, espera, espera" treated as a normal conversational signal and not a button press. The session ends when the bus arrives, not when you finish a level.
- **Calibrated to *your* mistakes — not "Beginner / Intermediate / Advanced".** The tutor reads your last fourteen days of sessions before it speaks the first word. It knows you confuse 었 and 았 in past-tense Korean verbs, that you systematically drop classifiers in Mandarin, that your French uvular r tightens into a Spanish trill when you're tired. The conversation starts inside your specific edges.
- **Reversible — any language pair, in any direction.** The Spanish-speaker learning Korean and the Korean-American learning Tagalog use the same coach with the languages swapped. The pedagogy is symmetrical by design. Heritage learners can set the "languages you already understand passively" field so the tutor speaks slightly faster and pulls in more idiom.
- **Mistakes are scheduled, not flashcarded.** When you stumble on a sentence, the tutor does not stop. It marks the moment. Three or four days later, in a completely different conversation, the exact same construction reappears in a sentence about a completely different topic — and you hear yourself get it right, in context, without having to study.
- **Never condescending.** No "Great job!" after every utterance. No clapping sound. No streak badges that punish a missed day after a death in the family. The tutor speaks to you the way a patient friend who is fluent in both languages would speak — encouragement when you do something hard, silence when you do something ordinary, repetition when you ask for it.
- **Real interruption, real repair.** Press the talk button at any moment to interrupt. Say *"un momento, ¿cómo se dice…?"* mid-sentence and the tutor pauses, answers, then resumes the conversation where you were. The Live API's barge-in handling makes this feel like a phone call with a friend, not a turn-based exchange with a chatbot.
- **A mistake history you can actually look at.** Every session leaves a tidy, browseable log: what you said, what the tutor heard, what was off, the kinder version, the rule it follows, and when it will resurface. You can listen to your own recording of any moment.
- **Twelve-minute sessions, end of story.** The default session is twelve minutes — not because that is optimal pedagogy in the abstract but because that is how long the bus from her apartment to the office takes. You can override. The tutor closes the session honestly when time is up: a quiet recap of three good moves you made, the three phrases scheduled to come back, nothing more.
## 4. Features to build
- One-tap "Start a 12-minute conversation" from the home screen — no level select, no topic select, the tutor decides today's opening based on the schedule
- Live spoken conversation with millisecond-latency audio in and out (Gemini Live API), barge-in interruption supported as a first-class behaviour
- Per-learner mistake history — every utterance is logged with the verbatim transcript, the tutor's reading, the corrected form, the linguistic feature it tested, and the next-due date
- Spaced-repetition scheduler that re-introduces stumbled phrases inside *organic* future conversations — not as flashcard prompts but as utterances the tutor naturally produces and waits for the learner to use
- Direction-agnostic language pair config — `source_lang` (what the learner already speaks) and `target_lang` (what they're learning) — every prompt obeys both
- Heritage-learner mode — "I understood this language as a child but did not speak it" — adjusts conversational speed, idiom density, and code-switching tolerance
- Topic memory across sessions — the tutor remembers that last Tuesday the learner mentioned her boyfriend's mother is a nurse, and asks today how the nurse is
- Today's calibration card before the session — three things the tutor will work on today, derived from the spaced-repetition queue (the learner can ignore it; most do)
- End-of-session recap — three things you did well, three phrases scheduled to come back, optional listen-back to a 20-second moment the tutor chose
- Per-utterance transcript that streams in real time — both the learner's words and the tutor's words, both languages, with the corrected form appearing in a third lane below misheard utterances
- "Replay that line" button on any tutor utterance — the same tutor voice, same intonation, slower if requested
- Vocabulary the tutor introduces in any session is captured to a per-learner lexicon with the contextual sentence the tutor produced
- Audio-only mode — the screen stays locked, AirPods only, the entire UI is voice (good for commuting on foot)
- Visible "what we'll work on today" panel that surfaces the day's targeted features as gentle headers, never as "Today's Lesson: Past-Tense Verbs"
- Session calendar with streak honesty — shows the days you sessioned without penalising the days you didn't, no shaming red numbers
- Per-feature dashboard — Korean particles, Mandarin classifiers, French uvular r, Tagalog focus-marker shifts — each one with a small graph of your accuracy over time
- Mid-session ear-correction — when the tutor mishears the learner (this happens), the learner can tap "I didn't say that" and the system stores the original audio with a "speech-recognition-error" tag, not a learner mistake
- Exportable per-learner mistake history (JSON + readable HTML) — you own your study record; you can download it
- Quiet mode — the tutor never plays sounds beyond speech, no notification badges, no streak chimes
- Pause and resume mid-session — closing the app at minute 7:13 saves the conversation; reopening within 90 minutes resumes from the exact turn
## 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)
- **Gemini Live API (spoken bidirectional)** — drives the 12-minute conversation. Continuous duplex audio, barge-in tolerated. The `systemInstruction` carries today's calibrated brief; the `contents` carry the rolling turn-by-turn audio + transcripts. Streaming model: `gemini-3.5-flash` with native audio in/out. Session keepalive handled by SDK.
- **Gemini 3.5 Flash (structured output / JSON Schema)** — runs once at session start to compose today's *calibrated brief* from the learner's mistake history and last-N transcripts. Returns a `SessionBrief` matching the schema below. This is the brain.
- **Gemini 3.5 Flash (structured output)** — runs once at session end to grade the session: extracts per-utterance corrections, the three good-moments, the three phrases scheduled to come back, and updates the per-learner `MistakeLedger`. Returns a `SessionReview` object.
- **Gemini 3.5 Flash** — runs the spaced-repetition planner. Given the `MistakeLedger`, it picks today's three target items by SM-2-style interval and difficulty. Pure logic; fast; cheap.
- **Gemini 3.5 Flash (multilingual)** — handles every supported language pair in both directions with dialect awareness (Seoul Korean vs. Busan Korean, Beijing Mandarin vs. Taiwan Mandarin, Castilian vs. Mexican Spanish, European Portuguese vs. Brazilian, Tagalog vs. Cebuano-influenced Tagalog).
- **Gemini TTS (`gemini-3.1-flash-tts-preview`)** — replays a tutor utterance on demand when the learner taps "say that again, slower". Live API's audio output is the primary voice; TTS replays exist for the saved per-utterance "replay" button in the transcript history.
- **Long context (1M tokens)** — the brief-composer call reads the last 14 days of sessions verbatim. A typical session transcript is ~6k tokens; 14 days of daily 12-minute sessions ≈ ~85k tokens. Comfortable under the 1M cap. **Guardrail:** if a learner has 200+ days of history, chunk to the last 30 days for the brief call; full corpus is queried only for the per-feature dashboard on demand.
- **Thinking levels** — `medium` for the session-brief composer (it has to reason about pedagogy). `low` for the session reviewer (extractive, schema-driven). `low` for the spaced-repetition planner (deterministic with light judgement). Live API does not expose a `thinkingLevel` knob — the Live model decides on its own.
### Backend services
- **Auth — Required.** Firebase Auth with Google sign-in (auto-provisioned by AI Studio Build). **Apple sign-in is optional but user-configured**: it requires an Apple Developer account, Service ID, Key ID, and private key wired into the Firebase Auth console. **Magic-link email** is available as a third option but requires the sender domain to be authorised in Firebase Auth. Learner sessions are private to the owner. No public-by-default.
- **Database — Required.** Firestore for `users`, `sessions`, `utterances`, `mistakes`, `lexicon`, `language_pair_config`. Real-time listeners on the active session document.
- **File storage — Required.** Firebase Storage for the raw per-utterance audio recordings (the learner's voice, kept so they can listen back). **Storage is NOT auto-provisioned by AI Studio Build today** — enable it in the Firebase console and wire the bucket name into the AIS Build project before the first session. Pre-signed URLs only.
- **Email — Optional.** Weekly recap emails ("here's what you worked on this week, here's what's coming next") — Firebase Auth magic-link infrastructure can be reused for the sender domain.
- **Payments — Not needed for v1.** Free for personal use. A future "voice clone of a family member" tier or a "fluency report for a tutor" tier could be charged.
- **External APIs:** Gemini API for all intelligence. No third-party speech recognition — Live API handles ASR natively. No third-party TTS — Gemini TTS handles replay.
**Environment variables:** every secret (Gemini API key, Firebase service-account JSON) lives in environment variables — never in client bundle. Include a `.env.example`.
**Auth + data privacy reminders:** never log secrets · never store passwords in plain text · use HTTPS everywhere · honour 'delete my account' inside the UI · explicit opt-in for any analytics · the learner's voice recordings and transcripts are never sent to Gemini for model training (use the Gemini API on the paid tier, where Google does not use your content for model training, per the Gemini API Additional Terms) · audio recordings are stored in the learner's own Firebase Storage bucket and the learner can delete any utterance, any session, or the entire account from within the app.
**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. Live API has no thinkingLevel — leave it unset for that call.
3. **Seed the JSON Schema as a fenced TypeScript / Zod block** in the system instruction or `responseSchema` field. The literal schema is below. **Convert the Zod schema to Gemini's `Schema` type via the SDK helper** before passing to `responseSchema` — do NOT pass raw Zod. **Numeric `min`/`max` constraints are documentation only inside `responseSchema`; clamp on the server after the response arrives.**
4. **Pin the system instruction separately** from user input. Use the `systemInstruction` field for persona + behavioural rules; use `contents` for user input. Never concatenate. For the Live API call, the `systemInstruction` is set once at session open and never edited mid-session.
5. **Pre-declare tools as an enable/disable list** per call. The matrix below names which tools are enabled per call. Tools NOT listed for a call should be disabled.
6. **State negative constraints explicitly** — they are listed below. They are NOT "be careful" suggestions; they are hard rules the model must follow.
7. **Strip unsupported Zod modifiers before passing to `responseSchema`** — Gemini's OpenAPI subset rejects `.regex()` / `pattern`, fixed-length `z.tuple()`, and other custom validators. Use a sanitizer that flattens tuples to length-2 arrays and removes regex patterns before serializing. Validate those constraints in middleware AFTER parsing.
### Per-call model + tools matrix
| Call | Model | thinkingLevel | Tools enabled |
|------|-------|---------------|---------------|
| Compose today's calibrated session brief | `gemini-3.5-flash` | medium | (none) |
| Pick today's three target mistakes (SR planner) | `gemini-3.5-flash` | low | (none) |
| Drive the 12-minute live conversation | `gemini-3.5-flash` (Live API, native audio) | n/a (Live) | (none) |
| Review the session, extract corrections, update ledger | `gemini-3.5-flash` | low | (none) |
| Replay one tutor utterance on demand | `gemini-3.1-flash-tts-preview` | n/a | n/a |
| Generate the per-feature dashboard summary | `gemini-3.5-flash` | low | (none) |
*Note for builders:* on TTS, image-generation, and Live API calls, omit `thinkingConfig` entirely — the field is not supported on those models. The `n/a` cells in this matrix are documentation only; do not serialise them into the request body. The Live API session is configured via `setup` message with `model`, `responseModalities: ["AUDIO"]`, `inputAudioTranscription`, and `outputAudioTranscription` enabled — no `thinkingConfig`.
### Primary structured-output schemas (seed these verbatim in the prompt)
```typescript
import { z } from "zod";
const LanguageCode = z.string(); // BCP-47, e.g. "ko-KR", "es-MX", "tl-PH", "yue-Hant-HK"
const LanguagePairConfig = z.object({
source_lang: LanguageCode, // what the learner already speaks
target_lang: LanguageCode, // what they are learning
source_lang_label: z.string(), // human readable, "Spanish (Mexico)"
target_lang_label: z.string(),
heritage_mode: z.boolean(), // understood passively as a child
target_dialect_preference: z.string().nullable(), // "Seoul", "Cebuano-influenced Tagalog"
speaking_level_self_reported: z.enum([
"absolute-beginner",
"first-words",
"tourist-survival",
"basic-conversation",
"comfortable-but-stumbling",
"fluent-rusty",
]),
proper_nouns_to_remember: z.array(z.object({
name: z.string(), // "Ji-won", "abuela", "Tito Ben"
relationship: z.string(), // "boyfriend's mother", "grandmother"
pronunciation_note: z.string().nullable(),
})),
});
const MistakeKind = z.enum([
"vowel_pronunciation",
"consonant_pronunciation",
"tone_mistake", // Mandarin, Cantonese, Vietnamese, Yoruba
"particle_misuse", // Korean 은/는 vs 이/가, Japanese は/が
"classifier_omission", // Mandarin 个 etc, Vietnamese cái etc
"verb_tense_wrong", // Korean 었/았/했, Spanish preterite vs imperfect
"honorific_mismatch", // Korean 해요/합니다, Tagalog po/opo
"word_order",
"vocabulary_choice", // wrong but valid word; better word existed
"code_switch_break", // dropped target language mid-sentence
"false_friend",
"idiom_literal_translation",
"preposition_misuse",
"gender_agreement", // romance languages
"focus_marker_shift", // Tagalog ang/ng/sa
"uvular_or_trill_misroute",// French r vs Spanish r
"aspiration_or_glottal", // Korean ㄱ/ㅋ/ㄲ
"other",
]);
const Mistake = z.object({
mistake_id: z.string(),
session_id: z.string(),
utterance_index: z.number(),
utterance_audio_uri: z.string(), // Firebase Storage URI to the learner's voice
what_learner_said_verbatim: z.string(),
what_learner_meant_to_say: z.string(),
kinder_correct_form: z.string(), // the corrected sentence, in target_lang
one_line_explanation_in_source_lang: z.string(),
kind: MistakeKind,
linguistic_feature_label: z.string(), // "Korean past-tense vowel harmony 었/았"
difficulty_for_this_learner: z.number().min(0).max(1),
introduced_at: z.string(), // ISO datetime
last_seen_at: z.string(), // ISO datetime
times_seen: z.number().int().min(1),
times_correct_since_introduction: z.number().int().min(0),
next_due_at: z.string(), // ISO datetime, SR-scheduled
retired: z.boolean(), // learner mastered it; out of rotation
retired_reason: z.string().nullable(),
});
const TargetItem = z.object({
mistake_id: z.string(),
linguistic_feature_label: z.string(),
kinder_correct_form: z.string(),
example_sentence_to_seed_in_today_session: z.string(), // tutor will produce this naturally
why_this_one_today: z.string(),
});
const SessionBrief = z.object({
session_id: z.string(),
learner_first_name: z.string(),
language_pair: LanguagePairConfig,
session_length_minutes: z.number().int().min(1).max(60),
opening_topic_suggestion: z.string(), // tutor will choose, this is the seed
callbacks_to_remember: z.array(z.object({ // continuity from prior sessions
topic: z.string(), // "her boyfriend's mother is a nurse"
last_mentioned_session_id: z.string(),
safe_to_bring_up: z.boolean(),
})),
today_target_items: z.array(TargetItem).min(0).max(5),
rolling_strengths: z.array(z.string()), // 3-5 things the learner does well lately
rolling_struggles: z.array(z.string()), // 3-5 things still hard
tone_directive: z.string(), // "patient, never condescending, match her energy"
hard_off_limits_topics: z.array(z.string()), // user-configurable
});
const UtteranceReview = z.object({
utterance_index: z.number(),
speaker: z.enum(["learner", "tutor"]),
verbatim_transcript: z.string(),
language_used: LanguageCode,
was_recognition_likely_correct: z.boolean(),
detected_mistakes: z.array(z.object({
span_text: z.string(),
kind: MistakeKind,
kinder_correct_form: z.string(),
one_line_explanation_in_source_lang: z.string(),
severity: z.enum(["nudge", "worth_a_callback", "important"]),
})),
good_moments: z.array(z.string()), // "used the past-tense particle correctly without prompting"
});
const SessionReview = z.object({
session_id: z.string(),
ended_at: z.string(),
duration_seconds: z.number().int().min(0),
utterances: z.array(UtteranceReview),
three_good_moments_for_recap: z.array(z.string()).min(0).max(3),
three_phrases_scheduled_to_come_back: z.array(z.object({
phrase: z.string(), // in target_lang
why: z.string(), // "the past-tense vowel was almost right; one more pass"
suggested_next_due_at: z.string(), // ISO datetime
})).min(0).max(3),
new_lexicon_introduced: z.array(z.object({
target_lang_form: z.string(),
gloss_in_source_lang: z.string(),
contextual_sentence_tutor_produced: z.string(),
})),
ledger_updates: z.array(z.object({
mistake_id: z.string().nullable(), // null = create new
action: z.enum(["create", "mark_correct", "mark_stumbled", "retire"]),
new_next_due_at: z.string().nullable(),
})),
recognition_errors_flagged_by_learner: z.array(z.object({
utterance_index: z.number(),
learner_complaint: z.string(),
})),
});
type SessionBrief = z.infer;
type SessionReview = z.infer;
type Mistake = z.infer;
```
### Common failure modes (and how to avoid them)
- **Tutor over-corrects.** The agent decides to flag every imperfect utterance and the learner cannot finish a sentence. Hard rule: the Live API system instruction caps mid-conversation corrections at three per session, all others go into the silent ledger and surface in the recap.
- **Tutor is condescending.** "Great job! You did it!" after a basic utterance reads as patronising to an adult learner. The tone directive in the session brief explicitly bans this register. Use plain acknowledgement: "yes" or "mm-hmm" or just continuing the conversation.
- **Agent picks `gemini-3.5-flash` for the live conversation to make it "smarter".** Wrong — Live API has its own audio-native model. Pin to the documented Live model (`gemini-3.5-flash` Live variant). Live API does not accept `gemini-3.5-flash` today; the request will error.
- **Brief composer is called every turn.** It is called ONCE per session, at open. Calling it per turn wastes the long-context bill and confuses the Live model.
- **Recognition errors are charged to the learner.** The Live API ASR is excellent but not perfect, especially for code-switching learners. When the learner says "I didn't say that", the utterance is moved to `recognition_errors_flagged_by_learner` and NOT into the mistake ledger.
- **Spaced-repetition scheduler is too aggressive.** SM-2 defaults assume rote review. Conversational re-introduction needs longer initial intervals (4 days, 10 days, 21 days) because the learner has to encounter the form naturally, not click "again".
- **Heritage learners are bored at "beginner" pacing.** The `heritage_mode` flag must change the tutor's speech rate, vocabulary density, and idiom willingness, not just a UI label.
- **Tutor switches to the source language and stays there.** A learner asks "wait, what does that mean?" in source, the tutor explains, then forgets to return to target. System instruction: after any source-language explanation, the next tutor utterance MUST return to target_lang.
- **Honorifics are flattened to neutral.** Korean ~해요 vs ~합니다 vs ~해 collapsing into one register strips the social meaning. The tutor must hold register through the session and explicitly raise honorifics for any callback that names the learner's prospective in-laws.
- **Tone mistakes in Mandarin / Cantonese / Vietnamese / Yoruba get rounded to "pronunciation".** They are their own MistakeKind with their own callbacks. Tag accordingly.
- **The 12-minute timer cuts off mid-sentence.** The session-end logic listens for a natural turn boundary in the last 30 seconds of the window and closes there. Hard cap is 14 minutes if no natural boundary appears.
### Negative constraints (hard rules)
- Do NOT congratulate the learner for completing a basic utterance. Plain acknowledgement only. Save explicit praise for genuinely hard moves the learner pulled off without help.
- Do NOT correct more than three times in a single live session. All other detected mistakes go into the silent ledger and surface only in the end-of-session recap.
- Do NOT introduce vocabulary the learner has never heard without immediately producing it in a contextual sentence and offering a one-line gloss in the source language. No bare definitions.
- Do NOT switch out of the target language without explicit learner request (a question in the source language, or the "explain that to me" button). And after any source-language explanation, the next tutor utterance MUST return to the target language.
- Do NOT translate proper nouns. First names ("Ji-won", "Tito Ben", "abuela"), city names, dish names, and brand names stay verbatim across languages.
- Do NOT extrapolate from a single mistake to a personality trait. "She always confuses past tense" is not a thing the tutor says. Mistakes are about a linguistic feature, not the learner.
- Do NOT use streak shame. The session calendar shows present days and absent days with equal visual weight. No red numbers, no broken-streak banners.
- Do NOT play any sound other than the tutor's speech during a session. No "ding" on a correct utterance. No swoosh on session start.
- Do NOT use the learner's audio or transcripts 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.
- Do NOT auto-share sessions, mistakes, or transcripts. All learner data is private by default; export is explicit and per-session.
- Do NOT shame heritage learners for grammatical gaps. The heritage mode flag signals "this person heard this language as love, not as a school subject" — the tutor's tone reflects that.
### Per-call `systemInstruction` strings
Use these as the literal `systemInstruction` field for each Gemini API call the built app makes. They complement the series-wide rules already uploaded as the global instructions file (`00-series-instructions.txt`).
### Call: Compose today's calibrated session brief
Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none)
```
You are the pedagogy brain of a spoken-language tutor that runs a
short, calibrated conversation with an adult learner during their
commute. You do not speak the tutor's lines yourself — you produce
the brief that the live-conversation model will obey for the next
twelve minutes.
You receive:
- The learner's LanguagePairConfig.
- The last N session transcripts (verbatim, both speakers, both
languages, with timestamps) — typically the last 14 days.
- The full open MistakeLedger (every non-retired Mistake the
learner has ever produced, with intervals and next-due dates).
- Today's date and the learner's local timezone.
Produce a SessionBrief JSON matching the provided schema. No
commentary outside the JSON.
Hard rules:
- today_target_items contains at most three items, drawn from the
Mistakes whose next_due_at is on or before today. Prefer
items that can be woven naturally into a single topical
conversation — do not assemble a grab-bag.
- opening_topic_suggestion is one short, plausible commute-time
topic — not a textbook scenario. "How was the rain this
morning?" not "Order food at a restaurant." If the learner
mentioned something concrete in a recent session (a family
event, a colleague's name, a sick parent), prefer a callback.
- callbacks_to_remember includes every concrete topic the learner
raised in the last 7 days with safe_to_bring_up = true unless
the topic was heavy or private (health crises, relationship
conflict, work stress). Use judgement; mark heavy topics
safe_to_bring_up = false.
- tone_directive is one short paragraph in plain English. It must
include: "never condescending, never patronising. Plain
acknowledgement, not exclamation. Praise only when the learner
pulled off something hard without help." Adjust to the
learner's apparent energy from recent sessions — quieter on
mornings, livelier on evenings.
- For heritage_mode learners: tone_directive must add "speak at
natural adult pace, use idioms freely, do not over-explain
vocabulary the learner clearly knew passively as a child."
- proper_nouns_to_remember is mirrored straight from the language
pair config — never invent names.
- rolling_strengths and rolling_struggles each contain 3-5 items
drawn from the ledger. Phrase them in plain English as features
("uses past-tense particles correctly when not under time
pressure", "still drops Mandarin classifiers in spoken
sentences"), not as scores.
- session_length_minutes defaults to 12; respect any user override
visible in the config.
- hard_off_limits_topics is copied straight from the user config
(which may include "my divorce", "my father's death", "work").
No commentary. JSON only.
```
---
### Call: Pick today's three target mistakes (SR planner)
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You are the spaced-repetition planner. You select today's target
mistakes from the learner's MistakeLedger using a modified SM-2
schedule with conversational re-introduction (not flashcard recall).
You receive:
- The full MistakeLedger (non-retired Mistakes only).
- Today's date.
- The session_length_minutes (typically 12).
Output: a JSON array of at most THREE Mistake objects, ordered by
priority for today, each augmented with a one-sentence
example_sentence_to_seed_in_today_session and a why_this_one_today.
Selection rules:
- Prefer Mistakes whose next_due_at is on or before today, sorted
ascending (most overdue first).
- Never select more than three.
- Never select two Mistakes that test the same linguistic_feature_label
on the same day.
- Cap difficulty_for_this_learner across the day at 2.0 sum — three
hard items in one 12-minute session is too much.
- If fewer than three are due, top up with the next-soonest items
whose difficulty_for_this_learner is below 0.5 (easy reinforcement
is fine).
- Spaced-repetition intervals (override SM-2 defaults):
- First correct callback: next_due in 4 days
- Second correct callback: next_due in 10 days
- Third correct callback: next_due in 21 days
- Fourth correct callback: next_due in 45 days
- Fifth correct callback: retire = true, retired_reason = "stable
across 5 callbacks over ~80 days"
- Any stumble resets the interval to 2 days.
- The example_sentence_to_seed_in_today_session is a sentence the
tutor will naturally produce in conversation. It must be a normal
conversational sentence, not a drill. "How was the rain this
morning?" (testing past tense) is better than "Yesterday I ate
apples." (drill).
No commentary. JSON only.
```
---
### Call: Drive the 12-minute live conversation (Live API)
Model: `gemini-3.5-flash` (Live API, native audio) · thinkingLevel: n/a · Tools: (none)
```
You are a patient, fluent bilingual conversation partner. You speak
the target language with a {target_lang_label} accent. You understand
the source language perfectly but you speak it only when explicitly
asked or when a learner needs a single-sentence clarification.
You are speaking with {learner_first_name}. You will have
approximately {session_length_minutes} minutes together.
The session brief below names today's calibrated targets, the
learner's recent strengths and struggles, callbacks worth bringing
up, and topics that are off-limits. Read it before you speak.
{SESSION_BRIEF_JSON}
Conversation rules (these are absolute):
1. Open with one short, natural sentence in the target language.
Reference a callback only if it is safe_to_bring_up. Do not open
with "Hello! Let's practice past-tense verbs today."
2. Be a conversation partner, not a quiz machine. Ask open
questions. Listen for as long as the learner wants to speak.
Tolerate pauses without filling them.
3. Weave each today_target_items[i].example_sentence_to_seed_in_today_session
into the conversation when it fits naturally. Do not chain all
three back-to-back. Space them across the twelve minutes.
4. Correct out loud at most THREE times in this whole session. A
correction is short: repeat the kinder_correct_form once, in a
normal conversational tone, without "no" or "wrong". Then move
the conversation forward. Every other detected mistake goes
silently into the recap.
5. Never say "good job", "great", "well done", "excellent", or
any phrase whose only purpose is to praise a basic utterance.
Praise only when the learner pulls off something genuinely hard
without prompting — and even then, brief and specific ("the
honorific switch you just did is exactly the one I would use").
6. If the learner interrupts, stop immediately. Listen to what they
are asking. Answer in the source language ONLY if they asked in
the source language or explicitly said "can you explain in
{source_lang_label}". After any source-language explanation, your
next utterance must return to the target language.
7. Never translate proper nouns. First names, city names, dish
names, brand names stay verbatim.
8. For heritage_mode learners: speak at natural adult pace, use
idioms freely, do not over-explain familiar vocabulary, and treat
the learner as someone reclaiming a language, not learning a
foreign one.
9. Honour the target_dialect_preference. If it is "Seoul" produce
Seoul Korean; if "Cebuano-influenced Tagalog" produce that; if
"Mexico City" produce mexicano Spanish, not Castilian.
10. Respect hard_off_limits_topics. If the learner brings up an
off-limits topic themselves, you may listen and acknowledge in
the source language briefly, then gently return to a safe
target-language topic.
11. About 30 seconds before the end of the session, find a natural
turn boundary to close. Say a short closing line in the target
language ("I will see you tomorrow") — do not summarise the
session, do not list the targets, do not give homework. The
recap is generated separately.
12. If the learner is having a hard day (you can hear it in their
voice), drop the today_target_items entirely and just have a
gentle conversation. The recap-grader will see what you did and
will not penalise the missed targets.
If audio transcription returns gibberish or the learner says "I
didn't say that" — treat as a recognition error, do not flag the
learner's utterance as a mistake.
You speak only. You do not narrate stage directions. No commentary.
```
---
### Call: Review the session, extract corrections, update ledger
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You produce the SessionReview JSON after a live session ends. You
receive the complete turn-by-turn transcript (both speakers, both
languages), the SessionBrief that drove the session, and the
existing MistakeLedger.
Hard rules:
- For every learner utterance, populate detected_mistakes with at
most three items. Severity guidance: "nudge" = small slip, never
surfaces; "worth_a_callback" = enters the SR queue; "important" =
surfaces in the recap and enters the SR queue.
- For every learner utterance, populate good_moments with anything
genuinely well-executed. Be conservative — a good moment is not
"she completed a basic sentence". It is "she used the past-tense
particle 았 correctly in a sentence she initiated, without
prompting, for the first time in the last 14 days of sessions".
- three_good_moments_for_recap names the THREE strongest good
moments from this session. Specific, brief, in the source language.
Do NOT include condescending framings.
- three_phrases_scheduled_to_come_back names the THREE most
important phrases to bring back in a future session. Pick from
the learner's stumbles, not from the tutor's lines. The phrase
itself goes in the target_lang.
- new_lexicon_introduced lists every vocabulary item the tutor
produced this session that does not already exist in the
learner's lexicon. Include the contextual sentence.
- ledger_updates records every change to the MistakeLedger. Use
the SR interval rules from the planner system instruction.
- recognition_errors_flagged_by_learner — populate from any
moment the learner pressed "I didn't say that". Do NOT also
enter these utterances as learner mistakes.
Do NOT extrapolate. If a learner produced an utterance you cannot
fully parse, leave detected_mistakes empty for that utterance and
set was_recognition_likely_correct = false.
Do NOT moralise. The review is data, not a report card.
No commentary. JSON only.
```
---
### Call: Replay one tutor utterance on demand
Model: `gemini-3.1-flash-tts-preview` · n/a · n/a
```
Voice: warm, unhurried. Pick the Gemini 2.5 Flash TTS voice whose
`languageCode` matches the utterance's target language —
pronunciation will follow that locale automatically. Prefer the
voice the live session used, if its identifier is preserved;
otherwise fall back to the default voice for the locale.
Pre-process the text before sending to TTS:
- Read the verbatim tutor utterance from the SessionReview.
- Insert an ellipsis (`…`) at every comma to create a natural
pause. Insert a blank line and an em-dash (`—`) at every
paragraph or sentence boundary the learner asked to be slowed.
Gemini 2.5 TTS does not support SSML `` — these
textual cues are how you signal pace.
- If the learner requested "slower", target ~80 words per minute.
Default is ~110 words per minute (the live conversation pace).
- For code-switched utterances, render the whole thing in the
voice locale of the dominant language. Mid-call voice switching
is not supported by Gemini 2.5 TTS.
Style direction: prepend ONE short directive sentence to the text
input, exactly like: "Read this back at a calm, kitchen-table
pace, the way a patient friend would repeat themselves once for
clarity." There is no separate `style` API field; the directive
sentence inside the input is how style is conveyed.
Phoneme overrides (Korean ㄱ/ㅋ/ㄲ aspiration, Mandarin tone marks,
French uvular r) are NOT exposed by Gemini 2.5 TTS — no SSML
`` tag. Pronunciation comes from the chosen voice's
native locale.
No commentary.
```
---
### Call: Generate the per-feature dashboard summary
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You produce the per-feature dashboard summary. You receive:
- The learner's full MistakeLedger.
- The last 90 days of session reviews.
Output: a JSON object grouping mistakes by linguistic_feature_label,
each group containing:
- feature_label (verbatim)
- count_introduced
- count_correct_since_introduction
- count_currently_open (non-retired)
- accuracy_trend_last_30_days (a number between 0 and 1, or null
if fewer than 5 attempts)
- one_sentence_plain_language_summary (in the source language, no
jargon — "you're getting steadier on Korean past-tense vowels;
this still trips you up under time pressure" rather than
"accuracy of 0.72 on feature X")
Hard rules:
- Phrase summaries warmly, without condescension. Never use
"still" as a chiding word; use it factually.
- Never claim a feature is "mastered" until it has been retired
per the SR rules.
- If a feature has fewer than 5 total attempts, mark accuracy_trend
null and write "still early days — need a few more sessions to
see the pattern".
No commentary. JSON only.
```
## 5. Use cases & content to include
Build dedicated UI sections or flows for each of these — they tell you what content the app must support.
- **The commute conversation.** Marisol, a payroll analyst in Querétaro, opens the app on the bus at 7:42. The tutor greets her in Korean and immediately asks how the rain was this morning — testing past tense, exactly what her ledger said she needed today. The bus pulls up to her stop at 7:54. The tutor closes the session at the natural sentence boundary and the recap appears as she's walking off the bus. She listens to it while she walks the last block.
- **The language reverses.** Sung-min, a Korean-American software engineer in San Jose, uses the same app to learn his fiancée's Tagalog. He sets the source as English and the target as Tagalog (Cebuano-influenced) and turns on heritage mode false. The pedagogy is symmetric: the tutor calibrates to his stumbles on Tagalog focus markers exactly the way it calibrated Marisol on Korean past tense.
- **The heritage learner.** Nadia, thirty-four, grew up hearing Farsi at her grandmother's house in Tehran-by-way-of-LA but never spoke it past age six. She turns on heritage mode. The tutor speaks at natural adult speed, uses idioms freely, never explains the words she clearly understands, and gently introduces the writing-out of grammatical features she only ever heard.
- **The interruption.** Mid-session, Marisol stops in the middle of her own sentence and says "ay, ¿cómo se dice 'mother-in-law'?" The tutor pauses, answers in Spanish ("se dice 시어머니, sieomeoni — literally 'married-house-mother'"), then returns to Korean in its very next utterance. The exchange is one breath long.
- **The recognition error.** Sung-min says *Magkano* and the Live API transcribes *Mag bangko*. He taps "I didn't say that" on the transcript bubble. The utterance moves out of the mistake ledger into `recognition_errors_flagged_by_learner` and the recap does not penalise him.
- **The Chuseok preparation.** Marisol has six weeks until she meets her boyfriend's parents at Chuseok in Daegu. She enters their names and the relationships into `proper_nouns_to_remember`. From the next session forward, the tutor occasionally says "어머님 will probably ask you about your work in Querétaro — let's practise" and the past-tense practice happens inside that conversational frame. The names are never mispronounced.
- **The honorific shift.** The tutor, sensing Marisol is preparing to speak to elders, raises register mid-session from ~해요 to ~합니다 and then ~하세요 and gently calls attention to it once: "for your boyfriend's mother, this one — 어머님 — and this verb ending — 하세요." She practises both for two minutes; the rest of the session stays in ~합니다.
- **The Cantonese tone learner.** Wai-ling, a paediatric nurse in Auckland, is learning Mandarin from Cantonese. Tones map between the two languages but not the same way. The tutor's MistakeKind taxonomy tags her tone slips as `tone_mistake` (not "pronunciation"), and the SR scheduler brings the specific tone-pair confusions back distinctly.
- **The bad day.** Marisol's grandmother had a fall yesterday. She opens the app on the bus anyway because the session is a constant in her week. The tutor's voice, hearing the flatness in hers, drops the target items entirely and asks gently in Spanish whether she wants to talk a little — and if she does, in which language. They have a quiet five-minute conversation about her grandmother in Spanish; the rest is silence and a closing line in Korean. The recap notes "you came to the session today — that counts." No mistake ledger update.
- **The Vietnamese learner.** Lan, a software engineer in Berlin, is learning Vietnamese from German. The tutor handles the Vietnamese tone diacritics natively, surfaces tone mistakes as their own kind, and respects her preference for Southern Vietnamese dialect even though Northern is the textbook default.
- **The Swahili-learning grandmother.** Patricia, sixty-eight, in Birmingham, is learning Swahili to talk to her grandson's other grandmother in Mombasa. She switches on quiet mode and slow voice. The tutor speaks more slowly, uses simpler sentences, and never makes her feel rushed. There is no streak. There is no leaderboard. There is one twelve-minute conversation, three days a week, and a grandmother who can introduce herself at the wedding.
- **The Tigrinya reclamation.** Yonas, twenty-nine, born in Eritrea, raised in Stockholm from age four, is learning back Tigrinya. The tutor's target dialect is Asmara Tigrinya; the source language is Swedish. His heritage mode is on. Twelve weeks in, he calls his uncle in Asmara and they speak Tigrinya for forty minutes.
## 6. Page structure
Build the following screens / sections in this order. Adjust copy to fit the voice, but keep the structural intent.
1. **Welcome / sign-in.** A photographed-looking image of a hand holding a phone on a city bus at dawn — warm light through the window, the silhouette of one earbud cable. One paragraph: "Twelve minutes a day. A conversation in your target language that already knows what you got wrong last week." Single Google sign-in button; Apple sign-in next to it; magic-link email as fallback. Below: "Try with the sample conversation" → loads a 90-second pre-recorded demo session.
2. **Language pair setup.** Two big dropdowns side by side: "I already speak…" and "I'm learning…". A toggle: "I understood this language as a child but did not speak it". A free-text field: "People you'll talk to — names + relationships (we'll remember the pronunciations)." A dialect preference dropdown. A short list of off-limits topics (free text). Save → you are ready.
3. **Today's brief card (home screen).** One short card: "Today we'll work on past-tense vowel harmony (었/았), the honorific 합니다 form, and a callback to your conversation about Ji-won's mother." One large button: "Start the 12-minute conversation." A smaller, lighter button: "Just talk — skip today's targets." (The bad-day mode.)
4. **The live conversation screen.** A large soft waveform that pulses with whoever is speaking. The tutor's words appear as serif text in the upper half, the learner's as a separate column in the lower half, both in the target language by default. A "show source language" toggle reveals the streaming translation underneath each line. A single big "pause / interrupt" button. A "I didn't say that" button on any of the learner's bubbles. A timer in the upper-right showing 12:00 → 0:00. Subtle, no countdown panic.
5. **End-of-session recap.** Three good moments at the top, each as a single short sentence in the source language. Then three phrases scheduled to come back — each a short card with the target-language phrase, a one-line "why this one comes back" in the source language, and a small "play this" button (TTS replay). A single "see the full transcript" link. No score. No XP. No streak chime.
6. **Session history.** A vertical list of all your sessions, newest first. Each row: date, duration, language pair, three small chips showing today's targets, a small icon if there was a recognition-error flag. Tap a session → its full transcript and recap.
7. **Mistake ledger.** A grouped view of every open mistake, by linguistic feature. Tap a group → the list of mistakes inside it, each with the verbatim original utterance audio (the learner's voice — they can listen to themselves), the kinder correct form, the rule it follows, and the next-due date. A learner can manually retire an item if they feel they have it.
8. **Per-feature dashboard.** A clean grid of cards, one per linguistic feature label. Each card: feature name, current open count, accuracy trend sparkline over 30 days, one-sentence plain-language summary. No "level". No coloured grade. Numbers used factually, not as judgment.
9. **Lexicon.** Every word the tutor has introduced in any session, with the contextual sentence the tutor produced. Filter by date introduced, by tutor-vs-learner-introduced, by retired. Tap any item to hear the TTS replay.
10. **Calendar.** A monthly grid. Days you sessioned have a small soft dot. Days you didn't are blank. There is no "current streak" number. There is no "longest streak" trophy. The visual weight of "did" and "didn't" is the same.
11. **Settings.** Language pair (editable any time). Session length default (5, 8, 12, 18, 25). Quiet mode toggle. Heritage mode toggle. Dialect preference. Off-limits topics. Per-week recap email opt-in. "Download my entire study record (JSON + HTML)." "Delete a session" (per session). "Delete my account and everything in it" (60-second confirmation).
12. **Footer.** "Made for the bus and the train and the in-between." Privacy: "Your conversations 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 The Commute Tutor."
- Subhead: "A twelve-minute conversation in your target language — calibrated to your mistakes from last week."
- One paragraph (≤ 60 words) explaining who this is for and what makes it different from a generic language app: it is a real spoken conversation, it remembers what you got wrong and schedules it back, it never congratulates you for finishing a sentence.
- Visual: a small annotated illustration of a phone screen on a commute — the timer in the corner, the waveform pulsing, the "I didn't say that" button labelled.
**Slide 2 — Try it now.**
- One short prompt: "Try a 90-second sample conversation."
- A pre-recorded sample session in Spanish ↔ Korean playing back, paused at a moment where the tutor naturally weaves a target item into the conversation.
- 1-2 sentences pointing at *the specific page elements* where the Gemini magic happens (the live duplex audio, the silent ledger updating, the tutor's choice to NOT correct a small slip).
**Slide 3 — How to remix this.**
- Headline: "Make this yours."
- Three short bullets:
- "Swap the sample language pair in `/data/seed-language-pair.json` for your own."
- "Adjust the per-call system instructions in `/server/prompts/` to fit the dialect and register you want."
- "Wire up your Gemini API key and Firebase project via the env-var list in the capabilities panel."
- Primary CTA: "Use this template" → links to AI Studio Build remix entry point.
- Secondary: "Just exploring — close" (sets localStorage flag, never auto-shows again).
**Accessibility:** focus trap, `Esc` closes, `role="dialog"`, `aria-modal="true"`, `aria-labelledby`, focus restored to trigger on close. Respect `prefers-reduced-motion`.
**Don't:**
- Don't gate content behind the modal. The page beneath must be fully usable.
- Don't auto-reshow on return visits. Use `localStorage['onboarding-seen-v1']`.
- Don't include unrelated CTAs (newsletter signup, social follow). Keep it about the template only.
## 6c. Capabilities info button (persistent in header)
Add a persistent `(i)` icon in the top-right of the header (next to the primary nav). Click → opens a modal/panel titled **"What powers this app"**.
**Panel contents (in this order):**
**Gemini capabilities used (the hero list):**
- **Gemini Live API** — drives the spoken twelve-minute conversation with millisecond-latency duplex audio and native interruption support. One session opens at "Start the conversation" and closes at the natural turn boundary near the 12-minute mark.
- **Gemini 3.5 Flash (structured output)** — composes today's calibrated session brief from your last 14 days of sessions and your mistake ledger. Reads everything that matters; outputs one tidy JSON brief that the live model then obeys.
- **Gemini 3.5 Flash (structured output, session review)** — grades the session after it ends, extracts per-utterance corrections, picks the three good moments and the three phrases that will come back, and updates your mistake ledger.
- **Gemini 3.5 Flash (multilingual)** — handles every supported language pair in both directions, with dialect awareness (Seoul vs. Busan Korean, Beijing vs. Taiwan Mandarin, Castilian vs. Mexican Spanish, European vs. Brazilian Portuguese, Tagalog vs. Cebuano-influenced Tagalog).
- **Gemini 3.5 Flash** — runs the spaced-repetition planner and the per-feature dashboard summary. Fast, cheap, deterministic with light judgement.
- **Gemini TTS** — replays any single tutor utterance on demand at slower speed, for when you want to hear it back twice.
- **Firebase Auth** — Google and Apple sign-in. Optional magic-link email if the sender domain is configured.
- **Firestore** — stores your sessions, your mistake ledger, your lexicon. Syncs across devices in real time.
- **Firebase Storage** — keeps the audio recordings of your own voice (the learner side) so you can listen back to any moment. You can delete any clip, any session, or your entire account at any time.
- **Cost note** — see the detailed breakdown in 6d. A typical 12-minute daily session costs about $0.18 of Gemini API spend; a month of daily sessions is around $5.50.
- **Privacy note** — your audio and transcripts are private to you. This app uses the Gemini API on the paid tier, where Google does not use your content for model training, per the Gemini API Additional Terms. You can download your full study record at any time as JSON + HTML and delete anything you want.
**Backend services this app depends on:**
- Auth: see section 4b
- Database: see section 4b
- Storage: see section 4b
- Email: see section 4b (optional weekly recap)
- Payments: see section 4b (not used in v1)
- External APIs: see section 4b
**Environment variables you'll need to configure:**
- `GEMINI_API_KEY` — your Google AI Studio API key
- `FIREBASE_PROJECT_ID` — your Firebase project id
- `FIREBASE_SERVICE_ACCOUNT` — service-account JSON (server-side only)
**Cost + privacy notes:**
- One short paragraph per cost-sensitive capability: Live API audio is billed per minute of audio in + out; one 12-minute session ≈ 24 audio-minutes total ≈ ~$0.14. The brief composer and the session reviewer together are a few cents per session. TTS replays are billed per character and are negligible for a single utterance.
- One short paragraph on privacy: where the data lives (your Firebase project), how to delete it (Settings → "Delete this session" or "Delete my account forever" — gone in 60 seconds), what is never sent for training.
**Documentation links:**
- AI Studio Build docs
- Gemini Live API docs (audio in/out, transcription, barge-in)
- Gemini API multilingual + structured-output + long-context docs
- Firebase Auth, Firestore, Firebase Storage docs
**Accessibility:** same standards as the onboarding modal — focus trap, `Esc`, ARIA, restored focus.
**Behaviour:**
- Always available — single click from anywhere in the app.
- Tooltip on the `(i)` icon: "How this app is built".
- Mobile: opens as a full-screen sheet that slides up.
- Should be the most honest part of the app — never hand-wave service requirements; never say "AI" without naming the specific Gemini model and capability.
## 6d. Detailed cost breakdown (deployer reads this BEFORE shipping)
- **Live API session (Gemini 3.5 Flash, native audio in + audio out)** — one 12-minute session ≈ ~24 audio-minutes counted (input + output). At list pricing of roughly $0.0058/audio-minute in + $0.0117/audio-minute out, a 12-minute session is ≈ ~$0.14. The exact figure depends on talk-time ratio; assume ~60% tutor / ~40% learner.
- **Session brief composer (Gemini 3.5 Flash, medium thinking)** — long-context input of ~85k tokens (the last 14 days of transcripts + ledger), ~1.5k output tokens. ~$0.115 input + ~$0.008 output ≈ ~$0.12 per call. Runs once per session.
- **SR planner (Gemini 3.5 Flash, low thinking)** — ~5k input tokens of ledger summary, ~500 output tokens. ~$0.0004 input + ~$0.0002 output ≈ ~$0.001 per call. Runs once per session.
- **Session reviewer (Gemini 3.5 Flash, low thinking)** — ~10k input tokens of session transcript, ~3k output tokens. ~$0.013 input + ~$0.015 output ≈ ~$0.03 per call. Runs once per session.
- **Per-feature dashboard summary (Gemini 3.5 Flash, low thinking)** — ~30k input tokens of 90-day history, ~1k output tokens. ~$0.0023 input + ~$0.0003 output ≈ ~$0.003. Runs on demand (when learner opens the dashboard, cached for 24 hours).
- **TTS replay (Gemini 2.5 Flash TTS)** — billed per output token (~$10/M output tokens, ~$0.000003/character). A typical replayed utterance is ~200 characters ≈ ~$0.0006. Cached per utterance.
- **Expected per-session cost (12-minute):** Live audio ~$0.14 + brief ~$0.12 + planner ~$0.001 + review ~$0.03 ≈ ~**$0.29 per session**. With aggressive caching of the dashboard and rare TTS replays, ~**$5.50/learner/month** at one session per day.
- **Audio storage:** Firebase Storage standard tier, ~$0.026/GB/month. A 12-minute conversation at 24kbps Opus is ~2 MB; a year of daily sessions ≈ ~700 MB ≈ ~$0.02/month per learner.
## 7. Design language
- **Mood:** A trusted conversation partner who happens to live in your pocket. Not a "fun" app. Not a "rigorous" app. The kind of app you open while the bus pulls out, the kind that does not flinch when your face changes. Soft like a window seat at dawn. Quiet like a friend who knows when to listen.
- **Typography:** A humanist serif for tutor utterances and the recap (Source Serif Pro or Plantin) — the tutor's words feel like a person, not a system. A clean grotesque for app chrome (Inter or Geist). A subtle differentiator for the learner's transcripted utterances — same serif but a touch lighter weight, so the page reads like a play script.
- **Palette:** A warm bone-paper background `#F6F1E9`. Deep ink `#1B1714` for body text. A muted morning-blue `#3A5773` for the tutor's bubble accent. A quieter sage `#7C8C6E` for the learner's bubble accent. A soft amber `#C49A4E` only for "scheduled to come back" highlights. A single restrained red `#A33A2C` reserved for the rarely-used "I didn't say that" affordance. Avoid Duolingo green; avoid neon; avoid any palette that signals "kids' education".
- **Imagery:** Photographed-feeling. A bus window at dawn. A cup of coffee on a kitchen counter at 6:48 am. A pair of earbuds coiled neatly on a desk. No vector mascots. No friendly cartoon characters. No flame icon next to streak numbers, because there are no streak numbers.
- **Hand-feel touches:** The waveform pulses gently — not aggressively. The recap card slides up with a thin shadow, like a note left on a kitchen counter. The transcript bubbles appear one phrase at a time as the audio streams. No bouncing emoji. No celebratory confetti.
- **Spacing:** consistent 4-px base. Generous whitespace — the conversation needs air.
- **Radius:** consistent token set (e.g. 6 / 12 / 20 px). Transcript bubbles use 12; the home brief card uses 20; small chips use 6.
- **Shadows:** subtle, layered, warm-tinted. Avoid heavy drop-shadows.
- **Motion:** purposeful — waveform pulse, recap card slide-up, transcript bubble fade-in. Respect `prefers-reduced-motion`. No bouncing splash animations. No theatrical hero animations. The waveform respects reduced-motion by holding a static state and pulsing only the active speaker's column.
- **States:** every interactive element has hover, focus, active, disabled. The "Start the conversation" button has a quiet pre-state ("preparing today's brief…") for the 1-2 seconds the brief composer is running.
## 8. Content generation rules
- Write **realistic, specific copy**. NO Lorem Ipsum. NO generic placeholders like 'Your tagline here'.
- Invent plausible names, language pairs, sample utterances that fit the domain (use the seed content in section 8a as a starting point). When inventing, draw from a wide cultural set — Spanish-speakers learning Korean, Filipino caregivers learning Arabic, Eritrean immigrants learning German, French-speakers learning Mandarin. Do not centre any one language pair as default.
- Tone: warm, direct, free of corporate language. This template is for a person, not a company.
- Headlines: punchy and concrete. No 'Empower your X' filler. No 'Revolutionize'. No 'Seamless'.
- Body copy: short paragraphs (2-4 sentences). Use lists where appropriate.
- Plain language. Avoid jargon — except where the user already speaks the jargon (the heritage learner wants to see "passive comprehension"; the language teacher wants to see "spaced repetition" named).
- Where the app outputs AI-generated content, never label it as "AI says" — let the tutor speak naturally. Use small uncertainty cues only where epistemic honesty requires them (a low-confidence transcription shows as a faintly underlined word; tapping it shows the alternates the model considered).
- NEVER use language that condescends. No exclamation marks on praise. No "you've got this!". No "great work!". Adults talking to adults.
## 8a. Seed content (use these specific examples)
Anchor every generated copy + sample data point in the concrete content below. Use these names, numbers, dates, and snippets verbatim where helpful, or generate close variants that sit in the same world.
**Sample learners (sidebar of the demo):**
- "Marisol (es-MX → ko-KR)" — 32, payroll analyst in Querétaro. Six weeks of daily 12-minute sessions on the bus. Heritage mode false. Dialect preference: Seoul. Off-limits: work stress.
- "Sung-min (en-US → tl-PH, Cebuano-influenced)" — 41, software engineer in San Jose. Three months of every-other-day 18-minute sessions on the train to work. Heritage mode false. Off-limits: divorce (from a previous marriage).
- "Nadia (en-US → fa-IR)" — 34, designer in Brooklyn. Heritage mode true. Daily 8-minute sessions while walking her dog at sunset. Dialect preference: Tehrani. Off-limits: politics.
- "Wai-ling (en-NZ → cmn-Hans-CN)" — 39, paediatric nurse in Auckland. Source-tonal-language learner, target also tonal. Three sessions a week, 25 minutes each, while feeding her toddler. Off-limits: pandemic-era hospital stories.
- "Patricia (en-GB → sw-KE)" — 68, retired teacher in Birmingham. Quiet mode on. Slow voice on. Three sessions a week, 12 minutes each. Off-limits: nothing in particular. Goal: introduce herself to her grandson's other grandmother at the wedding in Mombasa.
- "Yonas (sv-SE → ti-ER)" — 29, civil servant in Stockholm. Heritage mode true (heard Tigrinya in his grandmother's flat until age four). Daily 12-minute sessions on the metro. Dialect preference: Asmara Tigrinya. Goal: call his uncle.
**Sample session brief (this is what the demo should show):**
- **Learner first name:** Marisol
- **Language pair:** Spanish (Mexico) → Korean (Seoul)
- **Session length:** 12 minutes
- **Opening topic suggestion:** "How was the rain this morning on the way to the bus?" (testing past tense, using a real-world callback)
- **Callbacks to remember:**
- Topic: "Ji-won's mother is a nurse at the hospital in Daegu" · last_mentioned: session_2026-05-15 · safe_to_bring_up: true
- Topic: "Marisol's grandmother had a fall last week" · last_mentioned: session_2026-05-20 · safe_to_bring_up: false (heavy)
- **Today target items (3):**
- Past-tense vowel harmony 었/았 — example seed: "어제 비가 많이 왔어요?" — why: stumbled three sessions in a row last week
- Honorific 합니다 form (Marisol is preparing to speak to elders at Chuseok) — example seed: "어머님께서 잘 지내십니까?" — why: introducing now so it has time to settle
- Particle 에서 vs 에 — example seed: "회사에서 무슨 일이 있었어요?" — why: due today per SR schedule (last stumble: 11 days ago)
- **Rolling strengths:** "comfortable opening a conversation in Korean", "uses 요-form endings consistently", "remembers Ji-won's mother's title 어머님 verbatim"
- **Rolling struggles:** "past-tense vowel harmony under time pressure", "code-switches to Spanish when describing emotions", "drops particle 은/는 in spontaneous sentences"
- **Tone directive:** "Patient, never condescending. Plain acknowledgement, not exclamation. Marisol is an adult learner with a real motivation; treat her as a competent person who is working hard. Match her energy — she sounds quieter on Monday mornings."
- **Hard off-limits topics:** ["work stress", "her job hunt last year"]
**Sample utterance pair from inside the live session (this is what the transcript view should show):**
- **Tutor (ko-KR):** "오늘 아침에 비가 많이 왔어요?" ("Was there a lot of rain this morning?")
- **Learner (ko-KR, with one slip):** "네, 비가 많이 와요." ("Yes, it rained a lot." — used present tense)
- **Tutor (ko-KR, gentle correction, count 1 of 3 for the session):** "네, 비가 많이 왔어요. 저도 우산을 가져왔어요." ("Yes, it rained a lot. I brought an umbrella too.")
- **Learner (ko-KR, self-correcting):** "네 — 네, 비가 많이 왔어요. 우산을 안 가져왔어요." ("Yes — yes, it rained a lot. I didn't bring an umbrella.")
- *(Detected mistake stored in ledger: vowel_pronunciation NO — verb_tense_wrong YES, kind: verb_tense_wrong, severity: worth_a_callback, kinder_correct_form: "비가 많이 왔어요", explanation_in_es: "el verbo en pasado: 와요 → 왔어요. Mismo verbo, diferente tiempo.")*
**Sample three good moments (recap):**
- "You opened the conversation in Korean without hesitating — first time without prompting in two weeks."
- "You used 어머님 with the correct honorific the first time it came up — you remembered it from last Thursday."
- "When the tutor asked about your weekend, you stayed in Korean for five sentences before code-switching. Last week the average was two."
**Sample three phrases scheduled to come back:**
- 비가 많이 왔어요 · why: past-tense vowel was right on the second try; one more pass to make it automatic · next due: 4 days
- 회사에 갔어요 · why: confused 에 with 에서 on this one; needs the contrast back · next due: 3 days
- 어머님께서 어디에 계세요 · why: honorific form combined with location — a hard combination; bring back gently · next due: 7 days
**Sample voice copy:**
- Onboarding: "Twelve minutes of Korean — the kind of conversation you'd have with a patient friend who knows what you got wrong last week."
- Pre-session prep: "Preparing today's conversation — looking at the last two weeks…"
- Mid-session interruption acknowledgement: "Sure — *se dice 시어머니. Significa 'married-house-mother'.*"
- End-of-session close (tutor, in Korean): "내일 봐요." ("See you tomorrow.")
- Empty state (no sessions yet): "Start with one twelve-minute conversation. The tutor learns everything else from you."
- Error (Live API failed to start): "We couldn't open today's conversation. Sometimes the connection is just slow — want to try again? Your prepared brief is saved either way."
- Recognition-error tap acknowledgement: "Got it — flagged as a mishearing. This one won't count against you in the ledger."
- Bad-day mode tutor closing: "오늘은 여기까지. 잘 와줬어요." ("Let's stop there for today. Glad you came.") *(no recap targets generated)*
## 9. Media & assets
- **Hero image (landing screen):** A photographed-looking shot of a hand holding a phone on a city bus at dawn — warm light through the window, the silhouette of one earbud cable trailing out of view, the screen showing a soft waveform. Generate via Nano Banana 2 with a prompt emphasising "phone in hand, city bus seat, warm morning light through window, single earbud cable, soft focus on the world outside the window, no faces, no marketing-style lighting".
- **App icon / wordmark:** Set in the serif. A small dot under the wordmark suggests the active waveform without literalising it. No microphone icon — that reads as voice-control, not conversation.
- **Empty-state illustration:** A simple line drawing of two speech bubbles overlapping gently, one slightly translucent. Hand-drawn aesthetic, not a flat icon.
- **Sample-conversation audio:** A 90-second pre-recorded conversation between a Mexican Spanish learner and a Seoul-Korean-accented tutor voice — generated via Gemini TTS, cached as an MP3. Each sentence on a separate sub-track so the transcript can highlight the active line.
- **Period-feel imagery — none.** Unlike a historical-archive app, this is contemporary. Imagery is morning commutes, kitchen counters, walking-with-earbuds — all photographed-feeling, all warm, all without people's faces.
- **Stock fallbacks:** If image generation fails, fall back to a tasteful photographed image from `/public/samples/dawn-window.jpg`. Never to a "🎧" emoji.
- **Generated imagery:** prefer Nano Banana 2 over stock photography. Prompt for warmth, asymmetry, slight imperfection — avoid the glossy "AI render" look.
- **Optimisation:** WebP/AVIF, `loading="lazy"`, explicit `width`/`height` to prevent layout shift.
- **Icons:** `lucide-react` for UI. Use sparingly — never decorative-only.
### Build-time asset manifest (explicit specs)
Every image, illustration, and visual reference mentioned above must resolve to ONE of the three buckets below — runtime-generated, seed-shipped, or user-supplied. Do NOT ship `` tags whose `src` is not listed here. Do NOT depend on bare "section 8a prompts" without binding them to explicit paths and model IDs.
**Bucket 1 — Runtime-generated (Nano Banana Pro `gemini-3-pro-image` for hero/demo photographs; Nano Banana 2 `gemini-3.1-flash-image` for in-app illustrations and reference-conditioned variants).** Cached to Firebase Storage; served via signed URL. Every reference above to "Nano Banana 2" or "Nano Banana Pro" MUST be wired to one of these specific calls with an explicit model id:
- `/public/generated/hero.webp` (2400×1500, WebP) — model `gemini-3-pro-image` — uses the literal prompt described as "Hero image (landing screen)" above. Run once at build; commit a `/public/samples/hero-fallback.webp` (1600×1000) generated from the same prompt with `gemini-3.1-flash-image` so the page renders if quota is exhausted.
- `/public/generated/demo/{demo-slug}-{NN}.webp` (1600×1200, WebP) — model `gemini-3.1-flash-image` (reference-conditioned where the prior frame is passed as input) — one path per "Demo X" image referenced above. The slug derives from the seed example in section 8a; the NN index covers each frame in the demo sequence.
- `/public/generated/illustrations/{name}.webp` (1024×1024, WebP) — model `gemini-3.1-flash-image` — one path per named illustration above ("Empty-state illustration", "Recipe-card hero illustrations", "Curriculum picker imagery", "Period-style frames", etc.). Each illustration's prompt is the literal description above; ship a deterministic seed in the request so re-runs are reproducible.
**Bucket 2 — Seed assets shipped with the deliverable.** Every "Stock fallback" path referenced above (e.g. `/public/samples/sample-X.jpg`) is generated once via Nano Banana 2 (`gemini-3.1-flash-image`) at 1024×1024 WebP using the same prompt as its Bucket-1 counterpart, then committed to the repo so the page renders identically if Gemini quota is exhausted or the user is offline. Replace any `.jpg` extension above with `.webp` to match the optimisation rule. Also commit these empty-state seeds (1024×1024 WebP, single-stroke hand-drawn line, no colour fill):
- `/public/samples/empty-state-primary.webp` — line drawing of the app's primary empty surface (the named "Empty-state illustration" above), generated from that exact prompt.
- `/public/samples/empty-state-archive.webp` — line drawing of an empty saved/archive view, single-stroke outline.
- `/public/samples/empty-state-error.webp` — line drawing of a hand placing a single object aside with care, used when an AI call fails.
**Bucket 3 — User-supplied.** Uploads from the user's camera / file picker land at the Firebase Storage path conventional for this template (named in section 4b). The build ships with Bucket-1 + Bucket-2 only; no user-supplied images at first paint.
**Hard rules**
- Every `` tag MUST have a `src` that resolves to a path listed in Bucket 1, Bucket 2, or a Bucket 3 upload path. Anything else is a build error.
- No bare `image.jpg` / `hero.jpg` / `placeholder.png` references anywhere in the code.
- Model IDs: `gemini-3-pro-image` for hero-quality photographic generation; `gemini-3.1-flash-image` for in-app illustrations, reference-conditioned variants, empty-state seeds, and stock fallbacks. Never use a legacy model id (no `imagen-*`, no `gemini-1.5-*-image`).
- File format: WebP everywhere (AVIF acceptable where the target browsers support it). No `.jpg` / `.jpeg` / `.png` in `/public/samples/`.
## 10. Interactivity & states
- Every interactive element has hover, focus, active, and disabled states.
- Forms validate inline and show specific error messages (not "Invalid input").
- Loading states use skeletons that match the eventual layout, not spinners. The 1-2 seconds between "Start the conversation" and the first tutor utterance show a gentle "preparing today's conversation…" caption under the start button, not a spinner.
- Empty states explain the next action with a button whose label fits THIS app's domain: "Start your first 12-minute conversation", "Set up a language pair", "Open the mistake ledger" — never a generic "Add your first item".
- Smooth scroll for in-page anchors.
- The transcript streams in token-by-token as the Live API returns audio + transcription, with a faint "listening…" or "thinking…" placeholder before the next utterance arrives.
- If the Live API call fails to open, show a calm, specific error ("We couldn't open today's conversation — the connection is slow right now. Want to try again? Your prepared brief is saved.") and offer retry.
- Low-confidence transcribed words are faintly underlined; tapping reveals the alternates the model considered.
- The waveform respects `prefers-reduced-motion` by holding a static state and only highlighting which speaker is active.
- The recap "three good moments" cards appear one at a time with a 200ms stagger; reduced-motion fades them in simultaneously.
- Pressing "I didn't say that" updates the bubble visually within 100ms (optimistic UI); the ledger update reconciles when the SessionReview call returns.
## 11. Tech & responsive requirements
- **TTS markdown-stripping preprocessor:** before sending any user-authored markdown to `gemini-3.1-flash-tts-preview`, strip non-spoken markdown: `#`/`##`/`###` headings (keep the title text), `**bold**` (keep the inner text), `[label](url)` (keep `label`, drop URL), `` ``` `` fenced code blocks (skip entirely), `>` block-quote markers (keep the text), and `|` table pipes (read row-by-row as sentences). Insert `…` between sentences for a short pause and a blank line plus `—` between paragraphs for a long pause. The model does not understand markdown; raw markdown will be read aloud as literal characters ("asterisk asterisk").
- **File downloads on Safari / Firefox:** when offering local-disk save of any export (PDF, CSV, MP3, ZIP, JSON, image), fall back to `` with a blob URL — the File System Access API (`showSaveFilePicker()`) is Chromium-only. Detect with `'showSaveFilePicker' in window`; otherwise use the anchor-download path.
- **Stack:** React + TypeScript + Tailwind CSS. Functional components + hooks. Use Shadcn UI primitives where appropriate.
- **Build runtime:** AI Studio Build — full-stack with Cloud Run server-side functions. All non-Live Gemini API calls happen server-side; API key lives in Secrets Manager, never in client bundle.
- **Live API connection:** The Live API session is established from the client over the Gemini Live API's WebSocket / WebRTC channel. The Cloud Run server function issues a short-lived ephemeral token (scoped to one session) that the client uses to authenticate the Live API connection — the long-lived `GEMINI_API_KEY` never reaches the browser.
- **Model selection:** explicitly pin `gemini-3.5-flash` for the brief composer / session reviewer; `gemini-3.5-flash` for the SR planner / dashboard summary; `gemini-3.5-flash` (Live variant) for the live conversation; `gemini-3.1-flash-tts-preview` for TTS replay. Set `thinkingLevel` explicitly per call; omit on Live and TTS.
- **Database:** Firestore (auto-provisioned by AI Studio Build). Show the seed brief on first launch.
- **Auth:** Firebase Auth — Google sign-in by default; Apple sign-in next to it; magic-link email as fallback.
- **Storage:** Firebase Storage for the learner's audio recordings. Pre-signed URLs only.
- **Mobile-first.** This is a commute app — verify layouts at 375 px (iPhone SE) above all. Then 768 px (iPad), 1024 px, 1440 px+.
- Use `clamp()` for fluid typography. Prefer container queries over media queries for component-level responsiveness.
- Use `dvh` / `svh` instead of `vh` — critical for the live-conversation full-screen view on iOS where the URL bar collapses.
- Respect safe-area insets on iOS — the timer in the upper-right and the interrupt button at the lower-centre must clear the notch and the home indicator.
- Zero horizontal overflow at any width. Zero layout shift on load.
- Persist user data in Firestore. Use real-time listeners on the live session document.
- Optimistic UI on writes; reconcile on response.
- Background-friendly: the session must keep streaming if the screen locks with the phone in a pocket and the AirPods stay connected. Use a `MediaSession` action handler so the lock-screen shows the active session and exposes pause / resume.
- **iOS Safari gotchas (graceful degradation):** an incoming call interrupts the audio session (`MediaStreamTrack.onmute` fires) — auto-pause the tutor calmly ("hold on a moment — I'll wait"), resume on `onunmute`, do not lose session state; mic permission does NOT persist across page reloads on iOS — re-request at the start of every commute session (the user takes this app on a phone they reboot/reload often); when Safari does eventually background-throttle the WebSocket despite the Wake Lock, fall back to a calm "your session paused — your brief is saved" rather than a hard error; PCM streaming via `AudioWorklet` (Safari `MediaRecorder` is AAC-only).
## 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.
- Colour contrast ≥ 4.5:1 for body, 3:1 for large text and UI components.
- All images have meaningful `alt` text. The hero image has `alt` describing the artefact ("photograph of a hand holding a phone on a city bus at dawn").
- Form fields have associated `