# Project export: LearnActively

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: OpenAI Build Week
- Tagline: LearnActively is an AI tutor that turns every conversation into active learning through interactive explanations, retrieval practice, and quizzes that help you truly understand and remember.
- Devpost: https://devpost.com/software/learnactively
- GitHub: https://github.com/sonyalow-smu611/LearnActively
- Video: https://www.youtube.com/embed/zzGtWu65A1Q?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 3 GitHub contributor(s) — Sonya low (7 commits), xuenlynn (2 commits), DarrennPoh (1 commits)

## Devpost submission (written by the team)

### Inspiration

We built LearnActively because most learning tools make it too easy to stay passive. A chatbot can explain something, but that does not mean you remember it. We wanted a tool that pushes people to think, practice, retrieve, and correct mistakes while they learn.

### What it does

LearnActively turns any topic into a structured learning workspace. It creates an intro, cheatsheet, concept roadmap, visual explanations, activities, flashcards, and quizzes. Instead of just giving an answer, it guides the learner through the topic and checks understanding along the way.

### How we built it

We built it with Next.js, React, TypeScript, Tailwind, and API routes. The AI layer uses separate agents for planning, explanations, activities, flashcards, quizzes, and feedback. We use Zod for structured outputs, Drizzle with Supabase Postgres for storage, ReactFlow for roadmaps, and Mermaid for diagrams.

### Challenges we ran into

The hardest part was making the experience feel active instead of like a normal chatbot. We also had to coordinate many generated artifacts, stream them into the UI, and keep the data structured enough to render reliably. Balancing speed, quality, and useful practice was a big challenge.

### Accomplishments we're proud of

We are proud that LearnActively feels like a real learning session, not just a wall of text. The roadmap, flashcards, activities, and quiz all work together. We are also proud of the agent-based architecture because it makes the product easier to extend.

### What we learned

We learned that good AI learning tools need more than explanations. They need structure, feedback, and moments where the learner has to do the work. We also learned how important schemas are when turning AI output into a stable interactive product.

### What's next

Next, we want to connect the agents to stronger live model generation, improve personalization, and add spaced repetition. We also want better progress tracking, richer diagrams, file uploads, and deeper feedback so LearnActively can become a long-term learning companion.

## README (from the GitHub repository)

# LearnActively

LearnActively turns a topic into an active-learning session. You pick a topic and a starting level, and the app generates an introduction, an overview diagram, a cheatsheet, a concept roadmap, an explanation and practice activity for each roadmap node, flashcards, and a quiz. A tutor chat sits alongside to help when you get stuck.

It is built to make you work. Every artifact asks you to retrieve, apply, and correct, not just read.

The product spec lives in [spec.md](spec.md). It is the source of truth for scope, data contracts, and agent behavior. [AGENT.md](AGENT.md) holds the working rules for coding agents. The tickets this version was built from, with their status and verification notes, are in [docs/tickets/](docs/tickets/).

## What the app does

### `/` Landing page

1. **Pick a topic.**
   - Search the catalog of 300 topics: 100 tech, plus science, math, business, economics, social science, philosophy, law, and accounting. Autocomplete shows up to 8 ranked matches.
   - Or browse by domain tab, grouped by subcategory.
   - Anything not in the catalog can be typed and started as a free-text topic.
2. **Pick a starting level.** Choose Total beginner, Intermediate, or Advanced / challenging. The level changes where the roadmap starts and how practice works:
   - Beginners start at foundations, with hints shown.
   - Intermediate learners get one review node, with hints on request.
   - Advanced learners skip foundations entirely and get a stretch goal.

The Archive sidebar lists past sessions. If no database is configured, the page falls back to free-text entry plus 12 example topics.

### `/learn/[sessionId]` Learning workspace

Each session has its own URL, so refresh, back, and sharing all work.

- **Left: Tutor chat.**
  - Knows your topic, level, roadmap progress, and which roadmap node you have open.
  - Socratic by default: it asks what you tried and gives the smallest useful hint.
  - Gives a full solution only if you explicitly ask after attempting.
