# Project export: README.AI

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

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2025
- Tagline: To the abstract and beyond: README.ai creates tailored educational videos grounded in the research papers that you are trying to understand. Upload a paper, watch, learn, code, test.
- Devpost: https://devpost.com/software/readme-ai-wja0d9
- GitHub: https://github.com/sanjanataware/readme-ai.git
- Video: https://www.youtube.com/embed/t8pliUd7c0w?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 4 GitHub contributor(s) — Evan Chisholm (8 commits), Sanjana T. (8 commits), Rithvik (2 commits), Chakshan (1 commits)

## Devpost submission (written by the team)

### Overview

To the abstract and beyond

### Inspiration

Scientists spend 23% of their total work time reading research papers While this is an important part of scientific training, we believe that oftentimes there is a comprehension barrier in academic research papers that are not generally written to explain something to a novice. We are a group of student researchers who understand the feeling of being unable to make it past the abstract when trying to understand a complex paper. We wanted to change that, making research more accessible and providing different avenues of understanding. This is where the idea for README.ai came from.

### What it does

Watch, understand, implement - research made accessible README.ai takes in a research paper and… Create a clean tailored explainer video with a voice-over (akin to 3Blue1Brown or Khan Academy Videos) with targeted and accurate animations grounded in the research paper (minimizing video hallucinations) Create a clean tailored explainer video with a voice-over (akin to 3Blue1Brown or Khan Academy Videos) with targeted and accurate animations grounded in the research paper (minimizing video hallucinations) Highlights key concepts of the paper with text citations on a web interface Highlights key concepts of the paper with text citations on a web interface Generate a quiz for users to test their understanding of the concepts from the paper Generate a quiz for users to test their understanding of the concepts from the paper Hosts a library of your analyzed papers and educational videos to reference past materials Hosts a library of your analyzed papers and educational videos to reference past materials

### How we built it

We started by testing the video with Manim and Google Veo with PDF and seeing how it makes a video using Claude API Then we built a GitHub readme extractor using the Claude and GitHub API, then integrated Claude API to make the readme easier to understand We worked on the Front end where it can take in pdf and generate the quiz to check for understanding along with returning key concepts Then we weaved in the LMNT API for voice with a script that we generated from the paper to go along with video animation We then integrated the Audio with the video and then connected the backend with the frontend, and created a folder to store the videos.

### Challenges we ran into

We had to figure out how to sync the video and audio generation and linking (one or the other was working but not both) We solved this by testing out different libraries to find the most optimal pipeline configuration. Also, we ran into difficulties integrating parallel API calls, so we ended up testing out different integrations to minimize latency.

### Accomplishments we're proud of

Creating a robust platform that can handle multiple research papers Designing our video generation pipeline that is tailored to scientific papers, prioritizing meaningful animations and narration to break down complex topics by prioritizing specific video creation tools (not relying solely on video creation models) Creating a fully functional web platform that is easy to use and user-centric

### What we learned

How to use the Claude API to generate summaries, code, and quiz questions. How to use Claude Code for quick scaffolding and code development. How to use Veo for dynamic video generation. How to use Manim code to generate animations. How to deploy our application with Vercel.

### What's next

Integrate automatically generated interactive demos within the web application to enable users to understand the paper alongside the video explanation better. Create a chatbot that the user can query during the video and at the various sections of the web application to ask clarifying questions regarding the research paper.

## README (from the GitHub repository)

# README.ai (read me pls)

**To the abstract and beyond**

## Inspiration
*Scientists spend 23% of their total work time reading research papers*

While this is an important part of scientific training, we believe that oftentimes there is a comprehension barrier in academic research papers that are not generally written to explain something to a novice. 
We are a group of student researchers who understand the feeling of being unable to make it past the abstract when trying to understand a complex paper. 
We wanted to change that, making research more accessible and providing different avenues of understanding. This is where the idea for README.ai came from. 


## What it does
*Watch, understand, implement - research made accessible*

README.ai takes in a research paper and…

1. Create a clean tailored explainer video with a voice-over (akin to 3Blue1Brown or Khan Academy Videos) with targeted and accurate animations grounded in the research paper (minimizing video hallucinations)

2. Highlights key concepts of the paper with text citations on a web interface

3. Generate a quiz for users to test their understanding of the concepts from the paper

4. Hosts a library of your analyzed papers and educational videos to reference past materials

## How we built it
1. We started by testing the video with Manim and Google Veo with PDF and seeing how it makes a video using Claude API
2. Then we built a GitHub readme extractor using the Claude and GitHub API, then integrated Claude API to make the readme easier to understand
3. We worked on the Front end where it can take in pdf and generate the quiz to check for understanding along with returning key concepts
4. Then we weaved in the LMNT API for voice with a script that we generated from the paper to go along with video animation
5. We then integrated the Audio with the video and then connected the backend with the frontend, and created a folder to store the videos.

## Challenges we ran into
We had to figure out how to sync the video and audio generation and linking (one or the other was working but not both) We solved this by testing out different libraries to find the most optimal pipeline configuration. Also, we ran into difficulties integrating parallel API calls, so we ended up testing out different integrations to minimize latency. 

