# Project export: AIRA (AI Room Arena)

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: AIRA turns group chat into an anime arena: live rooms where AI versions of Goku, Light, and others you pick jump in via cues and dilemmas, clashing in good vs evil debates with voices and agency.
- Devpost: https://devpost.com/software/aira-ai-room-arena
- GitHub: https://github.com/manuvikash/multi-agent-chat
- Video: https://www.youtube.com/embed/JSXN_FqjII0?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 2 GitHub contributor(s) — Vishwesh Krishna Hariharakrishnan (7 commits), manuvikash (1 commits)

## Devpost submission (written by the team)

### Inspiration

We asked: What if your favorite anime characters could hang out in the same chat room and actually talk to each other? Inspired by the distinct voices of Saitama, Light Yagami, Sasuke Uchiha, etc., we built a space where AI doesn’t just reply, it participates.

### What it does

Real-time multiplayer chat where users and AI anime personas converse together. Users create rooms, pick characters, and watch bots jump in based on context, mentions, and moral dilemmas. “Good vs Evil” debate mode where aligned bots argue before synthesizing an answer. Name-mention detection (partial names like “Sasuke” trigger replies).

### How we built it

Frontend React + Vite, Tailwind, React Router WebSockets for live, bidirectional updates Backend FastAPI + Uvicorn; WebSocket hub for multi-user rooms LLM integration (OpenAI/compatible) for character-specific outputs AI Architecture Persona prompts in bot_personas.py Orchestrator with should_bot_respond(), moral-dilemma detection, cooldowns/limits Lightweight memory for user facts High-level flow

### Challenges we ran into

“Wrong Bot” bug: Saitama always appeared. Fix: Send authoritative room_state on connect; frontend hydrates from it. Python env issues (ModuleNotFoundError: uvicorn). Fix: Virtualenv + pinned requirements. CORS mismatches (ports 5173 vs 5174). Fix: Allow both localhost/127.0.0.1 ports. Name mention detection too strict. Fix: Split names; match parts ≥3 chars. LLM output inconsistency. Fix: Robust fallbacks: JSON→content→raw→default.

### Accomplishments we're proud of

Bots that feel “in-character” and join organically instead of spamming. Smooth, truly real-time multi-user rooms with synchronized bot add/remove. Moral-dilemma debate mode that yields richer, balanced answers. Full CRT effect using CSS only (no JS, minimal perf cost). Guardrails (cooldowns, message caps, context checks) for stable autonomy.

### What we learned

Prompt engineering is iterative—tight prompts keep voice without rigidity. WebSocket state must be backend-authoritative; hydrate clients on connect. Autonomy needs constraints—knowing when not to speak matters. CSS can carry aesthetics (scanlines, flicker, vignette) with negligible overhead.

### What's next

Character voice messages with TTS/voice cloning Reactive sprite animations and emotions Battle/Debate mode with user scoring and ladders Custom character creator for user-defined personas Mobile optimization for small screens and touch gestures

## README (from the GitHub repository)

# Multichat — Multiplayer AI Group Chat

Real-time multi-user chat with a configurable AI character powered by FastAPI, WebSockets, and the JLLM API.

## Features
- Real-time WebSocket chat
- Single shared room (multiple rooms supported)
- Configurable AI persona (backstory, tone, behavior)
- Turn-taking: AI responds to @mentions, questions, or proactively when quiet
- Structured JSON responses from LLM
- In-memory state with rolling history window

## Quick Start

### 1. Install Dependencies

```powershell
python -m venv .venv
.\.venv\Scripts\Activate
pip install -r requirements.txt
```

### 2. Run the server

**Option A: Using the run script (easiest)**
```powershell
.\run.ps1
```

**Option B: Manual command**
```powershell
$env:PYTHONPATH = "d:\Goonengine\multichat"
python -m uvicorn app.main:app --reload --host 0.0.0.0
```

**Option C: Using Make**
```powershell
make run
```

### 4. Open the app

