# Project export: AdWhisper

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: Bias detection in advertising with Agentic AI.
- Devpost: https://devpost.com/software/adwhisper
- GitHub: https://github.com/RonCodes88/AdWhisper
- Video: https://www.youtube.com/embed/FCAawGQPhWY?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 3 GitHub contributor(s) — adarshm11 (10 commits), RonCodes88 (10 commits), agamjots (7 commits)

## Devpost submission (written by the team)

### Inspiration

There has been a wave of recent advertisement campaigns that have drawn media attention for possibly promoting racial, sexist, and other biases. In an age where social media can catch everything, it is important for businesses to make sure their advertising campaigns are inclusive and supportive to the community they intend to sell to.

### What it does

AdWhisper utilizes agentic AI to identify potential concerns with ads regarding biases that might be covertly present.

### How we built it

We centered our design around the use of Fetch.ai uAgents, with multiple agents each carrying out a specific function of the analysis pipeline. These agents were hosted on the Fetch.ai AgentVerse and connected to the Fetch.ai ASI:One LLM for improved capabilities, with extra context provided by retrieval-augmented generation (RAG) via ChromaDB. We used Anthropic's Claude LLM for multimodal data processing, and built our application using Next.js and FastAPI.

### Challenges we ran into

It was challenging to learn how to utilize multiple technologies that we had not used before, including uAgents and ChromaDB. None of us had worked with the Fetch.ai platform, but we were able to learn using their comprehensive documentation.

### Accomplishments we're proud of

We were proud to be able to build, customize, and deploy multiple Fetch.ai uAgents on the AgentVerse platform, as these agents served as the driving force of our analysis platform. We also constructed a RAG pipeline using a ChromaDB vector database, improving the efficiency and accuracy of our agents.

### What we learned

We learned how to use agentic AI and vector databases for RAG.

### What's next

We would love to improve our analysis pipeline even more, utilizing more agents and RAG tools.

## README (from the GitHub repository)

# AdWhisper


## Detected evidence (automated analysis)

Indexed codebase: 23 recognized source files, 185 KB.
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- Hugging Face (technology) — detected in the code
- Next.js (technology) — detected in the code
- OpenAI (technology) — detected in the code
- Python (language) — detected in the code
- PyTorch (technology) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- AI coding agent: Cursor — evidence: config files committed to the repository

## Codebase structure (from repository index)

### Files (31 of 31)

```
AGENT_INTEGRATION_COMPLETE.md
backend/.cursorrules
backend/.gitignore
backend/AGENT_ARCHITECTURE.md
backend/agents/__init__.py
backend/agents/ingestion_agent.py
backend/agents/scoring_agent.py
backend/agents/shared_models.py
backend/agents/text_bias_agent.py
backend/agents/visual_bias_agent.py
backend/check_backend_status.py
backend/chroma.py
backend/main.py
backend/requirements.txt
backend/test_connection.py
backend/youtube_processor.py
frontend/.gitignore
frontend/app/(landing)/page.tsx
frontend/app/components/cta-section.tsx
frontend/app/components/navbar.tsx
frontend/app/globals.css
frontend/app/layout.tsx
frontend/app/portal/page.tsx
frontend/app/upload/page.tsx
frontend/eslint.config.mjs
frontend/next.config.ts
frontend/package.json
frontend/postcss.config.mjs
frontend/README.md
frontend/tsconfig.json
README.md
```

### Dependencies

- backend/requirements.txt: chromadb, fastapi, openai, pillow, protobuf@>=4.21.6,<6.0.0, pydantic, python-dotenv, pytube, requests, sentence-transformers, torch, transformers, uagents, uagents-adapter, uvicorn[standard], youtube-transcript-api
- frontend/package.json: @tailwindcss/postcss@^4, @types/node@^20, @types/react@^19, @types/react-dom@^19, eslint@^9, eslint-config-next@16.0.0, next@16.0.0, react@19.2.0, react-dom@19.2.0, tailwindcss@^4, typescript@^5

### Recent commits (newest first)

- Merge pull request #6 from RonCodes88/scroll
- fix the scroll
- this thing DOES NOT WORK BRO
- Merge pull request #5 from RonCodes88/fe-changes
- Merge pull request #4 from RonCodes88/test-agents
- Merge branch 'main' into test-agents
- frontend fixes
- fixed test agents
- Merge pull request #3 from RonCodes88/youtubeUpload
- Merge branch 'main' into youtubeUpload
- agents
- db
- gitignore
- Merge pull request #2 from RonCodes88/companyportal
- remove db file
- finished background gradient and other small issues
- company portal
- changed theme and coloring for landing and upload page
- finished front end connection with backend
- added fetchai agents

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