## Accomplishments that we're proud of
1. Creating a robust platform that can handle multiple research papers
2. Designing our video generation pipeline that is tailored to scientific papers, prioritizing meaningful animations and narration to break down complex topics by prioritizing specific video creation tools (not relying solely on video creation models) 
3. Creating a fully functional web platform that is easy to use and user-centric

## What we learned
1. How to use the Claude API to generate summaries, code, and quiz questions.
2. How to use Claude Code for quick scaffolding and code development.
3. How to use Veo for dynamic video generation.
4. How to use Manim code to generate animations.
5. How to deploy our application with Vercel.

## What's next for README.AI
1. Integrate automatically generated interactive demos within the web application to enable users to understand the paper alongside the video explanation better.
2. Create a chatbot that the user can query during the video and at the various sections of the web application to ask clarifying questions regarding the research paper.



## Detected evidence (automated analysis)

Indexed codebase: 81 recognized source files, 513 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- HTML (language) — detected in the code
- Next.js (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
- Vercel AI SDK (technology) — detected in the code
- Google Gemini (technology) — claimed on Devpost, not found in the code
- AI coding agent: Claude Code — evidence: config files committed to the repository

## Codebase structure (from repository index)

### Files (86 of 86)

```
.gitignore
app/api/analyze/route.ts
app/components/concepts-display.tsx
app/components/github-repos-panel.tsx
app/components/quiz-panel.tsx
app/components/video-library.tsx
app/components/video-panel.tsx
app/components/video-player.tsx
app/globals.css
app/layout.tsx
app/page.tsx
CLAUDE.md
components.json
components/theme-provider.tsx
components/ui/accordion.tsx
components/ui/alert-dialog.tsx
components/ui/alert.tsx
components/ui/aspect-ratio.tsx
components/ui/avatar.tsx
components/ui/badge.tsx
components/ui/breadcrumb.tsx
components/ui/button.tsx
components/ui/calendar.tsx
components/ui/card.tsx
components/ui/carousel.tsx
components/ui/chart.tsx
components/ui/checkbox.tsx
components/ui/collapsible.tsx
components/ui/command.tsx
components/ui/context-menu.tsx
components/ui/dialog.tsx
components/ui/drawer.tsx
components/ui/dropdown-menu.tsx
components/ui/form.tsx
components/ui/hover-card.tsx
components/ui/input-otp.tsx
components/ui/input.tsx
components/ui/label.tsx
components/ui/menubar.tsx
components/ui/navigation-menu.tsx
components/ui/pagination.tsx
components/ui/popover.tsx
components/ui/progress.tsx
components/ui/radio-group.tsx
components/ui/resizable.tsx
components/ui/scroll-area.tsx
components/ui/select.tsx
components/ui/separator.tsx
components/ui/sheet.tsx
components/ui/sidebar.tsx
components/ui/skeleton.tsx
components/ui/slider.tsx
components/ui/sonner.tsx
components/ui/switch.tsx
components/ui/table.tsx
components/ui/tabs.tsx
components/ui/textarea.tsx
components/ui/toast.tsx
components/ui/toaster.tsx
components/ui/toggle-group.tsx
components/ui/toggle.tsx
components/ui/tooltip.tsx
components/ui/use-mobile.tsx
components/ui/use-toast.ts
gatherREADME.py
hooks/use-mobile.tsx
hooks/use-toast.ts
lib/config.ts
lib/github-api.ts
lib/utils.ts
next.config.mjs
package.json
postcss.config.mjs
README.md
server.py
styles/globals.css
tailwind.config.ts
tsconfig.json
video_gen/__init__.py
video_gen/config_gen.py
video_gen/gen.py
video_gen/github_extract.py
video_gen/manim_generator.py
video_gen/voice_gen.py
web_tutorial/pdf_to_html_tutorial.py
web_tutorial/unet.html
```

### Dependencies

- package.json: @ai-sdk/anthropic@latest, @hookform/resolvers@^3.9.1, @radix-ui/react-accordion@latest, @radix-ui/react-alert-dialog@latest, @radix-ui/react-aspect-ratio@latest, @radix-ui/react-avatar@latest, @radix-ui/react-checkbox@latest, @radix-ui/react-collapsible@latest, @radix-ui/react-context-menu@latest, @radix-ui/react-dialog@latest, @radix-ui/react-dropdown-menu@latest, @radix-ui/react-hover-card@latest, @radix-ui/react-label@latest, @radix-ui/react-menubar@latest, @radix-ui/react-navigation-menu@latest, @radix-ui/react-popover@latest, @radix-ui/react-progress@latest, @radix-ui/react-radio-group@latest, @radix-ui/react-scroll-area@latest, @radix-ui/react-select@latest, @radix-ui/react-separator@latest, @radix-ui/react-slider@latest, @radix-ui/react-slot@latest, @radix-ui/react-switch@latest, @radix-ui/react-tabs@latest, @radix-ui/react-toast@latest, @radix-ui/react-toggle@latest, @radix-ui/react-toggle-group@latest, @radix-ui/react-tooltip@latest, @tailwindcss/typography@^0.5.16, @types/node@^22, @types/react@^19, @types/react-dom@^19, ai@latest, autoprefixer@^10.4.20, class-variance-authority@^0.7.1, clsx@^2.1.1, cmdk@latest, date-fns@4.1.0, embla-carousel-react@latest, input-otp@latest, lucide-react@^0.454.0, next@15.2.4, next-themes@latest, postcss@^8.5, react@^19, react-day-picker@latest, react-dom@^19, react-hook-form@latest, react-markdown@^10.1.0, react-resizable-panels@latest, recharts@latest, sonner@latest, tailwind-merge@^2.5.5, tailwindcss@^3.4.17, tailwindcss-animate@^1.0.7, typescript@^5, vaul@latest, zod@latest

### Recent commits (newest first)

- Update README.md
- Merge branch 'main' of https://github.com/sanjanataware/readme-ai
- added speed toggle
- add web tutorial
- small bits
- better markdown rendering
- more stuff
- Merge branch 'main' of https://github.com/sanjanataware/readme-ai
- stuff
- added upload to backend functionality as well as video rendering
- gatherREADME.py_2
- gatherREADME.py_1
- feat: fix schema validation error with robust parsing
- feat: fix schema validation with robust parsing and fallback
- chore: sync main changes to readme-ai project
- Update README.md
- chore: sync main changes to readme-ai project
- chore: sync main changes to readme-ai project
- Initialized repository for project New Project

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

### CLAUDE.md

```markdown
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

README.ai is a Next.js 15 application that transforms research papers into interactive learning experiences. It uses AI (Anthropic Claude) to extract key concepts from uploaded PDF documents and generates quiz questions for interactive learning. The app is deployed on Vercel and synced with v0.dev.

## Common Commands

### Development
- `pnpm dev` - Start development server
- `pnpm build` - Build for production  
- `pnpm start` - Start production server
- `pnpm lint` - Run ESLint

### Package Management
- Uses `pnpm` as package manager (not npm/yarn)
- `pnpm install` - Install dependencies

## Architecture

### Core Components
- **Main App**: `app/page.tsx` - Primary upload and analysis interface with drag/drop PDF upload
- **API Route**: `app/api/analyze/route.ts` - Handles PDF processing using Anthropic Claude API
- **Quiz Panel**: `app/components/quiz-panel.tsx` - Interactive quiz interface
- **Concepts Display**: `app/components/concepts-display.tsx` - Shows extracted concepts
- **Video Panel**: `app/components/video-panel.tsx` - Video generation interface with job tracking
- **Video Library**: `app/components/video-library.tsx` - Lists all generated videos with management options
- **Video Player**: `app/components/video-player.tsx` - In-app video player with fullscreen support

### Key Features
- PDF upload with drag/drop support
- AI-powered concept extraction using Claude 3.5 Sonnet
- Quiz generation with multiple choice questions
- **Video generation from PDFs using external FastAPI server**
- **Video library with in-app playback and management**
- **Full-featured video player with fullscreen mode**
- Job tracking with real-time status updates
- Video deletion and download capabilities
- Fallback data when AI processing fails
- Responsive design with Tailwind CSS

### Tech Stack
- **Framework**: Next.js 15 with App Router
- **AI**: Anthropic Claude API (@ai-sdk/anthropic)
- **UI**: Radix UI components with custom styling
- **Styling**: Tailwind CSS with custom design system
- **Forms**: React Hook Form with Zod validation
- **Icons**: Lucide React

### Configuration Notes
- TypeScript and ESLint errors are ignored during builds (see next.config.mjs)
- Uses pnpm workspace configuration
- Custom path mapping: `@/*` points to root directory
- Dark mode support configured in Tailwind
- **Video API URL configured in `lib/config.ts` (defaults to localhost:8000)**

### Data Models
```typescript
interface Concept {
  title: string
  summary: string
  citations: string[]
  importance: string
}

interface QuizQuestion {
  question: string
  options: string[]
  correctAnswer: number
  concept: string
}

interface VideoJobStatus {
  job_id: string
  status: "pending" | "processing" | "completed" | "failed"
  created_at: string
  completed_at?: string
  error?: string
  video_path?: string
  quality: "low_quality" 
[truncated — 1366 more characters]
```

### package.json

```
{
  "name": "my-v0-project",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "build": "next build",
    "dev": "next dev",
    "lint": "next lint",
    "start": "next start"
  },
  "dependencies": {
    "@ai-sdk/anthropic": "latest",
    "@hookform/resolvers": "^3.9.1",
    "@radix-ui/react-accordion": "latest",
    "@radix-ui/react-alert-dialog": "latest",
    "@radix-ui/react-aspect-ratio": "latest",
    "@radix-ui/react-avatar": "latest",
    "@radix-ui/react-checkbox": "latest",
    "@radix-ui/react-collapsible": "latest",
    "@radix-ui/react-context-menu": "latest",
    "@radix-ui/react-dialog": "latest",
    "@radix-ui/react-dropdown-menu": "latest",
    "@radix-ui/react-hover-card": "latest",
    "@radix-ui/react-label": "latest",
    "@radix-ui/react-menubar": "latest",
    "@radix-ui/react-navigation-menu": "latest",
    "@radix-ui/react-popover": "latest",
    "@radix-ui/react-progress": "latest",
    "@radix-ui/react-radio-group": "latest",
    "@radix-ui/react-scroll-area": "latest",
    "@radix-ui/react-select": "latest",
    "@radix-ui/react-separator": "latest",
    "@radix-ui/react-slider": "latest",
    "@radix-ui/react-slot": "latest",
    "@radix-ui/react-switch": "latest",
    "@radix-ui/react-tabs": "latest",
    "@radix-ui/react-toast": "latest",
    "@radix-ui/react-toggle": "latest",
    "@radix-ui/react-toggle-group": "latest",
    "@radix-ui/react-tooltip": "latest",
    "@tailwindcss/typography": "^0.5.16",
    "ai": "latest",
    "autoprefixer": "^10.4.20",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "cmdk": "latest",
    "date-fns": "4.1.0",
    "embla-carousel-react": "latest",
    "input-otp": "latest",
    "lucide-react": "^0.454.0",
    "next": "15.2.4",
    "next-themes": "latest",
    "react": "^19",
    "react-day-picker": "latest",
    "react-dom": "^19",
    "react-hook-form": "latest",
    "react-markdown": "^10.1.0",
    "react-resizable-panels": "latest",
    "recharts": "latest",
    "sonner": "latest",
    "tailwind-merge": "^2.5.5",
    "tailwindcss-animate": "^1.0.7",
    "vaul": "latest",
    "zod": "latest"
  },
  "devDependencies": {
    "@types/node": "^22",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "postcss": "^8.5",
    "tailwindcss": "^3.4.17",
    "typescript": "^5"
  }
}
```

### server.py

```python
from fastapi import FastAPI, BackgroundTasks, HTTPException, UploadFile, File
from fastapi.responses import FileResponse
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import json
import os
import uuid
from datetime import datetime
from typing import Dict, Optional
from video_gen.gen import generate_complete_video
from video_gen.config_gen import generate_video_config
from video_gen.github_extract import GitHubExtractor

app = FastAPI(title="Video Generation API", version="1.0.0")

# Add CORS middleware
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # Allows all origins
    allow_credentials=True,
    allow_methods=["*"],  # Allows all methods
    allow_headers=["*"],  # Allows all headers
)

# JSON file to store job status
JOBS_FILE = "jobs.json"

class VideoRequest(BaseModel):
    pdf_path: Optional[str] = None
    use_base64: bool = True
    quality: str = "low_quality"  # low_quality, medium_quality, high_quality

class VideoRequestWithUpload(BaseModel):
    quality: str = "low_quality"

class GitHubExtractRequest(BaseModel):
    pdf_path: Optional[str] = None
    fetch_readmes: bool = True
    simplify_readmes: bool = True

class JobStatus(BaseModel):
    job_id: str
    status: str  # pending, processing, completed, failed
    created_at: str
    completed_at: Optional[str] = None
    error: Optional[str] = None
    video_path: Optional[str] = None
    pdf_path: Optional[str] = None
    video_name: Optional[str] = None

class RenameVideoRequest(BaseModel):
    video_name: str

def load_jobs() -> Dict[str, Dict]:
    """Load jobs from JSON file"""
    if os.path.exists(JOBS_FILE):
        try:
            with open(JOBS_FILE, 'r') as f:
                return json.load(f)
        except:
            return {}
    return {}

def save_jobs(jobs: Dict[str, Dict]):
    """Save jobs to JSON file"""
    with open(JOBS_FILE, 'w') as f:
        json.dump(jobs, f, indent=2)

def update_job_status(job_id: str, status: str, **kwargs):
    """Update job status in JSON file"""
    jobs = load_jobs()
    if job_id in jobs:
        jobs[job_id]["status"] = status
        if status == "completed":
            jobs[job_id]["completed_at"] = datetime.now().isoformat()
        for key, value in kwargs.items():
            jobs[job_id][key] = value
        save_jobs(jobs)

async def process_video_generation(job_id: str, pdf_path: str, use_base64: bool, quality: str):
    """Background task to generate video"""
    try:
        update_job_status(job_id, "processing")
        
        # Step 1: Generate video configuration
        print(f"Job {job_id}: Generating video config from PDF...")
        response = generate_video_config(pdf_path, use_base64)
        config_text = response.content[0].text
        
        # Parse the JSON config
        try:
            config = json.loads(config_text)
        except json.JSONDecodeError:
            # Try to extract JSON from text if it's wrapped
            import re
            json_match = re.search(r'\{.*\}', config_text, re.DOTALL)
            if json_match:
                config = json.loads(json_match.group())
            else:
                raise ValueError("Could not parse JSON from config response")
        
        clips = config.get("clips", [])
        if not clips:
            raise ValueError("No clips found in configuration")
        
        # Step 2: Generate video
        print(f"Job {job_id}: Generating {len(clips)} clips...")
        output_path = f"outputs/video_{job_id}.mp4"
        os.makedirs("outputs", exist_ok=True)
        
        final_video = await generate_complete_video(clips, output_path, quality)
        
        # Update job as completed
        update_job_status(job_id, "completed", video_path=final_video)
        print(f"Job {job_id}: Completed successfully!")
        
    except Exception as e:
        error_msg = str(e)
        print(f"Job {job_id}: Failed with error: {error_msg}")
        update_job_status(job_id, "failed", error=error_msg)

@app.post("/upload-pdf")
async def upload_pdf(file: UploadFile = File(...)):
    """Upload PDF file"""
    if not file.filename.endswith('.pdf'):
        raise HTTPException(status_code=400, detail="File must be a PDF")
    
    # Create uploads directory
    os.makedirs("uploads", exist_ok=True)
    
    # Generate unique filename
    file_id = str(uuid.uuid4())
    file_path = f"uploads/{file_id}_{file.filename}"
    
    # Save uploaded file
    with open(file_path, "wb") as f:
        content = await file.read()
        f.write(content)
    
    return {"file_id": file_id, "file_path": file_path, "filename": file.filename}

@app.post("/generate-video")
async def generate_video(request: VideoRequest, background_tasks: BackgroundTasks):
    """Start video generation job with PDF path"""
    
    if not request.pdf_path:
        raise HTTPException(status_code=400, detail="PDF path is required")
    
    # Check if PDF exists
    if not request.pdf_path.startswith('http') and not os.path.exists(request.pdf_path):
        raise HTTPException(status_code=400, detail="PDF file not found")
    
    # Generate job ID
    job_id = str(uuid.uuid4())
    
    # Create job record
    job_data = {
        "job_id": job_id,
        "status": "pending",
        "created_at": datetime.now().isoformat(),
        "pdf_path": request.pdf_path,
        "quality": request.quality,
        "video_name": None
    }
    
    jobs = load_jobs()
    jobs[job_id] = job_data
    save_jobs(jobs)
    
    # Start background task
    background_tasks.add_task(
        process_video_generation,
        job_id,
        request.pdf_path,
        request.use_base64,
        request.quality
    )
    
    return {"job_id": job_id, "status": "pending", "message": "Video generation started"}

@app.post("/generate-video-upload")
async def generate_video_upload(
    background_tasks: BackgroundTasks,
    file: UploadFile = File(...),
    quality: str = "low_quali
[truncated — 6813 more characters]
```

### app/layout.tsx

```typescript
import type { Metadata } from "next";
import "./globals.css";

export const metadata: Metadata = {
  title: "README",
  description: "to abstract and beyond",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

```

### app/page.tsx

```typescript
"use client"

import type React from "react"

import { useState } from "react"
import { Upload, FileText, Brain, Sparkles, AlertCircle, CheckCircle, RefreshCw } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { config, type VideoJobStatus, type GitHubRepo } from "@/lib/config"
import { extractGitHubRepos } from "@/lib/github-api"
import QuizPanel from "./components/quiz-panel"
import ConceptsDisplay from "./components/concepts-display"
import VideoPanel from "./components/video-panel"
import VideoLibrary from "./components/video-library"
import VideoPlayer from "./components/video-player"
import { GitHubReposPanel } from "./components/github-repos-panel"

interface QuizQuestion {
  question: string
  options: string[]
  correctAnswer: number
  concept: string
}

interface Concept {
  title: string
  summary: string
  citations: string[]
  importance: string
}

interface AnalysisResult {
  concepts: Concept[]
  questions: QuizQuestion[]
}

export default function ResearchAnalyzer() {
  const [file, setFile] = useState<File | null>(null)
  const [isAnalyzing, setIsAnalyzing] = useState(false)
  const [analysis, setAnalysis] = useState<AnalysisResult | null>(null)
  const [dragActive, setDragActive] = useState(false)
  const [error, setError] = useState<string | null>(null)
  const [successMessage, setSuccessMessage] = useState<string | null>(null)
  const [isFallbackData, setIsFallbackData] = useState(false)
  const [selectedVideo, setSelectedVideo] = useState<VideoJobStatus | null>(null)
  const [refreshLibrary, setRefreshLibrary] = useState(0)
  const [githubRepos, setGithubRepos] = useState<GitHubRepo[]>([])
  const [isExtractingGithub, setIsExtractingGithub] = useState(false)

  const handleDrag = (e: React.DragEvent) => {
    e.preventDefault()
    e.stopPropagation()
    if (e.type === "dragenter" || e.type === "dragover") {
      setDragActive(true)
    } else if (e.type === "dragleave") {
      setDragActive(false)
    }
  }

  const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
    const files = e.target.files
    if (files && files[0] && files[0].type === "application/pdf") {
      setFile(files[0])
      setError(null)
      setAnalysis(null)
      setSuccessMessage(null)
      setIsFallbackData(false)
    } else if (files && files[0]) {
      setError("Please select a PDF file")
    }
  }

  const handleDrop = (e: React.DragEvent) => {
    e.preventDefault()
    e.stopPropagation()
    setDragActive(false)

    const files = e.dataTransfer.files
    if (files && files[0]) {
      if (files[0].type === "application/pdf") {
        setFile(files[0])
        setError(null)
        setAnalysis(null)
        setSuccessMessage(null)
        setIsFallbackData(false)
        setGithubRepos([])
      } else {
        setError("Please select a PDF file")
      }
    }
  }

  const analyzeDocument = async () => {
    if (!file) return

    setIsAnalyzing(true)
    setError(null)
    setSuccessMessage(null)
    setIsFallbackData(false)
    setGithubRepos([])
    setIsExtractingGithub(true)

    try {
      const formData = new FormData()
      formData.append("pdf", file)

      // Run analysis and GitHub extraction in parallel
      const [analysisResponse, githubReposResult] = await Promise.allSettled([
        fetch("/api/analyze", {
          method: "POST",
          body: formData,
        }),
        extractGitHubRepos(file, {
          fetchReadmes: true,
          simplifyReadmes: true
        })
      ])

      // Handle GitHub extraction result
      if (githubReposResult.status === 'fulfilled') {
        setGithubRepos(githubReposResult.value)
      } else {
        console.warn("GitHub extraction failed:", githubReposResult.reason)
      }
      setIsExtractingGithub(false)

      // Handle analysis response
      if (analysisResponse.status === 'fulfilled') {
        const response = analysisResponse.value
        
        if (response.ok) {
        const result = await response.json()
        setAnalysis(result)

        // Check if we got fallback data
        if (result.concepts?.[0]?.title === "Research Document Analysis") {
          setIsFallbackData(true)
          setError(
            "Analysis completed with sample data. The AI had difficulty processing this specific document. You can still explore the interface with the sample content below.",
          )
        } else {
          const githubCount = githubRepos.length
          setSuccessMessage(
            `Successfully analyzed "${file.name}" and extracted ${result.concepts?.length || 0} concepts, ${result.questions?.length || 0} quiz questions${githubCount > 0 ? `, and found ${githubCount} GitHub repositories` : ''}!`,
          )
        }
      } else {
        // Even if response is not ok, try to get the data
        try {
          const result = await response.json()
          if (result.concepts && result.questions) {
            setAnalysis(result)
            setIsFallbackData(true)
            setError("Analysis completed with sample data due to processing issues.")
          } else {
            throw new Error("No valid data received")
          }
        } catch {
          setError("Analysis failed. Please try again with a different document.")
        }
        }
      } else {
        setError("Analysis failed. Please try again with a different document.")
      }
    } catch (error) {
      console.error("Error analyzing document:", error)
      setError("Network error. Please check your connection and try again.")
    } finally {
      setIsAnalyzing(false)
      setIsExtractingGithub(false)
    }
  }

  return (
    <div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100">
      <div className="container mx-auto px-4 py-8">
        {/* Header */}
        <div className="text-center mb-12">
          <div className="flex items-center justify-center gap-3 mb-4"
