# Project export: Learn and Grow

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: Configurable, adaptive quiz generation with live tutor and multiplayer functionality
- Devpost: https://devpost.com/software/learn-and-grow
- GitHub: https://github.com/HarshithaS2023/berkeley_aihackathon
- Video: https://www.youtube.com/embed/C7oRIQco1MY?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 5 GitHub contributor(s) — Claude Sonnet 4.6 (18 commits), unknown (11 commits), srithanandra (10 commits), ninjacat272 (9 commits), Cursor (4 commits)

## Devpost submission (written by the team)

### Inspiration

Learn and Grow was inspired by the long hours students spend preparing for math tests—often repeating problems without knowing exactly where their understanding breaks down. We wanted to create a more responsive way to practice, with feedback that focuses on both the final answer and the student’s reasoning process.

### What it does

Learn and Grow creates personalized, adaptive quizzes from uploaded study materials or user-provided instructions. Users can configure the number of questions and starting difficulty, while the quiz automatically adjusts its difficulty based on their performance. Students can show their work using an embedded Excalidraw whiteboard or upload a photo of handwritten work. Our live AI tutor analyzes the work in progress and offers timely guidance through text or spoken feedback without immediately revealing the answer. After each session, users receive analytics covering accuracy, response time, difficulty progression, frequently missed concepts, common mistakes, strengths, and recommended next steps. Learn and Grow also includes a multiplayer mode that makes practicing more engaging and collaborative.

### How we built it

Frontend: React, TypeScript, Vite, Zustand, and Excalidraw Backend: Python and FastAPI Database and authentication: Supabase AI services: Claude Sonnet 4.6 and Deepgram Claude powers source analysis, question generation, whiteboard feedback, and grading. Deepgram converts tutor feedback into natural spoken audio. Zustand manages the active quiz lifecycle, while Supabase stores authenticated users’ completed-session analytics.

### Challenges we ran into

One of our biggest challenges was integrating multiple sponsor technologies into a single cohesive experience instead of treating them as disconnected features. We also worked to differentiate Learn and Grow from a standard AI quiz generator by focusing on adaptive difficulty, analysis of students’ reasoning, live tutoring, and multiplayer practice.##

### Accomplishments we're proud of

We’re especially proud of our live AI tutor, which analyzes whiteboard work while the student is solving a problem and provides targeted guidance through text and speech. We’re also proud of building an adaptive quiz engine, detailed learning analytics, and a multiplayer mode within the hackathon timeframe.

### What we learned

We learned how to collaborate effectively across frontend, backend, AI, and infrastructure responsibilities. We gained hands-on experience integrating AI APIs, building structured prompts and responses, managing shared application state, processing visual work, and embedding tools such as Excalidraw into a complete user experience.

### What's next

Next, we want to expand support beyond STEM subjects, improve grading verification and handwriting interpretation, and develop more sophisticated personalization based on a student’s long-term learning history.

## README (from the GitHub repository)

# Learn and Grow

Adaptive quiz app with AI-generated questions, whiteboard work analysis, spoken hints, session analytics, and optional multiplayer.

**Stack:** React + Vite (frontend), FastAPI + Claude (backend), Supabase (auth + saved sessions).

---

## Prerequisites

