# Project export: Grassroots

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: Master the Art of Political Phone Banking
- Devpost: https://devpost.com/software/grassroots-20y3kr
- GitHub: https://github.com/Yatha04/CalHacks
- Demo: https://phonebanker.vercel.app/
- Video: https://www.youtube.com/embed/Lakrx8_DBsI?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 3 GitHub contributor(s) — Yatha04 (15 commits), Sahil (6 commits), Kyle Liao (5 commits)

## Devpost submission (written by the team)

### Inspiration

Kyle recently took part in a campaign as a phone banker and made many calls to both appreciative and unappreciative New Yorkers. During this time, he noticed a gap that nobody was filling. How do we support those grassroots volunteers, helping them prepare for difficult yet important personal questions while providing a scalable and cost-effective solution for campaigns.

### What it does

Grassroots is a personalized training tool for phone bankers utilizing versatile AI Voice Agents in order to mimic voters from all backgrounds and areas of the political landscape, including those in extremely unique scenarios. We've organized each voter's conversation type into easy, medium, and hard level,s allowing the user to progress with confidence. You can start talking to a specific voter, and repeatedly practice until you It classifies each voter's conversation into easy, medium, and hard, allowing the user to climb up with confidence. You can start talking to a voter profile and practice until you are ready to move forward. After your conversation, we show you your call transcription so that you can better reflect on your experience. This is then stored in our backend and is accessible by Poke using an MCP server integration, where it can rate, evaluate, and recommend improvements.

### How we built it

We utilized a NextJS (Frontend), Supabase (Backend + Auth), Google Gemini (LLM), ElevenLabs (Voices), VAPI (Voice Agent Orchestration), Poke (MCP + Text).

### Challenges we ran into

Pivoted away from utilizing SNAP AR. Had to deal with bad wifi.

### Accomplishments we're proud of

We created a fullstack app with MCP integrations.

### What we learned

We learned how to utilize MCP integrations.

### What's next

We will continue to improve our infrastructure.

## README (from the GitHub repository)

# CalHacks: Grassroots Training Platform

> **Master the Art of Political Phone Banking with AI-Powered Training**

A comprehensive training platform that combines realistic voter simulations with advanced performance analytics to help political volunteers excel at phone banking. Built for CalHacks, this project features both a Next.js training application and an MCP server for AI-powered call analysis.

## 🎯 Project Overview

The Grassroots Training Platform is a **LeetCode-style training system** for political phone banking volunteers. It provides:

- **Realistic AI-powered voter simulations** using voice agents
- **Progressive difficulty levels** (Easy → Medium → Hard)
- **Comprehensive performance analytics** with detailed feedback
- **Progress tracking** and improvement insights
- **AI-powered call analysis** via MCP server integration

## 🏗️ Architecture

This project consists of two main components:

### 1. **Training Application** (`phone-banker-training/`)
A Next.js web application that provides the core training experience.

### 2. **MCP Server** (`phone-banker-mcp-server/`)
A Model Context Protocol server that exposes call data to AI assistants for advanced analysis.

```
┌─────────────────────┐    ┌─────────────────────┐
│   Training App      │    │    MCP Server       │
│   (Next.js)         │    │   (Node.js/Express) │
│                     │    │                     │
│ • Voter Profiles    │    │ • Call Analytics    │
│ • Voice Calls       │    │ • Transcripts       │
│ • Performance UI    │    │ • Recordings        │
│ • Progress Tracking │    │ • AI Integration    │
└─────────────────────┘    └─────────────────────┘
           │                           │
           └───────────┬───────────────┘
                       │
            ┌─────────────────────┐
            │   External Services │
            │                     │
            │ • Vapi (Voice AI)   │
            │ • Supabase (DB)     │
            │ • Claude/Poke (AI)  │
            └─────────────────────┘
```

## 🚀 Quick Start

### Option 1: Explore the Training App (5 minutes)

```bash
cd phone-banker-training
npm install
npm run dev
```

