# Project export: PitchLab

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: PitchLab generates catered mock sales pitch calls for sales reps to learn, practice, and apply before the real deal. Help reps get their reps in
- Devpost: https://devpost.com/software/pitchlab-clxhuj
- GitHub: https://github.com/Kevinxygu/calhacks.12.0
- Video: https://www.youtube.com/embed/MiJudXabmlQ?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Result: winner (Rox: Best Use of Rox)
- Team: 1 GitHub contributor(s) — Kevin Gu (10 commits)

## Devpost submission (written by the team)

### Inspiration

Around 79% of sales pitches fail, with every bad pitch costing companies millions in lost revenue. However, there isn't enough practice. Managers are too busy, role-play mocks are not entirely accurate to the real thing, and you only get one quick shot with a real prospect. What if there was a product that could mimic the exact prospect you'll be speaking too?

### What it does

PitchLab generates a simulated prospect for you to practice sales pitches with based on your learning goals, company-specific data, and industry research. We use large language models (LLM) and AI agents to act autonomously on a company's messy, real-world data. After analyzing company sales data, we generate a simulated pitch call environment with an AI prospect. This AI prospect speaks to you in real time, and is catered based on the prospective company, the sales rep's learning goals, and company product suite

### How we built it

We used NextJS, TypeScript, TailwindCSS, ReactJS for the front-end development. We used Go for the backend. Claude, ElevenLabs, and AI agents (similar to what Rox offers) create a perfect user prospect that sales reps can practice with. Here are the presentation slides! https://docs.google.com/presentation/d/1nO3dRgHFU7kB5YlKxWr52657JoPnywPV00_sTdDzK0Y/edit?usp=sharing

## README (from the GitHub repository)

# calhacks.12.0
## Inspiration
Around 79% of sales pitches fail, with every bad pitch costing companies millions in lost revenue. However, there isn't enough practice. Managers are too busy, role-play mocks are not entirely accurate to the real thing, and you only get one quick shot with a real prospect.

What if there was a product that could mimic the exact prospect you'll be speaking too?

## What it does
PitchLab generates a simulated prospect for you to practice sales pitches with based on your learning goals, company-specific data, and industry research. We use large language models (LLM) and AI agents to act autonomously on a company's messy, real-world data. After analyzing company sales data, we generate a simulated pitch call environment with an AI prospect. This AI prospect speaks to you in real time, and is catered based on the prospective company, the sales rep's learning goals, and company product suite


## Detected evidence (automated analysis)

Indexed codebase: 27 recognized source files, 205 KB.
- CSS (language) — detected in the code
- Go (language) — detected in the code
- Next.js (technology) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code

## Codebase structure (from repository index)

### Files (42 of 42)

```
.dockerignore
backend/.air.toml
backend/.dockerignore
backend/.env.example
backend/.gitignore
backend/cmd/server/main.go
backend/Dockerfile
backend/go.mod
backend/go.sum
backend/internal/handlers/persona.go
backend/internal/integrations/claude.go
backend/internal/integrations/elevenlabs.go
backend/internal/models/persona.go
backend/internal/services/persona_service.go
docker-compose.yml
frontend/.dockerignore
frontend/.env.example
frontend/.gitignore
frontend/app/feedback/page.tsx
frontend/app/globals.css
frontend/app/layout.tsx
frontend/app/page.tsx
frontend/app/persona/page.tsx
frontend/app/simulation/page.tsx
frontend/components/DifficultySelector.tsx
frontend/components/FileUpload.tsx
frontend/components/FormInput.tsx
frontend/components/FormTextArea.tsx
frontend/components/SelectInput.tsx
frontend/components/ui/button.tsx
frontend/components/ui/Spinner.tsx
frontend/Dockerfile
frontend/eslint.config.mjs
frontend/lib/api.ts
frontend/lib/types.ts
frontend/next.config.ts
frontend/package.json
frontend/postcss.config.mjs
frontend/README.md
frontend/tsconfig.json
README.md
setup/README.md
```

### Dependencies