- **Right: Workspace tabs.**
  - **Overview:** introduction, key terms, misconceptions, and a generated SVG diagram.
  - **Roadmap:** an interactive concept tree. Click a node for its explanation (with a diagram), a retrieval prompt, the activity, hints, the solution, and *Mark complete*.
  - **Cheatsheet:** a dense, print-style reference.
  - **Labs:** every node's activity in one place. Each activity includes starter code (for programming topics), a self-check list, level-appropriate hints, and a solution.
  - **Flashcards:** flip cards for retrieval practice.
  - **Quiz:** one question at a time, multiple choice and fill-in-the-blank only. Grading is exact: case, spacing, and punctuation are ignored, but "not a left join" is marked wrong. You end with a score, weak areas, and the roadmap nodes to revisit.

Artifacts stream in as they are generated: introduction and cheatsheet first, then the roadmap, then node explanations and activities (4 nodes at a time), then flashcards and quiz. If one fails, it retries once automatically. If it still fails, the tab shows **Couldn't generate this** with a **Retry** button that regenerates only what is missing. No placeholder content is ever shown in place of real output.

An unknown session URL shows a "Session not found" page with a link back to `/`.

### How practice adapts to the subject

The planner gives each session a **practice mode**. For catalog topics it comes from the topic's domain and kind; for free-text topics the model classifies it. The mode shapes the activities, flashcards, and quiz questions:

| Subject | Practice mode | Example activity |
|---|---|---|
| Languages, frameworks, tools | `code_lab` | Write or fix code from starter code |
| Computing and AI concepts | `design_trace` | Trace an algorithm or justify a design |
| Math | `worked_problem` | Multi-step problem or proof step |
| Accounting | `journal_calc` | Record journal entries |
| Science | `predict_explain` | Predict an outcome, then explain why |
| Economics, social science | `data_interpretation` | Draw a conclusion from described data |
| Law | `case_analysis` | Analyze a fact pattern using IRAC (issue, rule, application, conclusion) |
| Business | `scenario_decision` | Write a decision memo with tradeoffs |
| Philosophy | `argument_map` | Map premises, then raise and answer an objection |

## Setup

### Prerequisites

- Node.js 20 or newer (developed on Node 24)
- An OpenAI API key (**required**: the app does not generate anything without it)
- Optional: a Supabase project, for persistence and the topic catalog
- Optional: a Tavily API key, for web search on fast-changing topics

### 1. Install

```bash
npm install
```

### 2. Configure environment

Copy `.env.example` to `.env` (or `.env.local`) and fill it in:

```env
# Required
OPENAI_API_KEY=sk-...

# Optional: persistence + topic catalog. Use the Supabase *pooled* Postgres connection string
# (Project Settings → Database → Connection pooling, port 6543), not the REST API URL.
DATABASE_URL=postgresql://postgres.<project-ref>:<password>@aws-0-<region>.pooler.supabase.com:6543/postgres

# Optional: web search for volatile topics (frameworks, AI, law, etc.)
TAVILY_API_KEY=tvly-...

# Optional: model overrides (defaults shown)
OPENAI_MODEL_FAST=gpt-4.1-mini
OPENAI_MODEL_STRUCTURED=gpt-4.1
OPENAI_MODEL_REASONING=gpt-5

NEXT_PUBLIC_APP_NAME=LearnActively
NEXT_PUBLIC_APP_URL=http://localhost:3000
```

| Variable | Without it |
|---|---|
| `OPENAI_API_KEY` | Starting a session fails with "OPENAI_API_KEY is not configured" (HTTP 503). |
| `DATABASE_URL` | In-memory storage: sessions vanish on restart, and the landing page shows free text plus 12 examples instead of the catalog. |
| `TAVILY_API_KEY` | Volatile topics are generated without web sources. |

`NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY` are listed in `.env.example`, but the app does not use them yet. All database access goes through Drizzle and `DATABASE_URL`.

### 3. Set up the database (only if using `DATABASE_URL`)

```bash
npm run db:migrate
```

This creates the tables and seeds the 300-topic catalog. The seed uses `ON CONFLICT DO NOTHING`, so it is safe to re-run.

