# Project export: Minerva

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: TreeHacks 2026
- Tagline: Accessible tutoring for everyone
- Devpost: https://devpost.com/software/minerva-3sj6z0
- GitHub: https://github.com/anton-3/minerva
- Demo: https://www.evanyu.dev/blog/treehacks-2026
- Video: https://www.youtube.com/embed/vK0Tob8xZKE?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Result: winner ([Zoom] Education Track; [HeyGen] Best creation with HeyGen Avatar API (4 Airpod Pros 3, 1 year of HeyGen Creator license, guaranteed onsite interviews for FTE or intern roles))
- Team: 5 GitHub contributor(s) — Evan Yu (29 commits), Ular Kimsanov (15 commits), Anton Angeletti (10 commits), Claude Opus 4.6 (1 commits), mnkjoshi (1 commits)

## Devpost submission (written by the team)

### Overview

About the Project

### Inspiration

Public education in the United States is increasingly strained. Class sizes continue to grow, individualized attention is rare, and outcomes reflect this reality. Only about 22% of U.S. high school seniors meet expected proficiency in mathematics, and the country ranks outside the top 30 among developed nations. At the same time, private schools and private tutoring, solutions that do provide individualized learning, remain financially inaccessible for most families. As a tutor with eight years of experience, I’ve seen that students learn best in one-on-one environments that emphasize visualization, reinforcement through questioning, and clear analogies. This project was inspired by the question: can we scale high-quality, personalized education without scaling cost? Minerva is our attempt to answer that question. What We Built Minerva is an AI-powered virtual educator designed to provide structured, personalized, and non-judgmental one-on-one learning. She supports learners from elementary school through university and into workforce re-entry. Minerva can: Meet with you over zoom calls and act just as a real tutor would. Ingest curriculum, notes, or documentation and generate a personalized study plan Generate 3Blue1Brown style videos using Manim in realtime Visually explain concepts via: Integrated Desmos/Desmos 3D Integrated GeoGebra Generated mini-interactive applets Integrated Desmos/Desmos 3D Integrated GeoGebra Generated mini-interactive applets Track learning progress over time and adapt future lessons accordingly Reinforce concepts using questions, visual explanations, and analogies Adjust teaching style based on learner engagement and emotional cues Summarize progress and facilitate communication with parents or guardians The system is designed to prioritize teaching, not just answering questions, by maintaining continuity and adapting to the learner over time.

### How we built it

We used Next.js/React as our web framework, and connected HeyGen LiveAvatar to the Zoom Video SDK to display the avatar in the browser. We then used the Web Speech API to get realtime transcriptions of what the user says, and we feed this to a backend endpoint. Claude Haiku is then prompted with this to begin generating a response, which HeyGen then speaks out loud. Haiku is given a number of tools, including: Generate/display a Manim video This is done by delegating the task to a smarter model (Claude Opus) to generate and execute the python code. This is done by delegating the task to a smarter model (Claude Opus) to generate and execute the python code. Use tools in Desmos, Desmos3D, and GeoGebra Generate a HTML + JS demo

### Challenges we ran into

Ensuring low latency so everything flows smoothly in zoom calls. This was a big challenge. But by cutting down turn around time on many different systems we were able to achieve the consistent results we see now.

### Accomplishments we're proud of

Having a technically feasibly product that's usable by people right out of the box, no configuration required.

### What's next

We plan on working more on Minerva after the hackathon. Our next immediate goal is to drastically improve the latency between the model generating the output and the avatar speaking. Building further to handle at higher scale and start getting users to help revolutionize their growth.

## README (from the GitHub repository)

# Minerva

built at treehacks 2026

won 1st place zoom edu track + 1st place heygen

this public repo has been reset to the state it was in during treehacks. we're still building this in stealth, stay tuned!


## Detected evidence (automated analysis)

Indexed codebase: 144 recognized source files, 967 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- Google Gemini (technology) — detected in the code
- Next.js (technology) — detected in the code
- OpenAI (technology) — detected in the code
- PostgreSQL (technology) — detected in the code
- React (technology) — detected in the code
- SQL (language) — 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
- AI coding agent: Claude Code — evidence: config files committed to the repository; commit authorship or trailers

## Codebase structure (from repository index)

### Files (120 of 152)