[truncated — 8649 more characters]
```

### app/api/analyze/route.ts

```typescript
import { createAnthropic } from "@ai-sdk/anthropic"
import { generateText } from "ai"

export const maxDuration = 60

// Initialize Anthropic with the provided API key
const anthropicClient = createAnthropic({
  apiKey:
    "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",
})

// Fallback data function
const getFallbackData = () => ({
  concepts: [
    {
      title: "Research Document Analysis",
      summary:
        "This document contains research content that has been uploaded for analysis. The system extracts key concepts and generates educational materials.",
      citations: ["Document successfully processed", "AI analysis completed"],
      importance: "Understanding research documents is essential for academic and professional development.",
    },
    {
      title: "Knowledge Extraction",
      summary:
        "The process of identifying and extracting meaningful information from academic papers using artificial intelligence.",
      citations: ["AI-powered content analysis", "Automated information processing"],
      importance: "Automated knowledge extraction helps researchers quickly understand complex documents.",
    },
    {
      title: "Interactive Learning",
      summary:
        "Creating engaging educational experiences through quiz generation and concept mapping from research materials.",
      citations: ["Quiz-based learning methodology", "Interactive educational tools"],
      importance: "Interactive learning methods improve comprehension and retention of academic material.",
    },
  ],
  questions: [
    {
      question: "What is the primary purpose of this research analysis tool?",
      options: [
        "To extract key concepts and create educational content",
        "To store PDF files",
        "To edit documents",
        "To compress files",
      ],
      correctAnswer: 0,
      concept: "Research Document Analysis",
    },
    {
      question: "How does the system process research documents?",
      options: ["Manual review only", "AI-powered analysis", "Random sampling", "User annotation"],
      correctAnswer: 1,
      concept: "Knowledge Extraction",
    },
    {
      question: "What type of learning experience does this tool create?",
      options: ["Passive reading", "Interactive quizzes and concepts", "Video streaming", "Audio playback"],
      correctAnswer: 1,
      concept: "Interactive Learning",
    },
    {
      question: "What should you do if the analysis doesn't work as expected?",
      options: [
        "Delete the application",
        "Try a different document or contact support",
        "Restart your computer",
        "Clear browser cache",
      ],
      correctAnswer: 1,
      concept: "Research Document Analysis",
    },
    {
      question: "What is the benefit of automated knowledge extraction?",
      options: [
        "It replaces human thinking",
        "It helps quickly understand complex documents",
        "It eliminates the need for reading",
        "It only works with simple texts",
      ],
      correctAnswer: 1,
      concept: "Knowledge Extraction",
    },
    {
      question: "How can interactive learning improve understanding?",
      options: [
        "By making content harder to access",
        "By improving comprehension and retention",
        "By reducing study time to zero",
        "By eliminating the need for practice",
      ],
      correctAnswer: 1,
      concept: "Interactive Learning",
    },
  ],
})