After the first seed, **Supabase is the source of truth for topics.** Add or edit them in the Supabase dashboard. CHECK constraints keep `domain`, `kind`, and `scope` valid, and the landing page picks up changes within an hour.

### 4. Run

```bash
npm run dev
```

Open http://localhost:3000.

## Scripts

| Command | What it does |
|---|---|
| `npm run dev` | Start the dev server |
| `npm run build` / `npm start` | Production build / serve it |
| `npm run lint` | ESLint |
| `npm run test` | Unit tests (Vitest). Model calls are mocked; no keys or database needed. |
| `npm run test:e2e` | Browser tests (Playwright, desktop + mobile Chrome). Starts a dev server on port 3002. |
| `npm run db:generate` | Generate a migration after editing `db/schema.ts` |
| `npm run db:migrate` | Apply migrations to `DATABASE_URL` |

Run the e2e suite with live services switched off, so it cannot write to your database or call OpenAI:

```bash
DATABASE_URL= OPENAI_API_KEY= TAVILY_API_KEY= npm run test:e2e
```

Playwright's browser needs a one-time `npx playwright install chromium`.

## How it works

```txt
Landing (/)                     → POST /api/learn { topic, depth, topicId? }  → session created ("planning")
Workspace (/learn/:id)          → GET  /api/learn/:id                          → stored state
  if planning                   → GET  /api/learn/:id/stream (SSE)             → claims the session, runs the orchestrator
  if generating (another tab)   → polls GET /api/learn/:id every 3s
Retry button                    → POST /api/learn/:id/retry                    → requeues; stream regenerates only missing/failed artifacts
Quiz submit                     → POST /api/learn/:id/quiz                     → deterministic grading
Tutor chat                      → POST 

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 89 recognized source files, 286 KB.
- CSS (language) — detected in the code
- JavaScript (language) — detected in the code
- Next.js (technology) — detected in the code
- OpenAI (technology) — detected in the code
- React (technology) — detected in the code
- SQL (language) — detected in the code
- Supabase (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Vercel AI SDK (technology) — detected in the code
- Node.js (technology) — claimed on Devpost, not found in the code
- PostgreSQL (technology) — claimed on Devpost, not found in the code
- Vercel (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (97 of 97)

```
.env.example
.eslintrc.json
.gitignore
AGENT.md
app/api/chat/route.ts
app/api/chat/summarize/route.ts
app/api/learn/[sessionId]/progress/route.ts
app/api/learn/[sessionId]/quiz/route.ts
app/api/learn/[sessionId]/route.ts
app/api/learn/[sessionId]/stream/route.ts
app/api/learn/archive/route.ts
app/api/learn/route.ts
app/globals.css
app/layout.tsx
app/page.tsx
components.json
components/app-shell/learn-shell.tsx
components/archive/archive-view.tsx
components/chat/chat-panel.tsx
components/chat/chatbot-widget.tsx
components/chat/topic-popup.tsx
components/learning/cheatsheet-card.tsx
components/learning/difficulty-badge.tsx
components/learning/intro-card.tsx
components/learning/mermaid-renderer.tsx
components/learning/status-panel.tsx
components/loading-ui/wandering-eyes.tsx
components/quiz/quiz-runner.tsx
components/roadmap/concept-tree.tsx
components/roadmap/node-detail-panel.tsx
components/ui/answer-box.tsx
components/ui/badge.tsx
components/ui/flipping-card.tsx
components/ui/progress.tsx
components/ui/styled-list.tsx
components/ui/topic-learning-progress.tsx
components/workspace/topic-history-tab.tsx
components/workspace/workspace-tabs.tsx
db/client.ts
db/migrations/0000_quick_wind_dancer.sql
db/migrations/0001_allow_overview_svg_artifacts.sql
db/migrations/meta/_journal.json
db/migrations/meta/0000_snapshot.json
db/repository.ts
db/schema.ts
drizzle.config.ts
lib/ai/agents/activity-agent.ts
lib/ai/agents/cheatsheet-agent.ts
lib/ai/agents/concept-tree-agent.ts
lib/ai/agents/feedback-agent.ts
lib/ai/agents/flashcard-agent.ts
lib/ai/agents/introduction-agent.ts
lib/ai/agents/node-explanation-agent.ts
lib/ai/agents/planner-agent.ts
lib/ai/agents/quiz-agent.ts
lib/ai/agents/visual-explainer-agent.ts
lib/ai/chatbot.ts
lib/ai/generate.ts
lib/ai/orchestrator.ts
lib/ai/prompts.ts
lib/ai/schemas/activity.ts
lib/ai/schemas/api.ts
lib/ai/schemas/cheatsheet.ts
lib/ai/schemas/common.ts
lib/ai/schemas/concept-tree.ts
lib/ai/schemas/flashcards.ts
lib/ai/schemas/introduction.ts
lib/ai/schemas/learning-plan.ts
lib/ai/schemas/node-explanation.ts
lib/ai/schemas/overview-svg.ts
lib/ai/schemas/quiz.ts
lib/ai/schemas/stream-events.ts
lib/ai/search-policy.ts
lib/ai/topic-fallbacks.ts
lib/chat-history.ts
lib/tavily/client.ts
lib/tavily/search.ts
lib/topic-history.ts
lib/utils/cn.ts
lib/utils/env.ts
lib/utils/ids.ts
lib/utils/topic-progress.ts
next-env.d.ts
next.config.ts
package.json
playwright.config.ts
postcss.config.js
PRD.md
project_summary.md
README.md
tailwind.config.ts
tests/cheatsheet-card.test.ts
tests/e2e/learnactively-user-flow.spec.ts
tests/schemas.test.ts
tests/topic-progress.test.ts
tsconfig.json
vitest.config.ts
```

### Dependencies

- package.json: @ai-sdk/openai@^1.3.24, @playwright/test@^1.61.1, @supabase/ssr@^0.12.3, @supabase/supabase-js@^2.110.5, @types/node@^22.10.2, @types/react@^19.0.2, @types/react-dom@^19.0.2, @xyflow/react@^12.8.2, ai@^4.3.19, autoprefixer@^10.4.20, class-variance-authority@^0.7.1, clsx@^2.1.1, drizzle-kit@^0.31.1, drizzle-orm@^0.44.2, eslint@^8.57.1, eslint-config-next@15.3.4, framer-motion@^12.23.0, html2canvas@^1.4.1, jspdf@^4.2.1, katex@^0.16.22, lucide-react@^0.468.0, mermaid@^11.6.0, motion@^12.42.2, next@15.3.4, postcss@^8.5.6, postgres@^3.4.7, react@^19.0.0, react-dom@^19.0.0, tailwind-merge@^2.6.0, tailwindcss@^3.4.17, typescript@^5.7.2, vitest@^2.1.8, zod@^3.25.67

### Recent commits (newest first)

- Initialize README with project overview and instructions
- merging rebase
- Merge pull request #1 from sonyalow-smu611/sonya
- Merge branch 'main' into sonya
- navbar, library
- sonya
- navbar
- progress bar
- flashcards, chatbot,loading
- init commit

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

### project_summary.md

```markdown
Here’s the project architecture and how to run it.

