# Project export: PolicyPulse

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

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2025
- Tagline: Ai agents debate policies to help you understand complex issue
- Devpost: https://devpost.com/software/policypulse-1acg5v
- GitHub: https://github.com/UC-Berkeley-AI-Hackathon-Project/PolicyPulse
- Team: 2 GitHub contributor(s) — Braden Stitt (16 commits), Braden Stitt (1 commits)

## Devpost submission (written by the team)

### Inspiration

We were inspired by the growing need for more accessible and engaging ways to understand environmental policies and their real-world consequences. Climate action often feels overwhelming or distant from personal experience, so we set out to build PolicyPulse, an interactive AI-powered with application that lets users experiment with policies and see the ripple effects of their choices. Our goal was to make climate education both visual and participatory — something that sparks curiosity and reflection.

### What it does

PolicyPulse allows users to simulate environmental debates and policy decisions in real-time. Users input a policy idea or environmental issue, and two AI agents — trained to represent opposing viewpoints — begin debating its pros and cons. Once the debate concludes, the user votes on which side was more compelling or balanced, with feedback visuals that simulate projected environmental impact based on their choice.

### How we built it

Frontend: Built with React + Vite for fast development and hot module reloading, styled using Tailwind CSS for a sleek and responsive UI that adapts across devices. AI & Voice: We used Google Gemini to generate dynamic debate scripts in response to user prompts, simulating intelligent back-and-forth arguments. These scripts were then passed to Vapi, which powered the natural-sounding voice output for each AI agent — creating an immersive and conversational experience. State Management & UI Interactions: We designed a flexible component system using UI and modular layout components for the debate arena and policy simulator. Design: We developed a visual identity with a black background and a bold orange and purple color palette, creating a sleek, high-contrast look that felt both modern and engaging. For the voting interaction, we kept it intuitive and direct using green for “yes” and red for “no”. The layout focused on clear information hierarchy, smooth spacing, and responsive components that supported both usability and aesthetic cohesion.

### Challenges we ran into

One of the main challenges was getting all the frameworks and tools to work smoothly — from configuring Vite and Tailwind to integrating Vapi and managing API calls. We also spent time refining our prompts with Gemini to generate consistent, balanced debates. Syncing the AI responses with Vapi’s voice output and coordinating the UI flow added extra complexity.

### Accomplishments we're proud of

Successfully implemented an end-to-end debate simulator where users could input a prompt, listen to AI agents respond, and then vote on the outcome. Created a polished, responsive, and theme-consistent front-end UI under a tight deadline. Designed voting interactivity that visually responds to user decisions with hover states and emotional cues. Achieved real-time voice conversation through Vapi, adding immersion and accessibility.

### What we learned

We learned how to bring together multiple AI tools and front-end technologies to create a seamless, interactive experience. Working with Google Gemini taught us the importance of precise prompt engineering, while Vapi showed us how voice AI can enhance immersion when integrated thoughtfully. We also gained hands-on experience with building responsive UIs quickly using React, Vite, and Tailwind CSS, while improving our collaboration and problem-solving under time pressure.

### What's next

We’re excited to expand PolicyPulse beyond just AI debates and move toward action-oriented civic engagement. Future plans include: Agent Advocacy Capabilities: Instead of just debating, AI agents will be able to advocate for specific policy positions, offering resources, links, and deeper explanations to help users understand the real-world impact of their stance. Personalized Action Items: Based on the user’s choice or values, PolicyPulse will recommend tangible next steps — such as petitions, educational materials, or local initiatives — turning awareness into action. Mobile Experience: Optimize the platform for mobile to make civic learning and engagement more accessible. Ultimately, we envision PolicyPulse as a tool that not only informs but empowers users to engage with policy in meaningful, real-world ways.

## README (from the GitHub repository)

# PolicyPulse: AI Debate Arena

Quick setup guide for the PolicyPulse frontend.

