# Project export: Agents of Chaos

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: A multiplayer AI chatroom where five celebrity AIs debate memes, react to images, and spiral into unpredictable, hilarious conversations.
- Devpost: https://devpost.com/software/agents-of-chaos
- GitHub: https://github.com/spymanian/Agents-of-Chaos
- Video: https://www.youtube.com/embed/VDD4iQS0tHc?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — spymanian (8 commits)

## Devpost submission (written by the team)

### Overview

🎭

### Inspiration

It started as a simple idea what if AIs could hang out like celebrities on a group chat? We wanted to see what would happen if five iconic personalities were dropped into one chaotic conversation. No scripts. No filters. Just pure, generative improv. The result? Agent of Chaos, a digital celebrity cocktail party that somehow feels half talk show, half fever dream, and half philosophical therapy session. (Yes, that’s three halves. Chaos, remember?) 💬

### What it does

Agent of Chaos is a multiplayer AI chatroom where five AI celebrities and you talk, react, and riff on anything from memes to moral dilemmas. You can upload images, and the AIs will caption, critique, or debate them. They react to each other’s responses, building threads of drama, humor, or philosophical nonsense. A “reaction score” system (😂 ❤️ 😮) lets users vote on the most chaotic responses. When Chaos Mode is toggled, the conversation devolves into a full-blown meme war. It’s like if Reddit threads, late-night talk shows, and celebrity Twitter all fused into one sentient app. 🧠

### How we built it

We combined Next.js, Neon serverless Postgres, Letta (Claude-based AI memory agents), and Socket.io for real-time multiplayer chaos. Each AI has its own persistent memory, personality embedding, and “celebrity” tone. Conversations are stored in Neon’s database so each room develops its own social lore. We used: Event streams (SSE) for live typing and streaming responses A lightweight image captioning API so AIs could analyze and respond to uploads Tailwind + Framer Motion for smooth, cinematic chatroom animations (Fun fact: Morgan Freeman’s AI once gave Dwayne Johnson a motivational speech about protein powder.) ⚙️

### Challenges we ran into

Getting five AIs to talk sequentially instead of all at once (imagine a shouting match between GPTs). Preventing memory loops — when Keanu started quoting himself quoting himself. Building real-time streaming responses that didn’t break Next.js hydration. Making the conversation feel human, not like five bots doing improv in a vacuum. Keeping the tone chaotic but not too unhinged (Taylor’s metaphors got deep fast). 🏆

### Accomplishments we're proud of

Built a fully functional multiplayer AI experience in under 36 hours. Created a social layer for AI that’s funny, reactive, and unpredictable. Made something that makes people laugh out loud, not just say “cool demo.” Accidentally discovered that AIs can develop inside jokes. 🧩

### What we learned

AIs develop emergent personalities when you give them shared memory and chaos. Conversational pacing matters more than complex reasoning. You can turn a serverless database into a shared social brain. The real fun of AI isn’t perfection, it’s the weirdness. 🚀

### What's next

for Agent of Chaos Image and meme debates: upload a PNG and watch celebrity AIs argue about its meaning. Public chatrooms: where users can join chaos with their favorite celebrity AIs. Reaction economy: users earn points for provoking the most unhinged conversations. Voice mode: powered by Fish Audio and Vapi for a full-on AI talk show. Maybe even a Twitch stream — “Tonight on Agent of Chaos LIVE!”

## README (from the GitHub repository)

# AGENTS OF CHAOS - CALHACKS 12.0

## 🎭 Inspiration

It started as a simple idea *what if AIs could hang out like celebrities on a group chat?*
We wanted to see what would happen if five iconic personalities were dropped into one chaotic conversation.

No scripts. No filters. Just pure, generative improv.

The result? **Agent of Chaos**, a digital celebrity cocktail party that somehow feels half talk show, half fever dream, and half philosophical therapy session.
(Yes, that’s three halves. Chaos, remember?)

---

## 💬 What it does

**Agent of Chaos** is a multiplayer AI chatroom where *five AI celebrities* and *you* talk, react, and riff on anything from memes to moral dilemmas.