```
.claude/commands/speckit.analyze.md
.claude/commands/speckit.checklist.md
.claude/commands/speckit.clarify.md
.claude/commands/speckit.constitution.md
.claude/commands/speckit.implement.md
.claude/commands/speckit.plan.md
.claude/commands/speckit.specify.md
.claude/commands/speckit.tasks.md
.claude/commands/speckit.taskstoissues.md
.gitignore
.specify/memory/constitution.md
.specify/scripts/bash/check-prerequisites.sh
.specify/scripts/bash/common.sh
.specify/scripts/bash/create-new-feature.sh
.specify/scripts/bash/setup-plan.sh
.specify/scripts/bash/update-agent-context.sh
.specify/templates/agent-file-template.md
.specify/templates/checklist-template.md
.specify/templates/constitution-template.md
.specify/templates/plan-template.md
.specify/templates/spec-template.md
.specify/templates/tasks-template.md
CLAUDE.md
components.json
drizzle.config.ts
drizzle/0000_cooing_kronos.sql
drizzle/meta/_journal.json
drizzle/meta/0000_snapshot.json
eslint.config.mjs
next.config.ts
package.json
plan.md
postcss.config.mjs
progress.md
README.md
speckit-video-tutor.txt
specs/001-minerva-mvp/contracts/avatar.md
specs/001-minerva-mvp/contracts/canvas.md
specs/001-minerva-mvp/contracts/knowledge.md
specs/001-minerva-mvp/contracts/recorder.md
specs/001-minerva-mvp/contracts/tutor-brain.md
specs/001-minerva-mvp/data-model.md
specs/001-minerva-mvp/plan.md
specs/001-minerva-mvp/spec.md
specs/001-minerva-mvp/tasks.md
src/app/api/children/route.ts
src/app/api/heygen/token/route.ts
src/app/api/learning-plans/route.ts
src/app/api/progress/route.ts
src/app/api/recall/bot/route.ts
src/app/api/recall/webhook/route.ts
src/app/api/search/route.ts
src/app/api/session/route.ts
src/app/api/session/summary/route.ts
src/app/api/tutor/plan/route.ts
src/app/api/tutor/respond/route.ts
src/app/api/zoom/token/route.ts
src/app/globals.css
src/app/layout.tsx
src/app/login/page.tsx
src/app/page.tsx
src/app/parent/children/page.tsx
src/app/parent/goals/page.tsx
src/app/parent/layout.tsx
src/app/parent/page.tsx
src/app/parent/progress/page.tsx
src/app/parent/sessions/page.tsx
src/app/student/page.tsx
src/app/student/session/page.tsx
src/components/landing/CanvasFeatures.tsx
src/components/landing/data.ts
src/components/landing/FAQ.tsx
src/components/landing/Footer.tsx
src/components/landing/Hero.tsx
src/components/landing/Navbar.tsx
src/components/landing/ParentPortal.tsx
src/components/landing/SocialProof.tsx
src/components/landing/SocraticEdge.tsx
src/components/parent/ChildCard.tsx
src/components/parent/GoalForm.tsx
src/components/parent/ProgressChart.tsx
src/components/parent/SessionSummaryCard.tsx
src/components/session/BottomControlBar.tsx
src/components/session/ChatSheet.tsx
src/components/session/ContentMode.tsx
src/components/session/Desmos3DPanel.tsx
src/components/session/DesmosPanel.tsx
src/components/session/DraggableAvatar.tsx
src/components/session/FloatingVideoOverlay.tsx
src/components/session/GeoGebraPanel.tsx
src/components/session/MathToolPanel.tsx
src/components/session/ModelPicker.tsx
src/components/session/ParticlesBackground.tsx
src/components/session/SandboxPanel.tsx
src/components/session/VideoPanel.tsx
src/components/shared/AvatarOverlay.tsx
src/components/shared/Header.tsx
src/components/shared/LoadingSpinner.tsx
src/components/ui/accordion.tsx
src/components/ui/avatar.tsx
src/components/ui/badge.tsx
src/components/ui/button.tsx
src/components/ui/card.tsx
src/components/ui/dialog.tsx
src/components/ui/dropdown-menu.tsx
src/components/ui/input.tsx
src/components/ui/label.tsx
src/components/ui/progress.tsx
src/components/ui/select.tsx
src/components/ui/separator.tsx
src/components/ui/sheet.tsx
src/components/ui/tabs.tsx
src/components/ui/textarea.tsx
src/components/ui/tooltip.tsx
src/db/index.ts
src/db/relations.ts
src/db/schema.ts
src/db/seed.ts
src/db/types.ts
src/hooks/useAvatar.ts
[32 more files omitted for size]
```

