From 02a189b9f245b1016c370c74ae232711917ce907 Mon Sep 17 00:00:00 2001 From: Markus Graf Date: Sun, 29 Mar 2026 11:50:06 +0200 Subject: [PATCH] docs(02): create phase plan for Gemini integration + asset pipeline --- .planning/ROADMAP.md | 11 +- .../02-01-PLAN.md | 232 +++++++++++ .../02-02-PLAN.md | 241 ++++++++++++ .../02-03-PLAN.md | 295 ++++++++++++++ .../02-04-PLAN.md | 256 ++++++++++++ .../02-05-PLAN.md | 369 ++++++++++++++++++ 6 files changed, 1402 insertions(+), 2 deletions(-) create mode 100644 .planning/phases/02-gemini-integration-asset-pipeline/02-01-PLAN.md create mode 100644 .planning/phases/02-gemini-integration-asset-pipeline/02-02-PLAN.md create mode 100644 .planning/phases/02-gemini-integration-asset-pipeline/02-03-PLAN.md create mode 100644 .planning/phases/02-gemini-integration-asset-pipeline/02-04-PLAN.md create mode 100644 .planning/phases/02-gemini-integration-asset-pipeline/02-05-PLAN.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index c4556cb..2a781ea 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -49,7 +49,14 @@ Plans: 2. Der Willkommens-Screen zeigt echte generierte Avatare statt Platzhalter 3. Ein Begruessungstext der Begleitfigur wird generiert (deutsch, max 25 Woerter) oder ein Fallback-Text erscheint wenn kein API-Key vorhanden ist 4. Ein Waldelement-Bild kann mit Stil-Referenz generiert werden; bei fehlendem API-Key zeigt die App Fallback-Bilder ohne Absturz -**Plans**: TBD +**Plans:** 5 plans + +Plans: +- [ ] 02-01-PLAN.md — Config loader, Gemini API client (text + image), retry logic, rate-limiting (TDD) +- [ ] 02-02-PLAN.md — Fallback texts (greetings, forest comments, letter intros) and fallback SVG images +- [ ] 02-03-PLAN.md — IndexedDB store accessors, build script for asset generation, generate all character assets +- [ ] 02-04-PLAN.md — Companion text module (getGreeting, getLetterIntro, getForestComment) with API-first fallback +- [ ] 02-05-PLAN.md — Wire avatars into welcome screen, companion greeting on forest screen, visual verification ### Phase 3: Komplettes Spielerlebnis **Goal**: Alle 3 Uebungsphasen verbunden, Wald waechst visuell, Stufen 1–6 vollstaendig spielbar @@ -85,6 +92,6 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 | Phase | Plans Complete | Status | Completed | |-------|----------------|--------|-----------| | 1. Grundgerüst + Tippmechanik | 0/5 | Planning complete | - | -| 2. Gemini-Integration + Asset-Pipeline | 0/? | Not started | - | +| 2. Gemini-Integration + Asset-Pipeline | 0/5 | Planning complete | - | | 3. Komplettes Spielerlebnis | 0/? | Not started | - | | 4. Polish + Audio | 0/? | Not started | - | diff --git a/.planning/phases/02-gemini-integration-asset-pipeline/02-01-PLAN.md b/.planning/phases/02-gemini-integration-asset-pipeline/02-01-PLAN.md new file mode 100644 index 0000000..6481dbf --- /dev/null +++ b/.planning/phases/02-gemini-integration-asset-pipeline/02-01-PLAN.md @@ -0,0 +1,232 @@ +--- +phase: 02-gemini-integration-asset-pipeline +plan: 01 +type: tdd +wave: 1 +depends_on: [] +files_modified: + - src/api/config.ts + - src/api/gemini.ts + - src/api/config.test.ts + - src/api/gemini.test.ts + - src/types.ts + - src/storage/db.ts +autonomous: true +requirements: [GAPI-01, GAPI-02, GAPI-03, GAPI-06] + +must_haves: + truths: + - "Config loader fetches and parses public/config.json with typed result" + - "Text generation calls Gemini REST API with retry logic (3 attempts, exponential backoff)" + - "Image generation calls Gemini REST API with reference images as base64 inlineData" + - "Rate-limiting prevents more than 1 image + 1 text call per exercise unit" + - "All API functions return null on failure (caller uses fallback)" + artifacts: + - path: "src/api/config.ts" + provides: "Config loader with GeminiConfig interface" + exports: ["loadConfig", "GeminiConfig"] + - path: "src/api/gemini.ts" + provides: "Isomorphic Gemini API client" + exports: ["generateText", "generateImage"] + - path: "src/api/config.test.ts" + provides: "Config loader tests" + - path: "src/api/gemini.test.ts" + provides: "Gemini client tests" + key_links: + - from: "src/api/gemini.ts" + to: "src/api/config.ts" + via: "loadConfig() provides API key and model names" + pattern: "loadConfig" + - from: "src/api/gemini.ts" + to: "src/storage/db.ts" + via: "rate-limit check reads/writes settings store" + pattern: "getSettings|saveSettings" +--- + + +Create the config loader and isomorphic Gemini API client with text generation, image generation, retry logic, and rate-limiting. + +Purpose: This is the foundation for all AI-powered features. Everything in Phase 2 depends on a working, tested Gemini API client. +Output: `src/api/config.ts`, `src/api/gemini.ts` with full test coverage. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md + +@src/types.ts +@src/storage/db.ts +@public/config.json + + + +```typescript +export interface Settings { + id: 1; + audioEnabled: boolean; + apiKey: string; +} +``` + + +```typescript +export function getSettings(db: IDBDatabase): Promise; +export function saveSettings(db: IDBDatabase, settings: Settings): Promise; +``` + + + + + + + Task 1: Config loader and Gemini API client with tests (TDD) + src/api/config.ts, src/api/config.test.ts, src/api/gemini.ts, src/api/gemini.test.ts, src/types.ts, src/storage/db.ts + + - src/types.ts (existing interfaces — extend Settings with apiCallsToday + lastApiCallDate per D-03) + - src/storage/db.ts (existing DB methods — pattern to follow) + - public/config.json (actual config structure: geminiApiKey, geminiModel, imageModel) + - SPEC.md sections 6.4 and 9.1 (API endpoints and config format) + + + Config loader (src/api/config.ts): + - Test: loadConfig() returns GeminiConfig with geminiApiKey, geminiModel, imageModel + - Test: loadConfig() returns null when fetch fails (e.g. 404) + - Test: loadConfig() returns null when JSON is malformed + - Test: loadConfig() caches result after first successful call (does not re-fetch) + + Gemini text client (src/api/gemini.ts — generateText): + - Test: generateText returns extracted text from Gemini response + - Test: generateText retries on 500 error up to 3 times with backoff + - Test: generateText returns null after 3 failed attempts + - Test: generateText sends correct request body (model, contents with systemInstruction) + + Gemini image client (src/api/gemini.ts — generateImage): + - Test: generateImage returns Blob from base64 image in Gemini response + - Test: generateImage sends reference images as inlineData parts + - Test: generateImage sets response_modalities to ["IMAGE"] in generationConfig + - Test: generateImage returns null on failure + + Rate-limiting: + - Test: checkRateLimit returns true when under limit (0 calls today) + - Test: checkRateLimit returns false when at limit (1 image + 1 text today) + - Test: incrementApiCall increments correct counter + - Test: rate limit resets when lastApiCallDate differs from today + + + **Step 1: Extend Settings type (per D-03)** + In `src/types.ts`, add to `Settings` interface: + ```typescript + apiCallsToday: { text: number; image: number }; + lastApiCallDate: string; // ISO date "YYYY-MM-DD" + ``` + + **Step 2: Create src/api/config.ts (per D-07, GAPI-01)** + ```typescript + export interface GeminiConfig { + geminiApiKey: string; + geminiModel: string; // e.g. "gemini-2.5-flash" + imageModel: string; // e.g. "gemini-3.1-flash-preview-image" + } + + let cachedConfig: GeminiConfig | null = null; + + export async function loadConfig(): Promise { + if (cachedConfig) return cachedConfig; + try { + const res = await fetch('/config.json'); + if (!res.ok) return null; + const data = await res.json(); + if (!data.geminiApiKey || !data.geminiModel || !data.imageModel) return null; + cachedConfig = data as GeminiConfig; + return cachedConfig; + } catch { + return null; + } + } + + // For Node.js usage in build script (per D-01) + export function loadConfigFromObject(obj: GeminiConfig): void { + cachedConfig = obj; + } + + export function resetConfigCache(): void { + cachedConfig = null; + } + ``` + + **Step 3: Create src/api/gemini.ts (per D-07, D-08, D-09, D-10)** + - `generateText(prompt: string, systemPrompt: string, config: GeminiConfig): Promise` + - POST to `https://generativelanguage.googleapis.com/v1beta/models/${config.geminiModel}:generateContent?key=${config.geminiApiKey}` + - Body: `{ contents: [{ role: "user", parts: [{ text: prompt }] }], systemInstruction: { parts: [{ text: systemPrompt }] } }` + - Extract `response.candidates[0].content.parts[0].text` + - Retry 3 times with delays [1000, 2000, 4000] ms on non-2xx responses (per D-09) + - Return null on final failure + + - `generateImage(prompt: string, referenceImages: Blob[], config: GeminiConfig): Promise` + - POST to `https://generativelanguage.googleapis.com/v1beta/models/${config.imageModel}:generateContent?key=${config.geminiApiKey}` + - Build parts array: for each referenceImage, convert to base64 and add as `{ inlineData: { mimeType: "image/png", data: base64 } }`, then add text part with prompt (per D-08) + - generationConfig: `{ responseModalities: ["IMAGE"] }` + - Extract base64 image from `response.candidates[0].content.parts[0].inlineData.data` + - Convert base64 to Blob and return + - Return null on failure (no retry for image — too expensive) + + - Rate-limiting helpers (per D-10, GAPI-06): + ```typescript + export async function checkRateLimit(db: IDBDatabase, type: 'text' | 'image'): Promise + export async function incrementApiCall(db: IDBDatabase, type: 'text' | 'image'): Promise + ``` + - Check `settings.lastApiCallDate` — if different from today, reset counts to 0 + - Max 1 image + 1 text per exercise unit (check `apiCallsToday.text < 1` or `apiCallsToday.image < 1`) + + **Step 4: Write tests** + - Use `vi.fn()` and `vi.spyOn(globalThis, 'fetch')` to mock fetch calls + - Use `fake-indexeddb` for rate-limit tests (already in devDeps) + - Test config loader with mocked fetch responses + - Test generateText with mocked Gemini responses + - Test generateImage with mocked responses containing base64 image data + - Test retry logic by failing first 2 calls then succeeding + - Test rate-limit read/write/reset cycle + + **Follow TDD cycle:** Write all tests first (RED), then implement (GREEN), then refactor. + + + cd /home/dev/workspace/zauberwald && npx vitest run src/api/ + + + - `grep -c "export async function loadConfig" src/api/config.ts` returns 1 + - `grep -c "export interface GeminiConfig" src/api/config.ts` returns 1 + - `grep -c "export async function generateText" src/api/gemini.ts` returns 1 + - `grep -c "export async function generateImage" src/api/gemini.ts` returns 1 + - `grep -c "export async function checkRateLimit" src/api/gemini.ts` returns 1 + - `grep "apiCallsToday" src/types.ts` matches (Settings extended per D-03) + - `grep "lastApiCallDate" src/types.ts` matches (Settings extended per D-03) + - `npx vitest run src/api/` passes with 0 failures + - At least 10 test cases across config.test.ts and gemini.test.ts + + Config loader fetches/caches config.json. generateText sends correct Gemini REST request with retry (3x exponential backoff). generateImage sends reference images as base64 inlineData with response_modalities: ["IMAGE"]. Rate-limiting checks and increments counters in Settings store with daily reset. All tests pass. + + + + + +- `npx vitest run src/api/` — all tests pass +- `grep "generateText\|generateImage\|loadConfig\|checkRateLimit" src/api/gemini.ts src/api/config.ts` shows all exports + + + +- Config loader parses public/config.json and caches result +- generateText calls Gemini REST with retry logic, returns string or null +- generateImage calls Gemini REST with base64 reference images, returns Blob or null +- Rate-limiting tracks text/image calls per day in Settings store +- All tests pass via `npx vitest run src/api/` + + + +After completion, create `.planning/phases/02-gemini-integration-asset-pipeline/02-01-SUMMARY.md` + diff --git a/.planning/phases/02-gemini-integration-asset-pipeline/02-02-PLAN.md b/.planning/phases/02-gemini-integration-asset-pipeline/02-02-PLAN.md new file mode 100644 index 0000000..ff31179 --- /dev/null +++ b/.planning/phases/02-gemini-integration-asset-pipeline/02-02-PLAN.md @@ -0,0 +1,241 @@ +--- +phase: 02-gemini-integration-asset-pipeline +plan: 02 +type: execute +wave: 1 +depends_on: [] +files_modified: + - src/companion/fallbacks.ts + - src/assets/fallback-images/blume.svg + - src/assets/fallback-images/pilz.svg + - src/assets/fallback-images/vogel.svg + - src/assets/fallback-images/schmetterling.svg + - src/assets/fallback-images/reh.svg + - src/assets/fallback-images/baum.svg + - src/assets/fallback-images/bach.svg +autonomous: true +requirements: [GAPI-04, GAPI-05] + +must_haves: + truths: + - "10 fallback greetings exist in simple German, max 25 words each, no performance praise" + - "10 fallback forest comments exist in simple German, max 25 words each" + - "Letter introductions exist for all keys in levels 1-6" + - "5-10 fallback SVG images depict forest elements in watercolor pastel style" + artifacts: + - path: "src/companion/fallbacks.ts" + provides: "Static fallback text arrays" + exports: ["fallbackGreetings", "fallbackForestComments", "fallbackLetterIntros"] + - path: "src/assets/fallback-images/blume.svg" + provides: "Fallback flower image" + - path: "src/assets/fallback-images/pilz.svg" + provides: "Fallback mushroom image" + key_links: + - from: "src/companion/fallbacks.ts" + to: "src/companion/characters.ts" + via: "fallback texts reference companion personalities" + pattern: "CompanionType" +--- + + +Create all fallback content: static German texts for companion dialogue and SVG fallback images for forest elements. + +Purpose: The app must work without an API key. Fallbacks are the graceful degradation path. +Output: `src/companion/fallbacks.ts` with all text arrays, 7 SVG fallback images. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md + +@src/companion/characters.ts +@src/game/levels.ts +@SPEC.md (section 9.2 for prompt templates — use as guide for fallback tone) + + + +```typescript +export type CompanionType = "fee" | "einhorn" | "fuchs" | "eule"; + +export interface CompanionDefinition { + type: CompanionType; + name: string; + emoji: string; + personality: string; +} +``` + + +Level 1: F, J, Space +Level 2: D, K +Level 3: S, L +Level 4: A, Ö +Level 5: G, H +Level 6: E, I +``` + + + + + + + Task 1: Fallback texts module + src/companion/fallbacks.ts + + - src/companion/characters.ts (companion types and personalities) + - src/game/levels.ts (exact keys per level for letter introductions) + - SPEC.md section 9.2 (prompt templates — model the tone of fallback texts after these) + + + Create `src/companion/fallbacks.ts` with the following exports (per D-11, GAPI-04): + + **1. Fallback greetings** — 10 entries, keyed by time of day: + ```typescript + export type TimeOfDay = 'morgen' | 'mittag' | 'abend'; + + export interface FallbackGreeting { + text: string; + timeOfDay: TimeOfDay | 'any'; + } + + export const fallbackGreetings: FallbackGreeting[] = [ + // 3-4 morning, 3-4 any-time, 2-3 evening + // Example: { text: "Guten Morgen! Der Wald hat auf dich gewartet.", timeOfDay: "morgen" } + ]; + ``` + + Rules for ALL texts (per COMP-04): + - Max 25 words per text + - Simple German a 7-year-old can read + - No performance praise ("toll gemacht", "super", etc.) + - No English words + - Warm, friendly, nature-themed + + **2. Fallback forest comments** — 10 entries: + ```typescript + export const fallbackForestComments: string[] = [ + // Express wonder and joy about new forest elements + // Example: "Oh schau mal! Ein neuer Bewohner ist in unseren Wald gezogen!" + ]; + ``` + + **3. Fallback letter introductions** — one per key in levels 1-6: + ```typescript + export interface LetterIntro { + letter: string; + finger: string; // e.g. "linker Zeigefinger" + text: string; // The mnemonic/introduction text + } + + export const fallbackLetterIntros: LetterIntro[] = [ + // Level 1 + { letter: "f", finger: "linker Zeigefinger", text: "Das F ist da, wo dein linker Zeigefinger zu Hause ist. Spuerst du den kleinen Huegel?" }, + { letter: "j", finger: "rechter Zeigefinger", text: "Das J hat einen kleinen Strich unten. Dein rechter Zeigefinger findet ihn sofort!" }, + { letter: " ", finger: "Daumen", text: "Die grosse Leertaste ganz unten drueckst du mit dem Daumen. Einfach druecken!" }, + // Level 2: D (linker Mittelfinger), K (rechter Mittelfinger) + // Level 3: S (linker Ringfinger), L (rechter Ringfinger) + // Level 4: A (linker kleiner Finger), Ö (rechter kleiner Finger) + // Level 5: G (linker Zeigefinger Mitte), H (rechter Zeigefinger Mitte) + // Level 6: E (linker Mittelfinger oben), I (rechter Mittelfinger oben) + // Use nature/forest-themed mnemonics for each letter + ]; + ``` + + **4. Helper function to get fallback:** + ```typescript + export function getRandomFallbackGreeting(timeOfDay: TimeOfDay): string + export function getRandomFallbackForestComment(): string + export function getFallbackLetterIntro(letter: string): LetterIntro | undefined + ``` + + Write all 10 greetings, all 10 forest comments, and all letter intros for F, J, Space, D, K, S, L, A, OE, G, H, E, I (13 entries). All in German, all max 25 words. + + + cd /home/dev/workspace/zauberwald && node -e "import('./src/companion/fallbacks.ts').catch(() => process.exit(1))" 2>&1 || npx tsx -e "import { fallbackGreetings, fallbackForestComments, fallbackLetterIntros } from './src/companion/fallbacks'; console.log('greetings:', fallbackGreetings.length, 'comments:', fallbackForestComments.length, 'intros:', fallbackLetterIntros.length); if (fallbackGreetings.length < 10 || fallbackForestComments.length < 10 || fallbackLetterIntros.length < 13) process.exit(1); console.log('OK')" + + + - `grep -c "export const fallbackGreetings" src/companion/fallbacks.ts` returns 1 + - `grep -c "export const fallbackForestComments" src/companion/fallbacks.ts` returns 1 + - `grep -c "export const fallbackLetterIntros" src/companion/fallbacks.ts` returns 1 + - `grep -c "export function getRandomFallbackGreeting" src/companion/fallbacks.ts` returns 1 + - `grep -c "export function getFallbackLetterIntro" src/companion/fallbacks.ts` returns 1 + - File contains no English sentences in text values (only code identifiers in English) + - At least 10 greeting entries, 10 forest comment entries, 13 letter intro entries + + Fallback texts module exports 10 greetings (time-of-day keyed), 10 forest comments, 13 letter introductions for levels 1-6. All texts in simple German, max 25 words, no performance praise. + + + + Task 2: Fallback SVG images for forest elements + src/assets/fallback-images/blume.svg, src/assets/fallback-images/pilz.svg, src/assets/fallback-images/vogel.svg, src/assets/fallback-images/schmetterling.svg, src/assets/fallback-images/reh.svg, src/assets/fallback-images/baum.svg, src/assets/fallback-images/bach.svg + + - SPEC.md section 5.1 (visual style: watercolor, pastel, warm) + - SPEC.md section 7.2 (color palette CSS custom properties) + - src/styles/main.css (existing color values to match) + + + Create 7 SVG fallback images in `src/assets/fallback-images/` (per D-12, GAPI-05): + + Each SVG should be: + - 200x200 viewBox + - Simple, clean shapes using pastel colors from the palette + - Watercolor-inspired: soft gradients, rounded shapes, no hard edges + - Child-friendly, warm aesthetic + - No text in the images + + Colors to use (from SPEC.md palette): + - Greens: #6DB87D, #E8F5E4 + - Pink: #E88BAE + - Purple: #9B7DC4 + - Blue: #7DB8D4 + - Gold: #E8B84B + - Cream: #FFF8F0 + + Files to create: + 1. `blume.svg` — A simple flower with rounded petals (pink/purple) + 2. `pilz.svg` — A mushroom with red cap and white dots (classic fairy tale) + 3. `vogel.svg` — A small bird (blue/green) + 4. `schmetterling.svg` — A butterfly with pastel wings + 5. `reh.svg` — A young deer (simple silhouette, warm brown) + 6. `baum.svg` — A small magical tree with round crown + 7. `bach.svg` — A small stream with stones (blue tones) + + Each SVG should use `` with radial or linear gradients for the watercolor feel. Use `opacity` and `filter` for softness where appropriate. Keep SVGs under 3KB each for performance. + + Also create an index file for easy imports: + This is NOT needed — Vite handles static imports. The fallback images will be referenced by path in the companion module (Plan 04). + + + cd /home/dev/workspace/zauberwald && ls src/assets/fallback-images/*.svg | wc -l + + + - `ls src/assets/fallback-images/*.svg | wc -l` returns 7 + - Each SVG file starts with ` + 7 SVG fallback images exist in src/assets/fallback-images/, each depicting a forest element in pastel watercolor style, all under 5KB, no text. + + + + + +- `npx tsx -e "import { fallbackGreetings } from './src/companion/fallbacks'; console.log(fallbackGreetings.length)"` outputs >= 10 +- `ls src/assets/fallback-images/*.svg | wc -l` outputs 7 + + + +- Fallback texts module has 10 greetings, 10 forest comments, 13 letter intros — all German, max 25 words +- 7 SVG fallback images depict forest elements in pastel watercolor style +- All content is child-appropriate and matches the Zauberwald aesthetic + + + +After completion, create `.planning/phases/02-gemini-integration-asset-pipeline/02-02-SUMMARY.md` + diff --git a/.planning/phases/02-gemini-integration-asset-pipeline/02-03-PLAN.md b/.planning/phases/02-gemini-integration-asset-pipeline/02-03-PLAN.md new file mode 100644 index 0000000..76c2bd8 --- /dev/null +++ b/.planning/phases/02-gemini-integration-asset-pipeline/02-03-PLAN.md @@ -0,0 +1,295 @@ +--- +phase: 02-gemini-integration-asset-pipeline +plan: 03 +type: execute +wave: 2 +depends_on: ["02-01"] +files_modified: + - src/storage/db.ts + - scripts/generate-assets.ts + - package.json + - src/assets/companions/fee-lila/character-sheet.png + - src/assets/companions/fee-lila/avatar.png + - src/assets/companions/einhorn-stella/character-sheet.png + - src/assets/companions/einhorn-stella/avatar.png + - src/assets/companions/fuchs-finn/character-sheet.png + - src/assets/companions/fuchs-finn/avatar.png + - src/assets/companions/eule-elsa/character-sheet.png + - src/assets/companions/eule-elsa/avatar.png + - src/assets/style-reference/initial-forest-scene.png +autonomous: true +requirements: [ASST-01, ASST-02] +user_setup: + - service: gemini + why: "Asset generation requires Gemini API key" + env_vars: + - name: geminiApiKey + source: "public/config.json — already exists from Phase 1 setup" + +must_haves: + truths: + - "npm run generate-assets produces 4 character sheets, 4 avatars, and 1 style reference" + - "Generated PNG files exist in src/assets/companions/{type}/ and src/assets/style-reference/" + - "IndexedDB accessor methods exist for companionAssets, styleReference, and forestElements stores" + artifacts: + - path: "scripts/generate-assets.ts" + provides: "Build script for pre-generating character assets" + - path: "src/storage/db.ts" + provides: "Extended DB wrapper with all store accessors" + exports: ["saveCompanionAssets", "getCompanionAssets", "saveStyleReference", "getStyleReference", "saveForestElement", "getForestElements"] + - path: "src/assets/companions/fee-lila/character-sheet.png" + provides: "Fee Lila character sheet" + - path: "src/assets/companions/fee-lila/avatar.png" + provides: "Fee Lila avatar" + - path: "src/assets/style-reference/initial-forest-scene.png" + provides: "Style reference image for consistent art direction" + key_links: + - from: "scripts/generate-assets.ts" + to: "src/api/gemini.ts" + via: "imports generateImage for Gemini API calls" + pattern: "import.*generateImage.*from" + - from: "scripts/generate-assets.ts" + to: "public/config.json" + via: "reads API key via fs.readFileSync" + pattern: "readFileSync.*config\\.json" +--- + + +Add IndexedDB accessor methods for all stores, create the asset generation build script, and run it to produce all character sheets, avatars, and the style reference image. + +Purpose: Pre-generated assets eliminate onboarding wait times and ensure visual consistency across sessions. +Output: Extended db.ts, working generate-assets.ts script, 9 PNG files checked into git. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/02-gemini-integration-asset-pipeline/02-01-SUMMARY.md + +@src/types.ts +@src/storage/db.ts +@src/api/config.ts +@src/api/gemini.ts +@src/companion/characters.ts + + + +```typescript +export async function generateText(prompt: string, systemPrompt: string, config: GeminiConfig): Promise; +export async function generateImage(prompt: string, referenceImages: Blob[], config: GeminiConfig): Promise; +``` + + +```typescript +export interface GeminiConfig { + geminiApiKey: string; + geminiModel: string; + imageModel: string; +} +export function loadConfigFromObject(obj: GeminiConfig): void; +``` + + +```typescript +export interface CompanionAssets { + characterType: CompanionType; + characterSheet: Blob; + avatarImage: Blob; + generatedAt: string; +} + +export interface StyleReference { + id: 1; + imageBlob: Blob; + generatedAt: string; +} + +export interface ForestElement { + id?: number; + levelCompleted: number; + imageBlob: Blob; + imagePrompt: string; + description: string; + companionText: string; + position: { x: number; y: number }; + createdAt: string; +} +``` + + + + + + + Task 1: Add IndexedDB accessor methods for remaining stores + src/storage/db.ts + + - src/storage/db.ts (current code with getProgress/saveProgress/getSettings/saveSettings pattern) + - src/types.ts (CompanionAssets, StyleReference, ForestElement interfaces) + + + Add the following methods to `src/storage/db.ts` following the existing Promise-wrapped pattern (per D-04): + + **companionAssets store** (keyed by characterType): + ```typescript + export function saveCompanionAssets(db: IDBDatabase, assets: CompanionAssets): Promise + // Same pattern as saveProgress — tx "readwrite", store.put(assets) + + export function getCompanionAssets(db: IDBDatabase, characterType: CompanionType): Promise + // Same pattern as getProgress — tx "readonly", store.get(characterType) + ``` + + **styleReference store** (singleton, id: 1): + ```typescript + export function saveStyleReference(db: IDBDatabase, ref: StyleReference): Promise + // store.put(ref) — same as saveSettings pattern + + export function getStyleReference(db: IDBDatabase): Promise + // store.get(1) — same as getSettings pattern + ``` + + **forestElements store** (auto-increment): + ```typescript + export function saveForestElement(db: IDBDatabase, element: ForestElement): Promise + // store.add(element), return request.result as number (the auto-incremented id) + + export function getForestElements(db: IDBDatabase): Promise + // store.getAll(), return result as ForestElement[] + ``` + + Add required imports at top: `import type { CompanionAssets, CompanionType, ForestElement, StyleReference } from "../types";` + (Keep existing Progress and Settings imports.) + + + cd /home/dev/workspace/zauberwald && npx tsx -e "import { saveCompanionAssets, getCompanionAssets, saveStyleReference, getStyleReference, saveForestElement, getForestElements } from './src/storage/db'; console.log('All 6 methods exported OK')" + + + - `grep -c "export function saveCompanionAssets" src/storage/db.ts` returns 1 + - `grep -c "export function getCompanionAssets" src/storage/db.ts` returns 1 + - `grep -c "export function saveStyleReference" src/storage/db.ts` returns 1 + - `grep -c "export function getStyleReference" src/storage/db.ts` returns 1 + - `grep -c "export function saveForestElement" src/storage/db.ts` returns 1 + - `grep -c "export function getForestElements" src/storage/db.ts` returns 1 + - `grep "CompanionAssets\|CompanionType\|ForestElement\|StyleReference" src/storage/db.ts | head -1` shows the types imported + + All 6 new IndexedDB accessor methods added to db.ts — saveCompanionAssets, getCompanionAssets, saveStyleReference, getStyleReference, saveForestElement, getForestElements — following existing Promise-wrapped pattern. + + + + Task 2: Build script for asset generation + run it + scripts/generate-assets.ts, package.json + + - src/api/gemini.ts (generateImage function signature from Plan 01) + - src/api/config.ts (GeminiConfig interface, loadConfigFromObject) + - src/companion/characters.ts (companion definitions — 4 companions with type, name, personality) + - SPEC.md section 9.2 (Character Sheet prompt, Avatar prompt, visual descriptions per character) + - SPEC.md section 14.2 (asset generation flow) + - public/config.json (actual config file path) + + + **Step 1: Add tsx devDependency and npm script** + In `package.json`, add to devDependencies: `"tsx": "^4.0.0"` + Add script: `"generate-assets": "tsx scripts/generate-assets.ts"` + Run `npm install` after editing. + + **Step 2: Create `scripts/generate-assets.ts`** (per D-01, D-02, ASST-01) + + The script runs in Node.js via `npx tsx`. It: + + 1. Reads `public/config.json` via `fs.readFileSync('./public/config.json', 'utf-8')` and parses as `GeminiConfig` + 2. Uses `loadConfigFromObject()` from `src/api/config.ts` to set the config + 3. For each of the 4 companions, generates: + a. **Character Sheet** using the prompt from SPEC.md section 9.2: + ``` + Character sheet for a children's book. The character is {name}, {visualDescription}. + Show the character in 3 views: front view, side view, and a close-up of the face. + All views must show the EXACT SAME character with identical colors, proportions, and features. + Art style: Watercolor children's book illustration, warm pastel colors, soft edges, magical forest theme. White background. + Label each view clearly: "FRONT" "SIDE" "FACE" + The character must be: cute, friendly, approachable, with large expressive eyes. Suitable for a 7-year-old audience. + ``` + b. **Avatar** using the character sheet as reference image: + ``` + Create a single portrait of this exact character from the reference sheet. + Show only the face and upper body, looking directly at the viewer with a warm, friendly expression. + Same art style as the reference. Square format. No text. No background (or simple soft gradient). + ``` + + 4. Generates **Style Reference** — an initial forest scene: + ``` + Watercolor children's book illustration of a magical forest clearing. + Warm pastel colors, soft edges, dreamy atmosphere. A gentle meadow surrounded + by friendly trees, dappled sunlight, small flowers, and a hint of magic sparkles. + No text, no letters, no characters. This sets the visual style for all future forest images. + ``` + + 5. Saves each file using `fs.writeFileSync`: + - `src/assets/companions/fee-lila/character-sheet.png` + - `src/assets/companions/fee-lila/avatar.png` + - `src/assets/companions/einhorn-stella/character-sheet.png` + - `src/assets/companions/einhorn-stella/avatar.png` + - `src/assets/companions/fuchs-finn/character-sheet.png` + - `src/assets/companions/fuchs-finn/avatar.png` + - `src/assets/companions/eule-elsa/character-sheet.png` + - `src/assets/companions/eule-elsa/avatar.png` + - `src/assets/style-reference/initial-forest-scene.png` + + Visual descriptions per companion (from SPEC.md section 9.2): + - fee: "a small fairy with lavender-purple wings, a flowing lilac dress, big warm brown eyes, a tiny flower crown made of forget-me-nots, light skin, and short wavy auburn hair" + - einhorn: "a small unicorn with a white coat, a shimmering golden horn, a flowing mane in soft rainbow pastels (pink, lavender, light blue), big dark eyes with long lashes, and small silver hooves" + - fuchs: "a young red fox with warm orange-brown fur, a white chest and belly, a bushy tail with a white tip, bright curious amber eyes, and slightly oversized pointed ears" + - eule: "a small round owl with soft brown-and-cream feathers, a heart-shaped face, large round golden eyes, tiny ear tufts, and small talons perched on a mossy branch" + + The script should: + - Log progress to stdout: "Generating character sheet for {name}..." etc. + - Convert Blob responses to Buffer via `blob.arrayBuffer()` then `Buffer.from()` + - Handle errors gracefully: if one generation fails, log error and continue with others + - Exit with code 1 if any generation failed, 0 if all succeeded + - Add a 2-second delay between API calls to avoid rate limiting + + **Step 3: Run the script** + Execute `npm run generate-assets` and verify all 9 PNG files are created. + If any fail, check the error and retry (the script should be re-runnable). + + **Important:** The `generateImage` function in `src/api/gemini.ts` uses browser `fetch`. Node.js 18+ has native `fetch`, so this should work. If there are issues with `Blob` in Node.js, use `Buffer` and convert. The script may need to use `node:buffer` Blob or polyfill — handle at implementation time. + + + cd /home/dev/workspace/zauberwald && ls -la src/assets/companions/*/character-sheet.png src/assets/companions/*/avatar.png src/assets/style-reference/initial-forest-scene.png 2>&1 | grep -c ".png" + + + - `grep "generate-assets" package.json` shows the npm script + - `grep "tsx" package.json` shows tsx in devDependencies + - `test -f scripts/generate-assets.ts && echo "exists"` returns "exists" + - `ls src/assets/companions/fee-lila/character-sheet.png src/assets/companions/fee-lila/avatar.png` succeeds + - `ls src/assets/companions/einhorn-stella/character-sheet.png src/assets/companions/einhorn-stella/avatar.png` succeeds + - `ls src/assets/companions/fuchs-finn/character-sheet.png src/assets/companions/fuchs-finn/avatar.png` succeeds + - `ls src/assets/companions/eule-elsa/character-sheet.png src/assets/companions/eule-elsa/avatar.png` succeeds + - `ls src/assets/style-reference/initial-forest-scene.png` succeeds + - All 9 PNG files are > 1KB (not empty): `find src/assets -name "*.png" -size +1k | wc -l` returns 9 + + Build script generates 4 character sheets + 4 avatars + 1 style reference via Gemini API. All 9 PNG files exist in correct directories and are non-empty. `npm run generate-assets` is a working npm script. + + + + + +- `npm run generate-assets` completes without errors (or re-run successfully) +- `find src/assets -name "*.png" -size +1k | wc -l` returns 9 +- All 6 new DB methods compile: `npx tsc --noEmit src/storage/db.ts` + + + +- IndexedDB has accessors for all 5 stores (progress, settings from Phase 1 + companionAssets, styleReference, forestElements new) +- Build script reads config.json, calls Gemini API, saves 9 PNG files +- All generated assets are suitable quality (child-friendly watercolor style) + + + +After completion, create `.planning/phases/02-gemini-integration-asset-pipeline/02-03-SUMMARY.md` + diff --git a/.planning/phases/02-gemini-integration-asset-pipeline/02-04-PLAN.md b/.planning/phases/02-gemini-integration-asset-pipeline/02-04-PLAN.md new file mode 100644 index 0000000..42bc87c --- /dev/null +++ b/.planning/phases/02-gemini-integration-asset-pipeline/02-04-PLAN.md @@ -0,0 +1,256 @@ +--- +phase: 02-gemini-integration-asset-pipeline +plan: 04 +type: execute +wave: 2 +depends_on: ["02-01", "02-02"] +files_modified: + - src/companion/companion.ts + - src/companion/companion.test.ts +autonomous: true +requirements: [COMP-01, COMP-02, COMP-03, COMP-04] + +must_haves: + truths: + - "getGreeting tries Gemini API first, returns fallback text if API unavailable" + - "getLetterIntro tries Gemini API first, returns fallback text if API unavailable" + - "getForestComment tries Gemini API first, returns fallback text if API unavailable" + - "All returned texts are max 25 words, simple German, no performance praise" + artifacts: + - path: "src/companion/companion.ts" + provides: "Companion text module with API-first + fallback strategy" + exports: ["getGreeting", "getLetterIntro", "getForestComment"] + - path: "src/companion/companion.test.ts" + provides: "Tests for companion text module" + key_links: + - from: "src/companion/companion.ts" + to: "src/api/gemini.ts" + via: "calls generateText for AI-generated companion dialogue" + pattern: "generateText" + - from: "src/companion/companion.ts" + to: "src/companion/fallbacks.ts" + via: "imports fallback functions when API returns null" + pattern: "getRandomFallbackGreeting|getFallbackLetterIntro|getRandomFallbackForestComment" + - from: "src/companion/companion.ts" + to: "src/companion/characters.ts" + via: "looks up companion name and personality by type" + pattern: "companions" +--- + + +Create the companion text module that provides AI-generated or fallback text for greetings, letter introductions, and forest comments. + +Purpose: The companion is the emotional core of Zauberwald. This module makes her speak — via AI when possible, via curated fallbacks otherwise. +Output: `src/companion/companion.ts` with 3 exported functions, full test coverage. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/02-gemini-integration-asset-pipeline/02-01-SUMMARY.md + +@src/companion/characters.ts +@src/companion/fallbacks.ts +@src/api/gemini.ts +@src/api/config.ts + + + +```typescript +export async function generateText(prompt: string, systemPrompt: string, config: GeminiConfig): Promise; +export async function checkRateLimit(db: IDBDatabase, type: 'text' | 'image'): Promise; +export async function incrementApiCall(db: IDBDatabase, type: 'text' | 'image'): Promise; +``` + + +```typescript +export interface GeminiConfig { + geminiApiKey: string; + geminiModel: string; + imageModel: string; +} +export async function loadConfig(): Promise; +``` + + +```typescript +export type TimeOfDay = 'morgen' | 'mittag' | 'abend'; +export function getRandomFallbackGreeting(timeOfDay: TimeOfDay): string; +export function getRandomFallbackForestComment(): string; +export function getFallbackLetterIntro(letter: string): LetterIntro | undefined; +``` + + +```typescript +export interface CompanionDefinition { + type: CompanionType; + name: string; + emoji: string; + personality: string; +} +export const companions: CompanionDefinition[]; +``` + + + + + + + Task 1: Companion text module with API-first fallback strategy + src/companion/companion.ts, src/companion/companion.test.ts + + - src/companion/characters.ts (companion definitions with name, personality) + - src/companion/fallbacks.ts (fallback functions from Plan 02) + - src/api/gemini.ts (generateText signature, checkRateLimit, incrementApiCall) + - src/api/config.ts (loadConfig, GeminiConfig) + - SPEC.md section 9.2 (exact prompt templates for greeting, letter intro, forest comment) + + + getGreeting: + - Test: returns AI text when API succeeds and rate limit not exceeded + - Test: returns fallback text when loadConfig returns null (no API key) + - Test: returns fallback text when generateText returns null (API error) + - Test: returns fallback text when rate limit exceeded + - Test: passes correct system prompt with companion personality and time of day + - Test: calls incrementApiCall after successful API call + + getLetterIntro: + - Test: returns AI text when API succeeds + - Test: returns fallback LetterIntro text when API unavailable + - Test: system prompt includes letter, finger description, and companion personality + - Test: returned text includes the letter being introduced + + getForestComment: + - Test: returns AI text when API succeeds + - Test: returns fallback text when API unavailable + - Test: system prompt includes element description and companion personality + + + Create `src/companion/companion.ts` (per D-13): + + ```typescript + import { companions } from "./characters"; + import type { CompanionType } from "../types"; + import { loadConfig } from "../api/config"; + import { generateText, checkRateLimit, incrementApiCall } from "../api/gemini"; + import { + type TimeOfDay, + getRandomFallbackGreeting, + getFallbackLetterIntro, + getRandomFallbackForestComment, + } from "./fallbacks"; + + function getTimeOfDay(): TimeOfDay { + const hour = new Date().getHours(); + if (hour < 12) return "morgen"; + if (hour < 17) return "mittag"; + return "abend"; + } + + function getCompanion(type: CompanionType) { + return companions.find(c => c.type === type)!; + } + ``` + + **getGreeting(characterType: CompanionType, db: IDBDatabase): Promise** + 1. Call `loadConfig()` — if null, return `getRandomFallbackGreeting(getTimeOfDay())` + 2. Call `checkRateLimit(db, 'text')` — if false, return fallback + 3. Build system prompt from SPEC.md 9.2 template: + ``` + Du bist {name}, ein(e) {personality} im Zauberwald. + Du sprichst mit einem 7-jaehrigen Kind, das Tippen lernt. + + Regeln: + - Max. 2 kurze Saetze (insgesamt max. 25 Woerter) + - Einfache Sprache, kurze Woerter + - Freundlich und warm, nie belehrend + - Keine Bewertung von Leistung + - Kein Englisch + - Beziehe dich auf die Tageszeit: {timeOfDay} + ``` + 4. User prompt: `"Schreib eine Begruessung."` + 5. Call `generateText(userPrompt, systemPrompt, config)` + 6. If result is null, return fallback + 7. Call `incrementApiCall(db, 'text')` + 8. Return result + + **getLetterIntro(characterType: CompanionType, letter: string, fingerDescription: string, db: IDBDatabase): Promise** + 1. Same config/rate-limit pattern + 2. System prompt from SPEC.md 9.2: + ``` + Du bist {name}, ein(e) {personality} im Zauberwald. + Du stellst einem 7-jaehrigen Kind den Buchstaben {letter} vor. + + Regeln: + - Max. 2 Saetze (max. 25 Woerter) + - Erklaere, welcher Finger den Buchstaben drueckt: {fingerDescription} + - Verwende eine bildhafte Eselsbruecke, die zum Wald/Natur/Tiere-Thema passt + - Einfache Sprache + - Kein Englisch + ``` + 3. User prompt: `"Stelle den Buchstaben vor."` + 4. On failure: return `getFallbackLetterIntro(letter)?.text ?? "Druecke die leuchtende Taste!"` + + **getForestComment(characterType: CompanionType, elementDescription: string, db: IDBDatabase): Promise** + 1. Same config/rate-limit pattern + 2. System prompt from SPEC.md 9.2: + ``` + Du bist {name}, ein(e) {personality} im Zauberwald. + Ein Kind hat gerade eine Tippuebung abgeschlossen. Im Wald ist ein neues Wesen erschienen: {elementDescription}. + + Regeln: + - Max. 2 kurze Saetze (insgesamt max. 25 Woerter) + - Druecke Staunen und Freude aus + - Einfache Sprache fuer 7-Jaehrige + - Kein Lob fuer Leistung, sondern Begeisterung ueber das neue Waldelement + - Kein Englisch + ``` + 3. User prompt: `"Kommentiere das neue Element."` + 4. On failure: return `getRandomFallbackForestComment()` + + **Tests (src/companion/companion.test.ts):** + - Mock `fetch` (via vi.spyOn) for generateText calls + - Mock `loadConfig` to return config or null + - Use `fake-indexeddb` for rate-limit DB operations + - Test each function with API success, API failure, no config, rate-limited + - At least 9 test cases total (3 per function) + + + cd /home/dev/workspace/zauberwald && npx vitest run src/companion/companion.test.ts + + + - `grep -c "export async function getGreeting" src/companion/companion.ts` returns 1 + - `grep -c "export async function getLetterIntro" src/companion/companion.ts` returns 1 + - `grep -c "export async function getForestComment" src/companion/companion.ts` returns 1 + - `grep "loadConfig" src/companion/companion.ts` shows config loader is used + - `grep "checkRateLimit" src/companion/companion.ts` shows rate-limiting is checked + - `grep "getRandomFallbackGreeting\|getFallbackLetterIntro\|getRandomFallbackForestComment" src/companion/companion.ts` shows fallbacks imported + - `npx vitest run src/companion/companion.test.ts` passes with 0 failures and >= 9 tests + + Companion module exports getGreeting, getLetterIntro, getForestComment. Each tries Gemini API first (with rate-limit check), falls back to static texts. All tests pass. + + + + + +- `npx vitest run src/companion/` — all tests pass +- Each function handles: API success, API failure, no config, rate-limited + + + +- getGreeting returns AI greeting or fallback, respecting rate limits +- getLetterIntro returns AI letter introduction or fallback +- getForestComment returns AI forest comment or fallback +- All texts conform to COMP-04: max 25 words, simple German, no performance praise +- Tests cover all paths (API success, failure, no config, rate-limited) + + + +After completion, create `.planning/phases/02-gemini-integration-asset-pipeline/02-04-SUMMARY.md` + diff --git a/.planning/phases/02-gemini-integration-asset-pipeline/02-05-PLAN.md b/.planning/phases/02-gemini-integration-asset-pipeline/02-05-PLAN.md new file mode 100644 index 0000000..1a69457 --- /dev/null +++ b/.planning/phases/02-gemini-integration-asset-pipeline/02-05-PLAN.md @@ -0,0 +1,369 @@ +--- +phase: 02-gemini-integration-asset-pipeline +plan: 05 +type: execute +wave: 3 +depends_on: ["02-03", "02-04"] +files_modified: + - src/companion/characters.ts + - src/ui/screens.ts + - src/app.ts + - src/styles/main.css + - index.html +autonomous: false +requirements: [ASST-03, ASST-04] + +must_haves: + truths: + - "Welcome screen shows real generated avatar PNG images instead of emoji" + - "If avatar image fails to load, emoji fallback is shown" + - "Companion greeting text appears on forest screen for returning users" + - "Companion avatar is visible in lesson screen companion area" + artifacts: + - path: "src/companion/characters.ts" + provides: "Extended companion definitions with avatarUrl field" + exports: ["companions", "CompanionDefinition"] + - path: "src/ui/screens.ts" + provides: "Updated welcome screen with img elements" + - path: "src/app.ts" + provides: "Greeting text display on forest screen" + key_links: + - from: "src/companion/characters.ts" + to: "src/assets/companions/*/avatar.png" + via: "static imports of avatar PNGs for Vite URL resolution" + pattern: "import.*avatar.*from" + - from: "src/ui/screens.ts" + to: "src/companion/characters.ts" + via: "reads avatarUrl from CompanionDefinition for img src" + pattern: "avatarUrl" + - from: "src/app.ts" + to: "src/companion/companion.ts" + via: "calls getGreeting for returning user greeting" + pattern: "getGreeting" +--- + + +Wire generated avatars into the welcome screen, add companion greeting to forest screen, and show companion avatar in lesson screen. + +Purpose: This is where users see the AI-generated assets for the first time. The welcome screen transforms from emoji placeholders to real character art, and the companion becomes a visible presence. +Output: Updated characters.ts, screens.ts, app.ts with avatar images and greeting text. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/02-gemini-integration-asset-pipeline/02-03-SUMMARY.md +@.planning/phases/02-gemini-integration-asset-pipeline/02-04-SUMMARY.md + +@src/companion/characters.ts +@src/ui/screens.ts +@src/app.ts +@src/styles/main.css + + + +```typescript +export interface CompanionDefinition { + type: CompanionType; + name: string; + emoji: string; + personality: string; +} +export const companions: CompanionDefinition[]; +``` + + +```typescript +export async function getGreeting(characterType: CompanionType, db: IDBDatabase): Promise; +``` + + +```typescript +export async function initApp(): Promise; +export function showScreen(name: ScreenName): void; +``` + + + + + + + + + + + Task 1: Add avatar URLs to companion definitions and update welcome screen + src/companion/characters.ts, src/ui/screens.ts, src/styles/main.css + + - src/companion/characters.ts (current CompanionDefinition interface and companions array) + - src/ui/screens.ts (initWelcomeScreen — renders companion cards with emoji spans) + - src/styles/main.css (existing companion-card styles) + - src/assets/companions/fee-lila/avatar.png (verify file exists) + + + **Step 1: Update src/companion/characters.ts** (per D-05) + + Add static imports for each avatar PNG (Vite resolves these to hashed URLs): + ```typescript + import feeLilaAvatar from "../assets/companions/fee-lila/avatar.png"; + import einhornStellaAvatar from "../assets/companions/einhorn-stella/avatar.png"; + import fuchsFinnAvatar from "../assets/companions/fuchs-finn/avatar.png"; + import euleElsaAvatar from "../assets/companions/eule-elsa/avatar.png"; + ``` + + Extend `CompanionDefinition` interface: + ```typescript + export interface CompanionDefinition { + type: CompanionType; + name: string; + emoji: string; + personality: string; + avatarUrl: string; // NEW — Vite-resolved URL to avatar PNG + } + ``` + + Add `avatarUrl` to each companion entry: + ```typescript + { type: "fee", name: "Lila", emoji: "🧚", personality: "...", avatarUrl: feeLilaAvatar }, + { type: "einhorn", name: "Stella", emoji: "🦄", personality: "...", avatarUrl: einhornStellaAvatar }, + { type: "fuchs", name: "Finn", emoji: "🦊", personality: "...", avatarUrl: fuchsFinnAvatar }, + { type: "eule", name: "Elsa", emoji: "🦉", personality: "...", avatarUrl: euleElsaAvatar }, + ``` + + **Step 2: Update welcome screen in src/ui/screens.ts** (per D-05, D-06, ASST-03) + + In `initWelcomeScreen`, change the card innerHTML from emoji to img with onerror fallback: + + Replace: + ```typescript + card.innerHTML = ` + ${c.emoji} + ${c.name} + `; + ``` + + With: + ```typescript + card.innerHTML = ` + ${c.name} + + ${c.name} + `; + ``` + + Per D-06: If the img fails to load, the onerror handler hides the img and shows the emoji fallback. + + **Step 3: Add CSS for avatar images in src/styles/main.css** + + Add after existing `.companion-card__emoji` styles: + ```css + .companion-card__avatar { + width: 80px; + height: 80px; + border-radius: 50%; + object-fit: cover; + margin-bottom: 0.5rem; + } + ``` + + Also add a companion avatar class for the lesson screen: + ```css + .companion-area { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem 1rem; + background: var(--bg-cream); + border-radius: 1rem; + margin-bottom: 1rem; + } + + .companion-area__avatar { + width: 48px; + height: 48px; + border-radius: 50%; + object-fit: cover; + flex-shrink: 0; + } + + .companion-area__text { + font-family: 'Nunito', sans-serif; + font-size: 1rem; + color: var(--text-warm); + line-height: 1.4; + } + ``` + + + cd /home/dev/workspace/zauberwald && grep -c "avatarUrl" src/companion/characters.ts && grep -c "companion-card__avatar" src/ui/screens.ts && grep -c "companion-card__avatar" src/styles/main.css + + + - `grep "avatarUrl: string" src/companion/characters.ts` matches (interface extended) + - `grep -c "import.*avatar.*from" src/companion/characters.ts` returns 4 (one per companion) + - `grep "companion-card__avatar" src/ui/screens.ts` shows img element used in card + - `grep "onerror" src/ui/screens.ts` shows fallback handler (per D-06) + - `grep "companion-card__avatar" src/styles/main.css` shows avatar styling + - `grep "companion-area" src/styles/main.css` shows companion area styling for lesson screen + + Welcome screen renders avatar PNGs with emoji fallback on error. CompanionDefinition has avatarUrl field. CSS styles avatar images at 80px round in cards, 48px round in companion area. + + + + Task 2: Add companion greeting to forest screen and avatar to lesson screen + src/app.ts, src/ui/screens.ts, index.html + + - src/app.ts (initApp flow — returning user goes to forest screen) + - src/ui/screens.ts (initLessonScreen — current lesson screen setup) + - src/forest/scene.ts (initForestScreen — how forest screen is initialized) + - src/companion/companion.ts (getGreeting function from Plan 04) + - src/companion/characters.ts (updated with avatarUrl from Task 1) + - index.html (current HTML structure for all screens) + + + **Step 1: Add companion greeting area to forest screen** (per D-14, COMP-01) + + In `index.html`, inside `screen-forest` div, add a greeting container before the level grid: + ```html + + ``` + + In `src/app.ts`, update `initApp()` for returning users: + ```typescript + import { getGreeting } from "./companion/companion"; + import { companions } from "./companion/characters"; + ``` + + After `initForestScreen(db, startLessonFromForest)` for returning users, add: + ```typescript + // Show companion greeting (per COMP-01, D-14) + const companion = companions.find(c => c.type === progress.selectedCharacter); + if (companion) { + const greetingEl = document.getElementById("forest-greeting")!; + const avatarEl = document.getElementById("forest-greeting-avatar") as HTMLImageElement; + const textEl = document.getElementById("forest-greeting-text")!; + + avatarEl.src = companion.avatarUrl; + avatarEl.alt = companion.name; + greetingEl.style.display = "flex"; + + // Fetch greeting async (shows immediately with fallback) + getGreeting(progress.selectedCharacter, db).then(text => { + textEl.textContent = text; + }); + } + ``` + + **Step 2: Add companion avatar to lesson screen** (per ASST-04) + + In `index.html`, inside `screen-lesson` div, add a companion area above the letter-area: + ```html + + ``` + + In `src/ui/screens.ts` `initLessonScreen`, after getting the level parameter, look up the selected companion and show avatar: + ```typescript + import { companions } from "../companion/characters"; + import { getProgress } from "../storage/db"; + ``` + + Add near the beginning of `initLessonScreen`: + ```typescript + // Show companion avatar (per ASST-04) + getProgress(_db).then(progress => { + if (!progress) return; + const companion = companions.find(c => c.type === progress.selectedCharacter); + if (!companion) return; + const area = document.getElementById("lesson-companion")!; + const avatar = document.getElementById("lesson-companion-avatar") as HTMLImageElement; + avatar.src = companion.avatarUrl; + avatar.alt = companion.name; + area.style.display = "flex"; + }); + ``` + + Note: Per D-14, companion text does NOT appear during typing exercise in Phase 2. Only the avatar is shown. The "Entdecken" phase with letter introduction text comes in Phase 3. + + + cd /home/dev/workspace/zauberwald && grep -c "getGreeting" src/app.ts && grep -c "forest-greeting" index.html && grep -c "lesson-companion" index.html + + + - `grep "getGreeting" src/app.ts` shows greeting function is called + - `grep "forest-greeting" index.html` shows greeting container in forest screen + - `grep "lesson-companion" index.html` shows companion area in lesson screen + - `grep "companion.avatarUrl" src/app.ts` shows avatar URL is used + - `grep "lesson-companion-avatar" src/ui/screens.ts` shows avatar is set in lesson + - `grep 'import.*getGreeting' src/app.ts` shows import from companion module + - `grep 'import.*companions.*from.*characters' src/ui/screens.ts` shows characters imported + + Forest screen shows companion avatar + AI/fallback greeting text for returning users. Lesson screen shows companion avatar (no text per D-14 — Phase 3 adds letter intros). Both degrade gracefully without API. + + + + Task 3: Visual verification of Phase 2 integration + index.html + + Human verifies the complete Phase 2 integration visually in the browser. All automated work is complete — this checkpoint confirms the visual and functional result. + + What was built: + 1. Welcome screen now shows real generated avatar images (not emoji) for companion selection + 2. Forest screen shows companion greeting text (AI-generated or fallback) + 3. Lesson screen shows companion avatar + 4. All 9 generated PNG assets (4 character sheets, 4 avatars, 1 style reference) are in the asset directories + 5. Fallback SVG images exist for when API is unavailable + 6. Full Gemini API client with retry, rate-limiting, and fallback strategy + + How to verify: + 1. Run `npm run dev` and open `http://{vps-ip}:5173` in browser + 2. On welcome screen: Verify 4 companion cards show real avatar images (not emoji) + 3. Select a companion and layout, click "Los geht's" + 4. On forest screen: Verify a greeting text appears near the top with the companion avatar + 5. Start a lesson: Verify the companion avatar appears in the lesson screen + 6. Complete the lesson and return to forest: Greeting should still show + 7. Refresh the page: Returning user flow should show greeting on forest screen + + Fallback test (optional): + 8. Rename public/config.json temporarily to config.json.bak + 9. Refresh the app — greeting text should show a static fallback (no crash) + 10. Rename it back + + + cd /home/dev/workspace/zauberwald && npm run build 2>&1 | tail -5 + + User confirms: avatars display on welcome screen, greeting shows on forest screen, companion avatar visible in lesson. App works with and without API key. + + + + + +- `npm run dev` starts without errors +- `npm run build` completes without errors +- Welcome screen shows avatar images +- Forest screen shows companion greeting +- Lesson screen shows companion avatar +- App works with and without config.json (fallback path) + + + +- Generated avatars replace emoji in welcome screen cards +- Emoji fallback works when image fails to load +- Companion greeting appears on forest screen (AI or fallback) +- Companion avatar visible during lessons +- No console errors during normal usage + + + +After completion, create `.planning/phases/02-gemini-integration-asset-pipeline/02-05-SUMMARY.md` +