Open [http://localhost:3000](http://localhost:3000) to explore the UI and voter profiles.

### Option 2: Full Setup with Voice Calls (15 minutes)

1. **Set up external services:**
   - Create a [Vapi account](https://dashboard.vapi.ai) for voice AI
   - Create a [Supabase project](https://supabase.com) for data storage

2. **Configure environment:**
   ```bash
   cd phone-banker-training
   cp .env.example .env.local
   # Add your Vapi and Supabase credentials
   ```

3. **Set up database:**
   - Run the SQL from `DATABASE_SCHEMA.sql` in your Supabase SQL Editor

4. **Start the application:**
   ```bash
   npm run dev
   ```

5. **Make your first call:**
   - Click "Start Training"
   - Select "Working Mom - Queens" (Easy difficulty)
   - Allow microphone access
   - Start practicing!

## 🎮 Features

### Training Application

- **🗣️ Realistic Voter Simulations**: 7 pre-configured voter personas across NYC boroughs
- **📊 Three Difficulty Levels**:
  - **Easy**: Supportive voters (Working Mom, Small Business Owner, Retired Senior)
  - **Medium**: Persuasion-focused voters (Tech Worker, New Immigrant Family)
  - **Hard**: Skeptical voters (Disillusioned Democrat, Conservative Independent)
- **📈 Performance Analytics**: Detailed scoring on:
  - Confidence, Enthusiasm, Clarity
  - Persuasiveness, Empathy
- **📝 Real-time Transcription**: Review conversation transcripts
- **📊 Progress Tracking**: Monitor improvement over time

### MCP Server

- **📋 List Call Sessions**: Get recent training calls with metadata
- **🔍 Get Call Details**: Fetch complete transcripts and performance metrics
- **🎵 Get Call Recordings**: Retrieve Vapi recording URLs for audio analysis
- **📊 Get User Progress**: View aggregate statistics and progress tracking

## 🛠️ Tech Stack

### Training Application
- **Frontend**: Next.js 16, React 19, TypeScript
- **Styling**: Tailwind CSS
- **Voice AI**: Vapi AI SDK
- **Database**: Supabase (PostgreSQL)
- **Authentication**: Supabase Auth

### MCP Server
- **Runtime**: Node.js with TypeScript
- **Framework**: Express.js
- **Protocol**: Model Context Protocol (MCP)
- **Database**: Supabase client
- **Deployment**: Google Cloud Run

## 📂 Project Structure

```
CalHacks/
├── phone-banker-training/          # Main training application
│   ├── app/                        # Next.js App Router pages
│   │   ├── page.tsx               # Landing page
│   │   ├── dashboard/              # Voter profiles dashboard
│   │   └── practice/               # Call interface
│   ├── components/                 # React components
│   │   ├── ui/                    # Base UI components
│   │   ├── CallInterface.tsx      # Voice call interface
│   │   ├── VoterProfileCard.tsx  # Voter profile display
│   │   └── PerformanceReport.tsx # Analytics display
│   ├── lib/                       # Utility libraries
│   │   ├── vapi.ts              # Vapi AI integration
│   │   ├── supabase.ts          # Database client
│   │   ├── voterProfiles.ts     # Voter persona definitions
│   │   └── analytics.ts         # Performance analysis
│   └── DATABASE_SCHEMA.sql       # Database setup
│
└── phone-banker-mcp-server/        # MCP server for AI analysis
    ├── src/
    │   ├── server-http.ts        # HTTP server
    │   ├── tools/                # MCP tool implementations
    │   │   ├── list-call-sessions.ts
    │   │   ├── get-call-details.ts
    │   │   ├── get-call-recording.ts
    │   │   └── get-user-progress.ts
    │   └── utils/                # Utility modules
    │       ├── supabase-client.ts
    │       └── vapi-client.ts
    └── deploy.sh                 # Google Cloud Run deployment
```

## 🗣️ Voter Profiles

The platform includes 7 carefully crafted voter personas:

### Easy Difficulty
- **Working Mom - Queens**: Practical, cost-of-living focused
- **Small Business Owner - Brooklyn**: Tax-concerned, community-minded
- **Retired Senior - Staten Island**: Traditional, values experience

### Medium Difficulty
- **Millennial Tech Worker - Manhattan**: Progressive, questioning
- **New Immigrant Family - Brooklyn/Queens**: Family-focused, hopeful but guarded

### Hard Difficulty
- **Disillusioned Former Democrat - Bronx**: Cynical, needs genuine engagement
- **Independent Conservative - Staten Island**: Traditional values, skeptical of progressive policies

## 📊 Performance Metrics

After each call, volunteers receive detailed feedback on:

- **Confidence** (0-100): Voice tone, certainty, delivery strength
- **Enthusiasm** (0-100): Energy level, passion for the campaign
- **Clarity** (0-100): Message organization, speaking pace
- **Persuasiveness** (0-100): Addressing concerns, making compelling arguments
- **Empathy** (0-100): Active listening, acknowledging voter concerns

Plus:
- Identified strengths and improvement areas
- Key conversation moments
- Overall sentiment analysis
- Transcript review

## 🔧 Configuration

### Environment Variables

**Training Application** (`.env.local`):
```env
# Supabase Configuration (Required)
NEXT_PUBLIC_SUPABASE_URL=your_supabase_project_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key

# Vapi Configuration (Optional - for voice calls)
NEXT_PUBLIC_VAPI_PUBLIC_KEY=your_vapi_public_key
```

**MCP Server** (`.env`):
```env
# Supabase Configuration
SUPABASE_URL=your_supabase_url
SUPABASE_ANON_KEY=your_supabase_anon_key

# Vapi API Configuration
VAPI_API_KEY=your_vapi_server_api_key

# MCP Server Configuration
MCP_API_KEY=your_secure_api_key
PORT=8080
```

### Adding Custom Voter Profiles

Edit `phone-banker-training/lib/voterProfiles.ts`:

```typescript
{
  id: "custom-voter",
  name: "Your Custom Voter",
  difficulty: "medium",
  description: "Brief description",
  age: "Age range",
  location: "Borough/Area",
  occupation: "Job",
  income: "Income level",
  votingHistory: "Voting pattern",


[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 45 recognized source files, 186 KB.
- CSS (language) — detected in the code
- Express (technology) — detected in the code
- JavaScript (language) — detected in the code
- Next.js (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
- PostgreSQL (technology) — claimed on Devpost, not found in the code
- Python (language) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (56 of 56)

```
phone-banker-mcp-server/.gitignore
phone-banker-mcp-server/deploy.sh
phone-banker-mcp-server/nixpacks.toml
phone-banker-mcp-server/package.json
phone-banker-mcp-server/railway.json
phone-banker-mcp-server/README.md
phone-banker-mcp-server/server.js
phone-banker-mcp-server/src/server-http.ts
phone-banker-mcp-server/src/tools/get-call-details.ts
phone-banker-mcp-server/src/tools/get-call-recording.ts
phone-banker-mcp-server/src/tools/get-user-progress.ts
phone-banker-mcp-server/src/tools/list-call-sessions.ts
phone-banker-mcp-server/src/utils/supabase-client.ts
phone-banker-mcp-server/src/utils/vapi-client.ts
phone-banker-mcp-server/tsconfig.json
phone-banker-training/.gitignore
phone-banker-training/app/contact/page.tsx
phone-banker-training/app/dashboard/page.tsx
phone-banker-training/app/globals.css
phone-banker-training/app/layout.tsx
phone-banker-training/app/page.tsx
phone-banker-training/app/practice/page.tsx
phone-banker-training/CALL_SESSION_FIX.md
phone-banker-training/components/auth/AuthForm.tsx
phone-banker-training/components/auth/ProtectedRoute.tsx
phone-banker-training/components/auth/UserProfile.tsx
phone-banker-training/components/CallInterface.tsx
phone-banker-training/components/Footer.tsx
phone-banker-training/components/Header.tsx
phone-banker-training/components/PerformanceReport.tsx
phone-banker-training/components/ProgressTracker.tsx
phone-banker-training/components/SmoothScroll.tsx
phone-banker-training/components/ui/button.tsx
phone-banker-training/components/ui/card.tsx
phone-banker-training/components/VoterProfileCard.tsx
phone-banker-training/DATABASE_SCHEMA.sql
phone-banker-training/eslint.config.mjs
phone-banker-training/FIX_SUMMARY.md
phone-banker-training/lib/analytics.ts
phone-banker-training/lib/auth.tsx
phone-banker-training/lib/supabase.ts
phone-banker-training/lib/utils.ts
phone-banker-training/lib/vapi.ts
phone-banker-training/lib/voterProfiles.ts
phone-banker-training/migrations/fix_user_rls_policies.sql
phone-banker-training/next.config.ts
phone-banker-training/package.json
phone-banker-training/package.json.template
phone-banker-training/postcss.config.mjs
phone-banker-training/QUICK_START.md
phone-banker-training/README.md
phone-banker-training/SETUP_AUTH.md
phone-banker-training/tsconfig.json
phone-banker-training/types/index.ts
phone-banker-training/VAPI_TROUBLESHOOTING.md
README.md
```

### Dependencies

- phone-banker-mcp-server/package.json: @modelcontextprotocol/sdk@^1.20.0, @supabase/supabase-js@^2.76.1, @types/cors@^2.8.17, @types/express@^4.17.21, @types/node@^20.11.0, @types/node-fetch@^2.6.11, @typescript-eslint/eslint-plugin@^6.19.0, @typescript-eslint/parser@^6.19.0, axios@^1.6.0, cors@^2.8.5, dotenv@^16.3.0, eslint@^8.56.0, eventsource-parser@^1.1.0, express@^4.18.2, node-fetch@^2.7.0, ts-node@^10.9.0, tsx@^4.7.0, typescript@^5.3.0, zod@^3.22.0
- phone-banker-training/package.json: @supabase/supabase-js@^2.76.1, @tailwindcss/postcss@^4, @types/node@^20, @types/react@^19, @types/react-dom@^19, @vapi-ai/web@^2.5.0, clsx@^2.1.1, eslint@^9, eslint-config-next@16.0.0, lenis@^1.3.11, lucide-react@^0.548.0, next@16.0.0, react@19.2.0, react-dom@19.2.0, tailwind-merge@^3.3.1, tailwindcss@^4, typescript@^5

### Recent commits (newest first)

- Updated readme
- Delete .gitignore
- Delete phone-banker-mcp-server.zip
- Delete useless files
- rebranded to Grassroots
- updated auth ui
- updated last minute UI
- updated idek env variable
- Redeploy with vapi env vars
- Redeploy with env vars
- debug: Add temporary auth debug endpoint
- feat: Support API key auth via headers and query params for Poke integration
- fix: Update Railway to run MCP server with SSE endpoint
- auth_working
- google auth configured
- fixed auth, connected db, fixed vapi
- updated mcp connection with boba
- Merge branch 'main' of https://github.com/Yatha04/CalHacks
- color
- Merge branch 'main' of https://github.com/Yatha04/CalHacks

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

### phone-banker-training/FIX_SUMMARY.md

```markdown
# Fix Summary: User Profile Creation Error

## Problem
Users encountered an error `Error creating user profile: {}` during signup. The error occurred because the database's Row Level Security (RLS) policies were incomplete.

## Root Cause
The `users` table had RLS enabled but only included a SELECT policy. When new users tried to sign up, the `upsert` operation (which requires INSERT and UPDATE permissions) was blocked by RLS, causing the profile creation to fail.

## Solution Applied

### 1. Database Schema Updates (`DATABASE_SCHEMA.sql`)
- ✅ Added INSERT policy: Allows users to create their own profile during signup
- ✅ Added UPDATE policy: Allows users to update their own profile information
- ✅ Maintained existing SELECT policy: Users can read their own data

### 2. Improved Error Handling (`lib/auth.tsx`)
- ✅ Enhanced error logging to display detailed error information:
  - Error message
  - Error code
  - Error details
  - Error hints
- ✅ Better distinction between database errors and caught exceptions

### 3. Migration Script (`migrations/fix_user_rls_policies.sql`)
- ✅ Created standalone migration script for existing databases
- ✅ Can be run independently to fix RLS policies without recreating tables

### 4. Documentation Updates (`README.md`)
- ✅ Added comprehensive troubleshooting section
- ✅ Documented the specific fix for user profile creation errors
- ✅ Added general authentication and voice call troubleshooting
- ✅ Referenced migrations folder in setup instructions

## How to Apply the Fix

### For Existing Databases
1. Open your Supabase dashboard
2. Go to SQL Editor
3. Copy and run the SQL from `migrations/fix_user_rls_policies.sql`
4. Test signup functionality

### For New Databases
1. Run the complete `DATABASE_SCHEMA.sql`
2. All policies are now included automatically

## Files Modified
- `grassroots-training/DATABASE_SCHEMA.sql` - Added RLS policies
- `grassroots-training/lib/auth.tsx` - Improved error logging
- `grassroots-training/README.md` - Added troubleshooting documentation

## Files Created
- `grassroots-training/migrations/fix_user_rls_policies.sql` - Migration script
- `phone-banker-training/FIX_SUMMARY.md` - This summary document

## Testing
After applying the fix:
1. Try signing up with a new account
2. The user profile should be created successfully
3. No "Error creating user profile" should appear in the console
4. User should be able to sign in and access the dashboard

## Technical Details

### RLS Policies Added
```sql
-- Allow users to insert their own profile
CREATE POLICY "Users can insert own data" ON users
  FOR INSERT WITH CHECK (auth.uid() = id);

-- Allow users to update their own profile  
CREATE POLICY "Users can update own data" ON users
  FOR UPDATE USING (auth.uid() = id);
```

These policies ensure that:
- Users can only create profiles for themselves (their auth.uid() matches the id)
- Users can only update their own profile data
- Security is maintained through Supabase's built-in RLS system


```

### phone-banker-training/VAPI_TROUBLESHOOTING.md

```markdown
# Vapi Integration Troubleshooting Guide

This document covers common Vapi errors and how to resolve them.

## 🚨 Common Errors

### 1. "Meeting ended due to ejection: Meeting has ended"

**What it means:** Vapi forcefully terminated the call due to a configuration or service issue.

**Common causes:**
- Invalid voice provider or voice ID
- Invalid model configuration
- Insufficient Vapi credits
- Invalid API key or permissions
- Malformed assistant configuration

**Solutions:**

#### Quick Fix: Use Pre-configured Assistants (Recommended)

Instead of using inline configuration, create assistants in the Vapi Dashboard:

1. Go to https://dashboard.vapi.ai/assistants
2. Click "Create Assistant"
3. Configure:
   - **Model**: OpenAI GPT-3.5-turbo (reliable, fast)
   - **Voice**: Azure/andrew or PlayHT/jennifer
   - **System Prompt**: Your voter personality
   - **First Message**: "Hello?"
   - **Max Duration**: 600 seconds
4. Copy the Assistant ID
5. Add to voter profile:
   ```typescript
   {
     id: "voter-id",
     name: "Voter Name",
     vapiAssistantId: "asst_abc123...", // Add this
     // ... rest
   }
   ```

#### Alternative: Fix Inline Configuration

If you must use inline config, ensure:
- Valid voice provider: `azure`, `playht`, or `elevenlabs`
- Valid voice ID for the chosen provider
- Model: `gpt-3.5-turbo` or `gpt-4`
- Messages array with system message

### 2. "Failed to start call (unknown): Bad Request [cors]"

**What it means:** The assistant configuration format is incorrect.

**Solution:**
Check that your configuration matches this structure:
```typescript
{
  name: "Assistant Name",
  model: {
    provider: "openai",
    model: "gpt-3.5-turbo",
    messages: [
      {
        role: "system",
        content: "Your system prompt here"
      }
    ]
  },
  voice: {
    provider: "azure",
    voiceId: "andrew"
  },
  firstMessage: "Hello?",
  recordingEnabled: true,
  silenceTimeoutSeconds: 30,
  maxDurationSeconds: 600
}
```

### 3. Empty Error Objects `{}`

**What it means:** Vapi is emitting non-critical error events that can be ignored.

**Solution:** These are now automatically filtered out by the error handler.

## ✅ Verification Checklist

Before starting a call, verify:

- [ ] `NEXT_PUBLIC_VAPI_PUBLIC_KEY` is set in `.env.local`
- [ ] Vapi account has sufficient credits
- [ ] Microphone permissions are granted
- [ ] Internet connection is stable
- [ ] Using pre-configured assistant OR valid inline config

## 🔍 Debugging

The app now provides detailed console logging:

- 🔵 Blue: Information/status messages
- ✅ Green: Success messages
- ❌ Red: Error messages

Check the browser console for detailed diagnostic information when errors occur.

## 📊 Recommended Configuration

For the most reliable experience:

```typescript
// lib/voterProfiles.ts
export const voterProfiles: VoterProfile[] = [
  {
    id: "working-mom",
    name: "Working Mom - Queens",
    vapiAssistantId: "asst_your_id_here", // Create in Vapi Dashboard
    
[truncated — 580 more characters]
```

### phone-banker-training/package.json

```
{
  "name": "grassroots-training",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint"
  },
  "dependencies": {
    "@supabase/supabase-js": "^2.76.1",
    "@vapi-ai/web": "^2.5.0",
    "clsx": "^2.1.1",
    "lenis": "^1.3.11",
    "lucide-react": "^0.548.0",
    "next": "16.0.0",
    "react": "19.2.0",
    "react-dom": "19.2.0",
    "tailwind-merge": "^3.3.1"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "eslint": "^9",
    "eslint-config-next": "16.0.0",
    "tailwindcss": "^4",
    "typescript": "^5"
  }
}

```

### phone-banker-mcp-server/package.json

```
{
  "name": "grassroots-mcp-server",
  "version": "1.1.0",
  "description": "MCP server for Grassroots Training API",
  "mcpName": "io.github.r-huijts/grassroots-mcp",
  "repository": {
    "type": "git",
    "url": "https://github.com/Yatha04/CalHacks.git"
  },
  "main": "dist/server.js",
  "scripts": {
    "build": "tsc",
    "start": "node dist/server.js",
    "start:http": "node dist/server-http.js",
    "dev": "tsx src/server.ts",
    "dev:http": "tsx src/server-http.ts",
    "lint": "eslint . --ext .ts",
    "setup-auth": "tsx scripts/setup-auth.ts"
  },
  "keywords": [
    "mcp",
    "strava",
    "llm",
    "ai"
  ],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.20.0",
    "@supabase/supabase-js": "^2.76.1",
    "eventsource-parser": "^1.1.0",
    "axios": "^1.6.0",
    "cors": "^2.8.5",
    "dotenv": "^16.3.0",
    "express": "^4.18.2",
    "zod": "^3.22.0",
    "node-fetch": "^2.7.0"
  },
  "devDependencies": {
    "@types/cors": "^2.8.17",
    "@types/express": "^4.17.21",
    "@types/node": "^20.11.0",
    "@types/node-fetch": "^2.6.11",
    "@typescript-eslint/eslint-plugin": "^6.19.0",
    "@typescript-eslint/parser": "^6.19.0",
    "eslint": "^8.56.0",
    "ts-node": "^10.9.0",
    "tsx": "^4.7.0",
    "typescript": "^5.3.0"
  }
}

```

### phone-banker-mcp-server/server.js

```javascript
const express = require('express');
const cors = require('cors');
require('dotenv').config();

const app = express();
const PORT = process.env.PORT || 8080;

app.use(cors());
app.use(express.json());

app.get('/health', (req, res) => {
  res.json({ 
    status: 'healthy', 
    timestamp: new Date().toISOString(),
    version: '1.0.0'
  });
});

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

```

### phone-banker-training/app/layout.tsx

```typescript
import type { Metadata } from "next";
import { Geist, Geist_Mono, Quicksand } from "next/font/google";
import "./globals.css";
import { Header } from "@/components/Header";
import { Footer } from "@/components/Footer";
import { SmoothScroll } from "@/components/SmoothScroll";
import { AuthProvider } from "@/lib/auth";

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

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

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

export const metadata: Metadata = {
  title: "Grassroots Training Platform",
  description: "LeetCode-style training platform for mayoral election phone bank volunteers. Practice realistic voter conversations with AI-powered simulations.",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} ${quicksand.variable} antialiased flex flex-col min-h-screen`}
      >
        <AuthProvider>
          <SmoothScroll />
          <Header />
          {children}
          <Footer />
        </AuthProvider>
      </body>
    </html>
  );
}