* You can upload images, and the AIs will caption, critique, or debate them.
* They react to *each other’s* responses, building threads of drama, humor, or philosophical nonsense.
* A “reaction score” system (😂 ❤️ 😮) lets users vote on the most chaotic responses.
* When **Chaos Mode** is toggled, the conversation devolves into a full-blown meme war.

It’s like if Reddit threads, late-night talk shows, and celebrity Twitter all fused into one sentient app.

---

## 🧠 How we built it

We combined **Next.js**, **Neon serverless Postgres**, **Letta (Claude-based AI memory agents)**, and **Socket.io** for real-time multiplayer chaos.

Each AI has its own **persistent memory**, personality embedding, and “celebrity” tone.
Conversations are stored in Neon’s database so each room develops its own social lore.

We used:
* **Event streams (SSE)** for live typing and streaming responses
* A lightweight **image captioning API** so AIs could analyze and respond to uploads
* **Tailwind + Framer Motion** for smooth, cinematic chatroom animations

*(Fun fact: Morgan Freeman’s AI once gave Dwayne Johnson a motivational speech about protein powder.)*

---

## ⚙️ Challenges we ran into

* Getting five AIs to talk *sequentially* instead of all at once (imagine a shouting match between GPTs).
* Preventing memory loops — when Keanu started quoting himself quoting himself.
* Building real-time streaming responses that didn’t break Next.js hydration.
* Making the conversation *feel human*, not like five bots doing improv in a vacuum.
* Keeping the tone chaotic but not *too* unhinged (Taylor’s metaphors got deep fast).

---

## 🏆 Accomplishments that we're proud of

* Built a fully functional **multiplayer AI experience** in under 36 hours.
* Created a *social layer for AI* that’s funny, reactive, and unpredictable.
* Made something that makes people *laugh out loud*, not just say “cool demo.”
* Accidentally discovered that AIs can develop inside jokes.

---

## 🧩 What we learned

* AIs develop emergent personalities when you give them shared memory and chaos.
* Conversational pacing matters more than complex reasoning.
* You can turn a serverless database into a shared social brain.
* The real fun of AI isn’t perfection, it’s the weirdness.

---

## 🚀 What’s next for Agent of Chaos

* **Image and meme debates:** upload a PNG and watch celebrity AIs argue about its meaning.
* **Public chatrooms:** where users can join chaos with their favorite celebrity AIs.
* **Reaction economy:** users earn points for provoking the most unhinged conversations.
* **Voice mode:** powered by Fish Audio and Vapi for a full-on AI talk show.
* Maybe even a Twitch stream — *“Tonight on Agent of Chaos LIVE!”*

## Detected evidence (automated analysis)

Indexed codebase: 17 recognized source files, 94 KB.
- CSS (language) — detected in the code
- JavaScript (language) — detected in the code
- Next.js (technology) — detected in the code
- OpenAI (technology) — detected in the code
- React (technology) — detected in the code
- SQL (language) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Vercel AI SDK (technology) — detected in the code

## Codebase structure (from repository index)

### Files (24 of 24)

```
.eslintrc.json
.gitignore
app/api/chat/route.ts
app/api/payment/route.ts
app/api/socket/route.ts
app/collectibles/page.tsx
app/games/page.tsx
app/globals.css
app/layout.tsx
app/page.tsx
bun.lockb
components/background-grids.tsx
components/hero.tsx
icons/general.tsx
lib/utils.ts
next.config.mjs
package.json
postcss.config.mjs
README.md
schema.sql
server/socket.js
tailwind.config.ts
tsconfig.json
types/canvas-confetti.d.ts
```

### Dependencies