- backend/go.mod: github.com/bytedance/sonic@v1.14.0, github.com/bytedance/sonic/loader@v0.3.0, github.com/cloudwego/base64x@v0.1.6, github.com/gabriel-vasile/mimetype@v1.4.8, github.com/gin-contrib/sse@v1.1.0, github.com/gin-gonic/gin@v1.11.0, github.com/go-playground/locales@v0.14.1, github.com/go-playground/universal-translator@v0.18.1, github.com/go-playground/validator/v10@v10.27.0, github.com/goccy/go-json@v0.10.2, github.com/goccy/go-yaml@v1.18.0, github.com/joho/godotenv@v1.5.1, github.com/json-iterator/go@v1.1.12, github.com/klauspost/cpuid/v2@v2.3.0, github.com/leodido/go-urn@v1.4.0, github.com/mattn/go-isatty@v0.0.20, github.com/modern-go/concurrent@v0.0.0-20180228061459-e0a39a4cb421, github.com/modern-go/reflect2@v1.0.2, github.com/pelletier/go-toml/v2@v2.2.4, github.com/quic-go/qpack@v0.5.1, github.com/quic-go/quic-go@v0.54.0, github.com/twitchyliquid64/golang-asm@v0.15.1, github.com/ugorji/go/codec@v1.3.0, go.uber.org/mock@v0.5.0, golang.org/x/arch@v0.20.0, golang.org/x/crypto@v0.40.0, golang.org/x/mod@v0.25.0, golang.org/x/net@v0.42.0, golang.org/x/sync@v0.16.0, golang.org/x/sys@v0.35.0, golang.org/x/text@v0.27.0, golang.org/x/tools@v0.34.0, google.golang.org/protobuf@v1.36.9
- frontend/package.json: @tailwindcss/postcss@^4, @tanstack/react-query@^5.90.5, @tensorflow-models/face-detection@^1.0.3, @tensorflow-models/face-landmarks-detection@^1.0.6, @tensorflow/tfjs@^4.22.0, @tensorflow/tfjs-backend-webgl@^4.22.0, @types/node@^20, @types/react@^19, @types/react-dom@^19, eslint@^9, eslint-config-next@16.0.0, framer-motion@^12.23.24, lucide-react@^0.548.0, next@16.0.0, react@19.2.0, react-dom@19.2.0, sonner@^2.0.7, tailwindcss@^4, typescript@^5, zustand@^5.0.8

### Recent commits (newest first)

- Enhance README with inspiration and product details
- included new textArea input for notes
- added notes
- completed voice activation functionality for the interview
- FE populated
- verified docker-compose up works! all users should be good to setup now
- setup instructions done
- finished containerization. run docker-compose up in root and should be good :)
- setup FE + BE. next step is to containerize
- ADD TEAM
- Initial commit

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

### docker-compose.yml

```yaml
# docker-compose.yml
# This file defines all services (backend, frontend) and how they connect

version: '3.8'  # Docker Compose file format version

services:
  # ============================================================================
  # BACKEND SERVICE (Go)
  # ============================================================================
  backend:
    # Build configuration
    build:
      context: ./backend           # Where to find Dockerfile
      dockerfile: Dockerfile       # Dockerfile name
    
    # Container name (easier to identify in docker ps)
    container_name: sales-sim-backend
    
    # Port mapping: host:container
    # 8080:8080 means localhost:8080 on your machine -> port 8080 in container
    ports:
      - "8080:8080"
    
    # Environment variables
    # These are available inside the container
    environment:
      - GIN_MODE=debug            # Gin debug mode (more logging)
      - PORT=8080
    
    # Volumes: Mount local code into container
    # THIS IS KEY FOR HOT RELOAD!
    # Format: ./local/path:/container/path
    # When you edit files locally, changes appear in container immediately
    volumes:
      - ./backend:/app             # Mount entire backend directory
      - /app/tmp                   # Don't mount tmp (build artifacts)
    
    # Restart policy: always restart if container crashes
    restart: unless-stopped
    
    # Networks: Connect to custom network (so services can talk to each other)
    networks:
      - sales-sim-network

  # ============================================================================
  # FRONTEND SERVICE (Next.js)
  # ============================================================================
  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile
    
    container_name: sales-sim-frontend
    
    ports:
      - "3000:3000"
    
    environment:
      # Point frontend to backend using service name
      # In Docker Compose, services can reach each other by service name
      # "backend" resolves to the backend container's IP
      - NEXT_PUBLIC_API_URL=http://backend:8080/api
      - NEXT_PUBLIC_WS_URL=ws://backend:8080/ws
    
    # Volumes for hot reload
    volumes:
      - ./frontend:/app                    # Mount source code
      - /app/node_modules                  # Don't mount node_modules
      - /app/.next                         # Don't mount build cache
    
    # Depends on backend (start backend first)
    depends_on:
      - backend
    
    restart: unless-stopped
    
    networks:
      - sales-sim-network

# ============================================================================
# NETWORKS
# ============================================================================
# Custom network so containers can communicate
# Without this, containers are isolated
networks:
  sales-sim-network:
    driver: bridge  # Default network driver (like a virtual switch)
```