**Big Picture**
LearnActively is a Next.js 15 app with:

- Frontend: topic popup, split-pane workspace, tabs, roadmap, quiz UI.
- Backend: Next.js API routes.
- AI layer: planner + artifact agents + orchestrator.
- Data layer: Drizzle schema targeting Supabase Postgres.
- Validation: Zod schemas shared across backend and frontend-facing data contracts.

Current generation uses deterministic structured fallback agents, so the app works locally even before real OpenAI model calls are added.

**Frontend**
[app/page.tsx](/Users/aceslow/Downloads/personal-vibecoding/openai_hackathon/app/page.tsx)
Entry page. Renders `LearnShell`.

[app/layout.tsx](/Users/aceslow/Downloads/personal-vibecoding/openai_hackathon/app/layout.tsx)
Root HTML shell and metadata.

[app/globals.css](/Users/aceslow/Downloads/personal-vibecoding/openai_hackathon/app/globals.css)
Global Tailwind styles, cream/orange app feel, focus styles, and ReactFlow node styling.

[components/app-shell/learn-shell.tsx](/Users/aceslow/Downloads/personal-vibecoding/openai_hackathon/components/app-shell/learn-shell.tsx)
Main client controller. Handles:
- topic submission
- creating a session via `POST /api/learn`
- opening the SSE stream
- merging streamed artifacts into UI state
- refreshing persisted artifacts after generation
- marking roadmap nodes complete

