docs(02): create phase plan for Gemini integration + asset pipeline
This commit is contained in:
@@ -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 | - |
|
||||
|
||||
@@ -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"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
|
||||
@src/types.ts
|
||||
@src/storage/db.ts
|
||||
@public/config.json
|
||||
|
||||
<interfaces>
|
||||
<!-- Existing types from src/types.ts -->
|
||||
```typescript
|
||||
export interface Settings {
|
||||
id: 1;
|
||||
audioEnabled: boolean;
|
||||
apiKey: string;
|
||||
}
|
||||
```
|
||||
|
||||
<!-- Existing DB methods from src/storage/db.ts -->
|
||||
```typescript
|
||||
export function getSettings(db: IDBDatabase): Promise<Settings | null>;
|
||||
export function saveSettings(db: IDBDatabase, settings: Settings): Promise<void>;
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Config loader and Gemini API client with tests (TDD)</name>
|
||||
<files>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</files>
|
||||
<read_first>
|
||||
- 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)
|
||||
</read_first>
|
||||
<behavior>
|
||||
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
|
||||
</behavior>
|
||||
<action>
|
||||
**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<GeminiConfig | null> {
|
||||
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<string | null>`
|
||||
- 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<Blob | null>`
|
||||
- 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<boolean>
|
||||
export async function incrementApiCall(db: IDBDatabase, type: 'text' | 'image'): Promise<void>
|
||||
```
|
||||
- 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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/dev/workspace/zauberwald && npx vitest run src/api/</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `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
|
||||
</acceptance_criteria>
|
||||
<done>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.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `npx vitest run src/api/` — all tests pass
|
||||
- `grep "generateText\|generateImage\|loadConfig\|checkRateLimit" src/api/gemini.ts src/api/config.ts` shows all exports
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 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/`
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/02-gemini-integration-asset-pipeline/02-01-SUMMARY.md`
|
||||
</output>
|
||||
@@ -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"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.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)
|
||||
|
||||
<interfaces>
|
||||
<!-- From src/companion/characters.ts -->
|
||||
```typescript
|
||||
export type CompanionType = "fee" | "einhorn" | "fuchs" | "eule";
|
||||
|
||||
export interface CompanionDefinition {
|
||||
type: CompanionType;
|
||||
name: string;
|
||||
emoji: string;
|
||||
personality: string;
|
||||
}
|
||||
```
|
||||
|
||||
<!-- From src/game/levels.ts — levels 1-6 keys -->
|
||||
Level 1: F, J, Space
|
||||
Level 2: D, K
|
||||
Level 3: S, L
|
||||
Level 4: A, Ö
|
||||
Level 5: G, H
|
||||
Level 6: E, I
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Fallback texts module</name>
|
||||
<files>src/companion/fallbacks.ts</files>
|
||||
<read_first>
|
||||
- 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)
|
||||
</read_first>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>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')"</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `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
|
||||
</acceptance_criteria>
|
||||
<done>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.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Fallback SVG images for forest elements</name>
|
||||
<files>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</files>
|
||||
<read_first>
|
||||
- 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)
|
||||
</read_first>
|
||||
<action>
|
||||
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 `<defs>` 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).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/dev/workspace/zauberwald && ls src/assets/fallback-images/*.svg | wc -l</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `ls src/assets/fallback-images/*.svg | wc -l` returns 7
|
||||
- Each SVG file starts with `<svg` and contains `viewBox="0 0 200 200"`
|
||||
- Each SVG file is under 5KB: `find src/assets/fallback-images -name "*.svg" -size +5k | wc -l` returns 0
|
||||
- No SVG contains `<text` elements (no text in images)
|
||||
</acceptance_criteria>
|
||||
<done>7 SVG fallback images exist in src/assets/fallback-images/, each depicting a forest element in pastel watercolor style, all under 5KB, no text.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `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
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 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
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/02-gemini-integration-asset-pipeline/02-02-SUMMARY.md`
|
||||
</output>
|
||||
@@ -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"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.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
|
||||
|
||||
<interfaces>
|
||||
<!-- From src/api/gemini.ts (created in Plan 01) -->
|
||||
```typescript
|
||||
export async function generateText(prompt: string, systemPrompt: string, config: GeminiConfig): Promise<string | null>;
|
||||
export async function generateImage(prompt: string, referenceImages: Blob[], config: GeminiConfig): Promise<Blob | null>;
|
||||
```
|
||||
|
||||
<!-- From src/api/config.ts (created in Plan 01) -->
|
||||
```typescript
|
||||
export interface GeminiConfig {
|
||||
geminiApiKey: string;
|
||||
geminiModel: string;
|
||||
imageModel: string;
|
||||
}
|
||||
export function loadConfigFromObject(obj: GeminiConfig): void;
|
||||
```
|
||||
|
||||
<!-- From src/types.ts -->
|
||||
```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;
|
||||
}
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Add IndexedDB accessor methods for remaining stores</name>
|
||||
<files>src/storage/db.ts</files>
|
||||
<read_first>
|
||||
- src/storage/db.ts (current code with getProgress/saveProgress/getSettings/saveSettings pattern)
|
||||
- src/types.ts (CompanionAssets, StyleReference, ForestElement interfaces)
|
||||
</read_first>
|
||||
<action>
|
||||
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<void>
|
||||
// Same pattern as saveProgress — tx "readwrite", store.put(assets)
|
||||
|
||||
export function getCompanionAssets(db: IDBDatabase, characterType: CompanionType): Promise<CompanionAssets | null>
|
||||
// Same pattern as getProgress — tx "readonly", store.get(characterType)
|
||||
```
|
||||
|
||||
**styleReference store** (singleton, id: 1):
|
||||
```typescript
|
||||
export function saveStyleReference(db: IDBDatabase, ref: StyleReference): Promise<void>
|
||||
// store.put(ref) — same as saveSettings pattern
|
||||
|
||||
export function getStyleReference(db: IDBDatabase): Promise<StyleReference | null>
|
||||
// store.get(1) — same as getSettings pattern
|
||||
```
|
||||
|
||||
**forestElements store** (auto-increment):
|
||||
```typescript
|
||||
export function saveForestElement(db: IDBDatabase, element: ForestElement): Promise<number>
|
||||
// store.add(element), return request.result as number (the auto-incremented id)
|
||||
|
||||
export function getForestElements(db: IDBDatabase): Promise<ForestElement[]>
|
||||
// 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.)
|
||||
</action>
|
||||
<verify>
|
||||
<automated>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')"</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `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
|
||||
</acceptance_criteria>
|
||||
<done>All 6 new IndexedDB accessor methods added to db.ts — saveCompanionAssets, getCompanionAssets, saveStyleReference, getStyleReference, saveForestElement, getForestElements — following existing Promise-wrapped pattern.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Build script for asset generation + run it</name>
|
||||
<files>scripts/generate-assets.ts, package.json</files>
|
||||
<read_first>
|
||||
- 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)
|
||||
</read_first>
|
||||
<action>
|
||||
**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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>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"</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `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
|
||||
</acceptance_criteria>
|
||||
<done>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.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `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`
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 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)
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/02-gemini-integration-asset-pipeline/02-03-SUMMARY.md`
|
||||
</output>
|
||||
@@ -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"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.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
|
||||
|
||||
<interfaces>
|
||||
<!-- From src/api/gemini.ts (Plan 01) -->
|
||||
```typescript
|
||||
export async function generateText(prompt: string, systemPrompt: string, config: GeminiConfig): Promise<string | null>;
|
||||
export async function checkRateLimit(db: IDBDatabase, type: 'text' | 'image'): Promise<boolean>;
|
||||
export async function incrementApiCall(db: IDBDatabase, type: 'text' | 'image'): Promise<void>;
|
||||
```
|
||||
|
||||
<!-- From src/api/config.ts (Plan 01) -->
|
||||
```typescript
|
||||
export interface GeminiConfig {
|
||||
geminiApiKey: string;
|
||||
geminiModel: string;
|
||||
imageModel: string;
|
||||
}
|
||||
export async function loadConfig(): Promise<GeminiConfig | null>;
|
||||
```
|
||||
|
||||
<!-- From src/companion/fallbacks.ts (Plan 02) -->
|
||||
```typescript
|
||||
export type TimeOfDay = 'morgen' | 'mittag' | 'abend';
|
||||
export function getRandomFallbackGreeting(timeOfDay: TimeOfDay): string;
|
||||
export function getRandomFallbackForestComment(): string;
|
||||
export function getFallbackLetterIntro(letter: string): LetterIntro | undefined;
|
||||
```
|
||||
|
||||
<!-- From src/companion/characters.ts -->
|
||||
```typescript
|
||||
export interface CompanionDefinition {
|
||||
type: CompanionType;
|
||||
name: string;
|
||||
emoji: string;
|
||||
personality: string;
|
||||
}
|
||||
export const companions: CompanionDefinition[];
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Companion text module with API-first fallback strategy</name>
|
||||
<files>src/companion/companion.ts, src/companion/companion.test.ts</files>
|
||||
<read_first>
|
||||
- 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)
|
||||
</read_first>
|
||||
<behavior>
|
||||
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
|
||||
</behavior>
|
||||
<action>
|
||||
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<string>**
|
||||
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<string>**
|
||||
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<string>**
|
||||
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)
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/dev/workspace/zauberwald && npx vitest run src/companion/companion.test.ts</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `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
|
||||
</acceptance_criteria>
|
||||
<done>Companion module exports getGreeting, getLetterIntro, getForestComment. Each tries Gemini API first (with rate-limit check), falls back to static texts. All tests pass.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `npx vitest run src/companion/` — all tests pass
|
||||
- Each function handles: API success, API failure, no config, rate-limited
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 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)
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/02-gemini-integration-asset-pipeline/02-04-SUMMARY.md`
|
||||
</output>
|
||||
@@ -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"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.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
|
||||
|
||||
<interfaces>
|
||||
<!-- From src/companion/characters.ts (current) -->
|
||||
```typescript
|
||||
export interface CompanionDefinition {
|
||||
type: CompanionType;
|
||||
name: string;
|
||||
emoji: string;
|
||||
personality: string;
|
||||
}
|
||||
export const companions: CompanionDefinition[];
|
||||
```
|
||||
|
||||
<!-- From src/companion/companion.ts (Plan 04) -->
|
||||
```typescript
|
||||
export async function getGreeting(characterType: CompanionType, db: IDBDatabase): Promise<string>;
|
||||
```
|
||||
|
||||
<!-- From src/app.ts (current) -->
|
||||
```typescript
|
||||
export async function initApp(): Promise<void>;
|
||||
export function showScreen(name: ScreenName): void;
|
||||
```
|
||||
|
||||
<!-- Welcome screen HTML structure (from index.html) -->
|
||||
<!-- companion-grid contains dynamically created companion-card buttons -->
|
||||
<!-- Each card has companion-card__emoji span + companion-card__name span -->
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Add avatar URLs to companion definitions and update welcome screen</name>
|
||||
<files>src/companion/characters.ts, src/ui/screens.ts, src/styles/main.css</files>
|
||||
<read_first>
|
||||
- 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)
|
||||
</read_first>
|
||||
<action>
|
||||
**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 = `
|
||||
<span class="companion-card__emoji">${c.emoji}</span>
|
||||
<span class="companion-card__name">${c.name}</span>
|
||||
`;
|
||||
```
|
||||
|
||||
With:
|
||||
```typescript
|
||||
card.innerHTML = `
|
||||
<img class="companion-card__avatar" src="${c.avatarUrl}" alt="${c.name}"
|
||||
onerror="this.style.display='none'; this.nextElementSibling.style.display='block';">
|
||||
<span class="companion-card__emoji" style="display:none">${c.emoji}</span>
|
||||
<span class="companion-card__name">${c.name}</span>
|
||||
`;
|
||||
```
|
||||
|
||||
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;
|
||||
}
|
||||
```
|
||||
</action>
|
||||
<verify>
|
||||
<automated>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</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `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
|
||||
</acceptance_criteria>
|
||||
<done>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.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Add companion greeting to forest screen and avatar to lesson screen</name>
|
||||
<files>src/app.ts, src/ui/screens.ts, index.html</files>
|
||||
<read_first>
|
||||
- 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)
|
||||
</read_first>
|
||||
<action>
|
||||
**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
|
||||
<div id="forest-greeting" class="companion-area" style="display:none">
|
||||
<img id="forest-greeting-avatar" class="companion-area__avatar" src="" alt="">
|
||||
<p id="forest-greeting-text" class="companion-area__text"></p>
|
||||
</div>
|
||||
```
|
||||
|
||||
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
|
||||
<div id="lesson-companion" class="companion-area" style="display:none">
|
||||
<img id="lesson-companion-avatar" class="companion-area__avatar" src="" alt="">
|
||||
<p id="lesson-companion-text" class="companion-area__text"></p>
|
||||
</div>
|
||||
```
|
||||
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/dev/workspace/zauberwald && grep -c "getGreeting" src/app.ts && grep -c "forest-greeting" index.html && grep -c "lesson-companion" index.html</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `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
|
||||
</acceptance_criteria>
|
||||
<done>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.</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<name>Task 3: Visual verification of Phase 2 integration</name>
|
||||
<files>index.html</files>
|
||||
<action>
|
||||
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
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/dev/workspace/zauberwald && npm run build 2>&1 | tail -5</automated>
|
||||
</verify>
|
||||
<done>User confirms: avatars display on welcome screen, greeting shows on forest screen, companion avatar visible in lesson. App works with and without API key.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `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)
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 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
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/02-gemini-integration-asset-pipeline/02-05-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user