A weekend hackathon demo: describe your room → AI asks clarifying questions → streams a furnished mid-century-modern living room in 3D → refine conversationally → buy the pieces from real stores.
Built on React 19 + Vite + R3F 9.5 + Fastify + @google/genai (Gemini 2.5 Flash) with editorial warm chrome (Fraunces + Inter, OKLCH palette) and a priority-tier-aware cinematic camera reveal.
# 1. Install
npm install
# 2. Set up environment
cp .env.example .env
# Edit .env and set GEMINI_API_KEY=<your key from https://aistudio.google.com/app/apikey>
# Enable Cloud Billing on the GCP project to unlock Tier 1 (150 RPM).
# 3. Run dev (starts Fastify on :3000 + Vite on :5173)
npm run dev
# 4. Open the demo
open http://localhost:5173Without a GEMINI_API_KEY, the server falls back to the golden cache in
public/cache/golden/*.json, so the demo runs end-to-end with zero API cost.
The cache is real curated content, not a stub — so use it freely while
iterating on UX.
VITE_DEMO_MODE=1 npm run devIn demo mode:
- Free-text prompt is replaced with a single "Stage this room" button
- Q&A overlay can't be dismissed (Escape and click-outside are blocked)
Cmd+Shift+Cforces the cache replay path (presenter escape hatch)Cmd+Shift+Rreloads the demo to the start state- Errors are swallowed silently into a hidden
window.__telemetrybuffer beforeunloadblocks accidental navigation
The locked demo viewport is 1440×900.
src/
├── client/ # React 19 + R3F frontend
│ ├── components/
│ │ ├── scene/ # 3D scene: Canvas, Room, Lighting, Furniture, CameraRig
│ │ ├── overlay/ # PromptInput, QAOverlay, RoomOutlineIntro, RefineButton
│ │ └── shell/ # ChatRail, SidePanel, Tagline, ShopLookButton
│ ├── hooks/ # useStreamingScene, useCameraChoreography, useFallbackCache
│ ├── lib/ # sse-client, scene-state reducer, demo-mode installer
│ └── styles/ # tokens.css (OKLCH), typography.css (Fraunces+Inter)
├── server/ # Fastify backend
│ ├── routes/ # POST /api/session, GET /api/design/stream (SSE)
│ ├── gemini/ # @google/genai pipeline + schemas + system prompts
│ ├── merge/ # JSON Merge Patch enforcer + scripted-target resolver
│ ├── autofix/ # Pure-logic post-processing (snap, nudge, clamp, validate)
│ └── cache/ # Golden cache loader + per-session state
└── shared/ # Types + catalogue, imported from both halves
├── catalogue.ts # CatalogueItem type + 14-piece MCM CATALOGUE constant
├── scene.ts # SceneState, Piece, ROOM dimensions
└── events.ts # SSE payload types
public/
├── cache/golden/ # phase1.json, phase2.json, refinement.json (committed)
├── models/ # Normalized GLB files (drop downloads here)
└── shop.html # Generated by `npm run build:shop`
The demo ships with 14 mid-century-modern pieces in src/shared/catalogue.ts.
Each entry has:
- A primitive geometry description (Three.js box / cylinder shapes) used by default — so the demo runs end-to-end without any GLB files.
- An empty
glb_urlslot ready to drop in a real GLB.
-
Browse https://polyhaven.com/models/furniture and download the GLBs into
assets/raw/. Use the catalogue id as the filename (e.g.assets/raw/sofa_mcm_01.glb). -
Run normalization (recenters to floor + XZ centerline; no Blender needed):
npm run normalize-assets
-
Update each entry's
glb_urlinsrc/shared/catalogue.ts:glb_url: "/models/sofa_mcm_01.glb",
-
Restart
npm run dev. The<FurnitureItem>renderer prefers the GLB but falls back to the primitive on load failure.
Once the live Gemini path works, capture a known-good run for demo reliability:
GEMINI_API_KEY=<key> npm run prewarm-cacheThe script:
- Calls Phase 1 with the scripted prompt
- Runs Phase 2 streaming with the scripted answers (
no TV / evening / solo) - Runs auto-fix + asserts ≥6 items + ≥1 anchor
- Runs the scripted refinement and asserts a non-empty diff
- Writes all 3 artifacts to
public/cache/golden/*.json
Re-run until you get a layout you like — the cache files are committed to git so the presenter laptop has a known-good run.
npm run dev # Both servers in dev mode
npm run build # Production client + server build + shop.html
npm run typecheck # Both client + server tsc passes
npm test # Vitest (auto-fix + enforcer suites)
npm run normalize-assets # Recenter Poly Haven GLBs in assets/raw/
npm run prewarm-cache # Capture a fresh golden cache (needs GEMINI_API_KEY)- Intro Open → 2-second SVG room outline draws itself
- Prompt Click "Stage this room"
- Q&A Three clarifying questions (TV / lighting / use)
- Reveal Streaming Phase 2 — sofa lands first (anchor camera lock), then primary, accent, decor in order
- Pull-back Camera pulls back to a hero overview
- Refine Click "Face the reading chair toward the bookshelf" — only the reading chair rotates, every other piece stays put
- Buy Click any item's Buy link, or "Shop this look" for the full list
Hackathon code, no formal license. Real retailer URLs are not affiliate links.
3D primitives are MIT-equivalent. If you wire in real Poly Haven GLBs, they're
CC0; Sketchfab assets require attribution (see AttributionsModal).