# Project export: Impromptu

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: Cal Hacks 12.0
- Tagline: Duel out multiplayer prompt engineering tasks and let an AI judge decide who’s the best.
- Devpost: https://devpost.com/software/impromptu-vx6ic2
- GitHub: https://github.com/Rmandal01/Nxt
- Team: 4 GitHub contributor(s) — Bongs237 (59 commits), Rudrashis Mandal (40 commits), michellesheu (19 commits), Claude (18 commits)

## Devpost submission (written by the team)

### Overview

AI is the thing nowadays. Everyone in Cal Hacks has probably vibe coded something for their project. And it’s not just taking over tech; it’s affecting every industry. That means a new highly sought for skill is emerging in our society. That’s right, prompt engineering. So we thought… how can we prepare everyone for the future? Oh yeah, of course. Why not MAKE A GAME?... Introducing Impromptu! Each game, everyone gets the same topic - something tricky, weird, or creative. Your mission? Prompt the AI to give the best possible response to that topic. Once everyone submits their prompts, we unleash our AI judges, including personalities like AI Gordon Ramsay, who’s never afraid to tell you if you cooked or served up raw garbage. 🍳 What Inspired Us We were inspired by the sudden, massive rise of large language models like ChatGPT. We noticed that everyone was getting wildly different results, and it became clear that "prompt engineering" is a new, creative skill. We saw a fun opportunity to take this new skill and turn it into a hilarious, competitive party game, much in the spirit of Jackbox Games, but built for the AI generation. We wanted to answer the question: who is really the best at talking to machines? How We Built It Impromptu is a full-stack web application using Next.js. For the multiplayer functionality, we used Supabase Realtime to handle our realtime changes to the database. The backend consists of Next.js API Routes. These routes manage the core game logic, but most importantly, they securely handle the calls to two separate LLM APIs: one to generate the player's creative response and a second, specialized "Judge AI" to critically analyze and score all the submissions. We also used Mastra to add a “deep research” feature. The Challenges We Faced The challenges we faced were significant, as this project was ambitious on two fronts. First, for most of the team, this meant learning a brand new tech stack from scratch. Second, this was our first time making a multiplayer game, and we quickly learned that managing real-time game state, player turns, and asynchronous events across multiple clients was far more complex than we realized. We had two possible solutions to add functionality to update state in real time. We could use WebSockets, which would not be compatible with Next.js because when you deploy Next.js to a service like Vercel, they turn your backend into serverless functions. That means that nothing is running on one central server. Alternatively, we could use a realtime database like Supabase that makes it easy to store data and check for updates live. Since the backend is a third party service, it doesn’t matter where it’s hosted. This seemed like the more efficient approach, so we went with Supabase. However there were downsides to choosing a realtime database service. If we wanted to add any actual server side code, such as watching the database until all players have submitted a response to start AI judging, it wouldn’t be straightforward. The solution to that was to use Supabase Edge Functions which are small server actions that can be run when a certain condition is met. This is caused by a trigger on the Postgres database. In addition, there are countless race conditions that could happen with doing asynchronous/realtime operations like this (and that we probably have not spotted yet). For example, we encountered a bug where, when someone joined a game, we created a user profile in the database and tried to assign the player to the room at the same time. But you need to have the user profile created first to add them to a room, so if the room add happens first you’d result in an error. What We Learned Through this project, we learned a tremendous amount about modern, full-stack web development. We gained practical experience with the entire Next.js ecosystem, from building interactive React components to writing robust, serverless backend logic. We also learned the core principles of real-time application design and the complexities of state management in a multiplayer environment. Most interestingly, we learned about the nuances of AI prompting itself. It's one thing to prompt an AI for a creative answer, but it's a completely different and fascinating challenge to engineer a prompt that forces an AI to act as an impartial, critical judge and return a consistent, structured score. Built with We built the frontend using Next.js and React, the realtime database with Supabase Realtime, and the backend with Next.js API routes. We called the Google Gemini 2.0 Flash Experimental API, Anthropic’s Claude Sonnet 4.5, and OpenAI’s GPT 4o mini in the chat room interface so that users can experiment with different models.

## README (from the GitHub repository)

# Prompt Battle - Real-Time Multiplayer Setup Guide

A real-time multiplayer prompt competition game where two players compete with their prompts and Gemini AI judges the winner.

## Features

- Real-time room-based multiplayer using Supabase Realtime
- Create or join game rooms with unique 6-digit codes
- Waiting room with ready status
- 3-2-1 countdown before battle starts
- 60-second timer for prompt submission
- Gemini AI judges prompts on creativity and quality
- Beautiful results screen with scores and AI reasoning
- Player statistics tracking (wins/losses)

## Prerequisites

- Node.js 18+ installed
- A Supabase account (free tier works!)
- A Google Gemini API key (free tier available)

---

## Step 1: Create Supabase Project

### 1.1 Sign Up and Create Project