### frontend/Dockerfile

```
# Use official Node.js image
FROM node:20-alpine

# Install pnpm globally
# This is like pip for Python or go get for Go
RUN npm install -g pnpm

# Set working directory
WORKDIR /app

# Copy package files first (for better caching)
# If package.json doesn't change, dependencies won't reinstall
COPY package.json pnpm-lock.yaml ./

# Install dependencies
# --frozen-lockfile ensures exact versions from lockfile
RUN pnpm install --frozen-lockfile

# Copy the rest of the source code
COPY . .

# Expose port 3000 (Next.js default dev port)
EXPOSE 3000

# Run Next.js in development mode (has built-in hot reload)
# This is like Flask's debug mode - it watches files and reloads automatically
CMD ["pnpm", "dev"]
```

### backend/Dockerfile

```
# Use official Go image
FROM golang:1.23-alpine

# Install Air for hot reloading (like nodemon for Node.js)
# Air watches Go files and rebuilds automatically when you save
RUN go install github.com/air-verse/air@v1.52.3

# Set working directory inside container
WORKDIR /app

# Copy go.mod and go.sum first (for better caching)
# Docker caches layers, so if these don't change, dependencies won't reinstall
COPY go.mod go.sum ./

# Download dependencies
# This layer is cached unless go.mod/go.sum change
RUN go mod download

# Copy the rest of the source code
# This happens after dependencies so changes to code don't require re-downloading deps
COPY . .

# Expose port 8080 (document what port the app uses)
EXPOSE 8080

# Run Air (hot reload server)
# Air will watch for file changes and rebuild automatically
CMD ["air", "-c", ".air.toml"]
```

### frontend/package.json

```
{
  "name": "frontend",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint"
  },
  "dependencies": {
    "@tanstack/react-query": "^5.90.5",
    "@tensorflow-models/face-detection": "^1.0.3",
    "@tensorflow-models/face-landmarks-detection": "^1.0.6",
    "@tensorflow/tfjs": "^4.22.0",
    "@tensorflow/tfjs-backend-webgl": "^4.22.0",
    "framer-motion": "^12.23.24",
    "lucide-react": "^0.548.0",
    "next": "16.0.0",
    "react": "19.2.0",
    "react-dom": "19.2.0",
    "sonner": "^2.0.7",
    "zustand": "^5.0.8"
  },
  "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"
  }
}

```

### backend/go.mod

```
module calhacks

go 1.23.0

require (
	github.com/gin-gonic/gin v1.11.0
	github.com/joho/godotenv v1.5.1
)

require (
	github.com/bytedance/sonic v1.14.0 // indirect
	github.com/bytedance/sonic/loader v0.3.0 // indirect
	github.com/cloudwego/base64x v0.1.6 // indirect
	github.com/gabriel-vasile/mimetype v1.4.8 // indirect
	github.com/gin-contrib/sse v1.1.0 // indirect
	github.com/go-playground/locales v0.14.1 // indirect
	github.com/go-playground/universal-translator v0.18.1 // indirect
	github.com/go-playground/validator/v10 v10.27.0 // indirect
	github.com/goccy/go-json v0.10.2 // indirect
	github.com/goccy/go-yaml v1.18.0 // indirect
	github.com/json-iterator/go v1.1.12 // indirect
	github.com/klauspost/cpuid/v2 v2.3.0 // indirect
	github.com/leodido/go-urn v1.4.0 // indirect
	github.com/mattn/go-isatty v0.0.20 // indirect
	github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
	github.com/modern-go/reflect2 v1.0.2 // indirect
	github.com/pelletier/go-toml/v2 v2.2.4 // indirect
	github.com/quic-go/qpack v0.5.1 // indirect
	github.com/quic-go/quic-go v0.54.0 // indirect
	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
	github.com/ugorji/go/codec v1.3.0 // indirect
	go.uber.org/mock v0.5.0 // indirect
	golang.org/x/arch v0.20.0 // indirect
	golang.org/x/crypto v0.40.0 // indirect
	golang.org/x/mod v0.25.0 // indirect
	golang.org/x/net v0.42.0 // indirect
	golang.org/x/sync v0.16.0 // indirect
	golang.org/x/sys v0.35.0 // indirect
	golang.org/x/text v0.27.0 // indirect
	golang.org/x/tools v0.34.0 // indirect
	google.golang.org/protobuf v1.36.9 // indirect
)

```