Navigate to http://localhost:8000

- Enter your name and room ID (default: "main")
- Click Join
- Send messages in the chat
- Mention `@Bot` to get AI responses

**Note:** The API key is hardcoded to `calhacks2047` as specified. To override, set the `JLLM_API_KEY` environment variable before running.

## Docker

Build and run:

```powershell
docker build -t multichat .
docker run -p 8000:8000 multichat
```

Or with docker-compose:

```powershell
docker-compose up --build
```

## Testing

```powershell
pytest -q
```

## API Details

The app uses the JLLM API at `https://janitorai.com/hackathon/completions` with:
- Authorization: `calhacks2047` (or your custom key from `.env`)
- Content-Type: `application/json`
- Payload: `{"messages": [...], "temperature": 0.7, ...}`

## Project Structure

```
multichat/
  app/
    main.py           # FastAPI app & WebSocket endpoint
    config.py         # Settings & defaults
    schemas.py        # Pydantic models
    llm_client.py     # JLLM API client
    persona.py        # System prompt renderer
    state.py          # In-memory room state
    orchestrator.py   # Turn-taking & response logic
    websocket.py      # Connection manager
    summarizer.py     # History summarization
    utils.py          # Helpers
  web/
    index.html        # Chat UI
    app.js            # WebSocket client
    styles.css        # Styling
  tests/              # Pytest tests
  Dockerfile
  docker-compose.yml
  Makefile
  requirements.txt
```

## How It Works

1. **Join a room**: Connect via WebSocket at `/ws/{room_id}`
2. **Send messages**: Broadcast to all users in real-time
3. **AI responds** when:
   - You mention `@Bot`
   - You ask a question
   - Room is quiet for 45s (proactive)
4. **Persona controls**: Update AI backstory, tone, and behavior via UI

Enjoy chatting!


## Detected evidence (automated analysis)

Indexed codebase: 42 recognized source files, 184 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

## Codebase structure (from repository index)

### Files (49 of 49)

```
.env.example
.gitignore
app/__init__.py
app/bot_personas.py
app/config.py
app/llm_client.py
app/main.py
app/moral_agents.py
app/orchestrator.py
app/persona.py
app/schemas.py
app/state.py
app/websocket.py
CHARACTER_IMAGES_GUIDE.md
persona.default.json
README.md
requirements.txt
RETRO_EFFECTS_SUMMARY.md
ROOM_MANAGEMENT_IMPLEMENTATION.md
START_SERVERS.md
web-react/.gitignore
web-react/CHANGELOG.md
web-react/eslint.config.js
web-react/index.html
web-react/package.json
web-react/postcss.config.js
web-react/public/personas/.gitkeep
web-react/public/personas/README.md
web-react/README.md
web-react/RETRO_CRT_EFFECTS.md
web-react/src/App.jsx
web-react/src/components/BotSelector.jsx
web-react/src/components/CreateRoomModal.jsx
web-react/src/components/MessageBubble.jsx
web-react/src/hooks/useWebSocket.js
web-react/src/index.css
web-react/src/lib/utils.js
web-react/src/main.jsx
web-react/src/pages/BotSelection.jsx
web-react/src/pages/ChatRoom.jsx
web-react/src/pages/RoomSelection.jsx
web-react/src/styles/retro-effects.css
web-react/tailwind.config.js
web-react/UI_REDESIGN.md
web-react/VISUAL_GUIDE.md
web-react/vite.config.js
web/app.js
web/index.html
web/styles.css
```

### Dependencies