[components/chat/topic-popup.tsx](/Users/aceslow/Downloads/personal-vibecoding/openai_hackathon/components/chat/topic-popup.tsx)
First interaction popup: topic input, depth selector, examples, start button.

[components/chat/chat-panel.tsx](/Users/aceslow/Downloads/personal-vibecoding/openai_hackathon/components/chat/chat-panel.tsx)
Left pane. Shows topic and generation/status messages.

[components/workspace/workspace-tabs.tsx](/Users/aceslow/Downloads/personal-vibecoding/openai_hackathon/components/workspace/workspace-tabs.tsx)
Right pane tab system. Routes artifacts into:
- Overview
- Roadmap
- Cheatsheet
- Labs
- Flashcards
- Quiz

[components/roadmap/concept-tree.tsx](/Users/aceslow/Downloads/personal-vibecoding/openai_hackathon/components/roadmap/concept-tree.tsx)
ReactFlow roadmap renderer.

[components/roadmap/node-detail-panel.tsx](/Users/aceslow/Downloads/personal-vibecoding/openai_hackathon/components/roadmap/node-detail-panel.tsx)
Selected roadmap node details: explanation, diagram source, activity, completion button.

[components/quiz/quiz-runner.tsx](/Users/aceslow/Downloads/personal-vibecoding/openai_hackathon/components/quiz/quiz-runner.tsx)
One-question-at-a-time quiz UI and submission.