### frontend/app/layout.tsx

```typescript
// app/layout.tsx
import './globals.css'
import type { Metadata } from 'next'
import { Onest } from 'next/font/google'

const onest = Onest({ subsets: ['latin'] })

export const metadata: Metadata = {
  title: 'Dynamic',
  description: 'Practice sales calls with AI-powered prospects that help you win up',
}

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

### frontend/app/page.tsx

```typescript
// app/page.tsx
'use client'

import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { FormInput } from '@/components/FormInput'
import { DifficultySelector } from '@/components/DifficultySelector'
import { FileUpload } from '@/components/FileUpload'
import { SelectInput } from '@/components/SelectInput'
import { Button } from '@/components/ui/button'
import { SimulationSettings } from '@/lib/types'
import { FormTextArea } from '@/components/FormTextArea'

export default function HomePage() {
  const router = useRouter()
  const [settings, setSettings] = useState<SimulationSettings>({
    difficulty: 'easy',
    prospectRole: '',
    companyName: '',
    callType: 'cold',
    enableCoaching: true,
    notes: '',
  })
  const [uploadedFile, setUploadedFile] = useState<File | null>(null)
  const [isLoading, setIsLoading] = useState(false)

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()
    setIsLoading(true)

    try {
      // Store settings for persona generation
      sessionStorage.setItem('simulationSettings', JSON.stringify(settings))
      
      // Navigate to persona generation page
      // The persona page will make the API call
      router.push('/persona')
    } catch (error) {
      console.error('Error:', error)
      // toast.error('Failed to start interview')
      setIsLoading(false)
    }
  }

  const isFormValid = settings.prospectRole && settings.companyName

  return (
    <div className="min-h-screen bg-palette-background-blue flex items-center justify-center p-8">
      <div className="w-full max-w-xl">
        <div className="text-center mb-8">
          <h1 className="text-3xl font-semibold text-white mb-2">
            Enter Interview Details
          </h1>
        </div>

        <form onSubmit={handleSubmit} className="flex flex-col gap-6">
          <FormInput
            label="Prospect Role"
            value={settings.prospectRole}
            onChange={(value) =>
              setSettings({ ...settings, prospectRole: value })
            }
            placeholder="e.g., VP of Engineering"
            required
          />

          <FormInput
            label="Company Name"
            value={settings.companyName}
            onChange={(value) =>
              setSettings({ ...settings, companyName: value })
            }
            placeholder="e.g., Acme Corp"
            required
          />

          <SelectInput
            label="Call type"
            value={settings.callType}
            onChange={(value) =>
              setSettings({ ...settings, callType: value as any })
            }
            options={['cold', 'follow-up', 'closing']}
          />

          <DifficultySelector
            value={settings.difficulty}
            onChange={(value) =>
              setSettings({ ...settings, difficulty: value })
            }
          />

          <FileUpload
            label="Previous client calls"
            onChange={setUploadedFile}
          />
          <FormTextArea
            label="Notes & Guidelines (Optional)"
            value={settings.notes || ''}
            onChange={(value) =>
              setSettings({ ...settings, notes: value })
            }
            placeholder="Example: Focus on our new AI features, emphasize ROI within 6 months, avoid discussing pricing until they show interest..."
            rows={5}
            maxLength={500}
          />

          <Button
            type="submit"
            disabled={!isFormValid || isLoading}
            className="w-full py-4 text-base mt-4"
          >
            {isLoading ? 'Starting Interview...' : 'Start Interview'}
          </Button>
        </form>
      </div>
    </div>
  )
}
```

### backend/cmd/server/main.go

```go
// backend/cmd/server/main.go
package main

// imports
import (
	"log"
	"net/http"
	"os"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/joho/godotenv"
	
	"calhacks/internal/handlers"  // ← Changed from "backend" to "calhacks"
)

