Interactive AI bedtime story app for kids ages 3-5. Pick a storytelling style, choose a theme, and play through a 5-beat choose-your-own-adventure story with AI-generated text and illustrations. Each beat gets a full-screen picture book illustration generated by OpenAI.
A parent picks a style (Whimsical Rhyme, Calm Bedtime, or Silly & Goofy) and a theme (Penguins, Space, Ocean, etc.), then the app generates an interactive story in 5 beats:
- Meet the Friend — introduces the character and setting
- Something Happens — a fun, surprising event
- Try a Thing — the character explores or attempts something
- Big Hooray — the happy climax
- Cozy Ending — calm wrap-up, no choices, just sleepy vibes
At each beat (except the last), the child picks between two choices that shape what happens next. All content is validated for age-appropriateness — no scary stuff, no villains, no sadness.
- React Router v7 (full-stack, SSR) with TypeScript
- Anthropic Claude / OpenAI — dual AI provider for story text, switchable via env var
- OpenAI GPT Image 1 Mini — one illustration per beat, children's book style
- Server-Sent Events for streaming story text + async image delivery
- PostgreSQL via Prisma ORM
- Tailwind CSS v4 — warm, rounded, child-friendly UI
- Docker Compose for local Postgres
Streaming JSON extraction — The AI returns structured JSON ({beat, segment, question, options}), but we want to stream the story text live. The server parses the JSON as it streams in, extracts just the segment value in real-time, and sends only the story prose to the client via SSE. The full JSON is validated after the stream completes.
Multi-layer content safety — Primary safety is prompt-level (the AI is instructed to never generate scary/violent content). Backup is a Zod validation pipeline + keyword blocklist that catches anything that slips through. Both choice options are required to lead to equally positive outcomes.
Retry with fallback — If the streamed response fails validation (bad JSON, missing fields, blocked word), the engine automatically retries with a non-streaming request. The user sees a brief pause but gets a valid beat.
app/
routes/
home.tsx # Landing page
library.tsx # Story history — in-progress and completed
story.new.tsx # Pick style + theme
story.$storyId.tsx # Full-screen picture book playback
api.story-beat.tsx # SSE endpoint for beat generation
lib/
ai.server.ts # Anthropic + OpenAI text generation (streaming + non-streaming)
image-engine.server.ts # OpenAI image generation, style spines, file saving
story-engine.server.ts # Orchestrator — prompts, AI calls, validation, DB writes, image triggering
story-prompts.server.ts # System prompts per style and beat
validators.server.ts # Zod schemas + content safety blocklist
auth.server.ts # Session cookies + bcrypt
db.server.ts # Prisma client singleton
# Start Postgres
docker compose up -d postgres
# Install deps + set up DB
npm install
npx prisma generate
npx prisma migrate dev
npx prisma db seed
# Copy env and add your API keys
cp .env.example .env
# Start dev server
npm run devApp runs at http://localhost:5555.
| Variable | What it does |
|---|---|
DATABASE_URL |
Postgres connection string |
SESSION_SECRET |
Signs session cookies |
ANTHROPIC_API_KEY |
Claude API key |
OPENAI_API_KEY |
OpenAI API key (also used for image generation) |
AI_PROVIDER |
"anthropic" (default) or "openai" |