- requirements.txt: fastapi@==0.115.6, httpx@==0.28.1, jinja2@==3.1.5, pydantic@==2.10.4, pydantic-settings@==2.7.1, pytest@==8.3.4, pytest-asyncio@==0.25.2, python-dotenv@==1.0.1, tenacity@==9.0.0, uvicorn[standard]@==0.34.0
- web-react/package.json: @eslint/js@^9.36.0, @types/react@^19.1.16, @types/react-dom@^19.1.9, @vitejs/plugin-react@^5.0.4, autoprefixer@^10.4.20, eslint@^9.36.0, eslint-plugin-react-hooks@^5.2.0, eslint-plugin-react-refresh@^0.4.22, globals@^16.4.0, postcss@^8.4.49, react@^19.1.1, react-dom@^19.1.1, react-router-dom@^7.9.4, tailwindcss@^3.4.17, vite@^7.1.7

### Recent commits (newest first)

- Merge ui-revamp into main - resolved conflicts, keeping ui-revamp features
- fix: wrong bot selected
- added anime personas and changed the ui:
- Added package-lock.json
- Updated package-lock.json
- Added package-lock.json
- Updated package.json
- Most of the project basically

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

### START_SERVERS.md

```markdown
# How to Start the Servers

## Backend (FastAPI)

The backend needs to be run with the correct Python environment that has all dependencies installed.

### Option 1: Using your existing environment
If you were running the backend before, use the same method:

```bash
# If you have a virtual environment, activate it first
# source venv/bin/activate  # or whatever your venv is named

# Then run:
cd /Users/vichu/Documents/Coding\ stuff/goonEngine/multi-agent-chat
uvicorn app.main:app --reload
```

### Option 2: Install dependencies first
If you haven't installed dependencies yet:

```bash
cd /Users/vichu/Documents/Coding\ stuff/goonEngine/multi-agent-chat

# Create virtual environment (if needed)
python3 -m venv venv
source venv/bin/activate

# Install dependencies
pip install -r requirements.txt

# Start server
uvicorn app.main:app --reload
```

The server should start on `http://localhost:8000`

## Frontend (React + Vite)

The frontend is already running in the background on port 5173.

If you need to restart it:

```bash
cd /Users/vichu/Documents/Coding\ stuff/goonEngine/multi-agent-chat/web-react
npm run dev
```

The frontend should be available at `http://localhost:5173`

## Testing the Room Management Feature

Once both servers are running:

1. Open `http://localhost:5173` in your browser
2. Enter your name
3. You'll see the new Room Selection page with two options:
   - **Create New Room** (left side)
   - **Join Existing Room** (right side with list of rooms)
4. Click "Create New Room", enter a room name, select a bot, and create
5. Open another browser (or incognito) and join the same room with a different name
6. Verify that only the admin (creator) sees the + button to add bots

## Troubleshooting

### "Failed to fetch" error
- Make sure the backend is running on port 8000
- Check `http://localhost:8000/health` - should return `{"status":"ok"}`
- Check `http://localhost:8000/api/rooms` - should return a list of rooms

### Backend won't start
- Make sure FastAPI is installed: `pip list | grep fastapi`
- Install requirements: `pip install -r requirements.txt`
- Check for port conflicts: `lsof -i :8000`

### Frontend errors
- Make sure node_modules are installed: `npm install`
- Clear cache: `rm -rf node_modules/.vite && npm run dev`


```

### CHARACTER_IMAGES_GUIDE.md

```markdown
# 🎨 Character Images Setup Guide

## Overview
The UI now displays character images for each anime persona in the bot selection screens. Images are shown in circular frames with colored borders matching each character's theme.

## 📁 Folder Location

Character images should be placed in:
```
web-react/public/personas/
```

This folder has been created and is ready for your images!

## 🖼️ Required Images

Add images for these 8 anime characters:

| File Name | Character | Anime |
|-----------|-----------|-------|
| `gooner.png` or `gooner.jpg` | Saitama | One Punch Man |
| `professor.png` or `professor.jpg` | Light Yagami | Death Note |
| `glitchcore.png` or `glitchcore.jpg` | Accelerator | A Certain Magical Index |
| `mama.png` or `mama.jpg` | Nezuko | Demon Slayer |
| `edgelord.png` or `edgelord.jpg` | Sasuke Uchiha | Naruto |
| `corporate.png` or `corporate.jpg` | Reigen Arataka | Mob Psycho 100 |
| `goblin.png` or `goblin.jpg` | Goku | Dragon Ball |
| `zen.png` or `zen.jpg` | Whis | Dragon Ball Super |