### AGENT_INTEGRATION_COMPLETE.md

```markdown
# ✅ Agent Integration Complete!

## What We Fixed

### Problem
- Frontend was **stuck** on "Analyzing Video..."
- Backend was **blocking** while waiting for Ingestion Agent
- Multiple Python processes fighting for port 8000

### Solution
✅ **FastAPI Background Tasks** - Agent processing happens AFTER response sent  
✅ **No more blocking** - Frontend gets instant response  
✅ **Configurable agent calls** - Easy to enable/disable  
✅ **Comprehensive logging** - See exactly what's happening  

---

## How It Works Now

```
┌─────────────┐
│   Frontend  │  
│   (3000)    │  
└──────┬──────┘
       │ POST /api/analyze-youtube
       ↓
┌─────────────────────────────────────┐
│   FastAPI Backend (8000)            │
│                                     │
│  1. Receive request                 │
│  2. Generate request_id             │
│  3. Return response IMMEDIATELY ← ✅│ 
└──────┬──────────────────────────────┘
       │
       │ (Background Task - After Response Sent)
       ↓
┌─────────────────────────────────────┐
│   Ingestion Agent (8100) [Optional] │
│                                     │
│  • Generate embeddings              │
│  • Store in ChromaDB                │
│  • Route to analysis agents         │
└─────────────────────────────────────┘
```

## Current Setup

### Backend Files

**`main_simple.py`** - Simplified version (currently running)
- Instant responses
- Agent calls disabled by default
- Perfect for frontend development

**`main.py`** - Full version with ChromaDB
- All features
- Background agent processing
- Agent calls disabled by default

### Key Configuration

Both files have:
```python
ENABLE_AGENT_CALLS = False  # Set to True to enable background agent processing
```

---

## Testing Your Setup

### 1. Restart Backend (Kill Old Processes First)

```bash
# Kill all Python processes
killall -9 Python

# Start the simple backend
cd /Users/ronaldli/Desktop/Projects/calhacks/backend
./adwhisper/bin/python main_simple.py
```

### 2. Test in Browser

1. Go to http://localhost:3000/upload
2. Paste YouTube URL
3. Click "Analyze YouTube Video"
4. **Results appear in < 100ms!** ⚡

### 3. Watch the Logs

**Backend Terminal:**
```
======================================================================
🎬 REQUEST RECEIVED
======================================================================
URL: https://www.youtube.com/watch?v=TVGDny9eneo
⏭️  Skipping agent call (ENABLE_AGENT_CALLS = False)

📦 Building response to frontend...
✅ Sending response immediately (agent will run in background)
======================================================================

INFO:     127.0.0.1:52563 - "POST /api/analyze-youtube HTTP/1.1" 200 OK
```

**Browser Console:**
```
🎬 YouTube Analysis Submission Started
✅ URL validation passed
⏳ Setting loading state to true
📤 Sending request to backend
📥 Response received (10ms)  ← Super fast!
✅ Response parsed successfully
✅ Analysis result set in state
```

---

## Enabling Full Agent Pipeline

When you're re
[truncated — 2754 more characters]
```

### backend/AGENT_ARCHITECTURE.md

```markdown
# Ad Bias Detection - Multi-Agent System Architecture

## System Overview

This platform leverages Fetch.ai's multi-agent framework with ASI:ONE LLM integration to detect and analyze bias in advertising content (text, images, and videos). The system employs a distributed agent architecture where specialized agents work collaboratively to ingest, analyze, and score content for various forms of bias.

## Agent Architecture Diagram

```
User Input (Ad Content)
        ↓
┌───────────────────┐
│ Ingestion Agent   │
│ - Embeddings      │
│ - ChromaDB Store  │
└───────────────────┘
        ↓
    ┌───┴───┐
    ↓       ↓
┌────────┐ ┌────────┐
│ Text   │ │ Visual │
│ Bias   │ │ Bias   │
│ Agent  │ │ Agent  │
└────────┘ └────────┘
    ↓       ↓
    └───┬───┘
        ↓