// ... rest of your code stays the same
func main() {
	if err := godotenv.Load(); err != nil {
		log.Println("Warning: .env file not found")
	}

	router := gin.Default()
	router.Use(corsMiddleware())

	router.GET("/health", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"status":  "ok",
			"message": "Server is running",
			"time":    time.Now().Format(time.RFC3339),
			"service": "Calhacks 12.0 Backend",
		})
	})

	router.GET("/", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"message": "Welcome to the Calhacks 12.0 Backend!",
			"docs":    "Visit /health for the health check",
		})
	})

	api := router.Group("/api")
	{
		personaHandler := handlers.NewPersonaHandler()
		api.POST("/persona/generate", personaHandler.GeneratePersona)
	}

	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}

	log.Printf("🚀 Server starting on http://localhost:%s", port)
	log.Printf("📊 Health check: http://localhost:%s/health", port)
	log.Printf("🤖 Persona API: http://localhost:%s/api/persona/generate", port)

	if err := router.Run(":" + port); err != nil {
		log.Fatalf("Failed to start server: %v", err)
	}
}

func corsMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
		c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
		c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")

		if c.Request.Method == "OPTIONS" {
			c.AbortWithStatus(204)
			return
		}

		c.Next()
	}
}
```

### frontend/app/persona/page.tsx

```typescript
// app/persona/page.tsx
'use client'

import { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'
import { SimulationSettings, Persona } from '@/lib/types'
import { toast } from 'sonner'

const loadingPhrases = [
  'Brewing coffee...',
  'Ruffling papers...',
  'Opening notes...',
  'Adjusting tie...',
  'Clearing throat...',
  'Heading into the Zoom call...',
  'Reviewing your profile...',
  'Preparing objections...',
]

export default function PersonaPage() {
  const router = useRouter()
  const [currentPhrase, setCurrentPhrase] = useState(0)
  const [error, setError] = useState<string | null>(null)

  useEffect(() => {
    // Rotate through phrases
    const phraseInterval = setInterval(() => {
      setCurrentPhrase((prev) => (prev + 1) % loadingPhrases.length)
    }, 2000)

    // Generate persona
    generatePersona()

    return () => {
      clearInterval(phraseInterval)
    }
  }, [])

  const generatePersona = async () => {
    try {
      // Get settings from sessionStorage
      const settingsStr = sessionStorage.getItem('simulationSettings')
      if (!settingsStr) {
        throw new Error('No settings found')
      }

      const settings: SimulationSettings = JSON.parse(settingsStr)

      // Call backend API to generate persona
      const response = await fetch('http://localhost:8080/api/persona/generate', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(settings),
      })

      if (!response.ok) {
        throw new Error('Failed to generate persona')
      }

      const data = await response.json()

      // Store persona and greeting audio
      sessionStorage.setItem('persona', JSON.stringify(data.persona))
      sessionStorage.setItem('greetingAudio', data.greetingAudio)

      // Navigate to simulation
      router.push('/simulation')
    } catch (err) {
      console.error('Error generating persona:', err)
      setError('Failed to generate AI prospect. Please try again.')
      toast.error('Failed to generate AI prospect')
      
      // Redirect back to home after 3 seconds
      setTimeout(() => {
        router.push('/')
      }, 3000)
    }
  }

  return (
    <div className="min-h-screen bg-palette-background-blue flex flex-col items-center justify-center p-8">
      <div className="flex flex-col items-center gap-6">
        {/* Loading spinner */}
        <div className="relative w-32 h-32">
          <div className="w-full h-full rounded-full border-4 border-palette-green/20 border-t-palette-green animate-spin" />
        </div>

        {/* Status text */}
        {error ? (
          <div className="text-center">
            <p className="text-xl text-red-400 font-medium">{error}</p>
            <p className="text-sm text-white/60 mt-2">Redirecting...</p>
          </div>
        ) : (
          <>
            <p className="text-xl text-white font-medium animate-fade-in">
              {loadingPhrases[currentPhrase]}
            </p>
            <p className="text-sm text-white/60 mt-4">
              Generating your AI prospect...
            </p>
          </>
        )}
      </div>
    </div>
  )
}
```

### frontend/app/feedback/page.tsx

```typescript
// app/feedback/page.tsx
'use client'

import { ArrowUp, ArrowDown } from 'lucide-react'

interface ScoreCard {
  score: number
  change: number
  label: string
}

