# Project export: StoryLearn AI

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2026
- Tagline: Tired of confusing textbooks and endlessly prompting AI to explain a question? StoryLearn AIturns any topic into bite-sized chapters with clear visuals and active-recall quizzes to help it stick.
- Devpost: https://devpost.com/software/storylearn-ai
- GitHub: https://github.com/shreeya-12/aicalhacks
- Video: https://www.youtube.com/embed/CaZv04c4fSM?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 4 GitHub contributor(s) — Davin (18 commits), Shreeya S (15 commits), Claude Opus 4.8 (10 commits), Aashi Malik (6 commits)

## Devpost submission (written by the team)

### Inspiration

AI study tools and chatbots are generally too open-ended—you have to know what to ask, and the answers are huge blocks of text. We wanted something that takes a topic you're struggling with and turns it into a visual, structured lesson with built-in quizzes, so you actually retain what you learn. We were inspired by how kids’ picture books break down complex ideas using easier-to-understand language and clean visuals, and thought—why not do that for every age group? Learning through visuals isn’t restricted to any age or education level, and having this ease with learning makes it more appealing to all audiences.

### What it does

StoryStream first takes a user-inputted topic, such as "How photosynthesis works" or "The history of the printing press", and an education level. It then generates a chapter-by-chapter illustrated lesson, or a “story,” in other words, with AI-generated images tailored to that level of difficulty. After the lesson, it quizzes you with multiple-choice questions to reinforce what you learned, as active recall after learning something is key to retaining the information. Past lessons are saved in a sidebar for easy access, and Redis caching means repeat topics load instantly.

### How we built it

For the frontend, we used React, Vite, and TypeScript with a split-screen dashboard—story chapters on the left, generated images on the right, and an interactive quiz at the end. For the Multi-agent backend, we utilized a 3-agent pipeline built with FastAPI and Claude (Anthropic). Agent 1 researches the topic using Browserbase and Stagehand, Agent 2 turns the research into a multi-chapter narrative with images, and Agent 3 generates a targeted quiz. The image generation was done using Claude, and the AI-generated visuals were tailored to each chapter and difficulty level. For caching, Redis stores completed lessons so repeat searches load instantly instead of re-running the full pipeline. Finally, Claude Code was used to scaffold, debug, and iterate on both frontend and backend.

### Challenges we ran into

The first challenge we faced was that one of our teammates left our team after the hackathon started. He was initially leading our team and had the most experience, so this was a large setback, as we had to pivot to simpler ideas since we are all beginners in some way. Integrating Stagehand/Browserbase for live web research required debugging environment variables and API configuration under time pressure. Another challenge we faced was figuring out how we wanted to coordinate the research agents because hallucination was one of the most important issues we wanted to address. We went through several iterations focused on specifically fixing this issue. First, we were planning on using Band to navigate agent-to-agent communication, but we soon realized that with our system, with one researcher, one story creator, and one quiz generator, we did not need to have a concurrent communication pipeline for the different agents. So, we made it a continuous pipeline in our FastAPI backend. Then, after more deliberation, we decided to use Browserbase Stagehand for conducting research and collecting information using Google searches and Wikipedia, with two separate Claude-based agents using that information to create the chapters and images, then the quizzes. This was the most we were able to eliminate the hallucination issue in the project given the time we had, as the generated content was based solely on real, gathered information. Also, generation could take around 5 minutes end-to-end, especially for the greater levels of difficulty, so we had to design the UX around loading states and use Redis caching to make repeat views instant. Coordinating three agents to pass structured JSON cleanly between each other without schema mismatches was definitely a challenge.

### Accomplishments we're proud of

We were able to create a functional product and gained significant experience with using AI in software development and creating agentic systems. As a team of beginners who lost a key member early on, shipping a working multi-agent app in under 24 hours felt like a real win and learning experience.

### What we learned

We learned how multi-agent AI systems work in practice, breaking a complex task into specialized agents that pass structured data between each other. We also learned how to use Claude Code to rapidly build and iterate on a full-stack application. We were also introduced to Redis and learned about caching strategies for expensive AI-generated content, along with how to scope a hackathon project realistically when time and experience are limited.

### What's next

If we had more time, we would like to create a database to store and access old lessons rather than the short-term caching we currently have with Redis. We would also like to add a feature to export quiz questions as Quizlet sets. Along with Quizlet sets, it would be beneficial to add more interactive activities beyond quizzes, such as drag-and-drop diagrams and fill-in-the-blank. In addition, our pipeline currently takes a few minutes to generate lessons, so we would like to optimize it for faster generation while maintaining quality. We would also like to expand our scope and incorporate research papers utilizing the automation provided by Browserbase. And lastly, we would like to deploy the site to production.