export async function POST(request: Request) {
  try {
    const formData = await request.formData()
    const file = formData.get("pdf") as File

    if (!file) {
      return new Response("No PDF file provided", { status: 400 })
    }

    console.log("Processing file:", file.name, "Size:", file.size)

    // Try AI analysis first
    try {
      const result = await generateText({
        model: anthropicClient("claude-3-5-sonnet-latest"),
        messages: [
          {
            role: "user",
            content: [
              {
                type: "text",
                text: `Analyze this research paper and return ONLY a valid JSON object with this exact structure:

{
  "concepts": [
    {
      "title": "Brief concept name",
      "summary": "2-3 sentence explanation",
      "citations": ["quote 1", "quote 2"],
      "importance": "Why this matters"
    }
  ],
  "questions": [
    {
      "question": "Question text",
      "options": ["Option A", "Option B", "Option C", "Option D"],
      "correctAnswer": 0,
      "concept": "Related concept"
    }
  ]
}

Requirements:
- Extract 3-6 key concepts
- Create 4-8 multiple choice questions
- Each question needs exactly 4 options
- correctAnswer must be 0, 1, 2, or 3
- Keep citations under 80 characters
- Return ONLY the JSON, no other text`,
              },
              {
                type: "file",
                data: await file.arrayBuffer(),
                mimeType: "application/pdf",
              },
            ],
          },
        ],
        maxRetries: 1,
      })

      // Try to parse the AI response
      let parsedResult
      try {
        // Clean the response
        let cleanedText = result.text.trim()

        // Remove markdown code blocks if present
        cleanedText = cleanedText.replace(/```json\s*/g, "").replace(/```\s*/g, "")

        // Find JSON boundaries
        const jsonStart = cleanedText.indexOf("{")
        const jsonEnd = cleanedText.lastIndexOf("}") + 1

        if (jsonStart !== -1 && jsonEnd > jsonStart) {
          cleanedText = cleanedText.substring(jsonStart, jsonEnd)
        }

        parsedResult = JSON.parse(cleanedText)

        // Validate and fix the structure
        if (!parsedResult.concepts || !Array.isArray(parsedResult.concepts)) {
          throw new Error("Invalid concepts structure")
        }
        if (!parsedResult.questions || !Array.isArray(parsedResult.questions)) {
          throw new Error
[truncated — 2334 more characters]
```

### tailwind.config.ts

```typescript
import type { Config } from "tailwindcss";

const config: Config = {
    darkMode: ["class"],
    content: [
    "./pages/**/*.{js,ts,jsx,tsx,mdx}",
    "./components/**/*.{js,ts,jsx,tsx,mdx}",
    "./app/**/*.{js,ts,jsx,tsx,mdx}",
    "*.{js,ts,jsx,tsx,mdx}"
  ],
  theme: {
  	extend: {
  		colors: {
  			background: 'hsl(var(--background))',
  			foreground: 'hsl(var(--foreground))',
  			card: {
  				DEFAULT: 'hsl(var(--card))',
  				foreground: 'hsl(var(--card-foreground))'
  			},
  			popover: {
  				DEFAULT: 'hsl(var(--popover))',
  				foreground: 'hsl(var(--popover-foreground))'
  			},
  			primary: {
  				DEFAULT: 'hsl(var(--primary))',
  				foreground: 'hsl(var(--primary-foreground))'
  			},
  			secondary: {
  				DEFAULT: 'hsl(var(--secondary))',
  				foreground: 'hsl(var(--secondary-foreground))'
  			},
  			muted: {
  				DEFAULT: 'hsl(var(--muted))',
  				foreground: 'hsl(var(--muted-foreground))'
  			},
  			accent: {
  				DEFAULT: 'hsl(var(--accent))',
  				foreground: 'hsl(var(--accent-foreground))'
  			},
  			destructive: {
  				DEFAULT: 'hsl(var(--destructive))',
  				foreground: 'hsl(var(--destructive-foreground))'
  			},
  			border: 'hsl(var(--border))',
  			input: 'hsl(var(--input))',
  			ring: 'hsl(var(--ring))',
  			chart: {
  				'1': 'hsl(var(--chart-1))',
  				'2': 'hsl(var(--chart-2))',
  				'3': 'hsl(var(--chart-3))',
  				'4': 'hsl(var(--chart-4))',
  				'5': 'hsl(var(--chart-5))'
  			},
  			sidebar: {
  				DEFAULT: 'hsl(var(--sidebar-background))',
  				foreground: 'hsl(var(--sidebar-foreground))',
  				primary: 'hsl(var(--sidebar-primary))',
  				'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
  				accent: 'hsl(var(--sidebar-accent))',
  				'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
  				border: 'hsl(var(--sidebar-border))',
  				ring: 'hsl(var(--sidebar-ring))'
  			}
  		},
  		borderRadius: {
  			lg: 'var(--radius)',
  			md: 'calc(var(--radius) - 2px)',
  			sm: 'calc(var(--radius) - 4px)'
  		},
  		keyframes: {
  			'accordion-down': {
  				from: {
  					height: '0'
  				},
  				to: {
  					height: 'var(--radix-accordion-content-height)'
  				}
  			},
  			'accordion-up': {
  				from: {
  					height: 'var(--radix-accordion-content-height)'
  				},
  				to: {
  					height: '0'
  				}
  			}
  		},
  		animation: {
  			'accordion-down': 'accordion-down 0.2s ease-out',
  			'accordion-up': 'accordion-up 0.2s ease-out'
  		}
  	}
  },
  plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],
};
export default config;

```

### gatherREADME.py

```python
import PyPDF2
import requests
import json
import re
import base64
from typing import List, Optional, Dict

class PDFGitHubExtractor:
    def __init__(self, claude_api_key: str, github_token: str = None):
        """
        Initialize the extractor with your Claude API key and GitHub token
        
        Args:
            claude_api_key: Your Anthropic API key
            github_token: Your GitHub personal access token (optional, but recommended)
        """
        self.claude_api_key = claude_api_key
        self.github_token = github_token
        self.claude_headers = {
            "Content-Type": "application/json",
            "x-api-key": self.claude_api_key,
            "anthropic-version": "2023-06-01"
        }
        
        # GitHub API headers
        self.github_headers = {
            "Accept": "application/vnd.github.v3+json",
            "User-Agent": "PDF-GitHub-Extractor"
        }
        if self.github_token:
            self.github_headers["Authorization"] = f"token {self.github_token}"
    
    def extract_text_from_pdf(self, pdf_path: str) -> str:
        """
        Extract text content from a PDF file
        
        Args:
            pdf_path: Path to the PDF file
            
        Returns:
            Extracted text as string
        """
        try:
            with open(pdf_path, 'rb') as file:
                pdf_reader = PyPDF2.PdfReader(file)
                text = ""
                
                for page in pdf_reader.pages:
                    text += page.extract_text() + "\n"
                
                return text.strip()
        except Exception as e:
            raise Exception(f"Error reading PDF: {str(e)}")
    
    def find_github_links_with_claude(self, text: str) -> List[str]:
        """
        Use Claude API to find GitHub links in the text
        
        Args:
            text: Text content to analyze
            
        Returns:
            List of GitHub links found
        """
        prompt = f"""
        Please analyze the following text and extract any GitHub repository URLs or links. 
        Return only the GitHub URLs, one per line, with no additional text or formatting.
        If no GitHub links are found, return "No GitHub links found".
        
        Text to analyze:
        {text[:4000]}  # Limit text to avoid token limits
        """
        
        payload = {
            "model": "claude-sonnet-4-20250514",
            "max_tokens": 1000,
            "messages": [
                {
                    "role": "user",
                    "content": prompt
                }
            ]
        }
        
        try:
            response = requests.post("https://api.anthropic.com/v1/messages", headers=self.claude_headers, json=payload)
            response.raise_for_status()
            
            result = response.json()
            content = result['content'][0]['text'].strip()
            
            if "No GitHub links found" in content:
                return []
            
            # Extract GitHub URLs from the response
            github_pattern = r'https?://github\.com/[^\s<>"\']*'
            github_links = re.findall(github_pattern, content, re.IGNORECASE)
            
            # Clean up and deduplicate
            clean_links = []
            for link in github_links:
                # Remove trailing punctuation
                link = re.sub(r'[.,;)]*$', '', link)
                if link not in clean_links:
                    clean_links.append(link)
            
            return clean_links
            
        except requests.exceptions.RequestException as e:
            raise Exception(f"API request failed: {str(e)}")
        except KeyError as e:
            raise Exception(f"Unexpected API response format: {str(e)}")
    
    def parse_github_url(self, github_url: str) -> Dict[str, str]:
        """
        Parse a GitHub URL to extract owner and repository name
        
        Args:
            github_url: GitHub repository URL
            
        Returns:
            Dictionary with 'owner' and 'repo' keys
        """
        # Remove trailing slashes and .git extension
        url = github_url.rstrip('/').replace('.git', '')
        
        # Extract owner and repo from URL
        pattern = r'github\.com/([^/]+)/([^/]+)'
        match = re.search(pattern, url, re.IGNORECASE)
        
        if not match:
            raise ValueError(f"Invalid GitHub URL format: {github_url}")
        
        return {
            'owner': match.group(1),
            'repo': match.group(2)
        }
    
    def fetch_readme(self, github_url: str) -> Optional[str]:
        """
        Fetch README content from a GitHub repository
        
        Args:
            github_url: GitHub repository URL
            
        Returns:
            README content as text, or None if not found
        """
        try:
            repo_info = self.parse_github_url(github_url)
            owner = repo_info['owner']
            repo = repo_info['repo']
            
            # Try common README file names
            readme_names = ['README.md', 'README.rst', 'README.txt', 'README', 'readme.md']
            
            for readme_name in readme_names:
                api_url = f"https://api.github.com/repos/{owner}/{repo}/contents/{readme_name}"
                
                try:
                    response = requests.get(api_url, headers=self.github_headers)
                    
                    if response.status_code == 200:
                        content_data = response.json()
                        
                        # Decode base64 content
                        if content_data.get('encoding') == 'base64':
                            content = base64.b64decode(content_data['content']).decode('utf-8')
                            return content
                        else:
                            # If not base64, try direct content
                            retur
[truncated — 11663 more characters]
```

### lib/utils.ts

```typescript
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

```

### components/theme-provider.tsx

```typescript
'use client'

import * as React from 'react'
import {
  ThemeProvider as NextThemesProvider,
  type ThemeProviderProps,
} from 'next-themes'

export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
  return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

```

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