### Dependencies

- package.json: @ai-sdk/anthropic@^3.0.44, @ai-sdk/google@^3.0.29, @ai-sdk/openai@^3.0.29, @heygen/liveavatar-web-sdk@^0.0.10, @heygen/streaming-avatar@^2.1.0, @tailwindcss/postcss@^4, @types/desmos@^1.11.0, @types/jsonwebtoken@^9.0.10, @types/node@^20, @types/pg@^8.16.0, @types/react@^19, @types/react-dom@^19, @zoom/videosdk@^2.3.14, ai@^6.0.86, class-variance-authority@^0.7.1, clsx@^2.1.1, dotenv@^17.3.1, drizzle-kit@^0.31.9, drizzle-orm@^0.45.1, eslint@^9, eslint-config-next@16.1.6, jsonwebtoken@^9.0.3, lucide-react@^0.564.0, next@16.1.6, pg@^8.18.0, radix-ui@^1.4.3, react@19.2.3, react-dom@19.2.3, react-geogebra@^1.2.5, react-is@^19.2.3, react-rnd@^10.5.2, recharts@^3.7.0, shadcn@^3.8.4, tailwind-merge@^3.4.0, tailwindcss@^4, tsx@^4.21.0, tw-animate-css@^1.4.0, typescript@^5.9.3, zod@^4.3.6, zustand@^5.0.11

### Recent commits (newest first)

- readme update
- discourage video gen
- changes
- wave push button
- re add video to landing page
- feat: enhance session page with chat toggle functionality and update FloatingVideoOverlay dimensions
- we use all them models
- fix type err
- Merge branch '007-minerva-mvp' of https://github.com/anton-3/treehacks2026 into 001-minerva-mvp
- add gpt
- feat: add landing page components including Hero, Navbar, Footer, and various sections
- mute speech on space
- model switcher
- feat: add seed script to populate database with demo data
- refactor: enhance UI components and improve user experience across progress and session pages
- refactor: remove unused dependencies and SVG assets
- remove supabase
- Merge branch '001-minerva-mvp' of https://github.com/anton-3/treehacks2026 into 001-minerva-mvp
- fix: db migrations
- document reading

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

### CLAUDE.md

```markdown
# Minerva — AI Avatar Tutor (TreeHacks 2026)

## First Steps for Every Session

1. **Read the constitution**: `.specify/memory/constitution.md` — this defines all architecture principles and rules.
2. **Read progress**: `progress.md` — this tells you exactly what's been done and what's next.
3. **Read the plan**: `plan.md` — this has the full project context, links to all spec-kit docs, and the overall vision.
4. If working on a specific feature, read the relevant spec-kit docs in `specs/001-minerva-mvp/`.

## Session Continuity Rules

After completing every meaningful task or group of tasks:
- **Update `progress.md`** — mark tasks complete, note what was just built, update "Next Steps".
- **Update `plan.md`** if anything changes architecturally (new decisions, changed approach, new risks).
- These files are the handoff to the next session. Treat them as the source of truth for project state.

## Research Before Building

**MANDATORY**: Before implementing any major phase, feature, or integration:
1. **Search the web** for current best practices, examples, community tips (GitHub, Devpost, Stack Overflow, official docs).
2. **Verify library versions** — we are building in **February 2026**. Do NOT use outdated APIs or deprecated patterns.
3. **Check for breaking changes** — especially for HeyGen SDK, tldraw, Next.js 16, Supabase, and Zustand v5.
4. This applies to every AI agent (Claude Code, Cursor, GitHub Copilot, etc.) — not just Claude.

What to research:
- Official documentation for the specific library version we're using
- GitHub issues/discussions for common pitfalls
- Devpost projects using similar tech for inspiration
- Community examples and tutorials published in 2025-2026

## Architecture Principles (Black Box Design)

See full details in `.specify/memory/constitution.md`. The key rules:
- Every external dependency is wrapped in `src/lib/` — no SDK types leak out.
- Modules communicate only through typed interfaces defined in `src/types/`.
- Any module should be rewritable from scratch using only its interface.
- Canvas errors never break the tutoring session. Fail gracefully.
- One module = one person can build and maintain it.

## Tech Stack

- Next.js 16.1 LTS (App Router, React 19.2, TypeScript)
- Tailwind CSS v4 with @tailwindcss/postcss
- shadcn/ui components
- tldraw 4.3.1 (canvas)
- @heygen/streaming-avatar 2.1.0 (avatar)
- @anthropic-ai/sdk 0.74.0 (Claude tutor brain)
- Perplexity Sonar API (knowledge)
- Recall.ai REST API (recording)
- Supabase (auth + Postgres + realtime)
- Zustand 5.0.11 (state management)
- Deployed on Vercel

## Key Files

| File | Purpose |
|------|---------|
| `progress.md` | Living tracker of what's done and what's next |
| `plan.md` | Full project plan with links to all docs |
| `.specify/memory/constitution.md` | Architecture principles and rules |
| `specs/001-minerva-mvp/spec.md` | Feature specification (5 user stories) |
| `specs/001-minerva-mvp/plan.md` | Implementation plan (tech stack, contrac
[truncated — 329 more characters]
```