## README (from the GitHub repository)

# AI Hackathon @ UC Berkeley 2026

## StoryLearn AI

Turns any topic into an age-appropriate illustrated story (3 chapters for elementary, scaling up
to 8-10 for college) with 2 quiz questions per chapter, via a per-chapter agent pipeline: a
planner splits the topic into chapters, then each chapter gets its own Researcher (Browserbase/
Stagehand) -> Storyteller (Claude) -> Quiz Master (Claude) pass, followed by AI-generated images
(OpenAI `gpt-image-1`).

- [`server/`](server/) - FastAPI backend, agent pipeline, Redis cache (see [server/README.md](server/README.md))
- [`frontend/`](frontend/) - React + Vite app: topic form -> story -> quiz, with a history sidebar (see [frontend/README.md](frontend/README.md))


## Detected evidence (automated analysis)

Indexed codebase: 35 recognized source files, 99 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- OpenAI (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Redis (technology) — detected in the code
- TypeScript (language) — detected in the code
- AI coding agent: Claude Code — evidence: commit authorship or trailers

## Codebase structure (from repository index)

### Files (45 of 45)

```
.gitignore
frontend/.env.example
frontend/.gitignore
frontend/eslint.config.js
frontend/index.html
frontend/package.json
frontend/README.md
frontend/src/api.ts
frontend/src/App.css
frontend/src/App.tsx
frontend/src/components/GenerationProgress.tsx
frontend/src/components/ImageFrame.tsx
frontend/src/components/Quiz.tsx
frontend/src/components/Sidebar.tsx
frontend/src/components/StoryPanel.tsx
frontend/src/components/TopicForm.tsx
frontend/src/data/mockPhotosynthesis.ts
frontend/src/index.css
frontend/src/main.tsx
frontend/src/types.ts
frontend/tsconfig.app.json
frontend/tsconfig.json
frontend/tsconfig.node.json
frontend/vite.config.ts
LICENSE
README.md
scripts/test_cache.py
scripts/test_image.py
server/.env.example
server/agents/__init__.py
server/agents/_age_group_style.py
server/agents/_tool_schemas.py
server/agents/chapter_planner.py
server/agents/images.py
server/agents/quiz_master.py
server/agents/researcher.py
server/agents/storyteller.py
server/cache.py
server/config.py
server/main.py
server/models.py
server/pipeline.py
server/README.md
server/requirements.txt
server/sentry_init.py
```

### Dependencies

- frontend/package.json: @eslint/js@^10.0.1, @types/katex@^0.16.8, @types/node@^24.12.3, @types/react@^19.2.14, @types/react-dom@^19.2.3, @vitejs/plugin-react@^6.0.1, eslint@^10.3.0, eslint-plugin-react-hooks@^7.1.1, eslint-plugin-react-refresh@^0.5.2, globals@^17.6.0, katex@^0.17.0, react@^19.2.6, react-dom@^19.2.6, typescript@~6.0.2, typescript-eslint@^8.59.2, vite@^8.0.12
- server/requirements.txt: anthropic, fastapi, httpx, openai, pydantic, python-dotenv, redis, rich, sentry-sdk, stagehand-py, uvicorn[standard]

### Recent commits (newest first)

- Merge pull request #19 from shreeya-12/render-math-katex
- Render LaTeX math with KaTeX
- Merge pull request #18 from shreeya-12/fix-image-infinite-loading
- Fix infinite image skeleton when revisiting a cached lesson
- Merge pull request #17 from shreeya-12/capitalize-ai
- Capitalize AI in product name: StoryLearn Ai -> StoryLearn AI
- Merge pull request #16 from shreeya-12/fix-dark-mode-text
- Fix dark mode: use theme variables for chapter text
- Merge pull request #15 from shreeya-12/improve-lesson-readability-images
- Merge pull request #14 from shreeya-12/rename-story-learn-ai
- Add hover tooltips with definitions for key terms
- Improve lesson readability and image display
- Tweak name to StoryLearn Ai (Story and Learn joined)
- Rename product from StoryStream to Story Learn Ai
- Merge pull request #13 from shreeya-12/fix/browserbase-live-research
- Make quiz generation resilient to malformed tool output
- Merge pull request #12 from shreeya-12/fix/browserbase-live-research
- Improve lesson quality and UX: paragraphs, more chapters, loading bar
- Merge pull request #11 from shreeya-12/fix/browserbase-live-research
- Fix live Browserbase research: correct model name and crawl timeout

## Key source files (fetched from GitHub, selected and truncated for size)

### server/requirements.txt

```
fastapi
openai
uvicorn[standard]
pydantic
python-dotenv
anthropic
redis
sentry-sdk
httpx
stagehand-py
rich

```

### frontend/package.json

```
{
  "name": "frontend",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "lint": "eslint .",
    "preview": "vite preview"
  },
  "dependencies": {
    "katex": "^0.17.0",
    "react": "^19.2.6",
    "react-dom": "^19.2.6"
  },
  "devDependencies": {
    "@eslint/js": "^10.0.1",
    "@types/katex": "^0.16.8",
    "@types/node": "^24.12.3",
    "@types/react": "^19.2.14",
    "@types/react-dom": "^19.2.3",
    "@vitejs/plugin-react": "^6.0.1",
    "eslint": "^10.3.0",
    "eslint-plugin-react-hooks": "^7.1.1",
    "eslint-plugin-react-refresh": "^0.5.2",
    "globals": "^17.6.0",
    "typescript": "~6.0.2",
    "typescript-eslint": "^8.59.2",
    "vite": "^8.0.12"
  }
}

```

### server/main.py

```python
import logging

from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware

from cache import list_history
from config import settings
from models import GenerateRequest, HistoryItem, StoryPayload
from pipeline import run_pipeline
from sentry_init import init_sentry

logger = logging.getLogger(__name__)

init_sentry()

app = FastAPI(title="StoryLearn AI API")

app.add_middleware(
    CORSMiddleware,
    allow_origins=settings.cors_origins,
    allow_methods=["*"],
    allow_headers=["*"],
)


@app.get("/health")
async def health() -> dict[str, str]:
    return {"status": "ok"}


@app.post("/api/generate", response_model=StoryPayload)
async def generate(req: GenerateRequest) -> StoryPayload:
    try:
        return await run_pipeline(req.topic, req.age_group)
    except Exception as exc:
        raise HTTPException(status_code=502, detail=str(exc)) from exc


@app.get("/api/history", response_model=list[HistoryItem])
async def history() -> list[HistoryItem]:
    # History is a nice-to-have sidebar, not a critical path — fail open to
    # an empty list (e.g. if Redis is unreachable) rather than a 500.
    try:
        return await list_history()
    except Exception:
        logger.exception("Failed to fetch history, returning empty list")
        return []

```

### frontend/src/main.tsx

```typescript
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import 'katex/dist/katex.min.css'
import './index.css'
import App from './App.tsx'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

```

### frontend/src/App.tsx

```typescript
import { useState, useEffect } from "react";
import { TopicForm } from "./components/TopicForm";
import { StoryPanel } from "./components/StoryPanel";
import { ImageFrame } from "./components/ImageFrame";
import { Quiz } from "./components/Quiz";
import { Sidebar } from "./components/Sidebar";
import { GenerationProgress } from "./components/GenerationProgress";
import { mockPhotosynthesis } from "./data/mockPhotosynthesis";
import { generateStory, fetchHistory } from "./api";
import type { AgeGroup, HistoryItem, StoryPayload } from "./types";
import "./App.css";

type View = "home" | "lesson" | "quiz";

function App() {
  const [view, setView] = useState<View>("home");
  const [story, setStory] = useState<StoryPayload | null>(null);
  const [activeChapter, setActiveChapter] = useState(0);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [history, setHistory] = useState<HistoryItem[]>([]);

  useEffect(() => {
    fetchHistory().then(setHistory);
  }, []);

  async function handleGenerate(topic: string, ageGroup: AgeGroup) {
    setIsLoading(true);
    setError(null);
    try {
      const payload = await generateStory({ topic, age_group: ageGroup });
      setStory(payload);
      setActiveChapter(0);
      setView("lesson");
      // Refresh history — new entry may have been cached
      fetchHistory().then(setHistory);
    } catch (err) {
      setError(err instanceof Error ? err.message : "Failed to generate story");
    } finally {
      setIsLoading(false);
    }
  }

  function loadDemo() {
    setStory(mockPhotosynthesis);
    setActiveChapter(0);
    setError(null);
    setView("lesson");
  }

  function goHome() {
    setView("home");
    setStory(null);
    setActiveChapter(0);
    setError(null);
  }

  return (
    <div className="app-shell">
      <Sidebar history={history} onSelect={handleGenerate} isLoading={isLoading} />

      <div className="app-main">
        {view === "home" && (
          <div className="home-view">
            <div className="home-content">
              <div className="home-brand">
                <div className="home-logo">✦</div>
                <h1 className="home-title">StoryLearn AI</h1>
                <p className="home-tagline">
                  Turn any topic into an illustrated story and quiz — powered by AI.
                </p>
              </div>

              <div className="home-card">
                <TopicForm onGenerate={handleGenerate} isLoading={isLoading} />
                {isLoading && <GenerationProgress />}
                {error && <p className="error-msg">{error}</p>}
                <div className="home-divider">
                  <span>or</span>
                </div>
                <button className="demo-btn" onClick={loadDemo}>
                  Use Demo: Photosynthesis
                </button>
              </div>
            </div>
          </div>
        )}

        {view === "lesson" && story && (
          <div className="lesson-view">
            <header className="lesson-header">
              <div className="lesson-header-left">
                <span className="lesson-brand">✦ StoryLearn AI</span>
                <span className="lesson-topic">{story.topic}</span>
              </div>
              <button className="header-btn" onClick={goHome}>
                ← New Lesson
              </button>
            </header>

            <main className="lesson-main">
              <section className="lesson-story">
                <StoryPanel
                  chapters={story.chapters}
                  activeIndex={activeChapter}
                  onSelectChapter={setActiveChapter}
                  onComplete={() => setView("quiz")}
                />
              </section>
              <section className="lesson-image">
                <ImageFrame chapter={story.chapters[activeChapter]} />
              </section>
            </main>
          </div>
        )}

        {view === "quiz" && story && (
          <div className="quiz-view">
            <header className="lesson-header">
              <div className="lesson-header-left">
                <span className="lesson-brand">✦ StoryLearn AI</span>
                <span className="lesson-topic">{story.topic}</span>
              </div>
              <button className="header-btn" onClick={() => setView("lesson")}>
                ← Back to Story
              </button>
            </header>

            <main className="quiz-main">
              <div className="quiz-header">
                <h2 className="quiz-title">Quiz Time</h2>
                <p className="quiz-subtitle">
                  Let's see how much you remember from the story.
                </p>
              </div>
              <Quiz questions={story.quiz} onRestart={goHome} />
            </main>
          </div>
        )}
      </div>
    </div>
  );
}

export default App;

```

### frontend/vite.config.ts

```typescript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vite.dev/config/
export default defineConfig({
  plugins: [react()],
})

```

### server/sentry_init.py

```python
import sentry_sdk

from config import settings


def init_sentry() -> None:
    """No-op if SENTRY_DSN is unset, so this is safe to call in dev without an account."""
    if not settings.sentry_dsn:
        return
    sentry_sdk.init(
        dsn=settings.sentry_dsn,
        traces_sample_rate=1.0,
        environment="hackathon",
    )

```

### frontend/index.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>StoryLearn AI</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

```

### frontend/eslint.config.js

```javascript
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'

export default defineConfig([
  globalIgnores(['dist']),
  {
    files: ['**/*.{ts,tsx}'],
    extends: [
      js.configs.recommended,
      tseslint.configs.recommended,
      reactHooks.configs.flat.recommended,
      reactRefresh.configs.vite,
    ],
    languageOptions: {
      globals: globals.browser,
    },
  },
])

```

### scripts/test_image.py

```python
"""Standalone test: fire one gpt-image-1 call and save the result to a PNG.

Run from repo root:
    python scripts/test_image.py

Requires IMAGE_API_KEY to be set in server/.env (or the environment).
"""
from __future__ import annotations

import base64
import sys
from pathlib import Path

# Allow importing server modules (config.py lives there)
sys.path.insert(0, str(Path(__file__).parent.parent / "server"))

from config import settings  # noqa: E402

if not settings.image_api_key:
    sys.exit("ERROR: IMAGE_API_KEY is not set. Check server/.env")

import openai  # noqa: E402

IMAGE_MODEL = "gpt-image-1"
PROMPT = "A friendly cartoon sun reading a book in a bright classroom, children's illustration style"
OUTPUT_PATH = Path(__file__).parent / "test_output.png"


def main() -> None:
    client = openai.OpenAI(api_key=settings.image_api_key)

    print(f"Calling {IMAGE_MODEL} …")
    response = client.images.generate(
        model=IMAGE_MODEL,
        prompt=PROMPT,
        size="1024x1024",
    )

    b64 = response.data[0].b64_json
    if not b64:
        sys.exit("ERROR: response contained no b64_json data")

    print(f"Success — base64 length: {len(b64):,} chars")

    img_bytes = base64.b64decode(b64)
    OUTPUT_PATH.write_bytes(img_bytes)
    print(f"Saved PNG → {OUTPUT_PATH.resolve()}")


if __name__ == "__main__":
    main()

```

[23 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]