export default function FeedbackPage() {
  const scores: ScoreCard[] = [
    { score: 67, change: 10, label: 'Opening' },
    { score: 87, change: 2, label: 'Objection Handling' },
    { score: 91, change: -5, label: 'Closing' },
    { score: 75, change: -10, label: 'Listening' },
  ]

  const strengths = [
    {
      timestamp: '08:25',
      text: 'Really good job mapping the customer\'s pain point to Nerve\'s product offering. This was a key moment that captures what the customer was looking for',
    },
    {
      timestamp: '09:33',
      text: 'Solid closer; use this phrase next time in your pitches! You said "We are the dashboard for tomorrow" which the client repeated and resonated with',
    },
  ]

  const weaknesses = [
    {
      timestamp: '03:03',
      text: 'This is incorrect based on Nerve\'s product requirements. You mentioned it covers SOC 2 compliance, when it\'s actually HIPAA',
      link: 'Nerve\'s product requirements',
    },
  ]

  const suggestions = [
    {
      timestamp: '05:07',
      text: 'It was a good touch that you touched on Nerve as the product of choice. However, Ethos might have been a better choice since they were looking for only-digital solutions.',
      links: ['Nerve', 'Ethos'],
    },
  ]

  return (
    <div className="min-h-screen bg-palette-background-blue p-8">
      <div className="max-w-7xl mx-auto">
        {/* Score Cards */}
        <div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
          {scores.map((score) => (
            <div
              key={score.label}
              className="border border-white/20 rounded-lg p-6 flex flex-col items-center"
            >
              <div className="text-5xl font-bold text-white mb-2">
                {score.score}
              </div>
              <div className="flex items-center gap-1 mb-2">
                {score.change > 0 ? (
                  <>
                    <ArrowUp className="w-4 h-4 text-palette-green" />
                    <span className="text-palette-green text-sm font-medium">
                      +{score.change}%
                    </span>
                  </>
                ) : (
                  <>
                    <ArrowDown className="w-4 h-4 text-red-400" />
                    <span className="text-red-400 text-sm font-medium">
                      {score.change}%
                    </span>
                  </>
                )}
              </div>
              <div className="text-white/80 text-sm">{score.label}</div>
            </div>
          ))}
        </div>

        {/* Strengths */}
        <section className="mb-8">
          <h2 className="text-2xl font-bold text-white mb-4">Strengths</h2>
          <div className="space-y-4">
            {strengths.map((item, index) => (
              <div key={index} className="flex gap-3">
                <div className="flex-shrink-0 w-2 h-2 mt-2 rounded-full bg-palette-green" />
                <div>
                  <span className="text-white font-medium">{item.timestamp}</span>
                  <span className="text-white/80"> - {item.text}</span>
                </div>
              </div>
            ))}
          </div>
        </section>

        {/* Weaknesses */}
        <section className="mb-8">
          <h2 className="text-2xl font-bold text-white mb-4">Weaknesses</h2>
          <div className="space-y-4">
            {weaknesses.map((item, index) => (
              <div key={index} className="flex gap-3">
                <div className="flex-shrink-0 w-2 h-2 mt-2 rounded-full bg-red-400" />
                <div>
                  <span className="text-white font-medium">{item.timestamp}</span>
                  <span className="text-white/80"> - {item.text.split(item.link)[0]}</span>
                  <a href="#" className="text-white underline">
                    {item.link}
                  </a>
                  <span className="text-white/80">{item.text.split(item.link)[1]}</span>
                </div>
              </div>
            ))}
          </div>
        </section>

        {/* Suggestions */}
        <section>
          <h2 className="text-2xl font-bold text-white mb-4">Suggestions</h2>
          <div className="space-y-4">
            {suggestions.map((item, index) => (
              <div key={index} className="flex gap-3">
                <div className="flex-shrink-0 w-2 h-2 mt-2 rounded-full bg-yellow-400" />
                <div>
                  <span className="text-white font-medium">{item.timestamp}</span>
                  <span className="text-white/80"> - </span>
                  {item.text.split(item.links[0])[0]}
                  <a href="#" className="text-white underline">
                    {item.links[0]}
                  </a>
                  {item.text.split(item.links[0])[1].split(item.links[1])[0]}
                  <a href="#" className="text-white underline">
                    {item.links[1]}
                  </a>
                  {item.text.split(item.links[1])[1]}
                </div>
              </div>
            ))}
          </div>
        </section>
      </div>
    </div>
  )
}
```

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