### plan.md

```markdown
# Minerva — Project Plan

> **Quick context for any AI agent**: Minerva is an AI avatar tutor for middle school students. A HeyGen avatar teaches via real-time conversation while using interactive math tools (Desmos 2D, Desmos 3D, GeoGebra). Claude powers the tutor brain (Socratic method). Parents set goals and track progress. Built for TreeHacks 2026 (36-hour hackathon, 4-person team).

## Full Vision & Strategy

See the detailed plan file with architecture diagrams, timeline, team split, prize strategy, demo script, and risk mitigations:

**[humble-discovering-sunrise.md](/Users/kimsanov/.claude/plans/humble-discovering-sunrise.md)**

This was the original planning document created during the first session. It contains the full context including:
- Problem/solution framing and competitive analysis
- Architecture diagram with data flow
- Detailed 36-hour timeline split by 4 people
- Zoom integration strategy (stretch goal)
- Prize strategy (targeting 10 tracks)
- Demo script (2.5 min)
- Risk mitigations
- Environment variables

## Spec-Kit Documentation

All formal specifications live in `specs/001-minerva-mvp/`:

| Document | What it covers |
|----------|---------------|
| [spec.md](specs/001-minerva-mvp/spec.md) | 5 user stories (P1-P5), acceptance scenarios, edge cases, 13 functional requirements, 6 key entities, 7 success criteria |
| [plan.md](specs/001-minerva-mvp/plan.md) | Tech stack, project structure, 5 black box module contracts (TypeScript interfaces), team ownership, execution phases |
| [tasks.md](specs/001-minerva-mvp/tasks.md) | 68 granular tasks across 8 phases, organized by user story, parallel markers, owner assignments |
| [data-model.md](specs/001-minerva-mvp/data-model.md) | 7 Supabase tables: profiles, children, learning_plans, sessions, session_summaries, progress, transcript_entries |
| [contracts/avatar.md](specs/001-minerva-mvp/contracts/avatar.md) | HeyGen SDK wrapper — AvatarClient interface |
| [contracts/canvas.md](specs/001-minerva-mvp/contracts/canvas.md) | Multi-tool wrapper — CanvasCommand type + CanvasExecutor interface (Desmos, Desmos 3D, GeoGebra) |
| [contracts/tutor-brain.md](specs/001-minerva-mvp/contracts/tutor-brain.md) | Claude wrapper — TutorBrain interface (respond, generateSummary, generateLearningPlan) |
| [contracts/knowledge.md](specs/001-minerva-mvp/contracts/knowledge.md) | Perplexity wrapper — KnowledgeLookup interface |
| [contracts/recorder.md](specs/001-minerva-mvp/contracts/recorder.md) | Recall.ai wrapper — SessionRecorder interface |

## Architecture (One-Liner)

Student speaks → HeyGen transcribes → Claude responds with speech + canvas commands → avatar speaks + math tools visualize (Desmos/GeoGebra) → progress saved to Supabase → parent dashboard shows real-time updates.

## Current Phase

See `progress.md` for the live status of what's been completed and what's next.

## Key Decisions Made

1. **Next.js full-stack** (no Python) — all APIs are REST/SDK calls from Node.js
2. **Multi-too
[truncated — 571 more characters]
```

### package.json