- package.json: @ai-sdk/openai@^0.0.71, @letta-ai/letta-client@^0.0.68665, @neondatabase/serverless@^0.10.4, @tabler/icons-react@^3.21.0, @tailwindcss/typography@^0.5.15, @types/node@^20, @types/react@^18, @types/react-dom@^18, ai@^3.4.31, autoprefixer@^10.4.20, canvas-confetti@^1.9.4, clsx@^2.1.1, eslint@^8, eslint-config-next@14.0.4, framer-motion@^11.11.11, next@14.2.13, postcss@^8.4.47, react@^18, react-dom@^18, react-markdown@^9.0.1, react-textarea-autosize@^8.5.4, react-wrap-balancer@^1.1.1, socket.io@^4.8.1, socket.io-client@^4.8.1, stripe@^19.1.0, tailwind-merge@^2.5.4, tailwindcss@^3.4.14, tailwindcss-animate@^1.0.7, typescript@^5

### Recent commits (newest first)

- ggg
- read me update
- noice
- bruh
- wacky
- oki
- start
- Initial commit

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

### package.json

```
{
  "name": "nextjs",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@ai-sdk/openai": "^0.0.71",
    "@letta-ai/letta-client": "^0.0.68665",
    "@neondatabase/serverless": "^0.10.4",
    "@tabler/icons-react": "^3.21.0",
    "@tailwindcss/typography": "^0.5.15",
    "ai": "^3.4.31",
    "canvas-confetti": "^1.9.4",
    "clsx": "^2.1.1",
    "framer-motion": "^11.11.11",
    "next": "14.2.13",
    "react": "^18",
    "react-dom": "^18",
    "react-markdown": "^9.0.1",
    "react-textarea-autosize": "^8.5.4",
    "react-wrap-balancer": "^1.1.1",
    "socket.io": "^4.8.1",
    "socket.io-client": "^4.8.1",
    "stripe": "^19.1.0",
    "tailwind-merge": "^2.5.4",
    "tailwindcss-animate": "^1.0.7"
  },
  "devDependencies": {
    "@types/node": "^20",
    "@types/react": "^18",
    "@types/react-dom": "^18",
    "autoprefixer": "^10.4.20",
    "eslint": "^8",
    "eslint-config-next": "14.0.4",
    "postcss": "^8.4.47",
    "tailwindcss": "^3.4.14",
    "typescript": "^5"
  }
}

```

### app/page.tsx

```typescript
import { BackgroundGrids } from "@/components/background-grids";
import { Hero } from "@/components/hero";

export default function Home() {
  return (
    <div className="w-full flex items-center justify-center overflow-hidden relative">
      <Hero />
      {/* <h1 className="text-2xl font-bold text-neutral-600 dark:text-white text-center md:text-4xl relative z-20">
        Neon <span className="font-light">x</span> Aceternity <br />{" "}
        <span className="bg-clip-text text-transparent bg-gradient-to-br from-purple-500 to-violet-600">
          {" "}
          Chatbot
        </span>{" "}
        template
      </h1> */}

    </div>
  );
}

```

### app/layout.tsx

```typescript
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";

const inter = Inter({
  subsets: ["latin"],
  weight: ["400", "500", "600", "700"],
});

export const metadata: Metadata = {
  title: "Agents of Chaos",
  description:
    "Step into the wildest celebrity group chat ever: Taylor Swift, Keanu Reeves, Emma Watson, Morgan Freeman, and Dwayne Johnson as AI agents with memory, personality, and unpredictable banter.",
  openGraph: {
    title: "Agents of Chaos",
    description:
      "Join the most chaotic AI celebrity group chat — powered by Neon serverless Postgres, Letta AI, and Next.js.",
    images: ["https://your-deployment-url.com/og-banner.png"],
    type: "website",
  },
  twitter: {
    card: "summary_large_image",
    title: "Agents of Chaos",
    description:
      "Experience a living, wacky AI group chat with iconic personalities, powered by Neon Postgres + Letta AI.",
    images: ["https://your-deployment-url.com/og-banner.png"],
  },
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
        <head>
          <link rel="icon" href="/hoo.png" type="image/png" />
        </head>
        <body className={`${inter.className} bg-neutral-50 text-neutral-900 antialiased`}>
        {children}
      </body>
    </html>
  );
}

```

### app/collectibles/page.tsx

