370 lines
15 KiB
Markdown
370 lines
15 KiB
Markdown
---
|
|||
|
|
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>
|