A Dutch-language AI bedtime-story generator for kids. Build a fantasy world with characters, configure a story (chapters, age range, narrative arc, learning goals), and the app generates a multi-chapter illustrated story complete with educational quizzes, hero portraits, chapter illustrations, coloring pages, and a parent-only reading aid.
Built around the Creature Team universe — a pre-loaded cast of ~50 heroes, sidekicks, and villains kids can mix and match.
All UI text and generated stories are in Nederlands (Dutch). The rest of this README is in English so contributors can read it.
- Conversational world-building — chat with the world editor (Gemini structured output) to add characters, traits, places, items. Updates the world log in place.
- Story generation — pick chapter count, target age, narrative arc, and learning goals. Outline first, then chapters in parallel batches (3 at a time) with per-chapter retry. Each chapter ships with a self-read section, a parent-aid section (500-600 words), 5 quiz challenges, and a generated image prompt.
- Image generation pipeline — Imagen 4 powers character profile pictures, story covers, in-chapter illustrations, coloring pages, and per-character pose sheets. Generated assets are persisted to GCS and recovered on demand.
- Three-state character toggle — every character in the world is Mee (forced in), Thuis (forced out), or Willekeurig (random). Story cast capped at 7 characters for narrative coherence.
- Parent lock — gates settings, image regeneration, and cost-sensitive surfaces behind a PIN screen.
- Cloud sync — Firestore-backed sync so worlds, characters, and saved stories survive across devices. Auto-sync runs in the background.
- Parchment reader — the story renderer styles itself like a story book (Fredoka + Nunito, parchment background). Print mode generates separate parent and child booklets.
- Story archive — every generated story is saved; the variation tracker feeds past titles + synopses back into the prompt so the next story doesn't repeat itself.
- Framework: Next.js 16.1.6 (App Router, React 19, server actions for the API routes)
- Language: TypeScript 5 (strict mode)
- Styling: Tailwind CSS v4 via
@tailwindcss/postcss, dark theme with CSS variables inglobals.css - State: Zustand v5 with
persistmiddleware → auto-saves tolocalStorage(key:world-smid-store) - AI: Google Gemini (
gemini-3.1-pro-preview) via@google/genai, plus Imagen 4 for image generation - Storage: Firestore (worlds + archive) + GCS (generated images)
- Icons: lucide-react
- Fonts: Fredoka (display) + Nunito (body) via Google Fonts
src/
├── app/
│ ├── layout.tsx # Root layout (fonts, metadata)
│ ├── page.tsx # Main page (tab navigation)
│ ├── globals.css # Theme + Tailwind + print styles
│ └── api/ # 14 server-side endpoints:
│ ├── chat/ world-building chat (structured output)
│ ├── generate-story/ outline + chapter generation
│ ├── regenerate-chapter/ single-chapter regen with retry
│ ├── regenerate-prompts/ refresh imagePrompt strings
│ ├── generate-cover/ Imagen 4 cover image
│ ├── generate-illustration/ per-chapter image
│ ├── generate-coloring-page/ line-art for print
│ ├── generate-character-sheet/
│ ├── generate-character-sheet-prompt/
│ ├── generate-profile-picture/
│ ├── generate-single-pose/ per-pose character sheet
│ ├── upload-image/ GCS upload
│ ├── recover-images/ GCS recovery
│ ├── backfill-puzzles/ migration helper
│ └── worlds/{save,load}/ Firestore sync
├── components/
│ ├── ChatInterface.tsx # World-building chat UI
│ ├── WorldManager.tsx # World selector tabs
│ ├── WorldLogView.tsx # Character grid + 3-way toggle
│ ├── StoryConfigurator.tsx # Story param form
│ ├── StoryPreview.tsx # Reader (parent/child modes, print)
│ ├── StoryArchive.tsx # Saved stories
│ ├── CharacterEditor.tsx # Add/edit characters
│ └── ParentLock.tsx # PIN gate
└── lib/
├── store.ts # Zustand store + default Creature Team
├── creature-team-data.ts # Pre-loaded character roster
├── story-arcs.ts # Narrative arc templates
├── firestore.ts # Cloud sync
├── auto-sync.ts # Background sync loop
├── storage.ts # GCS upload/recovery helpers
├── migration.ts # Store version migration
└── print-helpers.ts # Booklet print layouts
Prompt-engineering reference lives at the repo root:
backdrop-prompts.md— backdrop/scene generation referencecharacter-prompts.md— character appearance + pose guidancevillain-prompts.md— villain-specific visual notes
npm install
cp .env.example .env.local # fill in GEMINI_API_KEY
npm run dev # http://localhost:3000Building for production:
npm run build
npm run startCloud Run, source deploy — no Dockerfile, no cloudbuild.yaml:
gcloud run deploy creature-team-verhalenmachine \
--source . \
--region europe-west4 \
--allow-unauthenticated \
--set-env-vars GEMINI_API_KEY=$GEMINI_API_KEYCloud Build auto-containerizes the Next.js app. The service runs unauthenticated; the Gemini API key lives only in the service config.
These are non-obvious rules the codebase depends on:
- The model name is locked.
gemini-3.1-pro-previewis the only AI model wired in; switching it requires verifying every prompt + the structured-output schemas still parse. Don't bump it casually. - Never
JSON.stringify(worldLog, null, 2)into a prompt. The world log holds ~50 characters with long descriptions; pretty-printed JSON blows the token budget. Always use the compact helpers instore.ts(buildWorldSummary()) and the per-character truncation (200 chars in story prompts, 150 chars in chat prompts). - Outline is passed as one-line-per-chapter into chapter prompts — not the full JSON. Same token-budget reason.
- Story cast is hard-capped at 7 characters. Beyond that, narrative
coherence collapses; the generator was rewritten in
6d79ad2to enforce this. - All UI text and prompts are in Dutch. The only English string in
the prompt pipeline is
imagePrompt(since the image model performs better on English prompts). - Components are
"use client". No SSR — the Zustand store relies onlocalStoragewhich doesn't exist server-side. - Print is first-class.
StoryPreviewopens an iframe and callswindow.print()with separate parent and child booklet layouts. - Image generation is async and best-effort. Story generation uses
Promise.allSettledso a single failing chapter doesn't kill the run.
This repo is published for portfolio + reference. The Creature Team character roster and the prompt-engineering reference markdown are part of the original creative work — please don't reuse the characters or their descriptions verbatim.