# Project export: Switch

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: Transforming reading from memorizing words to understanding them
- Devpost: https://devpost.com/software/switch-lw7n4j
- GitHub: https://github.com/onwaneri/reading-switch
- Video: https://www.youtube.com/embed/IgmPj-I9oh8?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 4 GitHub contributor(s) — Claude Opus 4.6 (12 commits), Patrick (7 commits), heldanna (6 commits), masonsalma (4 commits)

## Devpost submission (written by the team)

### Inspiration

Many students, especially those with dyslexia or reading challenges, are taught to memorize words instead of understanding how they are built. Structured Word Inquiry teaches students to analyze words scientifically by breaking them into bases, prefixes, and suffixes, but these tools are currently manual, slow, and difficult to scale. We wanted to make SWI interactive, visual, and accessible so that students can explore language in a more intuitive and engaging way.

### What it does

Our software allows a user to upload a book or text and click any word to explore it in depth. When a word is selected, the system generates a word sum, identifies the base and affixes, and displays a word family matrix. The user can also view definitions, listen to the pronunciation of the word, hear the word broken down into syllables, and see a simple visual icon representing the concept. In some cases, the user can also access an AI-generated explanation of the word’s history or etymology. The goal is to turn reading into an interactive learning experience that builds real linguistic understanding, particularly for students who may otherwise feel more averse to reading.

### How we built it

The frontend was built using Next.js and React to create an interactive reading interface where individual words can be selected and analyzed dynamically. The backend was written in Python, and Claude API to extract base morphemes, generate word matrices, verify derived words, and produce etymology explanations. Additional features such as text-to-speech for pronunciation and syllable breakdown were integrated using OpenAI's API, and we used the Noun Project API to source the images that accompany the definitions and provide a simple visual representation of each word.

### Challenges we ran into

One of the biggest challenges we had was integrating the front- and backend, since we all split up our work based on our strengths, and then had to merge all of our separate work into our cohesive final product. The time constraint was another challenge, since there were numerous features we would have loved to incorporate before the deadline, but we do plan to implement them in the future. We also encountered difficulty with parsing PDFs into clean text and when designing word matrices that display clearly and dynamically.

### Accomplishments we're proud of

We are proud that we built a working Structured Word Inquiry matrix generator and a click-to-analyze reading experience. We successfully implemented an AI-driven morphology pipeline that produces structured linguistic analysis in real time. We also built a robust backend with caching to improve performance and integrated pronunciation and visual aids to make the learning experience more accessible and engaging.

### What we learned

Through this project, we learned that linguistics and morphology are more complex than they initially appear. We also discovered how important prompt engineering is when structured outputs are required. Designing tools for education requires clarity, reliability, and thoughtful constraints to ensure that students receive accurate and meaningful information. We also gained experience coordinating a full-stack system that integrates frontend interaction with backend AI services.

### What's next

In the future, we would like to add a student reading analytics dashboard that helps teachers track progress and identify areas of difficulty. We also plan to build a classroom mode designed specifically for educators and to develop a prebuilt matrix library for commonly taught curriculum words. We have also considered adding gamified learning exercises, a mobile version for classroom use, and the possibility of combining AI with curated linguistic databases to improve reliability and speed. Why this matters: Reading struggles affect millions of students, particularly those with dyslexia. Tools that explain how language works, rather than relying on memorization, can dramatically improve literacy outcomes. By making Structured Word Inquiry interactive and scalable, we hope to make deeper language understanding accessible to many more learners.

## README (from the GitHub repository)

# Reading SWItch

An interactive picture book app that uses Structured Word Inquiry (SWI) to help kids learn to read. Upload a PDF of a picture book, tap any word, and see its morphological breakdown — prefixes, bases, suffixes, etymology, and word families.

Built at TreeHacks 2025.

## Setup

**Prerequisites:**
- Node.js 18+
- Python 3

```bash
# Install Node.js dependencies
npm install

# Install Python dependencies
pip install -r requirements.txt
```

Create `.env.local` with your Anthropic API key:

```
ANTHROPIC_API_KEY=sk-ant-...
```

Initialize data directories:

```bash
mkdir -p data/users data/recommended
echo "[]" > data/users/users.json
```

## Running

```bash
npm run dev
```

Open http://localhost:3000, create an account, and start reading!

## Adding Recommended Books

1. Place PDF files in the `data/recommended/` folder
2. Visit the library page (PDFs auto-process on first load)

That's it! PDFs are automatically processed when you first access the library. Pages render on-demand for instant display. Manual processing is also available with `npm run setup-recommended`.

## How It Works