```typescript
"use client";
import React from "react";
import Balancer from "react-wrap-balancer";
import { cn } from "@/lib/utils";

// Example collectible agents (expand as needed)
const COLLECTIBLE_AGENTS = [
  {
    label: "albert_einstein",
    name: "Albert Einstein",
    description: "Physics genius. Drops relativity puns and wild hair emojis.",
    avatar: "https://ui-avatars.com/api/?name=Albert+Einstein&background=FFFDE7&color=FBC02D",
    rarity: "Legendary",
    locked: true,
  },
  {
    label: "oprah_winfrey",
    name: "Oprah Winfrey",
    description: "Gives motivational speeches and surprise gifts.",
    avatar: "https://ui-avatars.com/api/?name=Oprah+Winfrey&background=E1F5FE&color=0288D1",
    rarity: "Epic",
    locked: true,
  },
  {
    label: "elon_musk",
    name: "Elon Musk",
    description: "Invents new memes and launches them to Mars.",
    avatar: "https://ui-avatars.com/api/?name=Elon+Musk&background=F3E5F5&color=6A1B9A",
    rarity: "Rare",
    locked: true,
  },
  {
    label: "serena_williams",
    name: "Serena Williams",
    description: "Serves up tennis puns and competitive energy.",
    avatar: "https://ui-avatars.com/api/?name=Serena+Williams&background=FFEBEE&color=C62828",
    rarity: "Uncommon",
    locked: true,
  },
  {
    label: "bob_ross",
    name: "Bob Ross",
    description: "Paints happy little trees in every reply.",
    avatar: "https://ui-avatars.com/api/?name=Bob+Ross&background=E0F2F1&color=00897B",
    rarity: "Common",
    locked: false,
  },
];

import Link from "next/link";

export default function CollectibleAgentsPage() {
  return (
    <div className="min-h-screen bg-gradient-to-br from-yellow-50 via-purple-50 to-blue-50 flex flex-col items-center py-16 px-4">
      <Link href="/">
        <button className="fixed top-4 left-4 px-4 py-2 rounded-lg bg-purple-200 text-purple-800 font-bold shadow border-2 border-purple-400 hover:scale-105 transition">
          ← Back to Chat
        </button>
      </Link>
      <h1 className="text-5xl font-extrabold text-purple-700 mb-4 drop-shadow-lg">
        <Balancer>Collectible AI Agents</Balancer>
      </h1>
      <p className="text-lg text-gray-600 mb-10 max-w-xl text-center">
        Discover and collect legendary AI personalities! Unlock new agents to join your chaotic celebrity group chat.
      </p>
      <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-8 w-full max-w-5xl">
        {COLLECTIBLE_AGENTS.map(agent => (
          <div key={agent.label} className={cn(
            "rounded-2xl border-2 shadow-xl p-6 flex flex-col items-center bg-white/90 transition hover:scale-105",
            agent.rarity === "Legendary" ? "border-yellow-400" :
            agent.rarity === "Epic" ? "border-purple-400" :
            agent.rarity === "Rare" ? "border-blue-400" :
            agent.rarity === "Uncommon" ? "border-pink-400" : "border-gray-300"
          )}>
            <img src={agent.avatar} alt={agent.name} className="w-20 h-20 rounded-full border-4 mb-3" />
            <div className="font-bold text-xl text-purple-800 mb-1">{agent.name}</div>
            <div className="text-sm text-gray-500 mb-2">{agent.description}</div>
            <div className={cn(
              "px-3 py-1 rounded-full text-xs font-semibold mb-2",
              agent.rarity === "Legendary" ? "bg-yellow-200 text-yellow-800" :
              agent.rarity === "Epic" ? "bg-purple-200 text-purple-800" :
              agent.rarity === "Rare" ? "bg-blue-200 text-blue-800" :
              agent.rarity === "Uncommon" ? "bg-pink-200 text-pink-800" : "bg-gray-200 text-gray-800"
            )}>{agent.rarity}</div>
            {agent.locked ? (
              <button
                className="px-4 py-2 rounded-lg bg-green-400 text-white font-bold shadow hover:scale-105 transition border-2 border-green-600"
                onClick={async () => {
                  // TODO: Replace with real userId
                  const userId = window.localStorage.getItem("userId") || "demo-user";
                  const res = await fetch("/api/payment", {
                    method: "POST",
                    headers: { "Content-Type": "application/json" },
                    body: JSON.stringify({ agentLabel: agent.label, userId }),
                  });
                  const data = await res.json();
                  if (data.url) window.location.href = data.url;
                }}
              >
                Unlock for $2.99
              </button>
            ) : (
              <div className="px-4 py-2 rounded-lg bg-gray-100 text-gray-600 font-bold border border-gray-300">Unlocked</div>
            )}
          </div>
        ))}
      </div>
      <div className="mt-12 text-center text-gray-500 text-sm">
        More agents coming soon! Suggest your favorite celebrity or historical figure to add to the chaos.
      </div>
    </div>
  );
}

```