┌───────────────────┐
│  Scoring Agent    │
│ - Aggregation     │
│ - Final Report    │
└───────────────────┘
```

---

## Agent Descriptions

### 1. Ingestion Agent
**Role:** Data Reception, Preprocessing, and Embedding Generation

**Description:**
The Ingestion Agent serves as the entry point for all ad content entering the system. It handles multi-modal data (text, images, videos) from the frontend and performs the following operations:

**Responsibilities:**
- Receive and validate incoming ad content from frontend API
- Extract and separate multi-modal components:
  - Text content (headlines, body copy, CTAs)
  - Visual content (images, video frames)
  - Metadata (target demographics, placement info)
- Generate embeddings using appropriate models:
  - Text: Sentence transformers (e.g., `all-MiniLM-L6-v2` or `text-embedding-ada-002`)
  - Images/Videos: Vision transformers (e.g., CLIP, ViT)
- Store embeddings in ChromaDB with metadata for retrieval
- Route content to specialized analysis agents
- Maintain data lineage and provenance tracking

**Tools/APIs:**
- `preprocess_content`: Cleans and normalizes input data
- `generate_text_embedding`: Creates text embeddings
- `generate_visual_embedding`: Creates visual embeddings
- `store_in_chromadb`: Persists embeddings with metadata
- `route_to_agents`: Dispatches content to analysis agents

**Output:**
- Structured content package with embeddings
- ChromaDB collection IDs
- Routing manifest for downstream agents

---

### 2. Text Bias Agent
**Role:** Text Content Analysis and Bias Detection

**Description:**
The Text Bias Agent is an expert system specialized in identifying linguistic bias patterns in advertising copy. It analyzes all textual elements for various forms of bias including gender, racial, age, socioeconomic, and cultural bias.

**Responsibilities:**
- Analyze textual content for bias indicators
- **RAG RETRIEVAL POINT #1**: Query ChromaDB for similar historical cases and bias patterns
- Identify specific bias types:
  - Gender bias (stereotyping, exclusionary language)
  - Racial/ethnic bias (cultural appropriation, stereotypes)
  - Age bias (ageism, generational stereotypes)
  - Socioeconomic bias (class assumptions)
  - Disabilit
[truncated — 10679 more characters]
```

### backend/requirements.txt

```
# Web Framework
fastapi
uvicorn[standard]

# Fetch.ai uAgents Framework
uagents
uagents-adapter

# Vector Database
chromadb
protobuf>=4.21.6,<6.0.0

# Embeddings & ML Models
sentence-transformers
openai
transformers
torch
pillow

# YouTube Processing
youtube-transcript-api
pytube

# Utilities
python-dotenv
requests
pydantic

```

### frontend/package.json

```
{
  "name": "frontend",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint"
  },
  "dependencies": {
    "react": "19.2.0",
    "react-dom": "19.2.0",
    "next": "16.0.0"
  },
  "devDependencies": {
    "typescript": "^5",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "@tailwindcss/postcss": "^4",
    "tailwindcss": "^4",
    "eslint": "^9",
    "eslint-config-next": "16.0.0"
  }
}

```

### backend/main.py