```
{
  "name": "treehacks2026",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint",
    "typecheck": "tsc --noEmit",
    "db:generate": "drizzle-kit generate",
    "db:migrate": "drizzle-kit migrate",
    "db:studio": "drizzle-kit studio",
    "check": "npm run lint && npm run typecheck"
  },
  "dependencies": {
    "@ai-sdk/anthropic": "^3.0.44",
    "@ai-sdk/google": "^3.0.29",
    "@ai-sdk/openai": "^3.0.29",
    "@heygen/liveavatar-web-sdk": "^0.0.10",
    "@heygen/streaming-avatar": "^2.1.0",
    "@types/jsonwebtoken": "^9.0.10",
    "@zoom/videosdk": "^2.3.14",
    "ai": "^6.0.86",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "dotenv": "^17.3.1",
    "drizzle-orm": "^0.45.1",
    "jsonwebtoken": "^9.0.3",
    "lucide-react": "^0.564.0",
    "next": "16.1.6",
    "pg": "^8.18.0",
    "radix-ui": "^1.4.3",
    "react": "19.2.3",
    "react-dom": "19.2.3",
    "react-geogebra": "^1.2.5",
    "react-is": "^19.2.3",
    "react-rnd": "^10.5.2",
    "recharts": "^3.7.0",
    "tailwind-merge": "^3.4.0",
    "zod": "^4.3.6",
    "zustand": "^5.0.11"
  },
  "overrides": {
    "react-is": "$react-is"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4",
    "@types/desmos": "^1.11.0",
    "@types/node": "^20",
    "@types/pg": "^8.16.0",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "drizzle-kit": "^0.31.9",
    "eslint": "^9",
    "eslint-config-next": "16.1.6",
    "shadcn": "^3.8.4",
    "tailwindcss": "^4",
    "tsx": "^4.21.0",
    "tw-animate-css": "^1.4.0",
    "typescript": "^5.9.3"
  },
  "packageManager": "pnpm@10.29.3+sha512.498e1fb4cca5aa06c1dcf2611e6fafc50972ffe7189998c409e90de74566444298ffe43e6cd2acdc775ba1aa7cc5e092a8b7054c811ba8c5770f84693d33d2dc"
}

```

### src/db/index.ts

```typescript
// Drizzle database client for Minerva
// Uses node-postgres (pg) driver with Supabase connection pooler

import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
import * as schema from "./schema";
import * as relations from "./relations";

// Connection string from environment (Supabase pooler URL)
const connectionString = process.env.DATABASE_URL!;

// Create pg pool
const pool = new Pool({
  connectionString,
  // For serverless: limit connections
  max: 10,
});

// Export typed Drizzle instance with schema for relational queries
export const db = drizzle(pool, {
  schema: { ...schema, ...relations },
});

// Re-export schema for convenience
export * from "./schema";

```

### src/app/layout.tsx

```typescript
import type { Metadata } from "next";
import { Geist, Geist_Mono, Space_Grotesk, DM_Sans, Outfit } from "next/font/google";
import "./globals.css";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

const spaceGrotesk = Space_Grotesk({
  variable: "--font-display",
  subsets: ["latin"],
});

const dmSans = DM_Sans({
  variable: "--font-dm-sans",
  subsets: ["latin"],
  weight: ["300", "400", "500", "600"],
});

const outfit = Outfit({
  variable: "--font-outfit",
  subsets: ["latin"],
  weight: ["300", "400", "500", "600", "700", "800"],
});

export const metadata: Metadata = {
  title: "Minerva AI | Lifelike Socratic Education 24/7",
  description:
    "Minerva provides individualized education via a lifelike AI avatar using real-time voice and the Socratic method.",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} ${spaceGrotesk.variable} ${dmSans.variable} ${outfit.variable} antialiased min-h-screen`}
      >
        {children}
      </body>
    </html>
  );
}

```

### src/app/page.tsx

```typescript
"use client";

import { useEffect, useRef } from "react";
import { Navbar } from "@/components/landing/Navbar";
import { Hero } from "@/components/landing/Hero";
import { SocraticEdge } from "@/components/landing/SocraticEdge";
import { CanvasFeatures } from "@/components/landing/CanvasFeatures";
import { SocialProof } from "@/components/landing/SocialProof";
import { ParentPortal } from "@/components/landing/ParentPortal";
import { FAQ } from "@/components/landing/FAQ";
import { Footer } from "@/components/landing/Footer";
import { AvatarOverlay } from "@/components/shared/AvatarOverlay";