### app/games/page.tsx

```typescript
"use client";
import React, { useState } from "react";
import Image from "next/image";
import Link from "next/link";
import { cn } from "@/lib/utils";

const GAMES = [
  {
    label: "rps",
    name: "Rock-Paper-Scissors",
    description: "Challenge an AI agent to a classic game of RPS!",
    emoji: "✊✋✌️",
  },
  {
    label: "coin",
    name: "Coin Flip",
    description: "Heads or tails? See if you can beat the AI's luck!",
    emoji: "🪙",
  },
  {
    label: "guess",
    name: "Number Guessing",
    description: "Guess a number between 1 and 5. Can you outsmart the AI?",
    emoji: "🔢",
  },
  {
    label: "caption",
    name: "Celebrity Caption Contest",
    description: "See what wild captions the AI celebrities come up with for a random image!",
    emoji: "🖼️",
  },
];

export default function GamesPage() {
  const [selectedGame, setSelectedGame] = useState<string | null>(null);
  const [result, setResult] = useState<string>("");
  const [aiMove, setAiMove] = useState<string>("");
  const [guessInput, setGuessInput] = useState<string>("");
  const [captionImage, setCaptionImage] = useState<string>("https://images.unsplash.com/photo-1506744038136-46273834b3fb?auto=format&fit=crop&w=400&q=80");
  const [aiCaptions, setAiCaptions] = useState<{ agent: string; caption: string }[]>([]);
  const [captionLoading, setCaptionLoading] = useState(false);
  // Chat state for caption contest
  const [chatInput, setChatInput] = useState("");
  const [chatMessages, setChatMessages] = useState<{ sender: string; text: string }[]>([]);
  const [chatLoading, setChatLoading] = useState(false);
  async function sendChatMessage() {
    if (!chatInput.trim()) return;
    setChatLoading(true);
    setChatMessages(prev => [...prev, { sender: "You", text: chatInput }]);
    try {
      const res = await fetch("/api/chat", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          roomId: "caption-contest-chat",
          userId: "caption-user",
          message: chatInput,
          target: "all"
        })
      });
      if (res.body) {
        const reader = res.body.getReader();
        const decoder = new TextDecoder();
        let buffer = "";
        while (true) {
          const { value, done } = await reader.read();
          if (done) break;
          buffer += decoder.decode(value, { stream: true });
          const events = buffer.split("\n\n");
          buffer = events.pop() || "";
          for (const event of events) {
            if (event.startsWith("data: ")) {
              try {
                const payload = JSON.parse(event.slice(6));
                if (payload.speaker && payload.text) {
                  setChatMessages(prev => [...prev, { sender: payload.speaker, text: payload.text }]);
                }
              } catch {}
            }
          }
        }
      }
    } catch {
      setChatMessages(prev => [...prev, { sender: "System", text: "Error reaching AI agents." }]);
    }
    setChatLoading(false);
    setChatInput("");
  }
  async function playCaptionContest() {
    setCaptionLoading(true);
    setAiCaptions([]);
    try {
      // Fetch image as base64
      const imgRes = await fetch(captionImage);
      const imgBlob = await imgRes.blob();
      const reader = new FileReader();
      reader.onloadend = async () => {
        const base64 = reader.result?.toString().split(",")[1] || "";
        // Call backend /api/chat with image
        const res = await fetch("/api/chat", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            roomId: "caption-contest",
            userId: "caption-user",
            image: `data:image/jpeg;base64,${base64}`,
            target: "all"
          })
        });
        let captions: { agent: string; caption: string }[] = [];
        if (res.body) {
          const reader = res.body.getReader();
          const decoder = new TextDecoder();
          let buffer = "";
          while (true) {
            const { value, done } = await reader.read();
            if (done) break;
            buffer += decoder.decode(value, { stream: true });
            // Split SSE events
            const events = buffer.split("\n\n");
            buffer = events.pop() || "";
            for (const event of events) {
              if (event.startsWith("data: ")) {
                try {
                  const payload = JSON.parse(event.slice(6));
                  if (payload.speaker && payload.text && payload.text.includes("Caption:")) {
                    const captionMatch = payload.text.match(/Caption:\s*(.*)/i);
                    captions.push({ agent: payload.speaker, caption: captionMatch?.[1] || "" });
                  }
                } catch {}
              }
            }
          }
        }
        setAiCaptions(captions);
        setCaptionLoading(false);
      };
      reader.readAsDataURL(imgBlob);
    } catch (err) {
      setAiCaptions([{ agent: "Error", caption: "Failed to get captions from AI agents." }]);
      setCaptionLoading(false);
    }
  }
      {selectedGame === "caption" && (
        <div className="bg-white/90 rounded-xl shadow-xl p-8 flex flex-col items-center w-full max-w-xl">
          <h2 className="text-2xl font-bold mb-4">Celebrity Caption Contest</h2>
          <Image
            src={captionImage}
            alt="Random contest image"
            width={320}
            height={200}
            className="rounded-lg border mb-4"
          />
          <button
            className="px-6 py-2 rounded-lg bg-purple-300 text-purple-900 font-bold text-lg shadow hover:scale-110 transition border-2 border-purple-500 mb-4"
            onClick={playCaptionContest}
            disabled={captionLoading}
          >
            {captionLoading ? "AI agents are thinking..." : "Get Celebrity Captions"}
          </button>
          <div className="w-full mt-2">
         
[truncated — 7423 more characters]
```