```python
from fastapi import FastAPI, HTTPException, BackgroundTasks
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import uvicorn
import uuid
from typing import Dict, Any
import requests

# Note: No uAgents imports needed here - we just make HTTP requests!
# ChromaDB is managed by agents, not by FastAPI

app = FastAPI()

# Configure CORS
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000"],  # Next.js default port
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

@app.on_event("startup")
async def startup_event():
    """Initialize services on startup"""
    print("🚀 Starting AdWhisper Backend...")
    print("✅ FastAPI server ready")
    print("📍 Listening on http://localhost:8000")
    print("🔗 CORS enabled for http://localhost:3000")
    print(f"📤 Will send HTTP requests to Ingestion Agent: {INGESTION_AGENT_REST_ENDPOINT}")
    print("")


# Configuration
ENABLE_AGENT_CALLS = True  # Set to False to disable agent communication
INGESTION_AGENT_REST_ENDPOINT = "http://localhost:8100/submit"


def call_ingestion_agent_background(request_id: str, ingestion_payload: Dict[str, Any]):
    """
    Background task to call Ingestion Agent via REST
    This runs AFTER the response is sent to the frontend (non-blocking)
    """
    print(f"\n{'='*70}")
    print(f"📤 CALLING INGESTION AGENT (Background Task)")
    print(f"{'='*70}")
    print(f"📝 Request ID: {request_id}")
    print(f"🎯 Endpoint: {INGESTION_AGENT_REST_ENDPOINT}")
    
    try:
        response = requests.post(
            INGESTION_AGENT_REST_ENDPOINT,
            json=ingestion_payload,
            headers={"Content-Type": "application/json"},
            timeout=10
        )
        
        if response.status_code == 200:
            result = response.json()
            print(f"✅ SUCCESS - Ingestion Agent responded!")
            print(f"📨 Agent Status: {result.get('status', 'unknown')}")
            print(f"💬 Agent Message: {result.get('message', 'No message')}")
        else:
            print(f"⚠️ WARNING - Ingestion Agent returned HTTP {response.status_code}")
            print(f"Response: {response.text[:200]}")
            
    except requests.exceptions.ConnectionError:
        print(f"❌ ERROR - Could not connect to Ingestion Agent")
        print(f"   Make sure it's running: python agents/ingestion_agent.py")
    except requests.exceptions.Timeout:
        print(f"⏱️ ERROR - Ingestion Agent timed out (took > 10s)")
    except Exception as e:
        print(f"❌ ERROR - Unexpected error: {type(e).__name__}: {str(e)}")
    finally:
        print(f"{'='*70}\n")


# Request/Response Models
class YouTubeAnalysisRequest(BaseModel):
    youtube_url: str


@app.get("/")
async def root():
    return {"message": "Welcome to AdWhisper API"}


@app.get("/health")
async def health():
    return {"status": "healthy"}


@app.get("/documents")
async def get_documents():
    """ChromaDB is managed by the Ingestion Agent"""
    return {"message": "ChromaDB operations are handled by the Ingestion Agent", "status": "not_available_here"}


@app.post("/api/analyze-youtube")
async def analyze_youtube_video(request: YouTubeAnalysisRequest, background_tasks: BackgroundTasks):
    """
    Frontend calls this endpoint with YouTube URL.
    Returns immediately with placeholder results.
    Calls Ingestion Agent in background (non-blocking).
    
    Flow:
    Frontend → FastAPI (instant response) → [Background: Ingestion Agent → Text/Visual Agents → Scoring Agent]
    """
    print("\n" + "="*70)
    print("🎬 NEW REQUEST RECEIVED")
    print("="*70)
    
    try:
        # Generate unique request ID
        request_id = str(uuid.uuid4())
        
        print(f"📝 Request ID: {request_id}")
        print(f"🔗 YouTube URL: {request.youtube_url}")
        
        # Create request payload for Ingestion Agent
        ingestion_payload = {
            "request_id": request_id,
            "content_type": "video",
            "text_content": None,  # Will be extracted by ingestion agent
            "image_url": None,
            "video_url": request.youtube_url,
            "metadata": {
                "source": "youtube",
                "youtube_url": request.youtube_url
            },
            "timestamp": ""
        }
        
        # Add background task to call agent AFTER response is sent (if enabled)
        if ENABLE_AGENT_CALLS:
            print(f"\n📋 Adding Ingestion Agent call to background tasks")
            background_tasks.add_task(
                call_ingestion_agent_background,
                request_id,
                ingestion_payload
            )
            agent_contacted = True
            agent_error = None
        else:
            print(f"\n⏭️  Skipping agent call (ENABLE_AGENT_CALLS = False)")
            agent_contacted = False
            agent_error = "Agent calls disabled"
        
        # Build response
        print(f"\n📦 Building response to frontend...")
        response_data = {
            "request_id": request_id,
            "youtube_url": request.youtube_url,
            "status": "processing",
            "message": "Analysis started - Ingestion Agent processing in background",
            "agent_contacted": agent_contacted,
            "bias_score": 7.5,
            "text_bias": {
                "score": 7.0,
                "issues": ["Text bias analysis in progress"],
                "examples": ["Sample example text"]
            },
            "visual_bias": {
                "score": 8.0,
                "issues": ["Visual bias analysis in progress"],
                "examples": ["Sample example visual"]
            },
            "recommendations": [
                "Consider using more inclusive language",
                "Increase diversity in visual representation"
            ]
        }
        
        print(f"✅ Response ready - Status: {response_data['status']}")
        print(f"✅ Sendin
[truncated — 1148 more characters]
```

### frontend/app/layout.tsx

```typescript
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { Navbar } from "./components/navbar"

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

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

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        <Navbar />
        {children}
      </body>
    </html>
  );
}

```

### frontend/app/portal/page.tsx