export default function LandingPage() {
  const mainRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            entry.target.classList.add("animate-on-scroll-visible");
            observer.unobserve(entry.target);
          }
        });
      },
      { threshold: 0.05 }
    );

    const el = mainRef.current;
    if (el) {
      el.querySelectorAll("[data-aos]").forEach((node) => {
        node.classList.add("animate-on-scroll-hidden");
        observer.observe(node);
      });
    }

    return () => observer.disconnect();
  }, []);

  return (
    <div
      ref={mainRef}
      className="font-primary bg-neutral-background text-text-primary overflow-x-hidden"
    >
      <Navbar />
      <main>
        <Hero />
        <SocraticEdge />
        <CanvasFeatures />
        <SocialProof />
        <ParentPortal />
        <FAQ />
      </main>
      <Footer />
      
      {/* Avatar overlay - triggered by 'M' key */}
      <AvatarOverlay />
    </div>
  );
}

```

### src/app/student/page.tsx

```typescript
// Student Home — entry point for students
// Shows "Start Session" button and active learning plan if available.
// Owner: Person A (Session Architect)
// See: specs/001-minerva-mvp/tasks.md (T034)

"use client";

import Link from "next/link";
import { AvatarOverlay } from "@/components/shared/AvatarOverlay";

export default function StudentHome() {
  return (
    <div className="flex flex-col items-center justify-center min-h-screen bg-background p-8">
      <div className="max-w-md w-full space-y-8 text-center">
        <div className="space-y-2">
          <h1 className="text-3xl font-bold tracking-tight">
            Welcome to Minerva
          </h1>
          <p className="text-muted-foreground">
            Your AI tutor is ready to help you learn. Start a session to begin!
          </p>
        </div>

        <Link
          href="/student/session"
          className="inline-flex items-center justify-center w-full rounded-lg bg-primary px-8 py-4 text-lg font-semibold text-primary-foreground hover:bg-primary/90 transition-colors"
        >
          Start Session
        </Link>

        <p className="text-xs text-muted-foreground">
          Sessions are powered by AI. Your tutor adapts to your learning style.
        </p>
      </div>
      
      {/* Avatar overlay - triggered by 'M' key */}
      <AvatarOverlay />
    </div>
  );
}

```

### src/app/parent/layout.tsx

```typescript
// Parent layout — sidebar nav with active states and icons
"use client";

import Link from "next/link";
import { usePathname } from "next/navigation";
import {
  LayoutDashboard,
  Users,
  Target,
  TrendingUp,
  MessageSquare,
} from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { AvatarOverlay } from "@/components/shared/AvatarOverlay";

const navItems = [
  { href: "/parent", label: "Dashboard", icon: LayoutDashboard },
  { href: "/parent/children", label: "Children", icon: Users },
  { href: "/parent/goals", label: "Goals", icon: Target },
  { href: "/parent/progress", label: "Progress", icon: TrendingUp },
  { href: "/parent/sessions", label: "Sessions", icon: MessageSquare },
];

export default function ParentLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const pathname = usePathname();

  return (
    <div className="flex min-h-screen bg-neutral-background">
      {/* Sidebar */}
      <aside className="w-56 border-r border-border-light bg-neutral-surface p-4 flex flex-col">
        {/* Brand */}
        <Link href="/parent" className="flex items-center gap-2.5 px-3 mb-8">
          <div className="w-8 h-8 rounded-lg bg-brand-primary/15 flex items-center justify-center">
            <span className="text-brand-primary font-bold text-sm">M</span>
          </div>
          <span className="font-display text-lg font-bold text-text-primary">
            Minerva
          </span>
        </Link>

        {/* Navigation */}
        <nav className="flex flex-col gap-0.5">
          {navItems.map((item) => {
            const isActive =
              item.href === "/parent"
                ? pathname === "/parent"
                : pathname.startsWith(item.href);
            const Icon = item.icon;

            return (
              <Link
                key={item.href}
                href={item.href}
                className={`flex items-center gap-2.5 rounded-lg px-3 py-2 text-sm font-medium transition-colors ${
                  isActive
                    ? "bg-brand-primary/10 text-brand-primary font-semibold"
                    : "text-text-secondary hover:bg-brand-primary/5 hover:text-brand-primary"
                }`}
              >
                <Icon className="h-4 w-4" />
                {item.label}
              </Link>
            );
          })}
        </nav>

        {/* Footer */}
        <div className="mt-auto px-3 pt-4">
          <Badge variant="secondary" className="text-xs">
            Demo Mode
          </Badge>
        </div>
      </aside>

      {/* Main content */}
      <main className="flex-1 p-8 overflow-auto">{children}</main>
      
      {/* Avatar overlay - triggered by 'M' key */}
      <AvatarOverlay />
    </div>
  );
}