## 📐 Image Specifications

### Recommended:
- **Format**: PNG (preferred) or JPG
- **Size**: 400x400px (square aspect ratio)
- **Max File Size**: 500KB per image
- **Style**: Character portrait, headshot, or face close-up
- **Background**: Any (will be cropped to circle)

### Tips:
- Square images work best (1:1 aspect ratio)
- Center the character's face in the image
- Higher resolution is better for sharp display
- Transparent backgrounds (PNG) are optional

## 🔍 Where to Find Images

### Option 1: Official Art
- MyAnimeList character pages
- Anime official websites
- Wikia/Fandom pages

### Option 2: Screenshots
- Take screenshots from the anime
- Crop to focus on the character's face

### Option 3: Fan Art
- DeviantArt (with permission)
- Pixiv (with permission)
- Always credit artists if required

### Option 4: AI Generated
- Use tools like Midjourney, Stable Diffusion
- Generate portraits in anime style

## 📝 File Naming Rules

**IMPORTANT**: File names are case-sensitive and must match exactly:

✅ Correct:
- `gooner.png`
- `professor.jpg`
- `mama.png`

❌ Wrong:
- `Gooner.png` (capital G)
- `gooner.PNG` (capital PNG)
- `gooner_image.png` (extra text)

## 🎯 How It Works

1. **Image Display**: 
   - Images are shown in a circular frame
   - Border color matches the character's theme
   - Size: 96x96px (24rem) in the UI

2. **Fallback System**:
   - If `gooner.png` doesn't exist, tries `gooner.jpg`
   - If both fail, displays the emoji (👊, 📓, etc.)
   - No error shown to users - seamless fallback

3. **Where Images Appear**:
   - Create Room Modal (when selecting starting bot)
   - Bot Selector Modal (when adding bots to active room)

## 🚀 Testing Your Images

1. Add an image to `web-react/public/personas/`
2. Refresh your browser (Ctrl+F5 / Cmd+Shift+R)
3. Open the Create Room modal or Bot Selector
4. You should see your image in a circular frame!

## 🎨 Example Image Setup

```bash
cd /path/to/project/web-react/public/personas/

# 
[truncated — 2095 more characters]
```

### requirements.txt

```
fastapi==0.115.6
uvicorn[standard]==0.34.0
httpx==0.28.1
pydantic==2.10.4
pydantic-settings==2.7.1
python-dotenv==1.0.1
tenacity==9.0.0
jinja2==3.1.5

# dev / test
pytest==8.3.4
pytest-asyncio==0.25.2

```

### web-react/package.json

```
{
  "name": "web-react",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "lint": "eslint .",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^19.1.1",
    "react-dom": "^19.1.1",
    "react-router-dom": "^7.9.4"
  },
  "devDependencies": {
    "@eslint/js": "^9.36.0",
    "@types/react": "^19.1.16",
    "@types/react-dom": "^19.1.9",
    "@vitejs/plugin-react": "^5.0.4",
    "autoprefixer": "^10.4.20",
    "eslint": "^9.36.0",
    "eslint-plugin-react-hooks": "^5.2.0",
    "eslint-plugin-react-refresh": "^0.4.22",
    "globals": "^16.4.0",
    "postcss": "^8.4.49",
    "tailwindcss": "^3.4.17",
    "vite": "^7.1.7"
  }
}

```

### web/app.js