```typescript
"use client"

import { useState } from "react"
import Link from "next/link"

// Mock data for company's ad history
const mockAdHistory = [
  {
    id: 1,
    name: "Summer Collection Launch",
    date: "2025-10-15",
    biasScore: 92,
    status: "approved",
    category: "Fashion",
    issues: ["Minor gendered language in headline"],
  },
  {
    id: 2,
    name: "Tech Product Demo",
    date: "2025-10-10",
    biasScore: 78,
    status: "revised",
    category: "Technology",
    issues: ["Lack of diversity in visuals", "Age stereotyping detected"],
  },
  {
    id: 3,
    name: "Holiday Special Offer",
    date: "2025-10-05",
    biasScore: 95,
    status: "approved",
    category: "Retail",
    issues: [],
  },
  {
    id: 4,
    name: "Fitness App Campaign",
    date: "2025-09-28",
    biasScore: 65,
    status: "rejected",
    category: "Health & Wellness",
    issues: ["Body shaming implications", "Gender stereotyping", "Exclusionary language"],
  },
  {
    id: 5,
    name: "Back to School",
    date: "2025-09-20",
    biasScore: 88,
    status: "approved",
    category: "Education",
    issues: ["Minor accessibility concerns"],
  },
]

const companyStats = {
  totalAdsScanned: 47,
  averageBiasScore: 84,
  adsApproved: 35,
  adsRevised: 8,
  adsRejected: 4,
  improvementRate: "+12%",
}

// Mock data for bias score trend over time
const biasScoreTrend = [
  { date: "Sep 1", score: 76 },
  { date: "Sep 8", score: 78 },
  { date: "Sep 15", score: 74 },
  { date: "Sep 22", score: 80 },
  { date: "Sep 29", score: 82 },
  { date: "Oct 6", score: 85 },
  { date: "Oct 13", score: 83 },
  { date: "Oct 20", score: 88 },
  { date: "Oct 27", score: 91 },
]

function StatCard({ title, value, subtitle }: { title: string; value: string | number; subtitle?: string }) {
  return (
    <div className="bg-white border border-border rounded-lg p-6 flex flex-col gap-2 shadow-sm">
      <div className="text-muted-foreground text-sm font-medium">{title}</div>
      <div className="text-foreground text-3xl font-bold">{value}</div>
      {subtitle && <div className="text-xs text-green-600 font-medium">{subtitle}</div>}
    </div>
  )
}

function BiasScoreChart({ data }: { data: typeof biasScoreTrend }) {
  const width = 800
  const height = 250
  const padding = { top: 20, right: 20, bottom: 40, left: 50 }
  const chartWidth = width - padding.left - padding.right
  const chartHeight = height - padding.top - padding.bottom

  const minScore = 60
  const maxScore = 100

  // Calculate points for the line
  const points = data.map((item, index) => {
    const x = padding.left + (index / (data.length - 1)) * chartWidth
    const y = padding.top + chartHeight - ((item.score - minScore) / (maxScore - minScore)) * chartHeight
    return { x, y, ...item }
  })

  // Create path for the line
  const linePath = points.map((point, index) => {
    if (index === 0) return `M ${point.x} ${point.y}`
    return `L ${point.x} ${point.y}`
  }).join(' ')

  // Create area path
  const areaPath = `${linePath} L ${points[points.length - 1].x} ${height - padding.bottom} L ${padding.left} ${height - padding.bottom} Z`

  // Y-axis labels
  const yLabels = [60, 70, 80, 90, 100]

  return (
    <div className="w-full overflow-x-auto">
      <svg viewBox={`0 0 ${width} ${height}`} className="w-full h-auto">
        {/* Grid lines */}
        {yLabels.map((label) => {
          const y = padding.top + chartHeight - ((label - minScore) / (maxScore - minScore)) * chartHeight
          return (
            <g key={label}>
              <line
                x1={padding.left}
                y1={y}
                x2={width - padding.right}
                y2={y}
                stroke="#e5e7eb"
                strokeWidth="1"
              />
              <text x={padding.left - 10} y={y + 4} textAnchor="end" className="text-xs fill-gray-500">
                {label}
              </text>
            </g>
          )
        })}

        {/* Area gradient */}
        <defs>
          <linearGradient id="areaGradient" x1="0" x2="0" y1="0" y2="1">
            <stop offset="0%" stopColor="#10b981" stopOpacity="0.3" />
            <stop offset="100%" stopColor="#10b981" stopOpacity="0.05" />
          </linearGradient>
        </defs>

        {/* Area under the line */}
        <path d={areaPath} fill="url(#areaGradient)" />

        {/* Line */}
        <path d={linePath} fill="none" stroke="#10b981" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" />

        {/* Data points */}
        {points.map((point, index) => (
          <g key={index}>
            <circle cx={point.x} cy={point.y} r="5" fill="white" stroke="#10b981" strokeWidth="3" />
            <circle cx={point.x} cy={point.y} r="2" fill="#10b981" />
          </g>
        ))}

        {/* X-axis labels */}
        {points.map((point, index) => (
          <text
            key={index}
            x={point.x}
            y={height - padding.bottom + 20}
            textAnchor="middle"
            className="text-xs fill-gray-500"
          >
            {point.date}
          </text>
        ))}

        {/* Score labels on hover */}
        {points.map((point, index) => (
          <text
            key={`score-${index}`}
            x={point.x}
            y={point.y - 15}
            textAnchor="middle"
            className="text-xs font-semibold fill-gray-700"
          >
            {point.score}
          </text>
        ))}
      </svg>
    </div>
  )
}

function AdHistoryRow({ ad }: { ad: typeof mockAdHistory[0] }) {
  const getStatusColor = (status: string) => {
    switch (status) {
      case "approved":
        return "bg-green-100 text-green-800"
      case "revised":
        return "bg-yellow-100 text-yellow-800"
      case "rejected":
        return "bg-red-100 text-red-800"
      default:
        return "bg-gray-100 text-gray-800"
    }
  }

  const getScoreColor = (score: number) => {
    if (score >= 90) return "text-green-600"
    if (score 
[truncated — 6023 more characters]
```