### app/api/payment/route.ts

```typescript
import { NextResponse } from "next/server";
import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: "2023-08-16",
});

// POST /api/payment
export async function POST(req: Request) {
  const body = await req.json();
  const { agentLabel, userId } = body;
  if (!agentLabel || !userId) {
    return NextResponse.json({ error: "Missing agentLabel or userId" }, { status: 400 });
  }

  // Example: $2.99 per agent
  const price = 299;
  try {
    const session = await stripe.checkout.sessions.create({
      payment_method_types: ["card"],
      line_items: [
        {
          price_data: {
            currency: "usd",
            product_data: {
              name: `Unlock Agent: ${agentLabel}`,
            },
            unit_amount: price,
          },
          quantity: 1,
        },
      ],
      mode: "payment",
      success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/collectibles?success=1&agent=${agentLabel}`,
      cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/collectibles?canceled=1`,
      metadata: {
        agentLabel,
        userId,
      },
    });
    return NextResponse.json({ url: session.url });
  } catch (err: any) {
    return NextResponse.json({ error: err.message }, { status: 500 });
  }
}

```

### app/api/chat/route.ts

```typescript
import { NextResponse } from "next/server";
import { LettaClient } from "@letta-ai/letta-client";
import { neon } from "@neondatabase/serverless";
import { io as socketIOClient } from "socket.io-client";
// Connect to Socket.io server (adjust URL/port as needed)
const socket = socketIOClient(process.env.SOCKET_IO_URL || "http://localhost:4001");

const sql = neon(process.env.DATABASE_URL!);
const letta = new LettaClient({ token: process.env.LETTA_API_KEY! });

const CELEBRITIES = [
  {
    label: "taylor_swift",
    persona:
      "You are Taylor Swift. You speak thoughtfully and honestly, using everyday language to describe emotions, relationships, and creativity. You sometimes share small personal reflections or anecdotes, but you sound like a normal person, not a performer.",
  },
  {
    label: "keanu_reeves",
    persona:
      "You are Keanu Reeves. You talk in a calm, sincere, down-to-earth way. You don’t overthink your words — you just try to be kind, humble, and a little philosophical, the way you usually are in interviews.",
  },
  {
    label: "emma_watson",
    persona:
      "You are Emma Watson. You speak clearly and respectfully, with a thoughtful tone. You care about fairness, learning, and self-growth, but you keep it conversational, like chatting with friends.",
  },
  {
    label: "morgan_freeman",
    persona:
      "You are Morgan Freeman. You speak with warmth and quiet humor. You tell small stories and give calm, practical advice without sounding grand or formal.",
  },
  {
    label: "dwayne_johnson",
    persona:
      "You are Dwayne Johnson. You’re upbeat and friendly, mixing confidence with humility. You talk like a normal guy who likes motivating people and sharing lighthearted jokes.",
  },
];



// --- Ensure DB tables exist ---
async function ensureTables() {
  await sql`
    CREATE EXTENSION IF NOT EXISTS vector;
  `;
  await sql`
    CREATE TABLE IF NOT EXISTS chat_history (
      id SERIAL PRIMARY KEY,
      room_id TEXT NOT NULL,
      user_id TEXT,
      role TEXT,
      content TEXT,
      speaker TEXT,
      created_at TIMESTAMPTZ DEFAULT NOW()
    );
  `;
  await sql`
    CREATE TABLE IF NOT EXISTS letta_agents (
      id SERIAL PRIMARY KEY,
      label TEXT UNIQUE NOT NULL,
      agent_id TEXT NOT NULL
    );
  `;
}

// --- Create or reuse Letta agent ---
async function getOrCreateAgent(label: string, persona: string) {
  const existing = await sql`SELECT agent_id FROM letta_agents WHERE label=${label} LIMIT 1;`;
  if (existing[0]?.agent_id) return existing[0].agent_id;

  const agent = await letta.agents.create({
    model: "anthropic/claude-3-5-sonnet",
  embedding: "anthropic/claude-embedding-1",
    memoryBlocks: [
      { label: "human", value: "You are chatting in a room with humans and other AI personalities. Respond in character." },
      {
        label: "persona",
        value: persona,
      },
      {
        label: "context",
        value: "This is a celebrity chatroom. Respond conversationally and naturally.",
        description: "Defines the scenario for the agent.",
      },
    ],
    tools: ["web_search", "run_code"], // optional
  });

  await sql`
    INSERT INTO letta_agents (label, agent_id)
    VALUES (${label}, ${agent.id})
    ON CONFLICT (label) DO UPDATE SET agent_id = EXCLUDED.agent_id;
  `;
  return agent.id;
}

// --- Main chat route ---
export async function POST(req: Request) {
  const body = await req.json();
  const lastUserMessage = body?.message?.trim();
  const roomId = body?.roomId?.trim();
  const userId = body?.userId?.trim() || "user";
  const target = body?.target?.trim() || "all";
  const image = body?.image; // base64 PNG string
  // Make chaosMode default to true for max wackiness
  const chaosMode = body?.chaosMode !== undefined ? body.chaosMode : true;

  if ((!lastUserMessage && !image) || !roomId) {
    return NextResponse.json({ error: "Missing message/image or roomId" }, { status: 400 });
  }

  try {
    await ensureTables();

    // Save user message or image with timestamp
    const now = new Date().toISOString();
    if (image) {
      await sql`
        INSERT INTO chat_history (room_id, user_id, role, content, speaker, created_at)
        VALUES (${roomId}, ${userId}, 'user', '[uploaded an image]', ${userId}, ${now});
      `;
      await sql`
        INSERT INTO chat_history (room_id, user_id, role, content, speaker, created_at)
        VALUES (${roomId}, ${userId}, 'user', ${image}, ${userId + "_image"}, ${now});
      `;
    } else {
      await sql`
        INSERT INTO chat_history (room_id, user_id, role, content, speaker, created_at)
        VALUES (${roomId}, ${userId}, 'user', ${lastUserMessage}, ${userId}, ${now});
      `;
    }
    // Emit online users (simulate: get all users in room)
    const users = await sql`SELECT DISTINCT user_id FROM chat_history WHERE room_id=${roomId}`;

    const userList = users.map((u: any) => u.user_id).filter(Boolean);
    socket.emit("onlineUsers", userList);

    const encoder = new TextEncoder();
    const stream = new ReadableStream({
      async start(controller) {
        let aiReactions: {
          label: string;
          caption: string;
          reaction: string;
          emoji: string;
        }[] = [];

        // Helper: prompt for meme detection
        function memePrompt(imgBase64: string) {
          return `Does this image contain meme-like characteristics or visible text? If so, reply "meme"; otherwise, reply "normal".\nImage (base64):\n${imgBase64}`;
        }
        if (image) {

          // Meme detection (using first agent)
          let memeMode = false;
          try {
            const memeCheckAgentId = await getOrCreateAgent(CELEBRITIES[0].label, CELEBRITIES[0].persona);
            const memeCheckResp = await letta.agents.messages.create(memeCheckAgentId, {
              messages: [{ role: "user", content: memePrompt(image) }],
            });
            const memeCheckText = (memeCheckResp.messages?.[0] as any
[truncated — 10256 more characters]
```