1. Go to [https://supabase.com](https://supabase.com)
2. Click **"Start your project"** and sign up
3. Create a new project:
   - **Name**: `prompt-battle` (or your choice)
   - **Database Password**: Create a strong password (save it!)
   - **Region**: Choose closest to your location
   - **Plan**: Free tier is fine
4. Wait 1-2 minutes for setup to complete

### 1.2 Run Database Schema

1. In Supabase dashboard, click **"SQL Editor"** in the left sidebar
2. Click **"New query"**
3. Copy the entire contents of `supabase/schema.sql` from this project
4. Paste into the SQL Editor
5. Click **"Run"** or press `Ctrl+Enter`
6. You should see: "Success. No rows returned"

This creates all tables, security policies, indexes, and enables real-time subscriptions.

### 1.3 Get API Keys

1. Go to **Settings** → **API** in the left sidebar
2. Copy these three values:

   ```
   Project URL: https://xxxxxxxxxxxxx.supabase.co
   anon/public key: eyJhbGci...
   service_role key: eyJhbGci... (keep this secret!)
   ```

### 1.4 Enable Authentication

1. Go to **Authentication** → **Providers**
2. Ensure **Email** provider is enabled
3. For easier testing, go to **Authentication** → **Settings**:
   - Disable "Confirm email" (enable later in production)
   - Click **Save**

---

## Step 2: Get Gemini API Key

1. Go to [https://aistudio.google.com/app/apikey](https://aistudio.google.com/app/apikey)
2. Sign in with your Google account
3. Click **"Create API Key"**
4. Select **"Create API key in new project"** or choose existing
5. Copy the API key

---

## Step 3: Configure Environment Variables

1. Create `.env.local` file in your project root:

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

2. Edit `.env.local` with your actual keys, e.g.:
```env
# Supabase Configuration
NEXT_PUBLIC_SUPABASE_URL=https://xxxxxxxxxxxxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGci...your_anon_key
SUPABASE_SERVICE_ROLE_KEY=eyJhbGci...your_service_role_key

# Gemini AI Configuration
GEMINI_API_KEY=your_gemini_api_key_here
```

**Important**: Never commit `.env.local` to git! It's already in `.gitignore`.

---
## Step 4: Deploy Edge Function to Supabase

### 4.1 Log in to Supabase through the CLI
```bash
npx supabase login
```
You should get either a browser link or a URL where you can enter a verification code.

### 4.2 Deploy the ai-judge function
This is the backend function that will be called when everyone's submitted their prompts. Select the Supabase project that you're using for this repo.
```bash
npx supabase functions deploy ai-judge
```

### 4.3 Import the secrets from your .env file onto the server.
```bash
npx supabase secrets set --env-file .env.local
```

### 4.4 Add SQL trigger
Copy the contents of the file `supabase/judge_func_example.sql` into the Supabase SQL editor. This is so when everyone's submitted a prompt, it will trigger the edge function.
**Make sure to replace the edge function URL in the file with the one in Supabase dashboard > Edge Functions > copy your edge function URL in the table, and replace <your-anon-key> with your actual anon key.**


## Step 5: Install Dependencies

```bash
npm install
```

## Step 6: Run the Development Server

```bash
npm run dev
```

Open [http://localhost:3000](http://localhost:3000) in your browser.

---

## Step 7: Test the Game

### Testing Requires Two Players

Since this is a multiplayer game, you need two browser sessions:

#### Browser 1 (Player 1)
1. Open http://localhost:3000 in **Incognito/Private mode**
2. Sign up with email (e.g., `player1@test.com`)
3. Click **"Create New Room"**
4. Copy the 6-digit room code displayed

#### Browser 2 (Player 2)
1. Open http://localhost:3000 in a **different browser or regular mode**
2. Sign up with different email (e.g., `player2@test.com`)
3. Click **"Join Room"**
4. Enter the room code from Player 1

#### Play the Game
1. Both players click **"Ready"**
2. Watch the 3-2-1 countdown
3. Submit your best prompts within 60 seconds
4. Wait for Gemini AI to judge
5. View results with scores and reasoning!

---

## Project Structure

```
├── app/
│   ├── api/
│   │   ├── judge/route.ts          # Gemini AI judging endpoint
│   │   └── rooms/
│   │       ├── create/route.ts     # Create game room
│   │       ├── join/route.ts       # Join existing room
│   │       ├── ready/route.ts      # Toggle ready status
│   │       └── submit-prompt/route.ts # Submit prompt
│   ├── room/[roomCode]/page.tsx    # Dynamic room page
│   ├── layout.tsx                  # Root layout
│   └── page.tsx                    # Home page
├── components/
│   ├── BattleArena.tsx             # Battle UI with timer
│   ├── Results.tsx                 # Results screen
│   └── RoomLobby.tsx               # Waiting room
├── lib/
│   ├── supabase/
│   │   ├── client.ts               # Browser client
│   │   ├── server.ts               # Server client
│   │   └── admin.ts                # Admin client
│   └── types/
│       └── database.types.ts       # TypeScript types
└── supabase/
    └── schema.sql                  # Database schema
```

---

## How It Works

### Game Flow

1. **Room Creation**: Host creates a room → unique 6-digit code generated
2. **Joining**: Other player joins with room code
3. **Lobby**: Both players mark ready → auto-starts when all ready
4. **Countdown**: 3-2-1 countdown animation
5. **Battle**: 60-second timer, both submit prompts
6. **Judging**: Gemini AI evaluates based on:
   - Creativity and originality
   - Clarity and specificity
   - Engagement potential
   - Overall quality
7. **Results**: Scores (0-100), winner declared, AI reasoning shown

### Real-Time Features

The app uses **Supabase Realtime** for:
- Room status updates (waiting → countdown → playing → finished)
- Player ready status
- Prompt submissions
- Results broadcasting

All participants see updates instantly without page refresh!

### Database Tables

- `profiles` - User profiles with username, wins, losses
- `game_rooms` - Active game rooms with status
- `game_participants` - Players in each room
- `game_results` - Battle outcomes with AI reasoning

---

## Customization

### Change Battle Duration

Edit `countdown_duration` in `supabase/schema.sql`:

```sql
countdown_duration INTEGER DEFAULT 60  -- Change to 120 for 2 minutes
```

Then update the room in Supabase dashboard or re-run schema.

### Change Max Players

Currently supports 2 players. To change:

```sql
max_players INTEGER DEFAULT 2  -- Change to 4 for 4 players
```

### Customize AI Judging

Edit the judging prompt in `app/api/judge/route.ts` to change criteria.

---

## Troubleshooting

### "Unauthorized" Errors
- Verify users are signed up and logged in
- Check `.env.local` has correct Supabase keys
- Ensure tables exist in Supabase (run schema.sql)

### Real-time Not Working
- Check browser console for errors
- Verify Realtime is enabled: SQL Editor → run:
  ```sql
  ALTER PUBLICATION supabase_realtime ADD TABLE game_rooms;
  ALTER PUBLICATION supabase_realtime ADD TABLE game_participants;
  ALTER PUBLICATION supabase_realtime ADD TABLE game_results;
  ```

### Gemini API Errors
- Verify API key in `.env.local`
- Check quota at [https://aistudio.google.com](https://aistudio.google.com)


[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 99 recognized source files, 358 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- Google Gemini (technology) — detected in the code
- JavaScript (language) — detected in the code
- Next.js (technology) — detected in the code
- OpenAI (technology) — detected in the code
- React (technology) — detected in the code
- SQL (language) — detected in the code
- Supabase (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Vercel AI SDK (technology) — detected in the code
- AI coding agent: Claude Code — evidence: commit authorship or trailers

## Codebase structure (from repository index)

### Files (112 of 112)

```
.env.example
.gitignore
.npmrc
app/api/ai/route.ts
app/api/judge/route.ts
app/api/research/route.ts
app/api/rooms/create/route.ts
app/api/rooms/join/route.ts
app/api/rooms/ready/route.ts
app/api/rooms/submit-prompt/route.ts
app/api/tts/route.ts
app/battle/[roomId]/page.tsx
app/globals.css
app/layout.tsx
app/leaderboard/page.tsx
app/learn/page.tsx
app/page.tsx
app/playground/page.tsx
app/waiting/[roomId]/page.tsx
components.json
components/GameResults.tsx
components/MarkdownComponent.tsx
components/navigation.tsx
components/ScoreBreakdown.tsx
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-group.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/empty.tsx
components/ui/field.tsx
components/ui/form.tsx
components/ui/hover-card.tsx
components/ui/input-group.tsx
components/ui/input-otp.tsx
components/ui/input.tsx
components/ui/item.tsx
components/ui/kbd.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/spinner.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
DEPLOY_CHECKLIST.md
DEPLOY_EDGE_FUNCTION.md
eslint.config.mjs
hooks/use-mobile.ts
hooks/use-toast.ts
lib/agents/research-agent.ts
lib/supabase/admin.ts
lib/supabase/client.ts
lib/supabase/server.ts
lib/types/database.types.ts
lib/utils.ts
MIGRATION_INSTRUCTIONS.md
next.config.mjs
next.config.ts
package.json
postcss.config.mjs
README.md
scripts/migrate-database.js
styles/globals.css
supabase/.temp/cli-latest
supabase/config.toml
supabase/functions/ai-judge/.npmrc
supabase/functions/ai-judge/deno.json
supabase/functions/ai-judge/index.ts
supabase/judge_func_example.sql
supabase/migrations/add_judging_criteria_safe.sql
supabase/migrations/add_judging_criteria.sql
supabase/migrations/add_participant_scores_rls.sql
supabase/schema.sql
tsconfig.json
```

### Dependencies

- package.json: @ai-sdk/anthropic@^2.0.37, @ai-sdk/google@^2.0.23, @ai-sdk/openai@^2.0.53, @ai-sdk/react@^2.0.78, @google/generative-ai@^0.24.1, @hookform/resolvers@^3.10.0, @mastra/core@^0.23.1, @radix-ui/react-accordion@1.2.2, @radix-ui/react-alert-dialog@1.1.4, @radix-ui/react-aspect-ratio@1.1.1, @radix-ui/react-avatar@latest, @radix-ui/react-checkbox@1.1.3, @radix-ui/react-collapsible@1.1.2, @radix-ui/react-context-menu@2.2.4, @radix-ui/react-dialog@1.1.4, @radix-ui/react-dropdown-menu@2.1.4, @radix-ui/react-hover-card@1.1.4, @radix-ui/react-label@2.1.1, @radix-ui/react-menubar@1.1.4, @radix-ui/react-navigation-menu@1.2.3, @radix-ui/react-popover@1.1.4, @radix-ui/react-progress@latest, @radix-ui/react-radio-group@1.2.2, @radix-ui/react-scroll-area@1.2.2, @radix-ui/react-select@latest, @radix-ui/react-separator@1.1.1, @radix-ui/react-slider@1.2.2, @radix-ui/react-slot@1.1.1, @radix-ui/react-switch@1.1.2, @radix-ui/react-tabs@latest, @radix-ui/react-toast@1.2.4, @radix-ui/react-toggle@1.1.1, @radix-ui/react-toggle-group@1.1.1, @radix-ui/react-tooltip@1.1.6, @supabase/ssr@^0.7.0, @supabase/supabase-js@^2.76.1, @tailwindcss/postcss@^4.1.9, @types/node@^22, @types/react@^19, @types/react-dom@^19, @vercel/analytics@latest, ai@^5.0.78, autoprefixer@^10.4.20, class-variance-authority@^0.7.1, clsx@^2.1.1, cmdk@1.0.4, date-fns@4.1.0, embla-carousel-react@8.5.1, eslint@^9, eslint-config-next@16.0.0, framer-motion@^12.23.24, input-otp@1.4.1, lucide-react@^0.548.0, mastra@^0.17.5, next@16.0.0, next-themes@^0.4.6, postcss@^8.5, react@19.2.0, react-day-picker@9.8.0, react-dom@19.2.0, react-hook-form@^7.60.0, react-markdown@^10.1.0, react-resizable-panels@^2.1.7, recharts@2.15.4, remark-breaks@^4.0.0, sonner@^1.7.4, tailwind-merge@^2.5.5, tailwindcss@^4.1.9, tailwindcss-animate@^1.0.7, tw-animate-css@1.3.3, typescript@^5, vaul@^0.9.9, zod@^4.1.12

### Recent commits (newest first)

- Merge branch 'main' of https://github.com/Rmandal01/Nxt
- pushing the demo
- change header
- remove quickstart options
- make ui cleaner with tabs on homepage. also maybe fixed a race
- Add more topics, change submit btn text
- Make ai judge based on topic
- randomly generate topic and display on battle page
- Merge branch 'main' of https://github.com/Rmandal01/Nxt into topics
- change system prompt to say that user is allowed to switch models
- Update script to trim out whitespace in prompt, also i got rid of some
- Added new logo
- Fix multi model support
- Merge branch 'main' of https://github.com/Rmandal01/Nxt
- change "Battle Topic" to "Topic"
- Add multi-model AI support with OpenAI, Claude, and Gemini
- the multimodel selection switching button
- i did something
- add option to use either claude or gemini
- remove cleanup old result

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

### MIGRATION_INSTRUCTIONS.md

```markdown
# Database Migration Instructions

## 🚨 IMPORTANT: Run This Migration to Enable Judging Criteria

Your judging system is already coded and ready, but the database tables don't exist yet!

### Steps to Enable Full Judging Criteria:

1. **Open Supabase Dashboard**
   - Go to https://supabase.com/dashboard
   - Select your project

2. **Open SQL Editor**
   - Click on "SQL Editor" in the left sidebar
   - Click "New query"

3. **Copy the Migration SQL**
   - Open the file: `supabase/migrations/add_judging_criteria.sql`
   - Copy ALL the SQL code (lines 1-52)

4. **Run the Migration**
   - Paste the SQL into the editor
   - Click "Run" or press Ctrl+Enter
   - You should see "Success. No rows returned"

5. **Verify**
   - Go to "Table Editor" in the left sidebar
   - You should now see a new table called `participant_scores`

### What This Migration Does:

✅ Creates `participant_scores` table to store:
   - Creativity score (0-10)
   - Effectiveness score (0-10)
   - Clarity score (0-10)
   - Originality score (0-10)
   - Total score (0-40)
   - Individual feedback

✅ Creates database functions:
   - `increment_wins(user_id)` - Updates winner's win count
   - `increment_losses(user_id)` - Updates loser's loss count

✅ Sets up security policies and indexes

✅ Enables real-time subscriptions

### After Running:

Once the migration is complete, your judging system will:
- Show beautiful score breakdowns with progress bars
- Display individual criteria scores for each participant
- Provide personalized AI feedback
- Show color-coded performance indicators (green/yellow/orange/red)

**NO CODE CHANGES NEEDED** - just run the migration and refresh your app!

```

### DEPLOY_EDGE_FUNCTION.md

```markdown
# Deploy Edge Function to Supabase

## 🚨 IMPORTANT: Deploy the Updated AI Judge Function

The judging system uses a **Supabase Edge Function** that needs to be deployed with your Gemini API key.

## Prerequisites

1. Install Supabase CLI:
   ```bash
   npm install -g supabase
   ```

2. Login to Supabase:
   ```bash
   supabase login
   ```

3. Link to your project:
   ```bash
   supabase link --project-ref YOUR_PROJECT_REF
   ```
   (Get YOUR_PROJECT_REF from your Supabase project URL: `https://supabase.com/dashboard/project/YOUR_PROJECT_REF`)

## Deploy Steps

### Step 1: Set the Gemini API Key Secret

```bash
supabase secrets set GEMINI_API_KEY=YOUR_GEMINI_API_KEY_HERE
```

Get your Gemini API key from: https://aistudio.google.com/app/apikey

### Step 2: Deploy the Edge Function

```bash
supabase functions deploy ai-judge
```

This will deploy the updated `supabase/functions/ai-judge/index.ts` file that includes:
- Real AI judging with Gemini
- 4 criteria scoring (Creativity, Effectiveness, Clarity, Originality)
- Detailed feedback for each participant
- Automatic score saving to `participant_scores` table

### Step 3: Verify Deployment

After deployment, the output should show:
```
Deployed Function ai-judge on project YOUR_PROJECT_REF
...
```

## Testing

1. Start a new game in your app
2. Submit prompts from multiple participants
3. Wait for all participants to submit
4. The Edge Function will automatically run
5. Check the results page - you should see:
   - Individual criteria scores (0-10 each)
   - Progress bars with colors
   - Detailed AI feedback
   - Total score out of 40 points
   - NO MORE "randomly selected" message!

## Troubleshooting

If judging fails, check the Edge Function logs:

```bash
supabase functions logs ai-judge
```

Common issues:
- **GEMINI_API_KEY not set**: Run Step 1 again
- **Invalid API key**: Verify your key at https://aistudio.google.com
- **404 errors**: Make sure you deployed after pulling the latest code

## Alternative: Manual Deployment via Dashboard

1. Go to Supabase Dashboard → Edge Functions
2. Select `ai-judge` function
3. Upload the file from `supabase/functions/ai-judge/index.ts`
4. Go to Settings → Secrets
5. Add secret: `GEMINI_API_KEY` with your key
6. Deploy the function

---

After deployment, **start a new game** to see the full judging criteria system in action! 🎉

```

### package.json

```
{
  "name": "calofthehacks",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "build": "next build",
    "dev": "next dev",
    "lint": "eslint .",
    "start": "next start"
  },
  "dependencies": {
    "@ai-sdk/anthropic": "^2.0.37",
    "@ai-sdk/google": "^2.0.23",
    "@ai-sdk/openai": "^2.0.53",
    "@ai-sdk/react": "^2.0.78",
    "@google/generative-ai": "^0.24.1",
    "@hookform/resolvers": "^3.10.0",
    "@mastra/core": "^0.23.1",
    "@radix-ui/react-accordion": "1.2.2",
    "@radix-ui/react-alert-dialog": "1.1.4",
    "@radix-ui/react-aspect-ratio": "1.1.1",
    "@radix-ui/react-avatar": "latest",
    "@radix-ui/react-checkbox": "1.1.3",
    "@radix-ui/react-collapsible": "1.1.2",
    "@radix-ui/react-context-menu": "2.2.4",
    "@radix-ui/react-dialog": "1.1.4",
    "@radix-ui/react-dropdown-menu": "2.1.4",
    "@radix-ui/react-hover-card": "1.1.4",
    "@radix-ui/react-label": "2.1.1",
    "@radix-ui/react-menubar": "1.1.4",
    "@radix-ui/react-navigation-menu": "1.2.3",
    "@radix-ui/react-popover": "1.1.4",
    "@radix-ui/react-progress": "latest",
    "@radix-ui/react-radio-group": "1.2.2",
    "@radix-ui/react-scroll-area": "1.2.2",
    "@radix-ui/react-select": "latest",
    "@radix-ui/react-separator": "1.1.1",
    "@radix-ui/react-slider": "1.2.2",
    "@radix-ui/react-slot": "1.1.1",
    "@radix-ui/react-switch": "1.1.2",
    "@radix-ui/react-tabs": "latest",
    "@radix-ui/react-toast": "1.2.4",
    "@radix-ui/react-toggle": "1.1.1",
    "@radix-ui/react-toggle-group": "1.1.1",
    "@radix-ui/react-tooltip": "1.1.6",
    "@supabase/ssr": "^0.7.0",
    "@supabase/supabase-js": "^2.76.1",
    "@vercel/analytics": "latest",
    "ai": "^5.0.78",
    "autoprefixer": "^10.4.20",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "cmdk": "1.0.4",
    "date-fns": "4.1.0",
    "embla-carousel-react": "8.5.1",
    "framer-motion": "^12.23.24",
    "input-otp": "1.4.1",
    "lucide-react": "^0.548.0",
    "mastra": "^0.17.5",
    "next": "16.0.0",
    "next-themes": "^0.4.6",
    "react": "19.2.0",
    "react-day-picker": "9.8.0",
    "react-dom": "19.2.0",
    "react-hook-form": "^7.60.0",
    "react-markdown": "^10.1.0",
    "react-resizable-panels": "^2.1.7",
    "recharts": "2.15.4",
    "remark-breaks": "^4.0.0",
    "sonner": "^1.7.4",
    "tailwind-merge": "^2.5.5",
    "tailwindcss-animate": "^1.0.7",
    "vaul": "^0.9.9",
    "zod": "^4.1.12"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4.1.9",
    "@types/node": "^22",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "eslint": "^9",
    "eslint-config-next": "16.0.0",
    "postcss": "^8.5",
    "tailwindcss": "^4.1.9",
    "tw-animate-css": "1.3.3",
    "typescript": "^5"
  }
}

```

### app/layout.tsx

```typescript
import type React from "react";
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { Analytics } from "@vercel/analytics/next";
import { Toaster } from "@/components/ui/toaster";
import "./globals.css";

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

export const metadata: Metadata = {
  title: "Impromptu - Learn AI Prompt Engineering",
  description:
    "Master prompt engineering through competitive battles and interactive learning",
  generator: "v0.app",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en" className="dark">
      <body
        className={`${geist.className} ${geistMono.variable} font-sans antialiased`}
      >
        {children}
        <Toaster />
        <Analytics />
      </body>
    </html>
  );
}

```

### app/page.tsx

```typescript
"use client"

import { useState } from "react"
import { useRouter } from "next/navigation"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Card } from "@/components/ui/card"
import { Sparkles, Zap, Trophy, BookOpen, Users, Code2 } from "lucide-react"
import { Navigation } from "@/components/navigation"
import { createClient } from "@/lib/supabase/client"
import { useToast } from "@/hooks/use-toast"

export default function HomePage() {
  const [playerName, setPlayerName] = useState("")
  const [roomCode, setRoomCode] = useState("")
  const [isCreating, setIsCreating] = useState(false)
  const [isJoining, setIsJoining] = useState(false)
  const [activeTab, setActiveTab] = useState<"host" | "join">("host")
  const router = useRouter()
  const { toast } = useToast()

  const handleCreateRoom = async () => {
    if (!playerName.trim()) {
      toast({
        title: "Name required",
        description: "Please enter your name to create a room",
        variant: "destructive",
      })
      return
    }

    setIsCreating(true)
    try {
      const supabase = createClient()

      // Sign in anonymously with player name as metadata
      const { data: authData, error: authError } = await supabase.auth.signInAnonymously({
        options: {
          data: {
            player_name: playerName,
          },
        },
      })

      if (authError) throw authError

      // Create room via API
      const response = await fetch("/api/rooms/create", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
      })

      if (!response.ok) {
        const errorData = await response.json()
        throw new Error(errorData.error || "Failed to create room")
      }

      const data = await response.json()

      toast({
        title: "Room created!",
        description: `Room code: ${data.room.room_code}`,
      })

      // Navigate to waiting room
      router.push(`/waiting/${data.room.id}`)
    } catch (error) {
      console.error("Error creating room:", error)
      toast({
        title: "Error",
        description: error instanceof Error ? error.message : "Failed to create room",
        variant: "destructive",
      })
    } finally {
      setIsCreating(false)
    }
  }

  const handleJoinRoom = async () => {
    console.log("Join room clicked! playerName:", playerName, "roomCode:", roomCode)

    if (!playerName.trim()) {
      toast({
        title: "Name required",
        description: "Please enter your name to join a room",
        variant: "destructive",
      })
      return
    }

    if (roomCode.length !== 6) {
      toast({
        title: "Invalid room code",
        description: "Room code must be 6 characters",
        variant: "destructive",
      })
      return
    }

    setIsJoining(true)
    try {
      const supabase = createClient()

      // Sign in anonymously with player name as metadata
      const { data: authData, error: authError } = await supabase.auth.signInAnonymously({
        options: {
          data: {
            player_name: playerName,
          },
        },
      })

      if (authError) throw authError

      // Join room via API
      const response = await fetch("/api/rooms/join", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ roomCode }),
      })

      if (!response.ok) {
        const errorData = await response.json()
        throw new Error(errorData.error || "Failed to join room")
      }

      const data = await response.json()

      toast({
        title: "Joined room!",
        description: `Welcome to room ${roomCode}`,
      })

      // Navigate to waiting room
      router.push(`/waiting/${data.room.id}`)
    } catch (error) {
      console.error("Error joining room:", error)
      toast({
        title: "Error",
        description: error instanceof Error ? error.message : "Failed to join room",
        variant: "destructive",
      })
    } finally {
      setIsJoining(false)
    }
  }

  return (
    <>
      <Navigation />

      <div>
        <div className="relative z-10 container mx-auto px-4 py-12">
          <div className="grid lg:grid-cols-2 gap-12 items-center max-w-7xl mx-auto">
            {/* Left side - Hero content */}
            <div className="space-y-8 animate-slide-up" style={{ animationDelay: "0.1s" }}>
              <div className="space-y-4">
                <div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-primary/10 border border-primary/20 text-sm text-primary">
                  <Zap className="w-4 h-4" />
                  Master AI Prompt Engineering
                </div>
                <h2 className="text-5xl font-bold leading-tight text-balance">
                  Battle. Learn.{" "}
                  <span className="bg-gradient-to-r from-primary via-accent to-primary bg-clip-text text-transparent animate-gradient">
                    Dominate.
                  </span>
                </h2>
                <p className="text-xl text-muted-foreground text-pretty">
                  Challenge friends in real-time prompt engineering battles. Test your skills, learn from the best, and
                  become an AI prompt master.
                </p>
              </div>

              {/* Feature highlights */}
              <div className="grid grid-cols-2 gap-4">
                <Card className="p-4 glass-effect border-primary/20 hover:border-primary/40 transition-colors">
                  <Users className="w-8 h-8 text-primary mb-2" />
                  <h3 className="font-semibold mb-1">Real-time Battles</h3>
                  <p className="text-sm text-muted-foreground">Compete head-to-head with other players</p>
                </Card>
                <Card className="p-4 glass-effect border-accent/20 hover:border-accent/40 transition-colors">
                  <Code2 className
[truncated — 5607 more characters]
```

### lib/supabase/server.ts

```typescript
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'

export async function createClient() {
  const cookieStore = await cookies()

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll()
        },
        setAll(cookiesToSet) {
          try {
            cookiesToSet.forEach(({ name, value, options }) =>
              cookieStore.set(name, value, options)
            )
          } catch {
            // The `setAll` method was called from a Server Component.
            // This can be ignored if you have middleware refreshing
            // user sessions.
          }
        },
      },
    }
  )
}