### frontend/app/(landing)/page.tsx

```typescript
"use client"

import type React from "react"

import { useState, useEffect, useRef } from "react"
import Link from "next/link"
import CTASection from "../components/cta-section"

function Badge({ icon, text }: { icon: React.ReactNode; text: string }) {
  return (
    <div className="px-[14px] py-[6px] bg-white shadow-[0px_0px_0px_4px_rgba(55,50,47,0.05)] overflow-hidden rounded-[90px] flex justify-start items-center gap-[8px] border border-accent/20 shadow-xs">
      <div className="w-[14px] h-[14px] relative overflow-hidden flex items-center justify-center">{icon}</div>
      <div className="text-center flex justify-center flex-col text-foreground text-xs font-medium leading-3 font-sans">
        {text}
      </div>
    </div>
  )
}

function FeatureCard({
  title,
  description,
  isActive,
  progress,
  onClick,
}: {
  title: string
  description: string
  isActive: boolean
  progress: number
  onClick: () => void
}) {
  return (
    <div
      className={`w-full px-6 md:px-6 py-8 md:py-9 overflow-hidden flex flex-col justify-start items-start cursor-pointer relative rounded-lg border min-h-[180px] md:min_h-[200px] transition-shadow ${
        isActive
          ? "bg-white border-primary/30 shadow-[0_0_0_1px_rgba(0,0,0,0.08)_inset]"
          : "bg-white border-border hover:shadow-sm"
      }`}
      onClick={onClick}
    >
      <div className="self-stretch flex justify-center flex-col text-foreground text-sm md:text-sm font-semibold leading-6 md:leading-6 font-sans mb-2">
        {title}
      </div>
      <div className="self-stretch text-muted-foreground text-[13px] md:text-[13px] font-normal leading-[22px] md:leading-[22px] font-sans">
        {description}
      </div>

      {isActive && (
        <div className="absolute bottom-0 left-0 w-full h-0.5 bg-border">
          <div className="h-full bg-primary transition-all duration-100 ease-linear" style={{ width: `${progress}%` }} />
        </div>
      )}
    </div>
  )
}

export default function LandingPage() {
  const [activeCard, setActiveCard] = useState(0)
  const [progress, setProgress] = useState(0)
  const mountedRef = useRef(true)
  const intervalRef = useRef<NodeJS.Timeout | null>(null)

  useEffect(() => {
    // Reset mounted ref when component mounts
    mountedRef.current = true
    
    const progressInterval = setInterval(() => {
      if (!mountedRef.current) return

      setProgress((prev) => {
        if (prev >= 100) {
          if (mountedRef.current) {
            setActiveCard((current) => (current + 1) % 3)
          }
          return 0
        }
        return prev + 2
      })
    }, 100)
    
    intervalRef.current = progressInterval

    return () => {
      if (intervalRef.current) {
        clearInterval(intervalRef.current)
        intervalRef.current = null
      }
      mountedRef.current = false
    }
  }, [])

  const handleCardClick = (index: number) => {
    if (!mountedRef.current) return
    setActiveCard(index)
    setProgress(0)
  }

  return (
    <div className="w-full min-h-screen relative bg-background overflow-x-hidden flex flex-col justify-start items-center">
      
      {/* Gradient Splotches - scroll with page */}
      <div className="absolute inset-0 pointer-events-none overflow-hidden w-full">
        {/* Secondary (coral/orange) gradient splotch - top left */}
        <div 
          className="absolute top-32 left-[-100px] w-[600px] h-[600px] rounded-full"
          style={{
            background: 'radial-gradient(circle, rgba(252, 211, 77, 0.5) 0%, rgba(251, 146, 60, 0.25) 40%, transparent 70%)',
            filter: 'blur(100px)'
          }}
        ></div>
        
        {/* Secondary (yellow/orange) gradient splotch - top right */}
        <div 
          className="absolute top-20 right-[-150px] w-[550px] h-[550px] rounded-full"
          style={{
            background: 'radial-gradient(circle, rgba(252, 211, 77, 0.45) 0%, rgba(251, 146, 60, 0.22) 40%, transparent 70%)',
            filter: 'blur(90px)'
          }}
        ></div>
        
        {/* Primary (dark) gradient splotch - center left */}
        <div 
          className="absolute top-[40%] left-[-80px] w-[450px] h-[450px] rounded-full"
          style={{
            background: 'radial-gradient(circle, rgba(56, 56, 58, 0.12) 0%, rgba(56, 56, 58, 0.06) 40%, transparent 70%)',
            filter: 'blur(70px)'
          }}
        ></div>
        
        {/* Secondary (coral) gradient splotch - center */}
        <div 
          className="absolute top-[50%] left-[50%] transform -translate-x-1/2 -translate-y-1/2 w-[700px] h-[700px] rounded-full"
          style={{
            background: 'radial-gradient(circle, rgba(251, 146, 60, 0.35) 0%, rgba(252, 211, 77, 0.18) 40%, transparent 70%)',
            filter: 'blur(110px)'
          }}
        ></div>
        
        {/* Secondary (yellow) gradient splotch - bottom right */}
        <div 
          className="absolute top-[80%] right-[-100px] w-[500px] h-[500px] rounded-full"
          style={{
            background: 'radial-gradient(circle, rgba(252, 211, 77, 0.4) 0%, rgba(251, 146, 60, 0.2) 40%, transparent 70%)',
            filter: 'blur(85px)'
          }}
        ></div>
      </div>

      <div className="relative flex flex-col justify-start items-center w-full">
        <div className="w-full max-w-none px-4 sm:px-6 md:px-8 lg:px-0 lg:max-w-[1060px] lg:w-[1060px] relative flex flex-col justify-start items-start min-h-screen">
          <div className="h-full absolute left-4 sm:left-6 md:left-8 lg:left-0 top-0 bg-border shadow-[1px_0px_0px_white] z-0"></div>
          <div className="h-full absolute right-4 sm:right-6 md:right-8 lg:right-0 top-0 bg-border shadow-[1px_0px_0px_white] z-0"></div>

          <div className="self-stretch pt-[9px] overflow-hidden border-b border-border flex flex-col justify-center items-center gap-4 sm:gap-6 md:gap-8 lg:gap-[66px] relative z-10">

            {/* Hero Section */}
            <div className="pt-1
[truncated — 7571 more characters]
```