[components/learning/*](/Users/aceslow/Downloads/personal-vibecoding/openai_hackathon/components/learning)
Reusable artifact renderers:
- `intro-card.tsx`
- `cheatsheet-card.tsx`
- `difficulty-badge.tsx`
- `status-panel.tsx`
- `mermaid-renderer.tsx`

**Backend API**
[app/api/learn/route.ts](/Users/aceslow/Downloads/personal-vibecoding/openai_hackathon/app/api/learn/route.ts)
`POST /api/le
[truncated — 4490 more characters]
```

### AGENT.md

```markdown
Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed.

Tradeoff: These guidelines bias toward caution over speed. For trivial tasks, use judgment.

1. Think Before Coding
Don't assume. Don't hide confusion. Surface tradeoffs.

Before implementing:

State your assumptions explicitly. If uncertain, ask.
If multiple interpretations exist, present them - don't pick silently.
If a simpler approach exists, say so. Push back when warranted.
If something is unclear, stop. Name what's confusing. Ask.
2. Simplicity First
Minimum code that solves the problem. Nothing speculative.

No features beyond what was asked.
No abstractions for single-use code.
No "flexibility" or "configurability" that wasn't requested.
No error handling for impossible scenarios.
If you write 200 lines and it could be 50, rewrite it.
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.

3. Surgical Changes
Touch only what you must. Clean up only your own mess.

When editing existing code:

Don't "improve" adjacent code, comments, or formatting.
Don't refactor things that aren't broken.
Match existing style, even if you'd do it differently.
If you notice unrelated dead code, mention it - don't delete it.
When your changes create orphans:

Remove imports/variables/functions that YOUR changes made unused.
Don't remove pre-existing dead code unless asked.
The test: Every changed line should trace directly to the user's request.

4. Goal-Driven Execution
Define success criteria. Loop until verified.

Transform tasks into verifiable goals:

"Add validation" → "Write tests for invalid inputs, then make them pass"
"Fix the bug" → "Write a test that reproduces it, then make it pass"
"Refactor X" → "Ensure tests pass before and after"
For multi-step tasks, state a brief plan:

1. [Step] → verify: [check]
2. [Step] → verify: [check]
3. [Step] → verify: [check]
Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.

5. Include comments throughout your code to explain the purpose of each function or variable. This will help others understand your reasoning and make it easier for them to review your code.


These guidelines are working if: fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes.

## Project Purpose

This project is **LearnActively**, a Next.js-based active-learning AI workspace hosted on Vercel.

LearnActively turns a user's learning goal into structured learning artifacts:

- Introduction
- Cheatsheet
- Interactive concept tree
- Visual node explanations
- Hands-on activities/labs
- Flashcards
- Quiz with immediate feedback and final weakness summary

The product should not behave like a passive chatbot. It should force active recall, application, and correction so the user remembers what they learn.

## Confirmed Stack

Use:

- Nex
[truncated — 12224 more characters]
```

### package.json

```
{
  "name": "learnactively",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "test": "vitest run",
    "test:e2e": "playwright test",
    "db:generate": "drizzle-kit generate",
    "db:migrate": "drizzle-kit migrate"
  },
  "dependencies": {
    "@ai-sdk/openai": "^1.3.24",
    "@supabase/ssr": "^0.12.3",
    "@supabase/supabase-js": "^2.110.5",
    "@xyflow/react": "^12.8.2",
    "ai": "^4.3.19",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "drizzle-orm": "^0.44.2",
    "framer-motion": "^12.23.0",
    "html2canvas": "^1.4.1",
    "jspdf": "^4.2.1",
    "katex": "^0.16.22",
    "lucide-react": "^0.468.0",
    "mermaid": "^11.6.0",
    "motion": "^12.42.2",
    "next": "15.3.4",
    "postgres": "^3.4.7",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "tailwind-merge": "^2.6.0",
    "zod": "^3.25.67"
  },
  "devDependencies": {
    "@playwright/test": "^1.61.1",
    "@types/node": "^22.10.2",
    "@types/react": "^19.0.2",
    "@types/react-dom": "^19.0.2",
    "autoprefixer": "^10.4.20",
    "drizzle-kit": "^0.31.1",
    "eslint": "^8.57.1",
    "eslint-config-next": "15.3.4",
    "postcss": "^8.5.6",
    "tailwindcss": "^3.4.17",
    "typescript": "^5.7.2",
    "vitest": "^2.1.8"
  }
}

```

### app/page.tsx

```typescript
import { LearnShell } from "@/components/app-shell/learn-shell";

export default function Home() {
  return <LearnShell />;
}

```

### app/layout.tsx

```typescript
import type { Metadata } from "next";
import { Fraunces, Inter } from "next/font/google";
import "./globals.css";

export const metadata: Metadata = {
  title: "LearnActively",
  description: "Active-learning AI workspace",
};

const inter = Inter({
  subsets: ["latin"],
  variable: "--font-inter",
  display: "swap",
});

const fraunces = Fraunces({
  subsets: ["latin"],
  variable: "--font-fraunces",
  display: "swap",
});

export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
  return (
    <html lang="en" className={`${inter.variable} ${fraunces.variable}`}>
      <body>{children}</body>
    </html>
  );
}

```

### app/api/learn/route.ts

```typescript
import { NextResponse } from "next/server";
import { createLearningSession } from "@/db/repository";
import { createLearningSessionRequestSchema } from "@/lib/ai/schemas/api";

export async function POST(request: Request) {
  try {
    const parsed = createLearningSessionRequestSchema.safeParse(await request.json());
    if (!parsed.success) {
      return NextResponse.json({ error: "Invalid learning session request", issues: parsed.error.flatten() }, { status: 400 });
    }

    const session = await createLearningSession(parsed.data);
    return NextResponse.json({ sessionId: session.id, status: session.status });
  } catch (error) {
    const message = error instanceof Error ? error.message : "Unable to create learning session";
    console.error("Failed to create learning session", error);
    return NextResponse.json({ error: "Unable to create learning session", detail: message }, { status: 500 });
  }
}

```

### app/api/chat/route.ts

```typescript
import { NextResponse } from "next/server";
import { z } from "zod";
import { generateChatbotReply, hasChatbotApiKey } from "@/lib/ai/chatbot";

const chatMessageSchema = z.object({
  role: z.enum(["assistant", "user"]),
  content: z.string().trim().min(1).max(4000),
});

const chatRequestSchema = z.object({
  messages: z.array(chatMessageSchema).min(1).max(20),
});

export async function POST(request: Request) {
  try {
    if (!hasChatbotApiKey()) {
      return NextResponse.json(
        { error: "OPENAI_API_KEY is not configured" },
        { status: 503 },
      );
    }

    const parsed = chatRequestSchema.safeParse(await request.json());
    if (!parsed.success) {
      return NextResponse.json(
        { error: "Invalid chat request", issues: parsed.error.flatten() },
        { status: 400 },
      );
    }

    const reply = await generateChatbotReply(parsed.data.messages);
    return NextResponse.json({ message: { role: "assistant", content: reply } });
  } catch (error) {
    const message = error instanceof Error ? error.message : "Unable to generate chat response";
    console.error("Failed to generate chat response", error);
    return NextResponse.json(
      { error: "Unable to generate chat response", detail: message },
      { status: 500 },
    );
  }
}

```

### app/api/learn/archive/route.ts

```typescript
import { NextResponse } from "next/server";
import { listArchivedLearningSessions } from "@/db/repository";

export async function GET() {
  try {
    const sessions = await listArchivedLearningSessions();
    return NextResponse.json({ sessions });
  } catch (error) {
    const message = error instanceof Error ? error.message : "Unable to load archive";
    console.error("Failed to load learning archive", error);
    return NextResponse.json({ error: "Unable to load archive", detail: message }, { status: 500 });
  }
}

```

### app/api/learn/[sessionId]/route.ts

```typescript
import { NextResponse } from "next/server";
import { getLearningSession, listArtifacts, listNodeProgress } from "@/db/repository";

export async function GET(_: Request, context: { params: Promise<{ sessionId: string }> }) {
  const { sessionId } = await context.params;
  const session = await getLearningSession(sessionId);
  if (!session) {
    return NextResponse.json({ error: "Learning session not found" }, { status: 404 });
  }

  const [artifacts, nodeProgress] = await Promise.all([
    listArtifacts(sessionId),
    listNodeProgress(sessionId),
  ]);

  return NextResponse.json({ session, artifacts, nodeProgress });
}

```

### app/api/chat/summarize/route.ts

```typescript
import { NextResponse } from "next/server";
import { z } from "zod";
import { generateText } from "ai";
import { createOpenAI } from "@ai-sdk/openai";
import { env } from "@/lib/utils/env";

const chatMessageSchema = z.object({
  role: z.enum(["assistant", "user"]),
  content: z.string().trim().min(1),
});

const summarizeRequestSchema = z.object({
  messages: z.array(chatMessageSchema).min(1).max(200),
});

const openai = createOpenAI({
  apiKey: env.OPENAI_API_KEY?.trim(),
  compatibility: "strict",
});

function hasOpenAiKey() {
  return Boolean(env.OPENAI_API_KEY?.trim());
}

export async function POST(request: Request) {
  try {
    if (!hasOpenAiKey()) {
      return NextResponse.json(
        { error: "OPENAI_API_KEY is not configured" },
        { status: 503 },
      );
    }

    const parsed = summarizeRequestSchema.safeParse(await request.json());
    if (!parsed.success) {
      return NextResponse.json(
        { error: "Invalid summarize request", issues: parsed.error.flatten() },
        { status: 400 },
      );
    }

    const messages = parsed.data.messages;

    // Build a concise conversation context
    const conversationText = messages
      .map((msg) => `${msg.role === "user" ? "User" : "Assistant"}: ${msg.content}`)
      .join("\n");

    const result = await generateText({
      model: openai(env.OPENAI_MODEL_FAST),
      system:
        "You are summarizing a user's learning session conversation. Create a brief, 1-2 sentence summary that captures the main topic or learning goal discussed. Be concise and actionable.",
      prompt: `Summarize this conversation in 1-2 sentences:\n\n${conversationText}`,
      temperature: 0.7,
      maxTokens: 80,
    });

    const summary = result.text.trim() || "Learning session completed";

    return NextResponse.json({ summary });
  } catch (error) {
    const message = error instanceof Error ? error.message : "Unable to generate summary";
    console.error("Failed to generate chat summary", error);
    return NextResponse.json(
      { error: "Unable to generate chat summary", detail: message },
      { status: 500 },
    );
  }
}

```

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