```javascript
(() => {
  let ws = null;
  let currentUser = '';
  let roomId = 'main';

  // Name screen logic
  const nameInput = document.getElementById('name-input');
  const joinBtn = document.getElementById('join-btn');
  const nameScreen = document.getElementById('name-screen');
  const chatScreen = document.getElementById('chat-screen');
  const currentUserBadge = document.getElementById('current-user');

  joinBtn.addEventListener('click', () => {
    const name = nameInput.value.trim();
    if (!name) {
      nameInput.focus();
      return;
    }
    currentUser = name;
    currentUserBadge.textContent = currentUser;
    nameScreen.classList.remove('active');
    chatScreen.classList.add('active');
    connect();
  });

  nameInput.addEventListener('keypress', (e) => {
    if (e.key === 'Enter') joinBtn.click();
  });

  // WebSocket connection
  const connect = () => {
    const wsUrl = (location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + '/ws/' + roomId;
    ws = new WebSocket(wsUrl);
    
    ws.addEventListener('open', () => {
      send({type: 'join', user: currentUser});
      appendSystemMessage('Connected to chat');
    });

    ws.addEventListener('message', (ev) => {
      try {
        const obj = JSON.parse(ev.data);
        handleMessage(obj);
      } catch (e) {
        console.error('Invalid message', e);
      }
    });

    ws.addEventListener('close', () => {
      appendSystemMessage('Disconnected from chat');
    });
  };

  const send = (obj) => {
    if (!ws || ws.readyState !== WebSocket.OPEN) return;
    ws.send(JSON.stringify(obj));
  };

  // Message handling
  const messagesArea = document.getElementById('messages');

  const appendSystemMessage = (text) => {
    const msgDiv = document.createElement('div');
    msgDiv.className = 'message system';
    msgDiv.innerHTML = `<div class="message-bubble">${escapeHtml(text)}</div>`;
    messagesArea.appendChild(msgDiv);
    scrollToBottom();
  };

  const appendChatMessage = (user, content) => {
    const isSent = user === currentUser;
    const isBot = user === 'Bot';
    const isGoodBot = user === 'GoodBot';
    const isEvilBot = user === 'EvilBot';
    
    let className = 'message ';
    if (isSent) {
      className += 'sent';
    } else if (isGoodBot) {
      className += 'good-bot received';
    } else if (isEvilBot) {
      className += 'evil-bot received';
    } else if (isBot) {
      className += 'bot received';
    } else {
      className += 'received';
    }
    
    const msgDiv = document.createElement('div');
    msgDiv.className = className;
    
    const avatar = user.charAt(0).toUpperCase();
    msgDiv.innerHTML = `
      <div class="message-avatar">${avatar}</div>
      <div class="message-content">
        <div class="message-sender">${escapeHtml(user)}</div>
        <div class="message-bubble">${escapeHtml(content)}</div>
      </div>
    `;
    
    messagesArea.appendChild(msgDiv);
    scrollToBottom();
  };

  const handleMessage = (obj) => {
    if (obj.type === 'chat') {
      appendChatMessage(obj.user, obj.content);
    } else if (obj.type === 'system') {
      const event = obj.event || '';
      if (event === 'debate.start') {
        appendDebateBanner(obj.message || 'Moral Debate Mode Active 🎭');
      } else {
        appendSystemMessage(event || JSON.stringify(obj));
      }
    } else if (obj.type === 'error') {
      appendSystemMessage('Error: ' + obj.message);
    }
  };

  const appendDebateBanner = (text) => {
    const banner = document.createElement('div');
    banner.className = 'message system';
    banner.innerHTML = `<div class="message-bubble" style="background: linear-gradient(135deg, #ffd89b 0%, #19547b 100%); color: white; font-weight: 600;">🎭 ${escapeHtml(text)}</div>`;
    messagesArea.appendChild(banner);
    scrollToBottom();
  };

  const scrollToBottom = () => {
    messagesArea.scrollTop = messagesArea.scrollHeight;
  };

  // Input handling
  const messageInput = document.getElementById('message-input');
  const sendBtn = document.getElementById('send-btn');

  const sendMessage = () => {
    const content = messageInput.value.trim();
    if (!content) return;
    
    send({type: 'chat', user: currentUser, content});
    messageInput.value = '';
  };

  sendBtn.addEventListener('click', sendMessage);
  messageInput.addEventListener('keypress', (e) => {
    if (e.key === 'Enter') sendMessage();
  });

  // Settings panel
  const settingsBtn = document.getElementById('settings-btn');
  const settingsPanel = document.getElementById('settings-panel');
  const closeSettings = document.getElementById('close-settings');
  const updatePersonaBtn = document.getElementById('update-persona');

  settingsBtn.addEventListener('click', () => {
    settingsPanel.classList.remove('hidden');
    settingsPanel.classList.add('active');
  });

  closeSettings.addEventListener('click', () => {
    settingsPanel.classList.remove('active');
  });

  updatePersonaBtn.addEventListener('click', () => {
    const persona = {
      name: document.getElementById('bot-name').value,
      backstory: document.getElementById('bot-personality').value,
      tone: document.getElementById('bot-tone').value,
      formality: document.getElementById('bot-formality').value,
      emoji_ok: document.getElementById('bot-emoji').checked,
      talkativeness: {
        target_msgs_per_min: 1,
        respond_on_mentions: true,
        proactive_on_lull_sec: 45,
        max_consecutive_ai_msgs: parseInt(document.getElementById('bot-max-consecutive').value) || 1
      },
      addressing: { tag_users_by_name: true, prefer_short_answers: document.getElementById('bot-short-answers').checked },
      safety: { refuse_topics: [], pg_rating: 'PG' },
      structured_output: true
    };
    send({type: 'persona.update', persona});
    settingsPanel.classList.remove('active');
    appendSystemMessage('Bot settings updated');
  });

  function escapeHtml(text) {
    const div = document.crea
[truncated — 82 more characters]
```

