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