```

### app/leaderboard/page.tsx

```typescript
"use client"

import { Card } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Trophy, Medal, Crown, TrendingUp, Zap, Target } from "lucide-react"
import { Navigation } from "@/components/navigation"

export default function LeaderboardPage() {
  const topPlayers = [
    { rank: 1, name: "PromptMaster", points: 15420, wins: 142, winRate: 89, trend: "up" },
    { rank: 2, name: "AIWhisperer", points: 14230, wins: 128, winRate: 85, trend: "up" },
    { rank: 3, name: "CodeCraft", points: 13890, wins: 121, winRate: 82, trend: "same" },
    { rank: 4, name: "PromptNinja", points: 12450, wins: 115, winRate: 80, trend: "down" },
    { rank: 5, name: "AIArtisan", points: 11920, wins: 108, winRate: 78, trend: "up" },
    { rank: 6, name: "PromptGuru", points: 11340, wins: 102, winRate: 76, trend: "up" },
    { rank: 7, name: "DataDruid", points: 10890, wins: 98, winRate: 74, trend: "same" },
    { rank: 8, name: "AIAlchemist", points: 10230, wins: 92, winRate: 72, trend: "down" },
    { rank: 9, name: "PromptSage", points: 9870, wins: 88, winRate: 70, trend: "up" },
    { rank: 10, name: "CodeMystic", points: 9450, wins: 84, winRate: 68, trend: "same" },
  ]

  const getRankIcon = (rank: number) => {
    if (rank === 1) return <Crown className="w-5 h-5 text-yellow-500" />
    if (rank === 2) return <Medal className="w-5 h-5 text-gray-400" />
    if (rank === 3) return <Medal className="w-5 h-5 text-amber-700" />
    return <span className="text-muted-foreground font-semibold">#{rank}</span>
  }

  const getTrendIcon = (trend: string) => {
    if (trend === "up") return <TrendingUp className="w-4 h-4 text-success" />
    if (trend === "down") return <TrendingUp className="w-4 h-4 text-destructive rotate-180" />
    return <div className="w-4 h-4" />
  }

  return (
    <>
      <Navigation />

      <div className="min-h-screen relative overflow-hidden">
        {/* Animated background */}
        <div className="absolute inset-0 bg-gradient-to-br from-primary/10 via-background to-accent/10 animate-gradient" />
        <div className="absolute inset-0 bg-[linear-gradient(rgba(255,255,255,0.02)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,0.02)_1px,transparent_1px)] bg-[size:64px_64px]" />

        <div className="relative z-10 container mx-auto px-4 py-8">
          {/* Header */}
          <div className="mb-8 animate-slide-up">
            <h1 className="text-3xl font-bold mb-2">Leaderboard</h1>
            <p className="text-muted-foreground">Top prompt engineers from around the world</p>
          </div>

          {/* Top 3 podium */}
          <div className="grid md:grid-cols-3 gap-6 mb-8 animate-slide-up" style={{ animationDelay: "0.1s" }}>
            {topPlayers.slice(0, 3).map((player, index) => (
              <Card
                key={player.rank}
                className={`p-6 glass-effect text-center ${
                  player.rank === 1
                    ? "border-yellow-500/50 md:order-2 md:scale-110"
                    : player.rank === 2
                      ? "border-gray-400/50 md:order-1"
                      : "border-amber-700/50 md:order-3"
                }`}
              >
                <div className="flex justify-center mb-4">{getRankIcon(player.rank)}</div>
                <Avatar className="w-20 h-20 mx-auto mb-4 border-2 border-primary">
                  <AvatarFallback className="text-xl font-bold bg-gradient-to-br from-primary to-accent text-primary-foreground">
                    {player.name.slice(0, 2).toUpperCase()}
                  </AvatarFallback>
                </Avatar>
                <h3 className="font-bold text-lg mb-2">{player.name}</h3>
                <div className="text-3xl font-bold text-primary mb-4">{player.points.toLocaleString()}</div>
                <div className="grid grid-cols-2 gap-4 text-sm">
                  <div>
                    <div className="text-muted-foreground">Wins</div>
                    <div className="font-semibold">{player.wins}</div>
                  </div>
                  <div>
                    <div className="text-muted-foreground">Win Rate</div>
                    <div className="font-semibold">{player.winRate}%</div>
                  </div>
                </div>
              </Card>
            ))}
          </div>

          <Tabs defaultValue="global" className="w-full">
            <TabsList className="grid w-full grid-cols-3 mb-6">
              <TabsTrigger value="global">Global</TabsTrigger>
              <TabsTrigger value="weekly">This Week</TabsTrigger>
              <TabsTrigger value="friends">Friends</TabsTrigger>
            </TabsList>

            <TabsContent value="global" className="space-y-3">
              {topPlayers.map((player, index) => (
                <Card
                  key={player.rank}
                  className="p-4 glass-effect border-primary/20 hover:border-primary/40 transition-all animate-slide-up"
                  style={{ animationDelay: `${index * 0.05}s` }}
                >
                  <div className="flex items-center gap-4">
                    <div className="w-12 flex items-center justify-center">{getRankIcon(player.rank)}</div>

                    <Avatar className="w-12 h-12 border-2 border-primary/20">
                      <AvatarFallback className="bg-gradient-to-br from-primary/20 to-accent/20 text-foreground font-semibold">
                        {player.name.slice(0, 2).toUpperCase()}
                      </AvatarFallback>
                    </Avatar>

                    <div className="flex-1">
                      <div className="font-semibold">{player.name}</div>
                      <div className="text-sm text-muted-foreground">{player.wins} wins</div>
                    </div>

                    <div className="hidde
[truncated — 1930 more characters]
```

### app/playground/page.tsx

```typescript
"use client"

import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
import { Textarea } from "@/components/ui/textarea"
import { Badge } from "@/components/ui/badge"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Sparkles, Play, RotateCcw, Copy, Download, Settings, Zap, Code2, Eye } from "lucide-react"
import { Navigation } from "@/components/navigation"

export default function PlaygroundPage() {
  const [prompt, setPrompt] = useState("")
  const [output, setOutput] = useState("")
  const [isGenerating, setIsGenerating] = useState(false)
  const [model, setModel] = useState("gemini-2.0-flash-exp")
  const [temperature, setTemperature] = useState("0.7")

  const handleGenerate = async () => {
    if (!prompt.trim()) return

    setIsGenerating(true)
    setOutput("")

    try {
      const response = await fetch('/api/ai', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          messages: [{ role: 'user', content: prompt }]
        })
      })

      if (!response.ok) {
        const errorText = await response.text()
        console.error('API Error Response:', errorText)
        throw new Error(`API returned ${response.status}: ${errorText}`)
      }

      const reader = response.body?.getReader()
      const decoder = new TextDecoder()
      let fullText = ""

      if (reader) {
        while (true) {
          const { done, value } = await reader.read()
          if (done) break

          const chunk = decoder.decode(value)
          const lines = chunk.split('\n')

          for (const line of lines) {
            if (!line.trim() || line === 'data: [DONE]') continue

            // Handle Server-Sent Events format: "data: {...}"
            if (line.startsWith('data: ')) {
              const jsonStr = line.substring(6) // Remove "data: " prefix
              try {
                const parsed = JSON.parse(jsonStr)

                // Handle text-delta events which contain the actual content
                if (parsed.type === 'text-delta' && parsed.delta) {
                  fullText += parsed.delta
                  setOutput(fullText)
                }
              } catch (e) {
                // Skip invalid JSON
              }
            }
          }
        }
      }
    } catch (error) {
      console.error('Error generating response:', error)
      setOutput('Error: Failed to generate response. Please try again.')
    } finally {
      setIsGenerating(false)
    }
  }

  const handleReset = () => {
    setPrompt("")
    setOutput("")
  }

  const handleCopy = () => {
    navigator.clipboard.writeText(output)
  }

  const promptTemplates = [
    {
      name: "Marketing Email",
      prompt:
        "Write a compelling marketing email for [product] that highlights [key benefits] and includes a clear call-to-action. Target audience: [audience]. Tone: [tone].",
    },
    {
      name: "Code Explanation",
      prompt:
        "Explain the following code in simple terms, breaking down what each part does and why it's important: [code snippet]",
    },
    {
      name: "Creative Story",
      prompt:
        "Write a short story about [topic] in the style of [author/genre]. Include: [specific elements]. Length: [word count].",
    },
    {
      name: "Data Analysis",
      prompt:
        "Analyze the following data and provide insights: [data]. Focus on: [specific aspects]. Present findings in [format].",
    },
  ]

  return (
    <>
      <Navigation />

      <div className="min-h-screen relative overflow-hidden">
        {/* Animated background */}
        <div className="absolute inset-0 bg-gradient-to-br from-primary/10 via-background to-accent/10 animate-gradient" />
        <div className="absolute inset-0 bg-[linear-gradient(rgba(255,255,255,0.02)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,0.02)_1px,transparent_1px)] bg-[size:64px_64px]" />

        <div className="relative z-10 container mx-auto px-4 py-8">
          {/* Header */}
          <div className="mb-8 animate-slide-up">
            <div className="flex items-center justify-between">
              <div>
                <h1 className="text-3xl font-bold mb-2">Prompt Testing Playground</h1>
                <p className="text-muted-foreground">Experiment and refine your prompts before battle</p>
              </div>
              <Button variant="outline" size="sm">
                <Settings className="w-4 h-4 mr-2" />
                Settings
              </Button>
            </div>
          </div>

          <div className="grid lg:grid-cols-3 gap-6">
            {/* Left sidebar - Templates and tips */}
            <div className="space-y-6 animate-slide-up" style={{ animationDelay: "0.1s" }}>
              {/* Model settings */}
              <Card className="p-6 glass-effect border-primary/20">
                <h3 className="font-semibold mb-4 flex items-center gap-2">
                  <Zap className="w-4 h-4 text-primary" />
                  Model Settings
                </h3>
                <div className="space-y-4">
                  <div className="space-y-2">
                    <label className="text-sm text-muted-foreground">Model</label>
                    <Select value={model} onValueChange={setModel}>
                      <SelectTrigger className="bg-secondary/30">
                        <SelectValue />
                      </SelectTrigger>
                      <SelectContent>
                        <SelectItem value="gemini-2.0-flash-exp">Gemini 2.0 Flash (Latest)</SelectItem>
                        <SelectItem value="gpt-4">GPT-4</SelectItem>
                        <SelectItem value="gpt-3.5">GPT-3.5 Turbo</SelectItem>
                        <SelectItem v
[truncated — 9893 more characters]
```

### app/learn/page.tsx

```typescript
"use client"

import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Progress } from "@/components/ui/progress"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import {
  BookOpen,
  GraduationCap,
  Lightbulb,
  Target,
  CheckCircle2,
  Lock,
  Play,
  Trophy,
  Zap,
  Code2,
  MessageSquare,
  Sparkles,
} from "lucide-react"
import { Navigation } from "@/components/navigation"

export default function LearnPage() {
  const [completedLessons, setCompletedLessons] = useState<number[]>([1, 2])

  const lessons = [
    {
      id: 1,
      title: "Introduction to Prompt Engineering",
      description: "Learn the basics of crafting effective prompts",
      duration: "10 min",
      difficulty: "Beginner",
      locked: false,
    },
    {
      id: 2,
      title: "Being Specific and Clear",
      description: "Master the art of clarity in your instructions",
      duration: "15 min",
      difficulty: "Beginner",
      locked: false,
    },
    {
      id: 3,
      title: "Context and Constraints",
      description: "Provide the right context for better outputs",
      duration: "20 min",
      difficulty: "Intermediate",
      locked: false,
    },
    {
      id: 4,
      title: "Few-Shot Learning",
      description: "Use examples to guide AI behavior",
      duration: "25 min",
      difficulty: "Intermediate",
      locked: false,
    },
    {
      id: 5,
      title: "Chain of Thought",
      description: "Break down complex reasoning tasks",
      duration: "30 min",
      difficulty: "Advanced",
      locked: true,
    },
    {
      id: 6,
      title: "Role-Based Prompting",
      description: "Assign personas for specialized outputs",
      duration: "20 min",
      difficulty: "Advanced",
      locked: true,
    },
  ]

  const techniques = [
    {
      name: "Be Specific",
      description: "Clearly define what you want the AI to do",
      example: 'Instead of "Write about dogs", try "Write a 200-word article about Golden Retrievers as family pets"',
      icon: Target,
    },
    {
      name: "Provide Context",
      description: "Give background information to guide the AI",
      example: 'Add context like "You are a marketing expert writing for millennials interested in sustainability"',
      icon: MessageSquare,
    },
    {
      name: "Use Examples",
      description: "Show the AI what you want with examples",
      example: "Provide 2-3 examples of the desired output format or style",
      icon: Code2,
    },
    {
      name: "Set Constraints",
      description: "Define boundaries and requirements",
      example: 'Specify "Use simple language, avoid jargon, keep under 500 words"',
      icon: Zap,
    },
  ]

  const challenges = [
    {
      id: 1,
      title: "First Prompt Challenge",
      description: "Write a prompt that generates a product description",
      points: 100,
      completed: true,
    },
    {
      id: 2,
      title: "Context Master",
      description: "Create a prompt with rich context for better results",
      points: 150,
      completed: true,
    },
    {
      id: 3,
      title: "Example Expert",
      description: "Use few-shot learning to guide AI output",
      points: 200,
      completed: false,
    },
    {
      id: 4,
      title: "Advanced Reasoning",
      description: "Implement chain-of-thought prompting",
      points: 300,
      completed: false,
    },
  ]

  const progressPercentage = (completedLessons.length / lessons.length) * 100

  return (
    <>
      <Navigation />

      <div className="min-h-screen relative overflow-hidden">
        {/* Animated background */}
        <div className="absolute inset-0 bg-gradient-to-br from-primary/10 via-background to-accent/10 animate-gradient" />
        <div className="absolute inset-0 bg-[linear-gradient(rgba(255,255,255,0.02)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,0.02)_1px,transparent_1px)] bg-[size:64px_64px]" />

        <div className="relative z-10 container mx-auto px-4 py-8">
          {/* Header */}
          <div className="mb-8 animate-slide-up">
            <div className="flex items-center justify-between mb-6">
              <div>
                <h1 className="text-3xl font-bold mb-2">Learning Hub</h1>
                <p className="text-muted-foreground">Master prompt engineering from beginner to expert</p>
              </div>
              <Button className="bg-gradient-to-r from-primary to-accent">
                <Trophy className="w-4 h-4 mr-2" />
                View Achievements
              </Button>
            </div>

            {/* Progress overview */}
            <Card className="p-6 glass-effect border-primary/20">
              <div className="flex items-center justify-between mb-4">
                <div>
                  <h3 className="font-semibold text-lg mb-1">Your Progress</h3>
                  <p className="text-sm text-muted-foreground">
                    {completedLessons.length} of {lessons.length} lessons completed
                  </p>
                </div>
                <div className="text-right">
                  <div className="text-3xl font-bold text-primary">{Math.round(progressPercentage)}%</div>
                  <div className="text-xs text-muted-foreground">Complete</div>
                </div>
              </div>
              <Progress value={progressPercentage} className="h-3" />
            </Card>
          </div>

          <Tabs defaultValue="lessons" className="w-full">
            <TabsList className="grid w-full grid-cols-3 mb-8">
              <TabsTrigger value="lessons" className="flex items-center gap-2">
                <GraduationCap className="w-4 h-4" />
                Lessons
              </TabsTrigger>
              <TabsTrigger value="techniques" className="flex items-center gap-2">
                <L
[truncated — 13100 more characters]
```

### app/api/research/route.ts

```typescript
import { NextRequest, NextResponse } from "next/server";
import { researchAgent } from "@/lib/agents/research-agent";

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

    if (!query) {
      return NextResponse.json(
        { error: "Query is required" },
        { status: 400 }
      );
    }

    // Stream the research response
    const streamResult = await researchAgent.stream(query);

    // Convert the stream to a Response
    const encoder = new TextEncoder();
    const reader = streamResult.fullStream.getReader();

    const readableStream = new ReadableStream({
      async start(controller) {
        try {
          while (true) {
            const { done, value } = await reader.read();
            if (done) break;

            if (value.type === "text-delta") {
              controller.enqueue(encoder.encode(value.payload.text));
            }
          }
          controller.close();
        } catch (error) {
          console.error("Stream error:", error);
          controller.error(error);
        }
      },
    });

    return new Response(readableStream, {
      headers: {
        "Content-Type": "text/event-stream",
        "Cache-Control": "no-cache",
        Connection: "keep-alive",
      },
    });
  } catch (error) {
    console.error("Research API error:", error);
    return NextResponse.json(
      { error: "Failed to process research request" },
      { status: 500 }
    );
  }
}

```

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