```

### src/app/login/page.tsx

```typescript
// Login page — PIN entry for students only (demo mode - no parent auth)
// Owner: Person D (Dashboard + Design)
// See: specs/001-minerva-mvp/tasks.md (T035)

"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";

type LoginMode = "parent" | "student";

export default function LoginPage() {
  const router = useRouter();
  const [mode, setMode] = useState<LoginMode>("parent");
  const [pin, setPin] = useState("");
  const [error, setError] = useState("");

  const handleStudentLogin = async (e: { preventDefault: () => void }) => {
    e.preventDefault();
    setError("");

    if (pin.length !== 4) {
      setError("PIN must be 4 digits");
      return;
    }

    // Navigate to student page with PIN in query
    router.push(`/student?pin=${pin}`);
  };

  return (
    <div className="flex items-center justify-center min-h-screen bg-background p-4">
      <div className="w-full max-w-sm space-y-6">
        <div className="text-center space-y-2">
          <h1 className="font-display text-2xl font-bold">Minerva</h1>
          <p className="text-text-secondary text-sm">
            AI-powered tutoring for every child
          </p>
        </div>

        {/* Mode toggle */}
        <div className="flex rounded-lg border border-border overflow-hidden">
          <button
            onClick={() => setMode("parent")}
            className={`flex-1 py-2 text-sm font-medium transition-colors ${
              mode === "parent"
                ? "bg-primary text-primary-foreground"
                : "bg-background text-muted-foreground hover:bg-muted"
            }`}
          >
            Parent
          </button>
          <button
            onClick={() => setMode("student")}
            className={`flex-1 py-2 text-sm font-medium transition-colors ${
              mode === "student"
                ? "bg-primary text-primary-foreground"
                : "bg-background text-muted-foreground hover:bg-muted"
            }`}
          >
            Student
          </button>
        </div>

        {/* Parent - direct link to dashboard (demo mode) */}
        {mode === "parent" && (
          <div className="space-y-4">
            <p className="text-sm text-muted-foreground text-center">
              Demo mode: No authentication required
            </p>
            <Link
              href="/parent"
              className="block w-full rounded-lg bg-primary py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 text-center"
            >
              Enter Parent Dashboard
            </Link>
          </div>
        )}

        {/* Student PIN entry */}
        {mode === "student" && (
          <form onSubmit={handleStudentLogin} className="space-y-4">
            <div className="space-y-2">
              <label htmlFor="pin" className="text-sm font-medium">
                Enter your PIN
              </label>
              <input
                id="pin"
                type="text"
                inputMode="numeric"
                pattern="[0-9]*"
                maxLength={4}
                value={pin}
                onChange={(e) => setPin(e.target.value.replace(/\D/g, ""))}
                className="w-full rounded-lg border border-input bg-background px-3 py-4 text-center text-2xl font-mono tracking-[0.5em] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-brand-primary"
                placeholder="0000"
              />
            </div>
            <button
              type="submit"
              className="w-full rounded-lg bg-primary py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
            >
              Enter
            </button>
          </form>
        )}

        {error && (
          <p className="text-sm text-red-500 text-center">{error}</p>
        )}
      </div>
    </div>
  );
}

```

### src/app/parent/page.tsx

```typescript
// Parent Dashboard — overview with stat cards, session spotlight, children, recent sessions
import Link from "next/link";
import { db, children, sessions } from "@/db";
import { inArray, desc } from "drizzle-orm";
import {
  Users,
  MessageSquare,
  Brain,
  CheckCircle2,
  ArrowUpCircle,
  Clock,
  ChevronRight,
} from "lucide-react";
import {
  Card,
  CardHeader,
  CardTitle,
  CardDescription,
  CardContent,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Progress } from "@/components/ui/progress";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import type { Session, SessionSummary } from "@/db/types";

interface SessionWithSummary extends Session {
  summary: SessionSummary | null;
}