```

### phone-banker-training/types/index.ts

```typescript
// Core type definitions for the phone banker training platform

export type DifficultyLevel = "easy" | "medium" | "hard";

export interface VoterProfile {
  id: string;
  name: string;
  difficulty: DifficultyLevel;
  description: string;
  age: string;
  location: string;
  occupation: string;
  income: string;
  votingHistory: string;
  keyIssues: string[];
  skepticism: string;
  personality: string;
  vapiAssistantId?: string; // Vapi assistant configured for this profile
}

export interface CallSession {
  id: string;
  userId: string;
  voterProfileId: string;
  startTime: Date;
  endTime?: Date;
  duration?: number; // in seconds
  transcript?: string;
  vapiCallId?: string;
  status: "in-progress" | "completed" | "abandoned";
}

export interface PerformanceMetrics {
  id: string;
  sessionId: string;
  confidence: number; // 0-100
  enthusiasm: number; // 0-100
  clarity: number; // 0-100
  persuasiveness: number; // 0-100
  empathy: number; // 0-100
  overallScore: number; // 0-100
  strengths: string[];
  areasForImprovement: string[];
  keyMoments: KeyMoment[];
  sentiment: "positive" | "neutral" | "negative";
  transcript?: string;
  createdAt: Date;
}

export interface KeyMoment {
  timestamp: number; // seconds into the call
  description: string;
  type: "success" | "challenge" | "missed-opportunity";
}