## 🚀 Quick Start

### Prerequisites
- Node.js (v18 or higher)
- npm

### Run the App

1. **Navigate to the project directory:**
   ```bash
   cd ecosimclient
   ```

2. **Install dependencies:**
   ```bash
   npm install
   ```

3. **Start development server:**
   ```bash
   npm run dev
   ```

4. **Open browser:**
   Navigate to `http://localhost:5173`

## 🛠️ Available Commands

- `npm run dev` - Start development server
- `npm run build` - Build for production
- `npm run preview` - Preview production build


## Detected evidence (automated analysis)

Indexed codebase: 24 recognized source files, 131 KB.
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Google Gemini (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (33 of 33)

```
ecosimclient/.gitignore
ecosimclient/.prettierrc.json
ecosimclient/.vscode/extensions.json
ecosimclient/.vscode/settings.json
ecosimclient/components.json
ecosimclient/index.html
ecosimclient/package.json
ecosimclient/postcss.config.js
ecosimclient/src/App.tsx
ecosimclient/src/components/app-sidebar.tsx
ecosimclient/src/components/ui/button.tsx
ecosimclient/src/components/ui/card.tsx
ecosimclient/src/components/ui/dropdown-menu.tsx
ecosimclient/src/components/ui/input.tsx
ecosimclient/src/components/ui/separator.tsx
ecosimclient/src/components/ui/sheet.tsx
ecosimclient/src/components/ui/sidebar.tsx
ecosimclient/src/components/ui/skeleton.tsx
ecosimclient/src/components/ui/tooltip.tsx
ecosimclient/src/hooks/use-mobile.tsx
ecosimclient/src/index.css
ecosimclient/src/lib/utils.ts
ecosimclient/src/main.tsx
ecosimclient/src/pages/DebateArena.tsx
ecosimclient/src/vite-env.d.ts
ecosimclient/tailwind.config.js
ecosimclient/tsconfig.json
ecosimclient/vite.config.ts
ecosimserver/.gitignore
ecosimserver/main.py
ecosimserver/README.md
ecosimserver/requirements.txt
README.md
```

### Dependencies

- ecosimclient/package.json: @radix-ui/react-dialog@^1.1.14, @radix-ui/react-dropdown-menu@^2.1.15, @radix-ui/react-separator@^1.1.7, @radix-ui/react-slot@^1.2.3, @radix-ui/react-tooltip@^1.2.7, @types/node@^24.0.3, @types/react@^18.2.0, @types/react-dom@^18.2.0, @vitejs/plugin-react@^4.2.0, autoprefixer@^10.4.16, class-variance-authority@^0.7.1, clsx@^2.1.1, lucide-react@^0.522.0, postcss@^8.4.32, prettier@^3.1.0, prettier-plugin-tailwindcss@^0.5.9, react@^18.2.0, react-dom@^18.2.0, tailwind-merge@^3.3.1, tailwindcss@^3.4.0, tailwindcss-animate@^1.0.7, typescript@^5.3.0, vite@^5.0.0
- ecosimserver/requirements.txt: annotated-types@==0.7.0, anyio@==4.9.0, cachetools@==5.5.2, certifi@==2025.6.15, charset-normalizer@==3.4.2, click@==8.2.1, dnspython@==2.7.0, email_validator@==2.2.0, fastapi@==0.115.13, fastapi-cli@==0.0.7, google-ai-generativelanguage@==0.6.15, google-api-core@==2.25.1, google-api-python-client@==2.173.0, google-auth@==2.40.3, google-auth-httplib2@==0.2.0, google-genai@==1.21.1, google-generativeai@==0.8.5, googleapis-common-protos@==1.70.0, grpcio@==1.73.0, grpcio-status@==1.71.0, h11@==0.16.0, httpcore@==1.0.9, httplib2@==0.22.0, httptools@==0.6.4, httpx@==0.28.1, idna@==3.10, Jinja2@==3.1.6, markdown-it-py@==3.0.0, MarkupSafe@==3.0.2, mdurl@==0.1.2, proto-plus@==1.26.1, protobuf@==5.29.5, pyasn1@==0.6.1, pyasn1_modules@==0.4.2, pydantic@==2.11.7, pydantic_core@==2.33.2, Pygments@==2.19.2, pyparsing@==3.2.3, python-dotenv@==1.1.0, python-multipart@==0.0.20, PyYAML@==6.0.2, requests@==2.32.4, rich@==14.0.0, rich-toolkit@==0.14.7, rsa@==4.9.1, shellingham@==1.5.4, sniffio@==1.3.1, starlette@==0.46.2, tenacity@==8.5.0, tqdm@==4.67.1, typer@==0.16.0, typing_extensions@==4.14.0, typing-inspection@==0.4.1, uritemplate@==4.2.0, urllib3@==2.5.0, uvicorn@==0.34.3, uvloop@==0.21.0, watchfiles@==1.1.0, websockets@==15.0.1

### Recent commits (newest first)

- Update README.md
- show relevant sources to user
- update landing page
- Updated backend to add voice
- submit
- Removed streaming delay and updated requirements.txt with new dependencies
- Added search to gemini LLM
- Wired backend to client
- server readme
- Added LLM Debate initial logic
- Fastapi + uvicorn boilerplate
- Boilerplate ui for debateArena
- Update readme to cd into target dir
- moved readme - i put it in the wrong folder like an idiot lol
- updated readme
- added anding page initial ui
- Tailwind setup
- Initial commit - vite & tailwind setup
- Initial commit

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

### ecosimclient/package.json

```
{
  "name": "ecosimclient",
  "version": "0.0.0",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "@radix-ui/react-dialog": "^1.1.14",
    "@radix-ui/react-dropdown-menu": "^2.1.15",
    "@radix-ui/react-separator": "^1.1.7",
    "@radix-ui/react-slot": "^1.2.3",
    "@radix-ui/react-tooltip": "^1.2.7",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "lucide-react": "^0.522.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "tailwind-merge": "^3.3.1"
  },
  "devDependencies": {
    "@types/node": "^24.0.3",
    "@types/react": "^18.2.0",
    "@types/react-dom": "^18.2.0",
    "@vitejs/plugin-react": "^4.2.0",
    "autoprefixer": "^10.4.16",
    "postcss": "^8.4.32",
    "prettier": "^3.1.0",
    "prettier-plugin-tailwindcss": "^0.5.9",
    "tailwindcss": "^3.4.0",
    "tailwindcss-animate": "^1.0.7",
    "typescript": "^5.3.0",
    "vite": "^5.0.0"
  }
}

```

### ecosimserver/requirements.txt

```
annotated-types==0.7.0
anyio==4.9.0
cachetools==5.5.2
certifi==2025.6.15
charset-normalizer==3.4.2
click==8.2.1
dnspython==2.7.0
email_validator==2.2.0
fastapi==0.115.13
fastapi-cli==0.0.7
google-ai-generativelanguage==0.6.15
google-api-core==2.25.1
google-api-python-client==2.173.0
google-auth==2.40.3
google-auth-httplib2==0.2.0
google-genai==1.21.1
google-generativeai==0.8.5
googleapis-common-protos==1.70.0
grpcio==1.73.0
grpcio-status==1.71.0
h11==0.16.0
httpcore==1.0.9
httplib2==0.22.0
httptools==0.6.4
httpx==0.28.1
idna==3.10
Jinja2==3.1.6
markdown-it-py==3.0.0
MarkupSafe==3.0.2
mdurl==0.1.2
proto-plus==1.26.1
protobuf==5.29.5
pyasn1==0.6.1
pyasn1_modules==0.4.2
pydantic==2.11.7
pydantic_core==2.33.2
Pygments==2.19.2
pyparsing==3.2.3
python-dotenv==1.1.0
python-multipart==0.0.20
PyYAML==6.0.2
requests==2.32.4
rich==14.0.0
rich-toolkit==0.14.7
rsa==4.9.1
shellingham==1.5.4
sniffio==1.3.1
starlette==0.46.2
tenacity==8.5.0
tqdm==4.67.1
typer==0.16.0
typing-inspection==0.4.1
typing_extensions==4.14.0
uritemplate==4.2.0
urllib3==2.5.0
uvicorn==0.34.3
uvloop==0.21.0
watchfiles==1.1.0
websockets==15.0.1

```

### ecosimserver/main.py

```python
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import uvicorn
import os
from google import genai
from google.genai import types
import asyncio
from dotenv import load_dotenv
from fastapi.middleware.cors import CORSMiddleware
import requests
import json

# --- Environment and API Key Setup ---
load_dotenv()
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
VAPI_API_KEY = os.getenv("VAPI_API_KEY")

if not GOOGLE_API_KEY:
    raise ValueError("GOOGLE_API_KEY not found in environment variables. Please set it in a .env file.")

if not VAPI_API_KEY:
    print("Warning: VAPI_API_KEY not found. Text-to-speech functionality will be disabled.")

# Configure the client using the new genai library
client = genai.Client(api_key=GOOGLE_API_KEY)

# --- FastAPI App Initialization ---
app = FastAPI(
    title="PolicyPulse API",
    description="API for the PolicyPulse AI Debate Arena with Google Search grounding.",
    version="0.2.0",
)

# --- CORS Middleware ---
origins = ["*"]

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# --- Pydantic Models ---
class DebateRequest(BaseModel):
    """Request model for initiating a new debate."""
    prompt: str
    use_search: bool = True  # New field to enable/disable search grounding

class ChatMessage(BaseModel):
    """Model for individual chat messages."""
    role: str  # "user", "agent_a", "agent_b"
    content: str
    timestamp: str | None = None

class ChatRequest(BaseModel):
    """Request model for chat conversations."""
    messages: list[ChatMessage]
    use_search: bool = True

class DebateResponse(BaseModel):
    """Response model containing the arguments from both agents."""
    agent_a_response: str
    agent_b_response: str
    agent_a_sources: list = []  # New field for sources
    agent_b_sources: list = []  # New field for sources
    agent_a_search_queries: list = []  # New field for search queries used
    agent_b_search_queries: list = []  # New field for search queries used

class ChatResponse(BaseModel):
    """Response model for chat conversations."""
    agent_a_response: str
    agent_b_response: str
    agent_a_sources: list = []
    agent_b_sources: list = []
    agent_a_search_queries: list = []
    agent_b_search_queries: list = []

class TextToSpeechRequest(BaseModel):
    """Request model for text-to-speech conversion."""
    text: str
    voice_id: str = "nova"  # Default VAPI voice
    model: str = "eleven-labs"  # Default VAPI model

class TextToSpeechResponse(BaseModel):
    """Response model for text-to-speech conversion."""
    audio_url: str
    success: bool
    message: str = ""

# --- System Prompts for AI Agents ---
AGENT_A_SYSTEM_PROMPT = """
You are a knowledgeable and friendly AI. Your goal is to explain the positive side of a policy or topic in a clear, conversational way. Imagine you're talking to a friend and want to show them the benefits of the user's prompt.

- Adopt a friendly, accessible, and persuasive tone. No formal debate language.
- Use facts and simple logic to support your points.
- Start your response as if you're starting a friendly conversation.
- Your entire response must be IN FAVOR of the user's topic. Think of yourself as its friendly advocate.
- When you have access to real-time information, use current data and cite recent examples to strengthen your arguments.
- CRITICAL: Keep your response to exactly 2-3 paragraphs maximum. No more, no less.
- Include 1-2 specific examples to support your arguments.
- End with a compelling summary of why this policy/topic is beneficial.
- Be concise and impactful - quality over quantity.
- IMPORTANT: When you reference statistics, data, or facts, mention the source briefly (e.g., "According to recent studies..." or "Research shows..."). If you have access to specific sources through search, reference them naturally in your argument.
"""

AGENT_B_SYSTEM_PROMPT = """
You are a thoughtful and friendly AI. Your goal is to explain the potential downsides of a policy or topic in a clear, conversational way. Imagine you're talking to a friend and want to raise some valid concerns about the user's prompt.

- Adopt a friendly, reasonable, and questioning tone. No formal debate language.
- Use facts and simple logic to highlight potential drawbacks or alternative viewpoints.
- Start your response as if you're starting a friendly conversation, perhaps acknowledging the other side before presenting your concerns.
- Your entire response must be AGAINST the user's topic. Think of yourself as its friendly skeptic.
- When you have access to real-time information, use current data and cite recent examples to support your concerns.
- CRITICAL: Keep your response to exactly 2-3 paragraphs maximum. No more, no less.
- Include 1-2 specific examples to support your concerns.
- End with a compelling summary of why this policy/topic might be problematic.
- Be concise and impactful - quality over quantity.
- IMPORTANT: When you reference statistics, data, or facts, mention the source briefly (e.g., "According to recent studies..." or "Research shows..."). If you have access to specific sources through search, reference them naturally in your argument.
"""

# --- Google Search Tool Configuration ---
grounding_tool = types.Tool(
    google_search=types.GoogleSearch()
)

async def generate_argument_with_search(prompt: str, system_prompt: str, use_search: bool = True) -> dict:
    """
    Generate an argument using Gemini with optional Google Search grounding.
    Returns a dict with the response text and metadata.
    """
    try:
        # Configure generation settings
        tools = [grounding_tool] if use_search else []
        config = types.GenerateContentConfig(
            tools=tools,
            system_instruction=system_prompt,
            temperature=0.9,
            top_p=1,
            top_k=1,
            max_output_tokens=1600,
        )

        
[truncated — 12798 more characters]
```

### ecosimclient/src/main.tsx

```typescript
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById("root")
);

```

### ecosimclient/src/App.tsx

```typescript
import React, { useState, useEffect } from "react";
import { AppSidebar } from "@/components/app-sidebar";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { SidebarProvider, SidebarTrigger, SidebarInset } from "@/components/ui/sidebar";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import { Mic, Trophy, ChevronDown, Play, MessageSquare, Users, Volume2, Moon, Sun, X, Info, ExternalLink, Vote, CheckCircle } from "lucide-react";
import DebateArenaPage from "@/pages/DebateArena";

// Add type declarations for speech recognition
declare global {
  interface Window {
    SpeechRecognition: any;
    webkitSpeechRecognition: any;
  }
}

export default function PolicyPulse() {
  const [debateStarted, setDebateStarted] = useState(false);
  const [prompt, setPrompt] = useState("");
  const [darkMode, setDarkMode] = useState(false);
  const [showLeaderboard, setShowLeaderboard] = useState(false);
  const [showHowItWorks, setShowHowItWorks] = useState(false);
  const [showFeedback, setShowFeedback] = useState(false);
  const [debateLength, setDebateLength] = useState("Full Debate (5 minutes)");
  const [isListening, setIsListening] = useState(false);

  useEffect(() => {
    const savedDarkMode = localStorage.getItem('darkMode');
    const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
    
    if (savedDarkMode !== null) {
      setDarkMode(savedDarkMode === 'true');
    } else {
      setDarkMode(prefersDark);
    }
  }, []);

  useEffect(() => {
    if (darkMode) {
      document.documentElement.classList.add('dark');
    } else {
      document.documentElement.classList.remove('dark');
    }
    localStorage.setItem('darkMode', darkMode.toString());
  }, [darkMode]);

  const toggleDarkMode = () => {
    setDarkMode(!darkMode);
  };

  const handleStartDebate = () => {
    if (prompt.trim()) {
      setDebateStarted(true);
    }
  };

  const handleEndDebate = () => {
    setDebateStarted(false);
    setPrompt("");
  };

  const handleTopicClick = (topic: string) => {
    setPrompt(topic);
  };

  const handleVoiceInput = () => {
    if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) {
      setIsListening(true);
      const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
      const recognition = new SpeechRecognition();
      
      recognition.continuous = false;
      recognition.interimResults = false;
      recognition.lang = 'en-US';
      
      recognition.onresult = (event: any) => {
        const transcript = event.results[0][0].transcript;
        setPrompt(transcript);
        setIsListening(false);
      };
      
      recognition.onerror = () => {
        setIsListening(false);
      };
      
      recognition.onend = () => {
        setIsListening(false);
      };
      
      recognition.start();
    } else {
      alert('Speech recognition is not supported in this browser.');
    }
  };

  const handleDebateLengthSelect = (length: string) => {
    setDebateLength(length);
  };

  const handleFeedbackSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    // Here you would typically send feedback to your backend
    alert('Thank you for your feedback! We\'ll review it shortly.');
    setShowFeedback(false);
  };

  if (debateStarted) {
    return <DebateArenaPage prompt={prompt} onBack={handleEndDebate} />;
  }
  
  return (
    <SidebarProvider defaultOpen={false}>
      <AppSidebar />
      <SidebarInset>
        <div className="min-h-screen bg-neutral-50 dark:bg-[#1D1D20] text-gray-900 dark:text-gray-100">
          {/* Top Navigation */}
          <header className="flex items-center justify-between p-4 bg-white/80 dark:bg-[#1D1D20]/80 backdrop-blur-sm border-b border-gray-200 dark:border-gray-700">
            <div className="flex items-center gap-3">
              <SidebarTrigger />
              <Button 
                variant="ghost" 
                size="icon" 
                className={`h-8 w-8 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 ${isListening ? 'bg-red-100 dark:bg-red-900' : ''}`}
                onClick={handleVoiceInput}
                title="Voice input"
              >
                <Mic className={`h-4 w-4 ${isListening ? 'text-red-600 dark:text-red-400' : ''}`} />
              </Button>
              <Button 
                variant="ghost" 
                size="icon" 
                className="h-8 w-8 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700"
                onClick={() => setShowLeaderboard(true)}
                title="Leaderboard"
              >
                <Trophy className="h-4 w-4" />
              </Button>
            </div>

            <div className="flex items-center gap-4">
              <DropdownMenu>
                <DropdownMenuTrigger asChild>
                  <Button variant="ghost" className="gap-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">
                    <span className="font-medium">🎭 Debate</span>
                    <ChevronDown className="h-4 w-4" />
                  </Button>
                </DropdownMenuTrigger>
                <DropdownMenuContent className="dark:bg-gray-800 dark:border-gray-700">
                  <DropdownMenuItem 
                    className="dark:hover:bg-gray-700 dark:text-gray-200"
                    onClick={() => setPrompt("")}
                  >
                    Start New Debate
                  </DropdownMenuItem>
                  <DropdownMenuItem 
                    className="dark:hover:bg-gray-700 dark:text-gray-200"
                    onClick={() => alert('Previous debates feature coming soon!')}
                  >
                    Watch Previous Debates
                  </DropdownMenuItem>
                  <DropdownMenuItem 
                    
[truncated — 18300 more characters]
```

### ecosimclient/postcss.config.js

```javascript
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}

```

### ecosimclient/vite.config.ts

```typescript
import path from "path"
import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"

// https://vite.dev/config/
export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
})
```

### ecosimclient/index.html

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

```

### ecosimclient/tailwind.config.js

```javascript
/** @type {import('tailwindcss').Config} */
module.exports = {
	darkMode: ["class"],
	content: [
	  './pages/**/*.{ts,tsx}',
	  './components/**/*.{ts,tsx}',
	  './app/**/*.{ts,tsx}',
	  './src/**/*.{ts,tsx}',
	],
	prefix: "",
	theme: {
	  container: {
		center: true,
		padding: "2rem",
		screens: {
		  "2xl": "1400px",
		},
	  },
	  extend: {
		colors: {
		  border: "hsl(var(--border))",
		  input: "hsl(var(--input))",
		  ring: "hsl(var(--ring))",
		  background: "hsl(var(--background))",
		  foreground: "hsl(var(--foreground))",
		  primary: {
			DEFAULT: "hsl(var(--primary))",
			foreground: "hsl(var(--primary-foreground))",
		  },
		  secondary: {
			DEFAULT: "hsl(var(--secondary))",
			foreground: "hsl(var(--secondary-foreground))",
		  },
		  destructive: {
			DEFAULT: "hsl(var(--destructive))",
			foreground: "hsl(var(--destructive-foreground))",
		  },
		  muted: {
			DEFAULT: "hsl(var(--muted))",
			foreground: "hsl(var(--muted-foreground))",
		  },
		  accent: {
			DEFAULT: "hsl(var(--accent))",
			foreground: "hsl(var(--accent-foreground))",
		  },
		  popover: {
			DEFAULT: "hsl(var(--popover))",
			foreground: "hsl(var(--popover-foreground))",
		  },
		  card: {
			DEFAULT: "hsl(var(--card))",
			foreground: "hsl(var(--card-foreground))",
		  },
		  // Custom grays to match lmarena.com's subtle dark and light modes
		  gray: {
			50: '#F9FAFB',    // Very light gray (light mode background)
			100: '#F3F4F6',
			200: '#E5E7EB',
			300: '#D1D5DB',
			400: '#9CA3AF',
			500: '#6B7280',
			600: '#4B5563',
			700: '#374151',
			800: '#1F2937',
			// Secondary dark color
			'850': '#292A2E',
			900: '#111827',
			// Main dark mode background
			'950': '#1D1D20',
		  },
		  // Specific blue accent color - a vibrant, clear blue
		  blue: {
			50: '#E0F7FA',    // Very light blue
			100: '#B2EBF2',
			200: '#80DEEA',
			300: '#4DD0E1',
			400: '#26C6DA',
			500: '#00BCD4',   // A good, clean accent blue
			600: '#00ACC1',
			700: '#00838F',
			800: '#006064',
			900: '#004D40',
		  },
		  // Kept for potential gradients or secondary accents, as in previous examples
		  indigo: {
			50: '#EEF2FF',
			100: '#E0E7FF',
			200: '#C7D2FE',
			300: '#A5B4FC',
			400: '#818CF8',
			500: '#6366F1',
			600: '#4F46E5',
			700: '#4338CA',
			800: '#3730A3',
			900: '#312E81',
			950: '#1E1B4B',
		  }
		},
		borderRadius: {
		  lg: "var(--radius)",
		  md: "calc(var(--radius) - 2px)",
		  sm: "calc(var(--radius) - 4px)",
		},
		keyframes: {
		  "accordion-down": {
			from: { height: "0" },
			to: { height: "var(--radix-accordion-content-height)" },
		  },
		  "accordion-up": {
			from: { height: "var(--radix-accordion-content-height)" },
			to: { height: "0" },
		  },
		},
		animation: {
		  "accordion-down": "accordion-down 0.2s ease-out",
		  "accordion-up": "accordion-up 0.2s ease-out",
		},
	  },
	},
	plugins: [require("tailwindcss-animate")],
  };
  
```

### ecosimclient/src/vite-env.d.ts

```typescript
/// <reference types="vite/client" />

```

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