- **Node.js** 18+ and npm
- **Python** 3.10+
- API keys: [Anthropic](https://console.anthropic.com/), [Deepgram](https://console.deepgram.com/) (text-to-speech)
- Optional: [Supabase](https://supabase.com/) project (sign-in, analytics, competitions)

---

## 1. Clone and install frontend dependencies

```bash
git clone <repo-url>
cd quizcraft
npm install
```

---

## 2. Python virtual environment

Create and activate a venv in the project root. **Always use this venv for the backend** so dependencies match `requirements.txt`.

### Windows (PowerShell)

```powershell
python -m venv venv
.\venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
pip install -r requirements.txt
```

### macOS / Linux

```bash
python3 -m venv venv
source venv/bin/activate
python -m pip install --upgrade pip
pip install -r requirements.txt
```

---

## 3. Environment variables

Copy the example file and fill in your keys:

```bash
cp .env.example .env
```

Edit `.env` in the **project root**. Vite loads `VITE_*` variables from this file during `npm run dev`.

### Backend (required)

| Variable | Description |
|----------|-------------|
| `ANTHROPIC_API_KEY` | Claude API key |
| `ANTHROPIC_MODEL` | Model id (default: `claude-sonnet-4-6-20251001`) |
| `DEEPGRAM_API_KEY` | Deepgram key for `/speak` (spoken hints) |
| `DEEPGRAM_SPEAK_MODEL` | Optional; default `aura-2-asteria-en` |

### Frontend API routing (recommended for local dev)

With the Vite proxy, the app calls `/api/...` and Vite forwards to the backend on port 3001:

```env
VITE_API_BASE=/api
VITE_API_PROXY_TARGET=http://127.0.0.1:3001
```

Alternatively, call the backend directly (no proxy):

```env
VITE_API_BASE=http://127.0.0.1:3001
```

### Supabase (optional — sign-in, analytics, competitions)

```env
VITE_SUPABASE_URL=https://xxxxxxxxxxxx.supabase.co
VITE_SUPABASE_ANON_KEY=eyJ...
```

After creating a Supabase project:

1. Enable **Email** under Authentication → Providers.
2. In the SQL Editor, run [`supabase/schema.sql`](supabase/schema.sql) for a new project.
3. If tables already exist with open RLS, also run [`supabase/auth_migration.sql`](supabase/auth_migration.sql).

---

## 4. Run the app

Use **two terminals**, both from the project root.

### Terminal 1 — Backend (with venv activated)

**Windows:**

```powershell
.\venv\Scripts\Activate.ps1
.\venv\Scripts\python -m uvicorn claude_api:app --host 127.0.0.1 --port 3001 --reload
```

**macOS / Linux:**

```bash
source venv/bin/activate
python -m uvicorn claude_api:app --host 127.0.0.1 --port 3001 --reload
```

Verify the backend:

```bash
curl http://127.0.0.1:3001/health
```

You should see `"status": "ok"`. If `deepgramConfigured` is `false`, check `DEEPGRAM_API_KEY` in `.env` and restart the server.

> **Note:** `npm run backend` uses `python3`, which may not exist on Windows. Prefer the venv commands above.

### Terminal 2 — Frontend

```bash
npm run dev
```

Open the URL Vite prints (usually `http://localhost:5173`).

---

## 5. Production build

```bash
npm run build
npm run preview
```

The backend must still be running separately for API routes unless you deploy it elsewhere and set `VITE_API_BASE` to that URL at build time.

---

## Troubleshooting

| Issue | Fix |
|-------|-----|
| `ECONNREFUSED 127.0.0.1:3001` | Start the backend on port 3001 before using the app |
| `/speak` returns 503 | Set `DEEPGRAM_API_KEY` in `.env` and restart uvicorn |
| `python3` not found (Windows) | Use `.\venv\Scripts\python` instead |
| Module not found after `pip install` | Activate venv, then `pip install -r requirements.txt` |
| Analytics / login errors | Configure Supabase vars and run the SQL migrations |
| API calls fail in dev | Ensure `VITE_API_BASE=/api` and backend is on the proxy target port |

---

## Project layout

| Path | Purpose |
|------|---------|
| `src/` | React frontend |
| `claude_api.py` | FastAPI backend (Claude, Deepgram, question queue) |
| `requirements.txt` | Python dependencies |
| `.env` | Secrets and config (not committed) |
| `.env.example` | Template for required variables |
| `supabase/` | Database schema and migrations |


## Detected evidence (automated analysis)

Indexed codebase: 61 recognized source files, 303 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
- Python (language) — detected in the code
- React (technology) — detected in the code
- Redis (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
- AI coding agent: Claude Code — evidence: commit authorship or trailers
- AI coding agent: Cursor — evidence: commit authorship or trailers

## Codebase structure (from repository index)

### Files (69 of 69)

```
.env.example
.gitignore
.npmrc
claude_api.py
eslint.config.js
index.html
package.json
README.md
requirements.txt
src/App.css
src/App.tsx
src/components/Analytics/AnalyticsPage.css
src/components/Analytics/AnalyticsPage.tsx
src/components/Analytics/PastQuestionsPanel.tsx
src/components/Auth/LoginPage.css
src/components/Auth/LoginPage.tsx
src/components/Auth/ProtectedRoute.tsx
src/components/Competition/Competition.css
src/components/Competition/CompetitionQuiz.tsx
src/components/Competition/CompetitionResults.tsx
src/components/Competition/CompetitionSetup.tsx
src/components/Competition/RivalPanel.tsx
src/components/Competition/WaitingRoom.tsx
src/components/HomePage.css
src/components/HomePage.tsx
src/components/SummaryPage.css
src/components/SummaryPage.tsx
src/components/Tts/ReadAloudButton.tsx
src/components/Tts/Tts.css
src/components/Tts/TtsSpeedControl.tsx
src/components/Upload/Upload.css
src/components/Upload/WorkUpload.tsx
src/components/Whiteboard/Whiteboard.css
src/components/Whiteboard/Whiteboard.tsx
src/components/WorkPanel/WorkPanel.css
src/components/WorkPanel/WorkPanel.tsx
src/contexts/AuthContext.tsx
src/hooks/useLivePeek.ts
src/hooks/useQuestionTimer.ts
src/hooks/useTts.ts
src/index.css
src/lib/adaptiveDifficulty.ts
src/lib/analyticsInsights.ts
src/lib/apiBase.ts
src/lib/competition.ts
src/lib/fileUtils.ts
src/lib/supabase.ts
src/lib/ttsSpeechText.ts
src/lib/workSubmission.ts
src/main.tsx
src/services/analyticsApi.ts
src/services/quizApi.ts
src/services/sessionApi.ts
src/services/ttsApi.ts
src/store/competitionStore.ts
src/store/quizStore.ts
src/store/useStore.ts
src/types.ts
src/types/analytics.ts
supabase/add_competition_questions.sql
supabase/auth_migration.sql
supabase/competition_ready_migration.sql
supabase/competition_schema.sql
supabase/schema.sql
supabase/team_analytics_migration.sql
tsconfig.app.json
tsconfig.json
tsconfig.node.json
vite.config.ts
```

### Dependencies

- package.json: @eslint/js@^10.0.1, @excalidraw/excalidraw@^0.18.1, @supabase/supabase-js@^2.108.2, @tailwindcss/vite@^4.3.1, @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, react@^19.2.6, react-dom@^19.2.6, react-is@^19.2.7, react-router-dom@^7.18.0, recharts@^3.8.1, tailwindcss@^4.3.1, typescript@~6.0.2, typescript-eslint@^8.59.2, vite@^8.0.12, zustand@^5.0.14
- requirements.txt: anthropic, fastapi, httpx, pydantic, python-dotenv, redis, uvicorn

### Recent commits (newest first)

- updated the readme
- repositioned_lamb
- Fix analytics charts: accurate tooltips, subject grouping, and team sessions.
- Adaptive question format: detect format from uploaded material, support multiple choice UI
- Update HomePage styles
- Parallel question prefetch, adaptive question format detection, strict grading
- Strict grading: only mark correct when final answer matches, add partially_correct state
- fixed UI
- Fix competition feature: missing prefetchInFlight, whiteboardGraded, and parallelize question generation
- Merge branch 'pre_v6' into v6
- Merge pre_gen_comp into v6
- Fix voice blank screen crash and restore macOS backend script
- Merge remote-tracking branch 'origin/raghav_backup' into v6
- Add pre-generated competition test (pre_gen_comp)
- Add fair challenge scoring and rival waiting screen
- deepgram live feedback integration
- Fix competition lobby flow and multiplayer quiz start.
- fixed prompts
- fixed ui + side live feedback
- Add multiplayer competition feature

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

### requirements.txt

```
anthropic
fastapi
pydantic
python-dotenv
redis
uvicorn
httpx

```

### package.json

```
{
  "name": "berkeley_aihackathon",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "backend": "python3 -m uvicorn claude_api:app --host 127.0.0.1 --port 3001 --reload",
    "build": "tsc -b && vite build",
    "lint": "eslint .",
    "preview": "vite preview"
  },
  "dependencies": {
    "@excalidraw/excalidraw": "^0.18.1",
    "@supabase/supabase-js": "^2.108.2",
    "@tailwindcss/vite": "^4.3.1",
    "react": "^19.2.6",
    "react-dom": "^19.2.6",
    "react-is": "^19.2.7",
    "react-router-dom": "^7.18.0",
    "recharts": "^3.8.1",
    "tailwindcss": "^4.3.1",
    "zustand": "^5.0.14"
  },
  "devDependencies": {
    "@eslint/js": "^10.0.1",
    "@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"
  }
}

```

### src/main.tsx

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

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

```

### src/App.tsx

```typescript
import { Component, useEffect, useState } from 'react'
import type { ReactNode } from 'react'
import {
  BrowserRouter,
  Navigate,
  Route,
  Routes,
  useNavigate,
} from 'react-router-dom'
import './App.css'
import lambMascot from './assets/lamb-mascot.png'
import LoginPage from './components/Auth/LoginPage'
import { ProtectedRoute } from './components/Auth/ProtectedRoute'
import HomePage from './components/HomePage'
import AnalyticsPage from './components/Analytics/AnalyticsPage'
import SummaryPage from './components/SummaryPage'
import { ReadAloudButton } from './components/Tts/ReadAloudButton'
import { TtsSpeedControl } from './components/Tts/TtsSpeedControl'
import './components/Tts/Tts.css'
import { AuthProvider } from './contexts/AuthContext'
import { WorkPanel } from './components/WorkPanel/WorkPanel'
import { useQuestionTimer } from './hooks/useQuestionTimer'
import { useTts } from './hooks/useTts'
import { useQuizStore } from './store/quizStore'
import CompetitionSetup from './components/Competition/CompetitionSetup'
import WaitingRoom from './components/Competition/WaitingRoom'
import CompetitionQuiz from './components/Competition/CompetitionQuiz'
import CompetitionResults from './components/Competition/CompetitionResults'

class QuizErrorBoundary extends Component<
  { children: ReactNode },
  { error: Error | null }
> {
  state = { error: null }

  static getDerivedStateFromError(error: Error) {
    return { error }
  }

  render() {
    if (this.state.error) {
      return (
        <main className="quiz-status error-status">
          <img src={lambMascot} alt="" />
          <h2>Something went wrong</h2>
          <p>{(this.state.error as Error).message}</p>
          <button
            className="quiz-primary"
            type="button"
            onClick={() => this.setState({ error: null })}
          >
            Try again
          </button>
        </main>
      )
    }
    return this.props.children
  }
}

const formatTime = (seconds: number) =>
  `${String(Math.floor(seconds / 60)).padStart(2, '0')}:${String(
    seconds % 60,
  ).padStart(2, '0')}`

function StatusScreen({ message }: { message: string }) {
  return (
    <main className="quiz-status">
      <img src={lambMascot} alt="" />
      <div className="spinner" />
      <h2>{message}</h2>
      <p>Llamma is getting everything ready.</p>
    </main>
  )
}


function QuizScreen() {
  const [answerText, setAnswerText] = useState('')
  const navigate = useNavigate()

  const phase = useQuizStore((state) => state.phase)
  const settings = useQuizStore((state) => state.settings)
  const currentQuestion = useQuizStore((state) => state.currentQuestion)
  const currentDifficulty = useQuizStore((state) => state.currentDifficulty)
  const results = useQuizStore((state) => state.results)
  const elapsedSeconds = useQuizStore((state) => state.elapsedSeconds)
  const visibleHints = useQuizStore((state) => state.visibleHints)
  const hintsUsed = useQuizStore((state) => state.hintsUsed)
const revealHint = useQuizStore((state) => state.revealHint)
  const submitCurrentQuestion = useQuizStore((state) => state.submitCurrentQuestion)
  const continueQuiz = useQuizStore((state) => state.continueQuiz)

  const {
    speak,
    stop,
    unlockAudio,
    prefetch,
    isTextReady,
    speed,
    setSpeed,
    isSpeaking,
    isLoading,
    isAudioUnlocked,
    error: ttsError,
  } = useTts()

  useQuestionTimer()

  useEffect(() => {
    if (phase === 'summary') navigate('/summary')
    if (phase === 'setup') navigate('/')
    if (phase === 'error') navigate('/error')
  }, [phase, navigate])

  useEffect(() => {
    if (!currentQuestion || phase === 'feedback') return
    prefetch(currentQuestion.question)
    for (const hint of currentQuestion.hints) {
      prefetch(`Hint: ${hint}`)
    }
  }, [currentQuestion?.id, phase, prefetch, currentQuestion?.question, currentQuestion?.hints])

  useEffect(() => {
    if (phase !== 'feedback') return
    const latest = results.at(-1)?.feedback
    if (!latest) return
    const speech = `${latest.feedback} ${latest.suggestedNextStep}`.trim()
    if (speech) prefetch(speech)
  }, [phase, results, prefetch])

  if (phase === 'generating') {
    return <StatusScreen message="Growing your next question…" />
  }
  if (phase === 'submitting') {
    return <StatusScreen message="Analyzing your work…" />
  }
  if (!currentQuestion) {
    return <StatusScreen message="Preparing your quiz…" />
  }

  const latestFeedback = results.at(-1)?.feedback
  const isFeedback = phase === 'feedback' && latestFeedback
  const isLastQuestion = results.length >= settings.numQuestions
  const displayedQuestionNumber =
    phase === 'feedback' ? results.length : results.length + 1
  const safeQuestionNumber = Math.min(displayedQuestionNumber, settings.numQuestions)
  const progress = (safeQuestionNumber / settings.numQuestions) * 100

  const feedbackSpeech = latestFeedback
    ? `${latestFeedback.feedback} ${latestFeedback.suggestedNextStep}`.trim()
    : ''

  const handleShowHint = () => {
    const nextHint = currentQuestion.hints[hintsUsed]
    revealHint()
    if (nextHint) void speak(`Hint: ${nextHint}`)
  }

  return (
    <main className="quiz-page">
      <header className="quiz-nav">
        <button
          type="button"
          className="quiz-brand"
          aria-label="Learn and Grow home"
          onClick={() => navigate('/')}
        >
          <img src={lambMascot} alt="" />
          <span>
            <strong>Learn and Grow</strong>
            <small>Adaptive practice</small>
          </span>
        </button>

        <div className="quiz-nav-progress">
          <span>
            Question {safeQuestionNumber} of {settings.numQuestions}
          </span>
          <div>
            <i style={{ width: `${progress}%` }} />
          </div>
        </div>

        <div className="quiz-meta">
          <span className="difficulty">
            <i />
            Level {currentDifficul
[truncated — 8012 more characters]
```

### index.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/png" href="/src/assets/lamb-mascot.png" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Learn and Grow</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

```

### 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,
    },
  },
])

```

### vite.config.ts

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

const apiProxyTarget = process.env.VITE_API_PROXY_TARGET ?? 'http://127.0.0.1:3001'

export default defineConfig({
  plugins: [react(), tailwindcss()],
  optimizeDeps: {
    entries: ['index.html'],
    include: ['@excalidraw/excalidraw'],
  },
  server: {
    watch: {
      ignored: ['**/venv/**', '**/.venv/**', '**/node_modules/**'],
    },
    fs: {
      deny: ['venv', '.venv'],
    },
    proxy: {
      '/api': {
        target: apiProxyTarget,
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
      },
    },
  },
})

```

### supabase/competition_ready_migration.sql

```sql
-- Run in Supabase SQL editor if competition tables already exist without `ready`.
alter table competition_participants
  add column if not exists ready boolean not null default false;

```

### supabase/add_competition_questions.sql

```sql
-- Migration: add pre-generated questions to competition sessions
-- Run this in your Supabase SQL editor if you already ran competition_schema.sql

alter table competition_sessions
  add column if not exists questions jsonb not null default '[]'::jsonb;

```

### supabase/team_analytics_migration.sql

```sql
-- Optional: let any signed-in user read all quiz sessions for shared team analytics.
-- Run once in Supabase → SQL Editor. Existing per-user policies stay; these add OR access.

create policy "team read all sessions"
  on sessions for select
  to authenticated
  using (true);

create policy "team read all questions"
  on questions for select
  to authenticated
  using (true);

create policy "team read all mistakes"
  on mistakes for select
  to authenticated
  using (true);

```

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