### frontend/app/upload/page.tsx

```typescript
"use client"

import { useState } from "react"

export default function UploadPage() {
  const [youtubeUrl, setYoutubeUrl] = useState("")
  const [isLoading, setIsLoading] = useState(false)
  const [analysisResult, setAnalysisResult] = useState<any>(null)
  const [error, setError] = useState("")

  const handleYoutubeSubmit = async (e: React.FormEvent) => {
    e.preventDefault()
    setError("")
    setAnalysisResult(null)

    console.log("========================================")
    console.log("🎬 YouTube Analysis Submission Started")
    console.log("========================================")
    console.log("📝 YouTube URL:", youtubeUrl)

    // Validate YouTube URL
    const youtubeRegex = /^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.be)\/.+$/
    if (!youtubeRegex.test(youtubeUrl)) {
      console.log("❌ Validation failed: Invalid YouTube URL")
      setError("Please enter a valid YouTube URL")
      return
    }
    console.log("✅ URL validation passed")

    setIsLoading(true)
    console.log("⏳ Setting loading state to true")

    try {
      const apiUrl = "http://localhost:8000/api/analyze-youtube"
      const payload = { youtube_url: youtubeUrl }
      
      console.log("\n📤 Sending request to backend:")
      console.log("   URL:", apiUrl)
      console.log("   Method: POST")
      console.log("   Payload:", payload)
      console.log("   Timestamp:", new Date().toISOString())
      
      const startTime = Date.now()
      
      const response = await fetch(apiUrl, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify(payload),
      })

      const responseTime = Date.now() - startTime
      console.log(`\n📥 Response received (${responseTime}ms)`)
      console.log("   Status:", response.status)
      console.log("   Status Text:", response.statusText)
      console.log("   OK:", response.ok)

      if (!response.ok) {
        console.log("❌ Response not OK")
        const errorText = await response.text()
        console.log("   Error body:", errorText)
        throw new Error(`Failed to analyze video (${response.status})`)
      }

      console.log("📦 Parsing JSON response...")
      const result = await response.json()
      console.log("✅ Response parsed successfully:")
      console.log(result)
      
      setAnalysisResult(result)
      console.log("✅ Analysis result set in state")
      console.log("========================================\n")
      
    } catch (err) {
      console.log("\n❌ ERROR OCCURRED:")
      console.log("   Type:", err instanceof Error ? err.constructor.name : typeof err)
      console.log("   Message:", err instanceof Error ? err.message : String(err))
      console.log("   Full error:", err)
      console.log("========================================\n")
      
      const errorMessage = err instanceof Error ? err.message : "An error occurred while analyzing the video"
      setError(errorMessage)
      
      // Check if it's a network error
      if (err instanceof TypeError && err.message.includes("fetch")) {
        setError("Cannot connect to backend server. Make sure it's running on http://localhost:8000")
      }
    } finally {
      console.log("🔄 Cleaning up - setting loading to false")
      setIsLoading(false)
    }
  }

  const getBiasScoreColor = (score: number) => {
    if (score >= 9) return "text-green-600"
    if (score >= 7) return "text-yellow-600"
    if (score >= 4) return "text-orange-600"
    return "text-red-600"
  }

  const getBiasScoreLabel = (score: number) => {
    if (score >= 9) return "Minimal Bias"
    if (score >= 7) return "Minor Bias"
    if (score >= 4) return "Moderate Bias"
    return "Significant Bias"
  }

  return (
    <div className="w-full min-h-screen relative bg-background overflow-x-hidden flex flex-col justify-start items-center">
      <div className="relative flex flex-col justify-start items-center w-full">
        <div className="w-full max-w-none px-4 sm:px-6 md:px-8 lg:px-0 lg:max-w-[1060px] lg:w-[1060px] relative flex flex-col justify-start items-start min-h-screen">
          {/* Border lines matching landing page */}
          <div className="h-full absolute left-4 sm:left-6 md:left-8 lg:left-0 top-0 bg-border shadow-[1px_0px_0px_white] z-0 w-px"></div>
          <div className="h-full absolute right-4 sm:right-6 md:right-8 lg:right-0 top-0 bg-border shadow-[1px_0px_0px_white] z-0 w-px"></div>

          <div className="self-stretch pt-[9px] overflow-hidden border-b border-border flex flex-col justify-center items-center gap-8 lg:gap-12 relative z-10">
            
            {/* Hero Section */}
            <div className="pt-16 sm:pt-20 md:pt-24 lg:pt-32 pb-8 sm:pb-12 md:pb-16 flex flex-col justify-start items-center px-2 sm:px-4 md:px-8 lg:px-0 w-full">
              <div className="w-full max-w-[800px] flex flex-col justify-center items-center gap-4 sm:gap-5 md:gap-6">
                <div className="text-center flex justify-center flex-col text-foreground text-[32px] sm:text-[42px] md:text-[52px] lg:text-[64px] font-bold leading-[1.1] font-serif">
                  Analyze your ad
                </div>
                <div className="w-full max-w-[600px] text-center flex justify-center flex-col text-muted-foreground text-base sm:text-lg md:text-xl leading-[1.5] font-sans font-medium">
                  Upload your YouTube ad link for instant bias detection and actionable recommendations
                </div>
              </div>

              {/* YouTube URL Input Card */}
              <div className="w-full max-w-[700px] mt-8 sm:mt-10 md:mt-12">
                <div className="bg-white shadow-[0px_0px_0px_0.75px_rgba(0,0,0,0.08)] overflow-hidden rounded-[6px] border border-border/50">
                  <form onSubmit={handleYoutubeSubmit} className="p-6 sm:p-8 space-y-5">
                    
                    <div className="flex items-center gap-3 pb-4 border-b border-border">
  
[truncated — 12241 more characters]
```

### frontend/next.config.ts

```typescript
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  async headers() {
    return [
      {
        source: "/fetchai.png",
        headers: [
          { key: "Cache-Control", value: "no-store, no-cache, must-revalidate, proxy-revalidate" },
          { key: "Pragma", value: "no-cache" },
          { key: "Expires", value: "0" },
        ],
      },
    ];
  },
};

export default nextConfig;

```

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