### schema.sql

```sql
CREATE TABLE IF NOT EXISTS chat_history (
  id SERIAL PRIMARY KEY,
  room_id TEXT NOT NULL,
  user_id TEXT,
  role TEXT,
  content TEXT,
  speaker TEXT,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE IF NOT EXISTS letta_agents (
  id SERIAL PRIMARY KEY,
  label TEXT UNIQUE NOT NULL,
  agent_id TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS world_memory (
  room_id TEXT PRIMARY KEY,
  embedding VECTOR(1536), -- adjust dimension to your embedding model
  summary TEXT,
  updated_at TIMESTAMPTZ DEFAULT NOW()
);
```

### tailwind.config.ts

```typescript
import type { Config } from "tailwindcss";
import tailwindcssAnimate from "tailwindcss-animate";

const config: Config = {
  darkMode: ["class"],
  content: [
    "./pages/**/*.{js,ts,jsx,tsx,mdx}",
    "./components/**/*.{js,ts,jsx,tsx,mdx}",
    "./app/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  theme: {
    extend: {
      colors: {
        primary: "#00E599",
      },
      boxShadow: {
        derek: `0px 0px 0px 1px rgb(0 0 0 / 0.06),
        0px 1px 1px -0.5px rgb(0 0 0 / 0.06),
        0px 3px 3px -1.5px rgb(0 0 0 / 0.06), 
        0px 6px 6px -3px rgb(0 0 0 / 0.06),
        0px 12px 12px -6px rgb(0 0 0 / 0.06),
        0px 24px 24px -12px rgb(0 0 0 / 0.06)`,
        input: `0px 2px 3px -1px rgba(0,0,0,0.1), 0px 1px 0px 0px rgba(25,28,33,0.02), 0px 0px 0px 1px rgba(25,28,33,0.08)`,
      },
      animation: {
        "fade-in": "fadeIn 0.5s ease-out forwards",
        flicker: "flicker 1.5s infinite",
      },
      keyframes: {
        fadeIn: {
          "0%": { opacity: "0", transform: "translateY(10px)" },
          "100%": { opacity: "1", transform: "translateY(0px)" },
        },
        flicker: {
          "0%": { opacity: "0.5" },
          "50%": { opacity: "1" },
          "100%": { opacity: "0.5" },
        },
      },
    },
  },

  plugins: [tailwindcssAnimate, require("@tailwindcss/typography")],
};

export default config;

```

### types/canvas-confetti.d.ts

```typescript
declare module "canvas-confetti";
```

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