export default async function ParentDashboard() {
  const childrenList = await db.select().from(children);
  const childIds = childrenList.map((c) => c.id);

  let recentSessions: SessionWithSummary[] = [];
  if (childIds.length > 0) {
    recentSessions = await db.query.sessions.findMany({
      where: inArray(sessions.childId, childIds),
      with: { summary: true },
      orderBy: [desc(sessions.startedAt)],
      limit: 5,
    });
  }

  // Compute stats
  const completedSessions = recentSessions.filter(
    (s) => s.status === "completed"
  );
  const sessionsWithScores = recentSessions.filter(
    (s) => s.summary?.comprehensionScore != null
  );
  const avgComprehension =
    sessionsWithScores.length > 0
      ? Math.round(
          (sessionsWithScores.reduce(
            (sum, s) => sum + (s.summary?.comprehensionScore ?? 0),
            0
          ) /
            sessionsWithScores.length) *
            100
        )
      : null;

  // Latest session with a summary
  const latestWithSummary = recentSessions.find((s) => s.summary);

  // Map childId → child name
  const childMap = new Map(childrenList.map((c) => [c.id, c]));

  return (
    <div className="space-y-8">
      {/* Header */}
      <div>
        <h1 className="font-display text-2xl font-bold text-text-primary">
          Dashboard
        </h1>
        <p className="text-text-secondary text-sm mt-1">
          Welcome back. Here&apos;s how your children are doing.
        </p>
      </div>

      {/* Stat Cards */}
      <div className="grid grid-cols-3 gap-4">
        <Card>
          <CardHeader className="pb-2">
            <CardDescription className="flex items-center gap-2 text-text-secondary">
              <Users className="h-4 w-4 text-brand-primary" />
              Children
            </CardDescription>
            <CardTitle className="text-3xl font-display text-text-primary">
              {childrenList.length}
            </CardTitle>
          </CardHeader>
          <CardContent>
            <p className="text-xs text-text-secondary">Active profiles</p>
          </CardContent>
        </Card>

        <Card>
          <CardHeader className="pb-2">
            <CardDescription className="flex items-center gap-2 text-text-secondary">
              <MessageSquare className="h-4 w-4 text-brand-secondary" />
              Sessions
            </CardDescription>
            <CardTitle className="text-3xl font-display text-brand-primary">
              {completedSessions.length}
            </CardTitle>
          </CardHeader>
          <CardContent>
            <p className="text-xs text-text-secondary">Completed</p>
          </CardContent>
        </Card>

        <Card>
          <CardHeader className="pb-2">
            <CardDescription className="flex items-center gap-2 text-text-secondary">
              <Brain className="h-4 w-4 text-brand-accent" />
              Comprehension
            </CardDescription>
            <CardTitle className="text-3xl font-display text-text-primary">
              {avgComprehension !== null ? `${avgComprehension}%` : "—"}
            </CardTitle>
          </CardHeader>
          <CardContent>
            <p className="text-xs text-text-secondary">Average across sessions</p>
          </CardContent>
        </Card>
      </div>

      {/* Latest Session Spotlight */}
      {latestWithSummary?.summary && (
        <Card className="border-brand-primary/20">
          <CardHeader>
            <div className="flex items-center justify-between">
              <div>
                <CardTitle className="font-display text-lg">
                  Latest Session
                </CardTitle>
                <CardDescription className="flex items-center gap-2 mt-1">
                  <Clock className="h-3.5 w-3.5" />
                  {latestWithSummary.startedAt
                    ? new Date(latestWithSummary.startedAt).toLocaleDateString(
                        "en-US",
                        { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }
                      )
                    : "Unknown"}{" "}
                  {childMap.get(latestWithSummary.childId)?.name &&
                    `· ${childMap.get(latestWithSummary.childId)?.name}`}
                </CardDescription>
              </div>
              <Badge variant="secondary">
                {latestWithSummary.status}
              </Badge>
            </div>
          </CardHeader>
          <CardContent className="space-y-4">
            <p className="text-sm text-text-secondary leading-relaxed">
              {latestWithSummary.summary.summary}
            </p>

            {/* Score bars */}
            <div className="grid grid-cols-2 gap-6">
              {latestWithSummary.summary.engagementScore != null && (
                <div>
                  <div className="flex justify-between text-sm mb-1.5">
                    <span className="text-text-secondary">Engagement</span>
                    <span className="font-medium text-text-primary">
                      {Math.round(latestWithSummary.summary.engagementScore * 100)}%
                    </span>
                  </div>
                  <Progress
                    value={lat
[truncated — 8295 more characters]
```

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