1. **Authentication** — File-based user accounts with bcrypt password hashing and HTTP-only cookie sessions
2. **Library** — Two collections: "My Library" (user's personal books) and "Recommended Library" (pre-processed PDFs)
3. **Recommended Processing** — PDFs in `data/recommended/` auto-process when library page loads:
   - Copy PDF to book directory
   - Extract word bounding boxes using `pdfminer.six`
   - Create book metadata (runs once, cached after)
4. **Upload** — Users can add their own PDFs via the web interface:
   - Server saves PDF and extracts word positions
   - Book is added to user's library
5. **Read** — Reader renders PDF pages on-demand using `pdfjs-dist` with invisible, clickable word overlays
6. **Analyze** — Tapping a word calls `/api/analyze` which uses Claude AI (Anthropic API) to return an SWI breakdown (word sum, morphemes, etymology, word family)
7. **Display** — Right-side panel shows color-coded morpheme chips and analysis at the selected depth level

## Project Structure

```
src/
├── app/
│   ├── page.tsx                     # Login / registration page
│   ├── library/page.tsx             # Library with My Library and Recommended carousels
│   ├── upload/page.tsx              # PDF upload page
│   ├── reader/page.tsx              # Book reader with SWI panel
│   └── api/
│       ├── auth/                    # Authentication endpoints (login, register, logout, session)
│       ├── library/route.ts         # User library management
│       ├── recommended/route.ts     # Recommended books with auto-processing
│       ├── analyze/route.ts         # SWI word analysis via Claude AI
│       └── upload/route.ts          # PDF processing and storage
├── components/
│   ├── DropZone.tsx                 # Drag-and-drop file upload
│   ├── BookCard.tsx                 # Book thumbnail card
│   ├── BookCarousel.tsx             # Horizontal scrolling carousel
│   ├── BookPage.tsx                 # Page image + word overlays
│   ├── WordOverlay.tsx              # Clickable word button
│   ├── SWIPanel.tsx                 # Right-side analysis panel
│   ├── DepthSelector.tsx            # Analysis depth toggle
│   └── PageSearch.tsx               # In-book page search
├── contexts/
│   └── AuthContext.tsx              # Authentication state provider
├── lib/
│   ├── userManager.ts               # User CRUD operations
│   ├── sessionManager.ts            # Session management
│   ├── bookManager.ts               # Book metadata and auto-processing
│   └── wordCache.ts                 # In-memory SWI analysis cache
└── types/
    ├── auth.ts                      # User and Session types
    └── book.ts                      # Book, WordPosition, SWIAnalysis types

scripts/
├── extract_words.py                 # Extract word bounding boxes from PDF
└── setup-recommended.mjs            # One-time script to process recommended PDFs

data/
├── users/
│   └── users.json                   # User accounts
└── recommended/
    ├── index.json                   # Processed recommended books index
    └── *.pdf                        # Source PDFs (user-populated)
```

## Key Integration Points

**SWI Analysis Logic** — Replace the mock `analyzeWithClaude()` function in `src/app/api/analyze/route.ts`. It should return data conforming to the `SWIAnalysis` type from `src/types/book.ts`.

**Frontend Styling** — Components have semantic structure ready for restyling. Morpheme color coding (blue=prefix, green=base, orange=suffix) is functional, everything else is open for design changes.

## Scripts

```bash
npm run dev       # Dev server at localhost:3000
npm run build     # Production build
npm run lint      # ESLint
```


## Detected evidence (automated analysis)

Indexed codebase: 52 recognized source files, 180 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- Next.js (technology) — detected in the code
- OpenAI (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — 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 (82 of 82)

```
.claude/settings.local.json
.gitignore
data/recommended/index.json
data/users/users.json
eslint.config.mjs
generate_tts.py
main.py
next.config.ts
package.json
postcss.config.mjs
public/books/075c48ed-3be3-4591-9980-2e4b16c1c8ad/book.json
public/books/3564041d-9715-4bc1-8eca-162eeead1063/book.json
public/books/537afa8c-cb94-473e-8bdf-733b1a8112ef/book.json
public/books/556aef5e-a854-4015-a0d5-a4f50b6569f8/book.json
public/books/70aac975-ac3d-49ac-b8e0-9298a52d96cb/book.json
public/books/71aa736d-befa-4d8e-9e73-aa18214abaa4/book.json
public/books/76ae140c-b7b2-4ae6-b371-cd47387499a7/book.json
public/books/7c19d500-e98e-4803-913d-a1d70e9629ec/book.json
public/books/8741b632-df68-4f41-b6fc-fb9c10590f5e/book.json
public/books/8c61a2c6-5f9d-413c-8c77-0b3687fdfb92/book.json
public/books/97a7975f-310f-4d50-82ae-16eb9e55aa69/book.json
public/books/9a017cff-2684-4162-a2eb-9c6b69e42d4c/book.json
public/books/a9b3d2df-c8cc-4bd6-839c-0888edbc19db/book.json
public/books/bec4e931-8138-4dc1-83fa-12c0b00684f7/book.json
public/books/c05f929c-ecce-48ac-afdf-fcd1d7d2f7c1/book.json
public/books/cf6f181f-3259-4134-a1ee-f12d298c79eb/book.json
public/books/defac3ce-84f8-4126-85cf-2233c0e6d7f4/book.json
public/books/f435654e-0ab7-4a2b-9553-adc214c37474/book.json
public/recommended.json
README.md
requirements.txt
scripts/extract_words.py
scripts/pdf_to_images.py
scripts/process_recommended.py
scripts/setup-recommended.mjs
scripts/setup-simple.mjs
src/app/api/analyze/route.ts
src/app/api/auth/login/route.ts
src/app/api/auth/logout/route.ts
src/app/api/auth/register/route.ts
src/app/api/auth/session/route.ts
src/app/api/books/[...path]/route.ts
src/app/api/chat/route.ts
src/app/api/download/route.ts
src/app/api/icons/route.ts
src/app/api/library/route.ts
src/app/api/recommended/route.ts
src/app/api/search/route.ts
src/app/api/tts/route.ts
src/app/api/upload/route.ts
src/app/globals.css
src/app/layout.tsx
src/app/library/page.tsx
src/app/page.tsx
src/app/reader/page.tsx
src/app/upload/page.tsx
src/components/BookCard.tsx
src/components/BookCarousel.tsx
src/components/BookPage.tsx
src/components/ChatAssistant.tsx
src/components/DepthSelector.tsx
src/components/DropZone.tsx
src/components/PageSearch.tsx
src/components/PDFPageRenderer.tsx
src/components/PDFThumbnail.tsx
src/components/SWIPanel.tsx
src/components/UploadForm.tsx
src/components/WordOverlay.tsx
src/contexts/AuthContext.tsx
src/hooks/useAudioCache.ts
src/hooks/useSocraticChat.ts
src/lib/bookManager.ts
src/lib/sessionManager.ts
src/lib/startup.ts
src/lib/suggestions.ts
src/lib/userManager.ts
src/lib/wordCache.ts
src/types/auth.ts
src/types/book.ts
src/types/chat.ts
test_main.py
tsconfig.json
```

### Dependencies

- package.json: @anthropic-ai/sdk@^0.74.0, @tailwindcss/postcss@^4, @types/bcryptjs@^2.4.6, @types/node@^20, @types/react@^19, @types/react-dom@^19, bcryptjs@^3.0.3, eslint@^9, eslint-config-next@16.1.6, next@16.1.6, pdfjs-dist@^4.10.38, react@19.2.3, react-dom@19.2.3, sharp@^0.34.5, tailwindcss@^4, tesseract.js@^7.0.0, typescript@^5, zod@^4.3.6
- requirements.txt: anthropic, openai, pdf2image, pdfminer.six, python-dotenv, requests, requests-oauthlib

### Recent commits (newest first)

- fixed display problems, added in delete function for saved pdfs
- Fix sidebar close behavior and improve TTS error handling
- Merge latest changes with custom panel improvements
- Update UI design and navigation
- Fix book loading on Render by serving files via API routes
- Merge remote add-ocr-upload, resolve users.json conflict
- Merge branch 'add-ocr-upload' of github.com:onwaneri/reading-switch into add-ocr-upload
- Hide depth selector buttons in header
- Update Claude Code settings with git permissions
- Add etymology field to API response
- Add headings to etymology section
- Update etymology display and clean up SWI panel props
- idk even know what i did
- almost final frontend
- almost final frontend
- Add Noun Project icon integration for word analysis
- Serve book files via API route to fix production static file issue
- fixed render issues
- Add Socratic chatbot to SWI word analysis panel
- integrated audio with rest of website

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

### requirements.txt

```
pdfminer.six
anthropic
openai
pdf2image
python-dotenv
requests
requests-oauthlib

```

### package.json

```
{
  "name": "reading-switch",
  "version": "0.1.0",
  "private": true,
  "engines": {
    "node": ">=18.0.0"
  },
  "scripts": {
    "dev": "next dev",
    "build": "node scripts/setup-simple.mjs && next build",
    "start": "next start",
    "lint": "eslint",
    "setup-books": "node scripts/setup-simple.mjs"
  },
  "dependencies": {
    "@anthropic-ai/sdk": "^0.74.0",
    "@types/bcryptjs": "^2.4.6",
    "bcryptjs": "^3.0.3",
    "next": "16.1.6",
    "pdfjs-dist": "^4.10.38",
    "react": "19.2.3",
    "react-dom": "19.2.3",
    "sharp": "^0.34.5",
    "tesseract.js": "^7.0.0",
    "zod": "^4.3.6"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "eslint": "^9",
    "eslint-config-next": "16.1.6",
    "tailwindcss": "^4",
    "typescript": "^5"
  }
}

```

### main.py

```python
"""
Backend for Structured Word Inquiry (SWI) word matrix generation.
Uses Claude API for morphological analysis.
"""

import json
import os
import sys
import tempfile
from typing import Dict
from anthropic import Anthropic
from openai import OpenAI
import base64
import requests
from requests_oauthlib import OAuth1

from dotenv import load_dotenv
# Load .env.local from the script's directory
env_path = os.path.join(os.path.dirname(__file__), ".env.local")
load_dotenv(env_path)
print(f"Loading env from: {env_path}", file=sys.stderr)



client = Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
openai_client = None
if os.environ.get("OPENAI_API_KEY"):
    openai_client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))

# In-memory cache for word matrices
matrix_cache: Dict[str, dict] = {}


def call_llm(prompt: str) -> str:
    """
    Call Claude API with the given prompt.

    Args:
        prompt: The prompt to send to Claude

    Returns:
        Claude's response as a string
    """
    try:
        message = client.messages.create(
            model="claude-sonnet-4-5-20250929",  # Latest Claude Sonnet 4.5
            max_tokens=2000,
            temperature=0.3,  # Lower temperature for more precise, consistent results
            messages=[
                {"role": "user", "content": prompt}
            ]
        )
        return message.content[0].text
    except Exception as e:
        print(f"Error calling Claude API: {e}", file=sys.stderr)
        raise


def clean_json_response(response: str) -> str:
    """
    Clean AI response by removing markdown code blocks if present.

    Args:
        response: Raw response from the AI

    Returns:
        Cleaned JSON string
    """
    response = response.strip()

    # Remove markdown code blocks if present
    if response.startswith("```"):
        # Remove opening fence (```json or ```)
        lines = response.split('\n')
        if lines[0].startswith("```"):
            lines = lines[1:]
        # Remove closing fence
        if lines and lines[-1].strip() == "```":
            lines = lines[:-1]
        response = '\n'.join(lines)

    return response.strip()


def extract_base(word: str) -> str:
    """
    Extract the base morpheme(s) from a given word using Claude API.

    Args:
        word: The input word to analyze

    Returns:
        The base morpheme(s) - single base or multiple bases separated by '+'
    """
    prompt = f"""You are an expert linguist specializing in structured word inquiry and English morphology.

Your task: Identify the BASE MORPHEME(S) (root) of the word "{word}"

CRITICAL RULES:
1. Return ONLY the base morpheme(s) - no prefixes, no suffixes
2. The base must be a real morpheme found in English dictionaries
3. The base should be the smallest meaningful unit that carries the core meaning
4. Do NOT invent bases - use established linguistic analysis
5. For common words, use well-documented bases appropriate for educational contexts
6. If the word has TWO base morphemes (compound word), separate them with ' + ' (e.g., "auto + mobile")
7. Most words have ONE base - only return multiple bases if it's truly a compound word

Examples:
Single base words:
- "construction" → "struct" (meaning: to build)
- "unhappiness" → "happy" (the base emotional state)
- "replay" → "play" (the core action)
- "education" → "duce" (meaning: to lead)
- "inspection" → "spect" (meaning: to look)
- "description" → "scribe" (meaning: to write)

Compound words (two bases):
- "automobile" → "auto + mobile" (self + move)
- "bibliography" → "biblio + graph" (book + write)
- "biography" → "bio + graph" (life + write)
- "telephone" → "tele + phone" (far + sound)

Word to analyze: {word}

Return ONLY the base morpheme(s) with no explanation, no hyphens in the bases themselves. If multiple bases, separate with ' + '."""

    try:
        base = call_llm(prompt).strip()
        print(f"Extracted base: '{base}'", file=sys.stderr)
        return base
    except Exception as e:
        print(f"Error extracting base for '{word}': {e}", file=sys.stderr)
        raise


def generate_matrix_with_icons(base: str) -> dict:
    """
    Generate a complete word matrix with icons for each morpheme.

    Args:
        base: The base morpheme(s) - can be single or multiple separated by ' + '

    Returns:
        Dictionary containing the word matrix with icon URLs
    """
    # First generate the matrix
    matrix = generate_matrix(base)

    # Fetch icons for bases
    for base_morph in matrix.get("bases", []):
        try:
            icon_url = fetch_noun_project_icon(base_morph["text"], base_morph["text"])
            if icon_url:
                base_morph["iconUrl"] = icon_url
        except Exception as e:
            print(f"Failed to fetch icon for base '{base_morph['text']}': {e}", file=sys.stderr)

    # Fetch icons for prefixes (use meaning for better results)
    for prefix in matrix.get("prefixes", []):
        try:
            search_term = prefix.get("meaning", prefix["text"])
            icon_url = fetch_noun_project_icon(search_term, prefix["text"])
            if icon_url:
                prefix["iconUrl"] = icon_url
        except Exception as e:
            print(f"Failed to fetch icon for prefix '{prefix['text']}': {e}", file=sys.stderr)

    # Fetch icons for suffixes (use meaning for better results)
    for suffix in matrix.get("suffixes", []):
        try:
            search_term = suffix.get("meaning", suffix["text"])
            icon_url = fetch_noun_project_icon(search_term, suffix["text"])
            if icon_url:
                suffix["iconUrl"] = icon_url
        except Exception as e:
            print(f"Failed to fetch icon for suffix '{suffix['text']}': {e}", file=sys.stderr)

    return matrix


def generate_matrix(base: str) -> dict:
    """
    Generate a complete word matrix for a given base morpheme(s) using Claude API.

    Args:
        base: The base morpheme(s) - can be single or multiple separated 
[truncated — 17248 more characters]
```

### src/app/layout.tsx

```typescript
import type { Metadata } from "next";
import { Nunito } from "next/font/google";
import "./globals.css";
import { AuthProvider } from "@/contexts/AuthContext";

const nunito = Nunito({ subsets: ["latin"], variable: "--font-nunito" });

export const metadata: Metadata = {
  title: "Reading SWItch",
  description: "Interactive picture book reader with Structured Word Inquiry",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en" className={nunito.variable}>
      <body className="font-[family-name:var(--font-nunito)]">
        <AuthProvider>{children}</AuthProvider>
      </body>
    </html>
  );
}

```

### src/app/page.tsx

```typescript
'use client';

import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/contexts/AuthContext';

export default function LoginPage() {
  const [isRegister, setIsRegister] = useState(false);
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');
  const [loading, setLoading] = useState(false);

  const { user, login, register, loading: authLoading } = useAuth();
  const router = useRouter();

  useEffect(() => {
    if (!authLoading && user) {
      router.push('/library');
    }
  }, [user, authLoading, router]);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setError('');
    setLoading(true);

    try {
      if (isRegister) {
        await register(username, password);
      } else {
        await login(username, password);
      }
      router.push('/library');
    } catch (err) {
      setError(err instanceof Error ? err.message : 'An error occurred');
    } finally {
      setLoading(false);
    }
  };

  if (authLoading) {
    return (
      <div className="min-h-screen flex items-center justify-center bg-amber-50">
        <div className="text-red-500 text-xl">Loading...</div>
      </div>
    );
  }

  return (
    <div className="min-h-screen flex items-center justify-center bg-amber-50">
      <div className="max-w-md w-full px-8">
        <div className="bg-white rounded-lg shadow-md p-8">
          <h1 className="text-4xl font-bold text-red-500 mb-2 text-center">
            Welcome to Switch
          </h1>
          <p className="text-gray-600 mb-8 text-center">
            {isRegister ? 'Create your account' : 'Sign in to your account'}
          </p>

          <form onSubmit={handleSubmit} className="space-y-4">
            <div>
              <label htmlFor="username" className="block text-sm font-medium text-gray-700 mb-1">
                Username
              </label>
              <input
                id="username"
                type="text"
                value={username}
                onChange={(e) => setUsername(e.target.value)}
                className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500"
                placeholder="Enter username"
                required
                autoComplete="username"
              />
            </div>

            <div>
              <label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
                Password
              </label>
              <input
                id="password"
                type="password"
                value={password}
                onChange={(e) => setPassword(e.target.value)}
                className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500"
                placeholder="Enter password"
                required
                autoComplete={isRegister ? 'new-password' : 'current-password'}
              />
            </div>

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

            <button
              type="submit"
              disabled={loading}
              className="w-full px-6 py-3 bg-red-500 text-white rounded-lg hover:bg-red-600 transition disabled:bg-gray-400 disabled:cursor-not-allowed"
            >
              {loading ? 'Please wait...' : isRegister ? 'Register' : 'Login'}
            </button>
          </form>

          <div className="mt-6 text-center">
            <button
              type="button"
              onClick={() => {
                setIsRegister(!isRegister);
                setError('');
              }}
              className="text-red-500 hover:text-red-600 text-sm font-medium"
            >
              {isRegister
                ? 'Already have an account? Login'
                : "Don't have an account? Register"}
            </button>
          </div>
        </div>
      </div>
    </div>
  );
}

```

### src/app/upload/page.tsx

```typescript
'use client';

import { useState, useCallback, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/contexts/AuthContext';
import { DropZone } from '@/components/DropZone';
import * as pdfjsLib from 'pdfjs-dist';

pdfjsLib.GlobalWorkerOptions.workerSrc = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjsLib.version}/pdf.worker.min.mjs`;

async function renderPdfPages(
  file: File,
  onProgress: (completed: number, total: number) => void
): Promise<Blob[]> {
  const arrayBuffer = await file.arrayBuffer();
  const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
  const total = pdf.numPages;
  const blobs: Blob[] = [];

  for (let i = 1; i <= total; i++) {
    onProgress(i - 1, total);
    const page = await pdf.getPage(i);
    const scale = 3.0;
    const viewport = page.getViewport({ scale });
    const canvas = document.createElement('canvas');
    canvas.width = viewport.width;
    canvas.height = viewport.height;
    const ctx = canvas.getContext('2d')!;
    await page.render({ canvasContext: ctx, viewport }).promise;
    const blob = await new Promise<Blob>((resolve) => {
      canvas.toBlob((b) => resolve(b!), 'image/png');
    });
    blobs.push(blob);
  }

  onProgress(total, total);
  return blobs;
}

export default function UploadPage() {
  const { user, loading: authLoading } = useAuth();
  const router = useRouter();
  const [uploading, setUploading] = useState(false);
  const [error, setError] = useState('');
  const [processingPage, setProcessingPage] = useState(0);
  const [totalPages, setTotalPages] = useState(0);

  useEffect(() => {
    if (!authLoading && !user) {
      router.push('/');
    }
  }, [user, authLoading, router]);

  const handleFile = useCallback(async (file: File) => {
    if (file.type !== 'application/pdf') {
      setError('Please upload a PDF file');
      return;
    }

    setError('');
    setUploading(true);
    setProcessingPage(1);
    setTotalPages(1);

    try {
      const formData = new FormData();
      formData.append('title', file.name.replace('.pdf', ''));
      formData.append('pdf', file);

      const res = await fetch('/api/upload', {
        method: 'POST',
        body: formData,
      });

      if (!res.ok) {
        const body = await res.json().catch(() => null);
        throw new Error(body?.error || 'Upload failed');
      }

      const bookData = await res.json();

      // Add to user's library
      await fetch('/api/library', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ bookId: bookData.id }),
      });

      router.push(`/reader?bookId=${bookData.id}`);
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Upload failed');
      setUploading(false);
    }
  }, [router]);

  const handleFileSelect = useCallback((file: File) => {
    handleFile(file);
  }, [handleFile]);

  if (authLoading) {
    return (
      <div className="min-h-screen flex items-center justify-center bg-amber-50">
        <div className="text-red-500 text-xl">Loading...</div>
      </div>
    );
  }

  if (!user) {
    return null;
  }

  const progress = totalPages > 0 ? Math.round((processingPage / totalPages) * 100) : 0;

  return (
    <div className="min-h-screen bg-amber-50 flex items-center justify-center p-8">
      <div className="bg-white rounded-lg shadow-lg p-8 max-w-2xl w-full">
        <div className="flex justify-between items-center mb-8">
          <h1 className="text-3xl font-bold text-red-500">Add Book</h1>
          <button
            onClick={() => router.push('/library')}
            className="text-gray-500 hover:text-gray-700 text-2xl"
          >
            ×
          </button>
        </div>

        <div className="mb-8">
          <h2 className="text-lg font-semibold text-gray-700 mb-4">Add PDF</h2>
          <DropZone onFileSelect={handleFileSelect} disabled={uploading} />
        </div>

        <div className="flex items-center gap-4 mb-8">
          <div className="flex-1 h-px bg-gray-300"></div>
          <span className="text-gray-500 text-sm">OR</span>
          <div className="flex-1 h-px bg-gray-300"></div>
        </div>

        <label className="block">
          <input
            type="file"
            accept="application/pdf"
            onChange={(e) => {
              const file = e.target.files?.[0];
              if (file) handleFile(file);
            }}
            disabled={uploading}
            className="hidden"
            id="file-input"
          />
          <span
            onClick={() => !uploading && document.getElementById('file-input')?.click()}
            className={`block w-full text-center px-6 py-3 bg-gray-200 text-gray-700 rounded-lg transition cursor-pointer ${
              uploading ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-300'
            }`}
          >
            select file from your computer
          </span>
        </label>

        {uploading && (
          <div className="mt-8">
            <div className="flex justify-between items-center mb-2">
              <span className="text-sm text-gray-600">
                Processing PDF...
              </span>
            </div>
            <div className="w-full h-2 bg-gray-200 rounded-full overflow-hidden">
              <div className="h-full bg-amber-500 transition-all duration-300 animate-pulse w-full"></div>
            </div>
          </div>
        )}

        {error && (
          <div className="mt-4 p-4 bg-red-50 border border-red-200 rounded-lg text-red-600 text-sm">
            {error}
          </div>
        )}
      </div>
    </div>
  );
}

```

### src/app/library/page.tsx

```typescript
'use client';

import { useState, useEffect, useCallback } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/contexts/AuthContext';
import { BookCarousel } from '@/components/BookCarousel';
import { Book } from '@/types/book';

export default function LibraryPage() {
  const { user, loading: authLoading, logout } = useAuth();
  const router = useRouter();
  const [myLibrary, setMyLibrary] = useState<Book[]>([]);
  const [recommended, setRecommended] = useState<Book[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState('');

  useEffect(() => {
    if (!authLoading && !user) {
      router.push('/');
    }
  }, [user, authLoading, router]);

  const handleLogout = async () => {
    await logout();
    router.push('/');
  };

  const loadLibraries = useCallback(async () => {
    try {
      setLoading(true);

      const [libraryRes, recommendedRes] = await Promise.all([
        fetch('/api/library', { credentials: 'include' }),
        fetch('/api/recommended', { credentials: 'include' }),
      ]);

      if (libraryRes.ok) {
        const libraryData = await libraryRes.json();
        setMyLibrary(libraryData.books || []);
      }

      if (recommendedRes.ok) {
        const recommendedData = await recommendedRes.json();
        setRecommended(recommendedData.books || []);
      }
    } catch (err) {
      setError('Failed to load libraries');
    } finally {
      setLoading(false);
    }
  }, []);

  useEffect(() => {
    if (user) {
      loadLibraries();
    }
  }, [user, loadLibraries]);

  // Reload libraries when user returns to the page
  useEffect(() => {
    const handleFocus = () => {
      if (user) {
        loadLibraries();
      }
    };

    window.addEventListener('focus', handleFocus);
    return () => window.removeEventListener('focus', handleFocus);
  }, [user, loadLibraries]);

  const handleBookClick = (bookId: string) => {
    router.push(`/reader?bookId=${bookId}`);
  };

  const handleAddClick = () => {
    router.push('/upload');
  };

  if (authLoading) {
    return (
      <div className="min-h-screen flex items-center justify-center bg-amber-50">
        <div className="text-red-500 text-xl">Loading...</div>
      </div>
    );
  }

  if (!user) {
    return null;
  }

  return (
    <div className="min-h-screen" style={{ background: '#FFF9EE' }}>
      <header
        className="flex-shrink-0 flex flex-row justify-between items-center py-[10px] px-6 isolate"
        style={{
          height: '50px',
          background: '#FFF9EE',
          boxShadow: '0px 4px 5.3px rgba(0, 0, 0, 0.25)'
        }}
      >
        {/* Empty left space */}
        <div style={{ width: '40px' }}></div>

        {/* Switch text - center */}
        <div className="flex items-center justify-center">
          <button
            onClick={() => window.location.href = '/library'}
            className="font-bold text-center cursor-pointer hover:opacity-80 transition"
            style={{
              fontFamily: 'system-ui, -apple-system, sans-serif',
              fontSize: '32px',
              lineHeight: '47px',
              color: '#061B2E'
            }}
          >
            Switch
          </button>
        </div>

        {/* Navigation buttons - far right */}
        <div className="flex items-center gap-3" style={{ zIndex: 0 }}>
          <button
            onClick={() => window.location.href = '/upload'}
            className="flex items-center justify-center"
            style={{ width: '40px', height: '40px' }}
            title="Upload Book"
          >
            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3.5" className="w-6 h-6" style={{ color: '#061B2E' }}>
              <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
              <polyline points="17 8 12 3 7 8" />
              <line x1="12" y1="3" x2="12" y2="15" />
            </svg>
          </button>

          <button
            onClick={handleLogout}
            className="flex items-center justify-center"
            style={{ width: '40px', height: '40px' }}
            title="Logout"
          >
            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3.5" className="w-6 h-6" style={{ color: '#061B2E' }}>
              <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
              <polyline points="16 17 21 12 16 7" />
              <line x1="21" y1="12" x2="9" y2="12" />
            </svg>
          </button>
        </div>
      </header>

      <main className="max-w-7xl mx-auto px-6 py-8 space-y-12">
        {error && (
          <div className="p-4 bg-red-50 border border-red-200 rounded-lg text-red-600">
            {error}
          </div>
        )}

        <section>
          <h2 className="text-2xl font-bold text-gray-800 mb-6">My Library</h2>
          {loading ? (
            <div className="text-center text-gray-600">Loading...</div>
          ) : (
            <BookCarousel
              books={myLibrary}
              showAddButton
              onAddClick={handleAddClick}
              onBookClick={handleBookClick}
            />
          )}
        </section>

        <section>
          <h2 className="text-2xl font-bold text-gray-800 mb-6">Recommended Library</h2>
          {loading ? (
            <div className="text-center text-gray-600">Loading...</div>
          ) : recommended.length > 0 ? (
            <BookCarousel
              books={recommended}
              onBookClick={handleBookClick}
            />
          ) : (
            <div className="text-center text-gray-600 py-8 bg-white rounded-lg">
              <p className="font-semibold mb-3 text-gray-800">Add Public Domain Books</p>
              <p className="text-sm mb-4">
                Download free PDFs from <a href="https://www.gutenberg.org" target="_blank" rel="noopener noreferrer" classNa
[truncated — 883 more characters]
```

### src/app/reader/page.tsx

```typescript
'use client';

import { useState, useEffect, useCallback, useRef, Suspense } from 'react';
import { useSearchParams, useRouter } from 'next/navigation';
import type { Book, BookPage as BookPageType, WordPosition, SWIAnalysis, DepthLevel } from '@/types/book';
import { BookPage } from '@/components/BookPage';
import { SWIPanel } from '@/components/SWIPanel';
import { PageSearch } from '@/components/PageSearch';
import { useAuth } from '@/contexts/AuthContext';
import { useSocraticChat } from '@/hooks/useSocraticChat';

function ReaderContent() {
  const searchParams = useSearchParams();
  const router = useRouter();
  const bookId = searchParams.get('bookId');
  const { user } = useAuth();

  const [book, setBook] = useState<Book | null>(null);
  const [bookError, setBookError] = useState<string | null>(null);
  const [bookLoading, setBookLoading] = useState(true);
  const [currentPage, setCurrentPage] = useState(0);
  const [selectedWord, setSelectedWord] = useState<WordPosition | null>(null);
  const [showSidebar, setShowSidebar] = useState(false);
  const [panelDisplayPage, setPanelDisplayPage] = useState<number | null>(null);
  const [analysis, setAnalysis] = useState<SWIAnalysis | null>(null);
  const [isAnalyzing, setIsAnalyzing] = useState(false);
  const [analysisError, setAnalysisError] = useState<string | null>(null);
  const depth: DepthLevel = 'deep';
  const spreadRef = useRef<HTMLDivElement>(null);
  const [spreadScale, setSpreadScale] = useState(1);
  const [isSearchOpen, setIsSearchOpen] = useState(false);
  const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
  const [isDeleting, setIsDeleting] = useState(false);
  const [inLibrary, setInLibrary] = useState(false);
  const [checkingLibrary, setCheckingLibrary] = useState(true);
  const {
    messages: chatMessages,
    isStreaming: isChatStreaming,
    error: chatError,
    sendMessage: sendChatMessage,
    reset: resetChat,
  } = useSocraticChat();
  const isPanelOpen = showSidebar;
  const showTwoPages = !isPanelOpen;
  const nextPage = book?.pages[currentPage + 1];

  // Load book data
  useEffect(() => {
    if (!bookId) return;

    console.log(`[Reader] Loading book: ${bookId}`);
    setBookLoading(true);
    setBookError(null);

    fetch(`/api/books/${bookId}/book.json`)
      .then(r => {
        if (!r.ok) {
          throw new Error(`HTTP ${r.status}: ${r.statusText}`);
        }
        return r.json();
      })
      .then(bookData => {
        console.log('[Reader] Book loaded:', bookData);
        setBook(bookData);
        setBookLoading(false);
      })
      .catch(err => {
        console.error('[Reader] Failed to load book:', err);
        setBookError(err instanceof Error ? err.message : 'Failed to load book');
        setBook(null);
        setBookLoading(false);
      });


    /*    fetch(`/api/books/${bookId}/book.json`)
      .then(r => r.json())
      .then((data: Book) => {
        // Normalize image URLs: rewrite old /books/ paths to /api/books/
        data.pages = data.pages.map(p => ({
          ...p,
          imageUrl: p.imageUrl.startsWith('/books/')
            ? p.imageUrl.replace('/books/', '/api/books/')
            : p.imageUrl,
        }));
        setBook(data);
      })
      .catch(() => setBook(null)); */
  }, [bookId]);

  // Check if book is in user's library
  useEffect(() => {
    if (!bookId || !user) {
      setCheckingLibrary(false);
      return;
    }

    setCheckingLibrary(true);
    fetch('/api/library', {
      credentials: 'include',
    })
      .then(r => r.json())
      .then(data => {
        const isInLib = data.books?.some((b: Book) => b.id === bookId) || false;
        setInLibrary(isInLib);
      })
      .catch(() => setInLibrary(false))
      .finally(() => setCheckingLibrary(false));
  }, [bookId, user]);

  const handleToggleLibrary = async () => {
    console.log('[Add to Library] Button clicked!');
    console.log('[Add to Library] bookId:', bookId);
    console.log('[Add to Library] user:', user);

    if (!bookId || !user) {
      console.error('[Add to Library] Missing bookId or user!');
      alert('Error: Not logged in or no book ID');
      return;
    }

    try {
      if (inLibrary) {
        console.log('[Add to Library] Removing from library...');
        const response = await fetch('/api/library', {
          method: 'DELETE',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ bookId }),
          credentials: 'include',
        });
        console.log('[Add to Library] Remove response:', response.status);
        setInLibrary(false);
      } else {
        console.log('[Add to Library] Adding to library...');
        const response = await fetch('/api/library', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ bookId }),
          credentials: 'include',
        });
        console.log('[Add to Library] Add response:', response.status);

        if (!response.ok) {
          const error = await response.json();
          console.error('[Add to Library] API error:', error);
          alert('Failed to add to library: ' + (error.error || 'Unknown error'));
          return;
        }

        setInLibrary(true);
        console.log('[Add to Library] Navigating to library...');
        // Navigate back to library after adding with a full reload
        setTimeout(() => {
          window.location.href = '/library';
        }, 500);
      }
    } catch (error) {
      console.error('[Add to Library] Failed to toggle library:', error);
      alert('Error: ' + (error instanceof Error ? error.message : 'Unknown error'));
    }
  };

  // Arrow key navigation — step by 2 when panel closed (2-page spread), 1 when open
  const step = showSidebar ? 1 : 2;
  useEffect(() => {
    if (!book) return;
    function onKey(e: KeyboardEvent) {
      if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
        e.preventDefault();
  
[truncated — 14354 more characters]
```

### src/app/api/search/route.ts

```typescript
import { NextRequest, NextResponse } from 'next/server';
import { spawn } from 'child_process';
import path from 'path';

export async function POST(request: NextRequest) {
  try {
    const { query } = await request.json();

    if (!query || typeof query !== 'string') {
      return NextResponse.json({ error: 'Query is required' }, { status: 400 });
    }

    const scriptPath = path.join(process.cwd(), 'scripts', 'search_archive.py');

    const results = await new Promise<string>((resolve, reject) => {
      const proc = spawn('python', [scriptPath, query]);
      let stdout = '';
      let stderr = '';

      proc.stdout.on('data', (data) => {
        stdout += data.toString();
      });

      proc.stderr.on('data', (data) => {
        stderr += data.toString();
      });

      proc.on('close', (code) => {
        if (code !== 0) {
          reject(new Error(`Search failed: ${stderr}`));
        } else {
          resolve(stdout);
        }
      });
    });

    const parsedResults = JSON.parse(results);

    return NextResponse.json({ results: parsedResults });
  } catch (error) {
    const message = error instanceof Error ? error.message : 'Search failed';
    return NextResponse.json({ error: message }, { status: 400 });
  }
}

```

### src/app/api/download/route.ts

```typescript
import { NextRequest, NextResponse } from 'next/server';
import { spawn } from 'child_process';
import path from 'path';
import { validateSession } from '@/lib/sessionManager';
import { addToLibrary } from '@/lib/userManager';

export async function POST(request: NextRequest) {
  try {
    const user = await validateSession();

    if (!user) {
      return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
    }

    const { identifier } = await request.json();

    if (!identifier || typeof identifier !== 'string') {
      return NextResponse.json({ error: 'Identifier is required' }, { status: 400 });
    }

    const scriptPath = path.join(process.cwd(), 'scripts', 'download_archive.py');

    const result = await new Promise<string>((resolve, reject) => {
      const proc = spawn('python', [scriptPath, identifier], {
        cwd: process.cwd(),
      });
      let stdout = '';
      let stderr = '';

      proc.stdout.on('data', (data) => {
        stdout += data.toString();
      });

      proc.stderr.on('data', (data) => {
        stderr += data.toString();
      });

      proc.on('close', (code) => {
        if (code !== 0) {
          reject(new Error(`Download failed: ${stderr}`));
        } else {
          resolve(stdout);
        }
      });
    });

    const parsedResult = JSON.parse(result);

    if (parsedResult.bookId) {
      await addToLibrary(user.id, parsedResult.bookId);
    }

    return NextResponse.json({ bookId: parsedResult.bookId, title: parsedResult.title });
  } catch (error) {
    const message = error instanceof Error ? error.message : 'Download failed';
    return NextResponse.json({ error: message }, { status: 400 });
  }
}

```

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