export interface UserProgress {
  userId: string;
  totalCalls: number;
  callsByDifficulty: {
    easy: number;
    medium: number;
    hard: number;
  };
  averageScore: number;
  completionRate: number;
  lastCallDate?: Date;
}

export interface VapiMessage {
  type: "function-call" | "transcript" | "conversation-update";
  functionCall?: {
    name: string;
    parameters: Record<string, unknown>;
  };
  transcript?: string;
  timestamp: number;
}

export interface AnalysisSummary {
  callId: string;
  summary: string;
  userMessages: string[];
  voterMessages: string[];
  successIndicators: string[];
  missedOpportunities: string[];
}


```

### phone-banker-training/app/page.tsx

```typescript
"use client";

import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Phone, Target, TrendingUp, Users } from "lucide-react";
import Link from "next/link";

export default function HomePage() {
  return (
    <div className="min-h-screen bg-white">
      {/* Hero Section */}
      <div className="container mx-auto px-4 py-16">
        <div className="text-center max-w-3xl mx-auto mb-16">
          <h1 className="text-6xl font-bold mb-4 text-black">
            Grassroots
          </h1>
          <h2 className="text-2xl font-semibold mb-6 text-blue-600">
            Master the Art of Political Phone Banking
          </h2>
          <p className="text-xl text-gray-700 mb-8">
            Because every phone call makes a difference.
          </p>
          <Link href="/dashboard">
            <Button size="lg" className="gap-2 text-lg px-8 py-6 bg-blue-600 hover:bg-blue-700 text-white font-bold border-2 border-black shadow-[0_4px_0_0_rgba(0,0,0,1)] hover:shadow-[0_6px_0_0_rgba(0,0,0,1)] transition-all">
              <Phone className="w-5 h-5" />
              Start Training
            </Button>
          </Link>
        </div>

        {/* Features */}
        <div className="grid md:grid-cols-3 gap-8 mb-16">
          <Card className="border-2 border-black hover:border-blue-600 transition-all shadow-[0_8px_0_0_rgba(0,0,0,1)] hover:shadow-[0_12px_0_0_rgba(59,130,246,1)] bg-white">
            <CardHeader>
              <Target className="w-12 h-12 text-blue-600 mb-4" />
              <CardTitle className="text-black font-bold text-xl">Realistic Simulations</CardTitle>
            </CardHeader>
            <CardContent>
              <p className="text-gray-700">
                Practice with AI-powered voter personas that respond naturally to
                your pitch, just like real voters would.
              </p>
            </CardContent>
          </Card>

          <Card className="border-2 border-black hover:border-blue-600 transition-all shadow-[0_8px_0_0_rgba(0,0,0,1)] hover:shadow-[0_12px_0_0_rgba(59,130,246,1)] bg-white">
            <CardHeader>
              <Users className="w-12 h-12 text-blue-600 mb-4" />
              <CardTitle className="text-black font-bold text-xl">Multiple Difficulty Levels</CardTitle>
            </CardHeader>
            <CardContent>
              <p className="text-gray-700">
                Start with supportive voters and work your way up to challenging
                conversations with skeptical constituents.
              </p>
            </CardContent>
          </Card>

          <Card className="border-2 border-black hover:border-blue-600 transition-all shadow-[0_8px_0_0_rgba(0,0,0,1)] hover:shadow-[0_12px_0_0_rgba(59,130,246,1)] bg-white">
            <CardHeader>
              <TrendingUp className="w-12 h-12 text-blue-600 mb-4" />
              <CardTitle className="text-black font-bold text-xl">Performance Analytics</CardTitle>
            </CardHeader>
            <CardContent>
              <p className="text-gray-700">
                Get detailed feedback on your confidence, persuasiveness, and
                empathy with actionable improvement tips.
              </p>
            </CardContent>
          </Card>
        </div>

        {/* How It Works */}
        <div id="how-it-works" className="max-w-4xl mx-auto scroll-mt-20">
          <h2 className="text-3xl font-bold text-black text-center mb-12">How It Works</h2>
          <div className="space-y-6">
            <div className="flex gap-4 items-start">
              <div className="w-10 h-10 rounded-full bg-blue-600 text-white flex items-center justify-center font-bold flex-shrink-0 shadow-[0_4px_0_0_rgba(0,0,0,1)]">
                1
              </div>
              <div>
                <h3 className="font-bold text-lg text-black mb-2">
                  Choose Your Difficulty Level
                </h3>
                <p className="text-gray-600">
                  Select from Easy, Medium, or Hard voter profiles based on your
                  experience level. Each profile has unique concerns and
                  skepticism.
                </p>
              </div>
            </div>

            <div className="flex gap-4 items-start">
              <div className="w-10 h-10 rounded-full bg-blue-600 text-white flex items-center justify-center font-bold flex-shrink-0 shadow-[0_4px_0_0_rgba(0,0,0,1)]">
                2
              </div>
              <div>
                <h3 className="font-bold text-lg text-black mb-2">
                  Practice Your Phone Call
                </h3>
                <p className="text-gray-600">
                  Engage in a realistic voice conversation with an AI-powered
                  voter. They&apos;ll respond naturally based on their profile,
                  concerns, and your approach.
                </p>
              </div>
            </div>

            <div className="flex gap-4 items-start">
              <div className="w-10 h-10 rounded-full bg-blue-600 text-white flex items-center justify-center font-bold flex-shrink-0 shadow-[0_4px_0_0_rgba(0,0,0,1)]">
                3
              </div>
              <div>
                <h3 className="font-bold text-lg text-black mb-2">
                  Review Your Performance
                </h3>
                <p className="text-gray-600">
                  Get instant feedback with detailed metrics on confidence,
                  enthusiasm, clarity, persuasiveness, and empathy. Learn from
                  your strengths and areas for improvement.
                </p>
              </div>
            </div>

            <div className="flex gap-4 items-start">
              <div className="w-10 h-10 rounded-full bg-blue-600 text-white flex items-center justify-center font-bold flex-shrink-0 shadow-[0_4px_0_0_rgba(0,0,0,1)]">
                4
              </div>
      
[truncated — 876 more characters]
```

### phone-banker-training/app/contact/page.tsx

```typescript
"use client";

import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Mail } from "lucide-react";

export default function ContactPage() {
  return (
    <div className="min-h-screen bg-white">
      <div className="container mx-auto px-4 py-16">
        <div className="max-w-3xl mx-auto">
          {/* Header */}
          <div className="text-center mb-12">
            <h1 className="text-4xl font-bold text-black mb-4">Contact Us</h1>
            <p className="text-lg text-gray-600">
              Have questions or need support? We're here to help!
            </p>
          </div>

          {/* Contact Card */}
          <div className="flex justify-center mb-12">
            <Card className="text-center max-w-sm">
              <CardHeader>
                <Mail className="w-8 h-8 text-blue-600 mx-auto mb-2" />
                <CardTitle className="text-lg text-black">Email</CardTitle>
              </CardHeader>
              <CardContent>
                <p className="text-sm text-gray-600">thekyleliao@gmail.com</p>
              </CardContent>
            </Card>
          </div>


          {/* FAQ Section */}
          <div className="mt-12">
            <h2 className="text-2xl font-bold text-black mb-6">
              Frequently Asked Questions
            </h2>
            <div className="space-y-4">
              <Card>
                <CardHeader>
                  <CardTitle className="text-lg text-black">
                    How do I get started?
                  </CardTitle>
                </CardHeader>
                <CardContent>
                  <p className="text-gray-600">
                    Simply click "Start Now" in the navigation to access the
                    dashboard and begin practicing with AI-powered voter
                    simulations.
                  </p>
                </CardContent>
              </Card>

              <Card>
                <CardHeader>
                  <CardTitle className="text-lg text-black">
                    Do I need any special equipment?
                  </CardTitle>
                </CardHeader>
                <CardContent>
                  <p className="text-gray-600">
                    You'll need a computer with a microphone and internet
                    connection. We recommend using Chrome or Edge for the best
                    experience.
                  </p>
                </CardContent>
              </Card>

              <Card>
                <CardHeader>
                  <CardTitle className="text-lg text-black">
                    How are the calls evaluated?
                  </CardTitle>
                </CardHeader>
                <CardContent>
                  <p className="text-gray-600">
                    Our AI analyzes your conversation for confidence, empathy,
                    clarity, persuasiveness, and enthusiasm, providing detailed
                    feedback after each call.
                  </p>
                </CardContent>
              </Card>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

```

### phone-banker-training/app/practice/page.tsx

```typescript
"use client";

import { Suspense, useState } from "react";
import { useSearchParams, useRouter } from "next/navigation";
import { getProfileById } from "@/lib/voterProfiles";
import { CallInterface } from "@/components/CallInterface";
import { PerformanceReport } from "@/components/PerformanceReport";
import { analyzeCallPerformance } from "@/lib/analytics";
import { PerformanceMetrics } from "@/types";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import Link from "next/link";
import { ProtectedRoute } from "@/components/auth/ProtectedRoute";
import { useAuth } from "@/lib/auth";
import { saveCallSession, savePerformanceMetrics, updateCallSession, ensureUserProfile } from "@/lib/supabase";

function PracticeContent() {
  const searchParams = useSearchParams();
  const router = useRouter();
  const { user } = useAuth();
  const profileId = searchParams.get("profile");

  const [stage, setStage] = useState<"calling" | "report">("calling");
  const [performanceMetrics, setPerformanceMetrics] =
    useState<PerformanceMetrics | null>(null);

  const profile = profileId ? getProfileById(profileId) : null;

  if (!profile) {
    return (
      <div className="min-h-screen bg-gray-50 flex items-center justify-center">
        <Card className="max-w-md">
          <CardContent className="pt-6 text-center">
            <p className="text-lg mb-4">Voter profile not found</p>
            <Link href="/dashboard">
              <Button>Back to Dashboard</Button>
            </Link>
          </CardContent>
        </Card>
      </div>
    );
  }

  const handleCallEnd = async (transcript: string, duration: number, vapiCallId?: string) => {
    console.log("📊 Call ended - Processing results...", { duration, vapiCallId });
    
    // Analyze the call performance
    const metrics = await analyzeCallPerformance(
      transcript,
      duration,
      profile.difficulty
    );

    // Create full performance metrics object
    const fullMetrics: PerformanceMetrics = {
      id: crypto.randomUUID(),
      sessionId: crypto.randomUUID(),
      ...metrics,
      transcript,
      createdAt: new Date(),
    };

    setPerformanceMetrics(fullMetrics);
    setStage("report");

    // Save to Supabase with authenticated user
    if (user) {
      try {
        console.log("💾 Saving call session to Supabase...");
        
        // Ensure user profile exists in database before saving call session
        const userExists = await ensureUserProfile(user.id, user.email || undefined, user.user_metadata?.full_name || user.user_metadata?.name);
        if (!userExists) {
          console.error("❌ Failed to ensure user profile exists. Cannot save call session.");
          alert("Failed to save call data. Please try logging out and back in.");
          return;
        }
        
        // Save call session to Supabase
        const callSession = await saveCallSession({
          userId: user.id,
          voterProfileId: profile.id,
          startTime: new Date(Date.now() - duration * 1000), // Calculate start time
          vapiCallId: vapiCallId
        });
        
        console.log("✅ Call session saved:", callSession.id);

        // Save performance metrics
        await savePerformanceMetrics({
          sessionId: callSession.id,
          confidence: fullMetrics.confidence,
          enthusiasm: fullMetrics.enthusiasm,
          clarity: fullMetrics.clarity,
          persuasiveness: fullMetrics.persuasiveness,
          empathy: fullMetrics.empathy,
          overallScore: fullMetrics.overallScore,
          strengths: fullMetrics.strengths,
          areasForImprovement: fullMetrics.areasForImprovement,
          keyMoments: fullMetrics.keyMoments,
          sentiment: fullMetrics.sentiment,
        });
        
        console.log("✅ Performance metrics saved");

        // Update call session with end time and duration
        await updateCallSession(callSession.id, {
          endTime: new Date(),
          duration: duration,
          transcript: transcript,
          status: "completed"
        });
        
        console.log("✅ Call session updated with final data");
      } catch (error) {
        const errorObj = error as any;
        console.error('❌ Error saving call data:', {
          message: errorObj?.message || 'Unknown error',
          details: errorObj?.details || 'No details',
          hint: errorObj?.hint || 'No hint',
          code: errorObj?.code || 'No code',
          error: errorObj
        });
        
        // Don't fail the UI if save fails - user can still see their report
        console.warn('⚠️ Call data not saved, but you can still view your performance report');
      }
    } else {
      console.warn('⚠️ Not authenticated - call data will not be saved');
    }
  };

  const handleContinue = () => {
    router.push("/dashboard");
  };

  return (
    <div className="min-h-screen bg-gray-50 py-8">
      <div className="container mx-auto px-4">
        {stage === "calling" && (
          <CallInterface
            profile={profile}
            onCallEnd={handleCallEnd}
            onCancel={() => router.push("/dashboard")}
          />
        )}

        {stage === "report" && performanceMetrics && (
          <PerformanceReport
            metrics={performanceMetrics}
            voterName={profile.name}
            onContinue={handleContinue}
          />
        )}
      </div>
    </div>
  );
}

export default function PracticePage() {
  return (
    <ProtectedRoute>
      <Suspense
        fallback={
          <div className="min-h-screen bg-gray-50 flex items-center justify-center">
            <div className="text-lg">Loading...</div>
          </div>
        }
      >
        <PracticeContent />
      </Suspense>
    </ProtectedRoute>
  );
}

```

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