diff --git a/.claude/plans/adaptive-hugging-blossom.md b/.claude/plans/adaptive-hugging-blossom.md new file mode 100644 index 00000000..a0082d93 --- /dev/null +++ b/.claude/plans/adaptive-hugging-blossom.md @@ -0,0 +1,121 @@ +# Sprite Stream Ticker — Live Ambient Display + +## Context + +The spritegen UI (`tools/sprite-generation/gui/`) currently has no real-time feedback when sprites are being generated. The dashboard polls `/api/progress` every 10s for aggregate counts, but there's no visual of actual images arriving. The goal is a beautiful ambient ticker across the top of the app that streams completed sprites horizontally — functioning as both a progress indicator and an idle screensaver. + +## Architecture + +``` +Backend (SSE + REST) Frontend (React) + +GET /api/variants/recent ─────────► Initial load (30 recent sprites) +GET /api/stream/variants ─SSE──────► Real-time push of new completions + │ + ▼ + SpriteStream.tsx + ├── CSS @keyframes marquee (infinite scroll) + ├── SSE EventSource consumer + ├── Glow animation on new arrivals + └── Pause on hover, click to navigate +``` + +## Changes + +### 1. `engine/registry.py` — Add `get_recent_variants()` + +New method on `SpriteRegistry` (after `get_pending_variants` ~line 427): + +```python +def get_recent_variants(self, limit: int = 30, since: str | None = None) -> list[dict]: +``` + +- JOIN `variants v` to `sprites s` on `v.sprite_id = s.id` +- Filter: `v.job_status = 'completed' AND v.raw_path IS NOT NULL` +- Optional: `AND v.created_at > ?` when `since` provided +- ORDER BY `v.created_at DESC`, LIMIT +- Return: `variant_id, sprite_id, category, entity_id, raw_path, processed_path, seed, created_at` + +### 2. `server.py` — Two new endpoints + +**REST:** `GET /api/variants/recent?limit=30` — calls `registry.get_recent_variants()`, returns JSON array. Goes in the "Progress & Queue" section. + +**SSE:** `GET /api/stream/variants` — async generator polling DB every 3s for new completions since last check. Sends `data: [...]` events with new variants, or SSE comment keepalives. Uses `StreamingResponse(media_type="text/event-stream")`. Add imports: `asyncio`, `StreamingResponse`. + +### 3. `gui/src/types.ts` — Add `RecentVariant` interface + +```typescript +export interface RecentVariant { + variant_id: number + sprite_id: string + category: string + entity_id: string + raw_path: string + processed_path: string | null + seed: number + created_at: string +} +``` + +### 4. `gui/src/api.ts` — Add fetch helpers + +- `fetchRecentVariants(limit = 30): Promise` +- `variantStreamUrl(): string` — returns SSE endpoint URL + +### 5. `gui/src/SpriteStream.tsx` — New component (the main piece) + +**Visual design:** +- Full-width strip at top of app, ~110px tall +- Background: gradient fade from `colors.surface` to transparent — ambient, not blocky +- Sprites scroll right-to-left infinitely via CSS `@keyframes` marquee +- Each sprite: 80px tall image + 10px entity label below, semi-transparent (opacity 0.7) +- New arrivals from SSE get a 3s highlight glow animation (`colors.highlight` box-shadow pulse) +- Hover pauses the scroll, raises opacity to 1.0 +- Click navigates to `/sprite/{id}` for review +- Empty state: subtle pulsing "Waiting for sprites..." text + +**Marquee technique:** +- Render the sprite array twice in a flex row (A + A clone) +- `translateX(0) → translateX(-50%)` animation creates seamless infinite loop +- Duration scales with sprite count (minimum 20s, ~2s per sprite) +- `will-change: transform` for GPU compositing + +**SSE lifecycle:** +- On mount: fetch initial sprites, open EventSource +- On SSE message: parse JSON, dedupe by variant_id, prepend to list, cap at 60 items +- On SSE error: retry with backoff (5s → 10s → 20s, cap 30s), fall back to 10s polling after 3 failures +- On unmount: close EventSource + +**Injected `