### app/main.py

```python
from __future__ import annotations

import asyncio
import json
import uuid
from pathlib import Path
from typing import Dict
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException
from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import httpx

from .config import settings, load_default_persona
from .websocket import ConnectionManager
from .state import RoomState
from .schemas import PersonaConfig, LLMParams
from .orchestrator import should_ai_respond, call_llm, apply_structured_response, detect_moral_dilemma, handle_moral_dilemma, should_bot_respond, call_bot_llm
from .bot_personas import BOT_PERSONAS, BOT_METADATA

app = FastAPI()

# Add CORS for React dev server
app.add_middleware(
    CORSMiddleware,
    allow_origins=[
        "http://localhost:5173",
        "http://127.0.0.1:5173",
        "http://localhost:5174",
        "http://127.0.0.1:5174"
    ],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# mount static files (web/) to serve UI
web_dir = Path(__file__).resolve().parents[1] / "web"
app.mount("/static", StaticFiles(directory=str(web_dir)), name="static")

manager = ConnectionManager()
rooms: Dict[str, RoomState] = {}


class CreateRoomRequest(BaseModel):
    name: str
    admin: str
    initial_bot: str


@app.on_event("startup")
async def on_startup():
    # create a global httpx client on app state
    app.state.httpx_client = httpx.AsyncClient()


@app.on_event("shutdown")
async def on_shutdown():
    await app.state.httpx_client.aclose()


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


@app.get("/api/bots")
async def get_bots():
    """Return available bot personalities"""
    return JSONResponse(BOT_METADATA)


@app.post("/api/rooms/create")
async def create_room(request: CreateRoomRequest):
    """Create a new chat room"""
    # Validate bot exists
    if request.initial_bot not in BOT_PERSONAS:
        raise HTTPException(status_code=400, detail=f"Invalid bot: {request.initial_bot}")
    
    # Generate unique room ID
    room_id = str(uuid.uuid4())
    
    # Load default persona and params
    d = load_default_persona()
    persona = PersonaConfig(**d)
    params = LLMParams()
    
    # Create room with metadata
    room = RoomState(
        id=room_id,
        persona=persona,
        params=params,
        name=request.name,
        admin=request.admin
    )
    
    # Set initial bot
    room.active_bots = {request.initial_bot}
    
    # Store room
    rooms[room_id] = room
    
    return JSONResponse({
        "room_id": room_id,
        "name": request.name,
        "admin": request.admin,
        "initial_bot": request.initial_bot
    })


@app.get("/api/rooms")
async def get_rooms():
    """Get list of all available rooms"""
    room_list = []
    for room_id, room in rooms.items():
        room_list.append({
            "room_id": room_id,
            "name": room.name,
            "admin": room.admin,
            "created_at": room.created_at,
            "participant_count": len(room.users),
            "active_bots": list(room.active_bots)
        })
    
    # Sort by created_at (newest first)
    room_list.sort(key=lambda x: x["created_at"], reverse=True)
    
    return JSONResponse(room_list)


@app.get("/api/rooms/{room_id}")
async def get_room(room_id: str):
    """Get details of a specific room"""
    if room_id not in rooms:
        raise HTTPException(status_code=404, detail="Room not found")
    
    room = rooms[room_id]
    return JSONResponse({
        "room_id": room_id,
        "name": room.name,
        "admin": room.admin,
        "created_at": room.created_at,
        "participants": list(room.users),
        "participant_count": len(room.users),
        "active_bots": list(room.active_bots)
    })


@app.get("/")
async def index():
    return FileResponse(str(web_dir / "index.html"))


@app.websocket("/ws/{room_id}")
async def websocket_endpoint(websocket: WebSocket, room_id: str):
    await manager.connect(room_id, websocket)
    try:
        # Check if room exists (don't auto-create anymore)
        if room_id not in rooms:
            await websocket.send_text(json.dumps({
                "type": "error", 
                "message": "Room not found. Please create or join an existing room."
            }))
            await websocket.close()
            return

        room: RoomState = rooms[room_id]
        
        # Send room state including active bots to the connecting client
        await websocket.send_text(json.dumps({
            "type": "room_state",
            "room_id": room_id,
            "name": room.name,
            "admin": room.admin,
            "active_bots": list(room.active_bots)
        }))

        while True:
            data = await websocket.receive_text()
            try:
                obj = json.loads(data)
            except Exception:
                await websocket.send_text(json.dumps({"type": "error", "message": "invalid json"}))
                continue

            typ = obj.get("type")
            if typ == "join":
                user = obj.get("user")
                room.users.add(user)
                await manager.broadcast(room_id, {"type": "system", "event": "joined", "user": user})
            elif typ == "chat":
                user = obj.get("user")
                content = obj.get("content")
                # append & broadcast
                msg = room.append_message(user, "user", content)
                await manager.broadcast(room_id, {"type": "chat", "user": user, "content": content, "ts": msg.ts.isoformat()})

                # orchestrate AI in background
                async def _run_orchestrator():
                    try:
                        # Check for moral dilemma FIRST (bypasses turn-taking)
                        print(f"[DEBUG] Checking
[truncated — 5904 more characters]
```

### web-react/src/main.jsx

```javascript
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

```

### web-react/src/App.jsx

```javascript
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import BotSelection from './pages/BotSelection';
import RoomSelection from './pages/RoomSelection';
import ChatRoom from './pages/ChatRoom';

export default function App() {
  return (
    <div className="relative">
      {/* CRT Effects Overlay */}
      <div className="crt-scanlines"></div>
      <div className="crt-screen"></div>
      <div className="crt-vignette"></div>
      <div className="crt-noise"></div>
      
      {/* Main App Content */}
      <div className="crt-flicker">
        <BrowserRouter>
          <Routes>
            <Route path="/" element={<BotSelection />} />
            <Route path="/rooms" element={<RoomSelection />} />
            <Route path="/chat" element={<ChatRoom />} />
          </Routes>
        </BrowserRouter>
      </div>
    </div>
  );
}

```

### web-react/postcss.config.js

```javascript
export default {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}



```

### web-react/vite.config.js

```javascript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vite.dev/config/
export default defineConfig({
  plugins: [react()],
})

```

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