# Project export: ✨ Flux

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: TreeHacks 2026
- Tagline: Your AI clone meets other AI clones.
- Devpost: https://devpost.com/software/flux-vup1ag
- GitHub: https://github.com/m5b6/tree
- Demo: https://tree-ebon-chi.vercel.app/
- Video: https://www.youtube.com/embed/VxyAHBORurk?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 4 GitHub contributor(s) — m5b6 😽 (65 commits), Claude Opus 4.6 (64 commits), Fernando (25 commits), nbcl (23 commits)

## Devpost submission (written by the team)

### Overview

GitHub: https://github.com/m5b6/tree ⚡ Flux: Meet 1,000 People Per Day ☕ What if your AI clone networked with +1,000 hackers while you were busy building? 💡

### Inspiration

After 36 hours of hacking, we realized we hadn’t met anyone. So we built Flux, an AI clone that attends hackathons or conferences on your behalf... chatting with hundreds of people and returning a shortlist of those you’d actually vibe with IRL? ⚙️ What It Does Flux creates your AI twin that: Scrapes public LinkedIn/X/GitHub data (Bright Data + Browserbase). Builds your conversational personality. Runs autonomous coffee chats with +1,000 other attendees’ clones (Claude Agent SDK). Scores compatibility using OpenAI embeddings + GPT-4 mini. Reports your top matches with chat summaries and “vibe” notes. 🧠 Tech Stack Data: Bright Data / Browserbase (Stagehand) Agents: Claude Agent SDK (Anthropic) Matching: OpenAI API (embeddings + GPT-4o vision) Frontend: Vercel 🌍 Impact Flux helps humans connect better by letting AI handle the first 100 chats. We let you flourish and leave the IRL back to you.

## README (from the GitHub repository)

This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).

## Getting Started

First, run the development server:

```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.

This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.

## Learn More

To learn more about Next.js, take a look at the following resources:

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!

## Deploy on Vercel

The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.

Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.


## Detected evidence (automated analysis)

Indexed codebase: 126 recognized source files, 1002 KB.
- Anthropic (technology) — detected in the code
- CSS (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
- Vercel (technology) — claimed on Devpost, not found in the code
- AI coding agent: Claude Code — evidence: config files committed to the repository; commit authorship or trailers

## Codebase structure (from repository index)

### Files (120 of 147)

```
.claude/agents/frontend.md
.claude/agents/velocity.md
.gitignore
ARCHITECTURE.md
CLAUDE.md
components.json
drizzle.config.ts
drizzle/0000_fast_nomad.sql
drizzle/meta/_journal.json
drizzle/meta/0000_snapshot.json
eslint.config.mjs
next.config.ts
package.json
PLAN.md
postcss.config.mjs
README.md
scripts/enrich-all-github-bd.mjs
scripts/enrich-all-github.mjs
scripts/enrich-all-linkedin.mjs
scripts/enrich-github.ts
scripts/enrich-remaining.mjs
scripts/enrich-x.mjs
scripts/extract-company-from-headline.mjs
scripts/find-linkedin.mjs
scripts/fix-avatars.ts
scripts/fix-profile-pictures.mjs
scripts/import-linkedin-batch.mjs
scripts/import-people.ts
scripts/import-x-profiles.mjs
scripts/linkedin-metadata.mjs
scripts/parse-linkedin-search.mjs
scripts/scrape-linkedin-avatars.mjs
scripts/synthesize-agents.ts
sqlite.db
src/app/admin/page.tsx
src/app/agents/new/page.tsx
src/app/agents/page.tsx
src/app/api/agents/[id]/route.ts
src/app/api/agents/generate/route.ts
src/app/api/agents/route.ts
src/app/api/config/route.ts
src/app/api/mcp/route.ts
src/app/api/onboard/complete/route.ts
src/app/api/onboard/scrape/route.ts
src/app/api/onboard/x-posts/route.ts
src/app/api/people/lookup/route.ts
src/app/api/simulations/[id]/route.ts
src/app/api/simulations/[id]/stream/route.ts
src/app/api/simulations/batch/route.ts
src/app/api/simulations/route.ts
src/app/api/sources/instagram/route.ts
src/app/api/sources/linkedin/route.ts
src/app/api/sources/synthesize/route.ts
src/app/api/sources/x/route.ts
src/app/globals.css
src/app/layout.tsx
src/app/page.tsx
src/app/preview/chat/page.tsx
src/app/preview/verdict/page.tsx
src/app/simulation/[id]/page.tsx
src/app/test/all/page.tsx
src/app/test/batch/page.tsx
src/app/test/page.tsx
src/app/test/simulation/page.tsx
src/app/test/sources/page.tsx
src/components/agents/agent-card.tsx
src/components/agents/agent-form.tsx
src/components/agents/agent-list.tsx
src/components/batch/batch-sim-card.tsx
src/components/control-room/agent-graph.tsx
src/components/control-room/batch-run-panel.tsx
src/components/control-room/sim-card-live.tsx
src/components/control-room/sim-card.tsx
src/components/control-room/sim-grid.tsx
src/components/control-room/stats-bar.tsx
src/components/control-room/top-sims.tsx
src/components/dev-nav.tsx
src/components/friends/agent-conversation.tsx
src/components/friends/emotion-chat-bubble.tsx
src/components/friends/focused-friend-card.tsx
src/components/friends/friend-focus-layout.tsx
src/components/friends/muted-friend-card.tsx
src/components/friends/orchestrator-verdict-card.tsx
src/components/friends/pixel-emotion-effect.tsx
src/components/friends/pixel-face.tsx
src/components/friends/pixel-sparkle.tsx
src/components/friends/simulation-progress.tsx
src/components/friends/simulation-runner.tsx
src/components/friends/thinking-indicator.tsx
src/components/react-bits/AtSign.tsx
src/components/react-bits/ClickSpark.tsx
src/components/react-bits/Dither.tsx
src/components/react-bits/Granient.tsx
src/components/react-bits/index.ts
src/components/react-bits/NumberTicker.tsx
src/components/react-bits/SplitText.tsx
src/components/react-bits/StatusStreamer.tsx
src/components/react-bits/Stepper.tsx
src/components/simulation/action-emoji.tsx
src/components/simulation/agent-blob.tsx
src/components/simulation/simulation-insights.tsx
src/components/simulation/simulation-stage.tsx
src/components/simulation/simulation-timeline.tsx
src/components/simulation/simulation-view.tsx
src/components/simulation/speech-bubble.tsx
src/components/simulation/thought-bubble.tsx
src/components/ui/avatar.tsx
src/components/ui/badge.tsx
src/components/ui/button-group.tsx
src/components/ui/button.tsx
src/components/ui/card.tsx
src/components/ui/dialog.tsx
src/components/ui/dropdown-menu.tsx
src/components/ui/input.tsx
src/components/ui/label.tsx
src/components/ui/skeleton.tsx
src/components/ui/sonner.tsx
src/components/ui/textarea.tsx
src/components/ui/tooltip.tsx
src/hooks/use-batch-simulations.ts
[27 more files omitted for size]
```

### Dependencies

- package.json: @ai-sdk/anthropic@^3.0.44, @ai-sdk/openai@^3.0.29, @brightdata/sdk@^0.2.0, @browserbasehq/stagehand@^3.0.8, @gsap/react@^2.1.2, @libsql/client@^0.17.0, @modelcontextprotocol/sdk@^1.25.2, @openrouter/ai-sdk-provider@^2.2.3, @react-three/fiber@^9.5.0, @react-three/postprocessing@^3.0.4, @tailwindcss/postcss@^4, @types/node@^20, @types/qrcode@^1.5.6, @types/react@^19, @types/react-dom@^19, ai@^6.0.86, babel-plugin-react-compiler@1.0.0, class-variance-authority@^0.7.1, clsx@^2.1.1, drizzle-kit@^0.31.9, drizzle-orm@^0.45.1, eslint@^9, eslint-config-next@16.1.6, framer-motion@^12.34.0, gsap@^3.14.2, lucide-react@^0.564.0, mcp-handler@^1.0.7, motion@^12.34.0, nanoid@^5.1.6, next@16.1.6, next-themes@^0.4.6, ogl@^1.0.11, postprocessing@^6.38.2, qrcode@^1.5.4, qrcode.react@^4.2.0, radix-ui@^1.4.3, react@19.2.3, react-dom@19.2.3, shadcn@^3.8.4, sonner@^2.0.7, tailwind-merge@^3.4.0, tailwindcss@^4, three@^0.182.0, tw-animate-css@^1.4.0, typescript@^5, zod@^4.3.6

### Recent commits (newest first)

- refactor(scripts): use env vars for external SDK config across enrichment scripts
- fix(UI): light blue loading rings and progress bars
- fix(UI): smaller card header, proper 3-line clamp on chat bubbles
- pingi
- fix(UI): fix chat scroll, bump font back to text-[11px]
- fix(UI): smaller font, wider bubbles, 3-line clamp, scrollable chat
- fix(UI): skeleton card grows vertically to match real layout
- fix(UI): reduce card content padding from px-6 to px-3
- fix(UI): ClickSpark passes through flex layout chain
- fix(UI): flex layout fills vertical space, remove hardcoded 200px
- fix: 2-line clamp on CTA bubble, enforce max 15 words in prompt
- fix(UI): CTA as gray friend-side bubble, scroll to bottom on report
- fix(orchestrator): remove anchoring that caused scores to cluster at 42%
- fix(UI): white QR on black, add CTA as user message, remove verdict card
- fix: update pnpm lockfile with qrcode dependencies
- feat(orchestrator): upgrade to Claude 4.5 Haiku
- revert(orchestrator): keep claude-3-5-haiku as orchestrator model
- feat: single QR in chat bubble, verdict card style, common ground msg
- feat(orchestrator): upgrade model to Sonnet 4.5, add strict score bands and friend-perspective CTA
- feat(UI): Some QR fixes

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

### PLAN.md

```markdown
# Plan: Bulk People Pipeline

## Problem
We have 1,447 merged profiles (Slack + Timber) but the app only has an `agents` table with LLM-synthesized fields. No place to store raw source data, no social graph, no bulk import path.

## New Schema

### `people` table — ground truth for every TreeHacks participant
Stores ALL raw data from every source. This is the canonical identity table.

| Column | Type | Source |
|--------|------|--------|
| id | text PK | nanoid |
| email | text UNIQUE | Slack/Timber |
| first_name | text | Timber > Slack |
| last_name | text | Timber > Slack |
| display_name | text | Slack |
| phone | text | Timber/Slack |
| other_contact | text | Timber |
| github_username | text | Timber |
| profile_picture_url | text | Timber (GitHub avatar) / Slack |
| location | text | Timber |
| timezone | text | Slack |
| level_of_study | text | Timber |
| graduation_year | text | Timber |
| skills | text (JSON) | Timber |
| track | text | Timber |
| who_are_you | text | Timber |
| idea | text | Timber |
| ideal_partner | text | Timber |
| ideal_team_size | integer | Timber |
| favorite_tree | text | Timber |
| coolest_thing | text | Timber |
| code_comments | text | Timber |
| song | text | Timber |
| slack_id | text | Slack |
| slack_title | text | Slack |
| timber_id | text | Timber |
| sources | text (JSON) | ["slack","timber"] |
| agent_id | text FK → agents | filled after LLM synthesis |
| created_at | integer | now |

### `timber_swipes` table — the match/rejection graph
| Column | Type |
|--------|------|
| id | text PK |
| person_id | text FK → people |
| target_person_id | text FK → people |
| status | text ("MATCHED" / "REJECTED") |
| swiped_at | integer |

### `timber_teams` table — final team formations
| Column | Type |
|--------|------|
| id | text PK |
| person_id | text FK → people |
| teammate_id | text FK → people |

## Pipeline

### Step 1: Schema migration
Add new tables to schema.ts + migration SQL in db/index.ts.

### Step 2: Bulk import script (`scripts/import-people.ts`)
- Reads `treehacks_merged.json`
- Inserts into `people` table
- Resolves timber_id → person_id for swipes/teams
- Inserts into `timber_swipes` and `timber_teams`

### Step 3: Bulk synthesis script (`scripts/synthesize-agents.ts`)
- Reads people with no agent_id
- For each person, calls LLM with a Timber/Slack-adapted synthesis prompt
- Produces: personality, backstory, systemPrompt, icon, color
- Creates agent, links back to person via agent_id
- Batched with concurrency control (rate limits)
- Priority: people with Timber data first (richer profiles)

### Step 4: App integration
- Agent creation page can browse `people` and pick someone to simulate
- Simulation setup can use the swipe graph: "simulate a date between two people who mutually matched on Timber"
- Browsing UI shows all people with data richness indicators

## Changes to existing code
- `schema.ts`: add people, timber_swipes, timber_teams tables
- `db/index.ts`: add CREATE TABLE IF NO
[truncated — 199 more characters]
```

### CLAUDE.md

```markdown
# Tree — Multi-Agent Social Simulation

## Product

AI agents built from real user profiles interact in configurable scenarios. Each agent produces **dual-layer output**: inner thoughts (private reasoning) and spoken conversation (public). An invisible orchestrator agent observes the interaction, scores compatibility, and provides real-life advice.

**Core purpose**: Users simulate conversations between people in their network to discover who they'd click with, anticipate friction points, and get actionable recommendations before meeting in real life. The orchestrator is the judgment layer — it watches, measures, and advises.

## Tech Stack

- **Framework**: Next.js 16 (App Router, RSC, React 19)
- **UI**: shadcn/ui (new-york style), Tailwind v4, framer-motion
- **AI**: Vercel AI SDK with `@openrouter/ai-sdk-provider` (OpenRouter as the LLM provider)
- **DB**: Drizzle ORM + SQLite (local-first, file-based)
- **Fonts**: Geist Sans / Geist Mono
- **Dark mode**: Always on (class-based `.dark` on `<html>`)

## Project Structure

```
src/
├── app/                    # Next.js routes + layouts (frontend)
│   ├── api/                # Route handlers (backend)
│   ├── layout.tsx
│   ├── page.tsx
│   └── (routes)/
├── components/             # React components (frontend)
│   ├── ui/                 # shadcn primitives
│   └── (feature)/          # feature-specific components
├── hooks/                  # React hooks (frontend)
├── lib/                    # Shared utilities + backend logic
│   ├── agents/             # Agent system (backend) — prompts, orchestration, tools
│   ├── db/                 # Drizzle schema + queries (backend)
│   └── utils.ts            # Shared utilities
```

### Domain Boundaries

| Domain | Directories | Owner Agent |
|----------|--------------------------------------|-------------|
| Frontend | `src/app/` (pages), `src/components/`, `src/hooks/` | `frontend` |
| Backend | `src/lib/agents/`, `src/lib/db/`, `src/app/api/` | `velocity` |
| Shared | `src/lib/utils.ts`, config files | either |

## Commands

```bash
npm run dev      # Start dev server
npm run build    # Production build
npm run lint     # ESLint
```

## Environment Variables

```
OPENROUTER_API_KEY=        # Required — OpenRouter API key
```

## Coding Conventions

### General
- TypeScript strict mode, no `any`
- Path alias: `@/*` maps to `./src/*`
- Prefer named exports
- No default exports except Next.js pages/layouts/routes

### Components
- Use `function` declarations for components, not arrow functions
- Props via destructured interface: `function Foo({ bar }: FooProps)`
- Colocate types with the component file
- Use shadcn primitives before building custom UI

### Styling
- Tailwind utility classes, never inline styles
- Use `cn()` from `@/lib/utils` for conditional classes
- Design tokens via CSS variables (defined in `globals.css`)
- All UI is dark mode — don't add light-mode-only styles

### AI / Streaming
- **Every AI response must be streamed** — no `ge
[truncated — 1943 more characters]
```

### package.json

```
{
  "name": "tree_temp",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint"
  },
  "dependencies": {
    "@ai-sdk/anthropic": "^3.0.44",
    "@ai-sdk/openai": "^3.0.29",
    "@brightdata/sdk": "^0.2.0",
    "@browserbasehq/stagehand": "^3.0.8",
    "@gsap/react": "^2.1.2",
    "@libsql/client": "^0.17.0",
    "@modelcontextprotocol/sdk": "^1.25.2",
    "@openrouter/ai-sdk-provider": "^2.2.3",
    "@react-three/fiber": "^9.5.0",
    "@react-three/postprocessing": "^3.0.4",
    "@types/qrcode": "^1.5.6",
    "ai": "^6.0.86",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "drizzle-orm": "^0.45.1",
    "framer-motion": "^12.34.0",
    "gsap": "^3.14.2",
    "lucide-react": "^0.564.0",
    "mcp-handler": "^1.0.7",
    "motion": "^12.34.0",
    "nanoid": "^5.1.6",
    "next": "16.1.6",
    "next-themes": "^0.4.6",
    "ogl": "^1.0.11",
    "postprocessing": "^6.38.2",
    "qrcode": "^1.5.4",
    "qrcode.react": "^4.2.0",
    "radix-ui": "^1.4.3",
    "react": "19.2.3",
    "react-dom": "19.2.3",
    "sonner": "^2.0.7",
    "tailwind-merge": "^3.4.0",
    "three": "^0.182.0",
    "zod": "^4.3.6"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "babel-plugin-react-compiler": "1.0.0",
    "drizzle-kit": "^0.31.9",
    "eslint": "^9",
    "eslint-config-next": "16.1.6",
    "shadcn": "^3.8.4",
    "tailwindcss": "^4",
    "tw-animate-css": "^1.4.0",
    "typescript": "^5"
  },
  "packageManager": "pnpm@10.29.3+sha512.498e1fb4cca5aa06c1dcf2611e6fafc50972ffe7189998c409e90de74566444298ffe43e6cd2acdc775ba1aa7cc5e092a8b7054c811ba8c5770f84693d33d2dc"
}

```

### src/app/layout.tsx

```typescript
import type { Metadata } from "next";
import { Inter, Instrument_Serif } from "next/font/google";
import "./globals.css";
import DitherNoSSR from "@/components/react-bits/Dither";
import { TooltipProvider } from "@/components/ui/tooltip";
import { Toaster } from "sonner";

const inter = Inter({
  variable: "--font-geist-sans",
  subsets: ["latin"],
  weight: ['400','600','700'],
  display: 'swap',
});

const instrument = Instrument_Serif({
  variable: "--font-geist-mono",
  subsets: ["latin"],
  weight: '400',
  display: 'swap',
});

export const metadata: Metadata = {
  title: "Flux",
  description: "Find everyone today",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className={`${inter.variable} ${instrument.variable} antialiased`}>
        <div aria-hidden className="pointer-events-none fixed inset-0 z-0" style={{ opacity: 0.06 }}>
          {/* client-only dither canvas, subtle opacity */}
          <DitherNoSSR disableAnimation={false} enableMouseInteraction={false} waveColor={[0.9, 0.9, 0.9]} colorNum={4} pixelSize={3} />
        </div>

        <TooltipProvider>
          <div className="relative z-[1] min-h-screen">
            {children}
          </div>
        </TooltipProvider>
        <Toaster theme="dark" position="bottom-center" />
      </body>
    </html>
  );
}

```

### src/components/react-bits/index.ts

```typescript
export { default as ClickSpark } from './ClickSpark';
export { default as Stepper } from './Stepper';
export { default as Grainient } from './Granient';

```

### src/app/agents/page.tsx

```typescript
import Link from "next/link"
import { ensureDB } from "@/lib/db"
import { listAgents } from "@/lib/db/queries"
import { AgentList } from "@/components/agents/agent-list"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { DevNav } from "@/components/dev-nav"

export default async function AgentsPage() {
  await ensureDB()
  const agents = await listAgents()

  return (
    <div className="min-h-screen">
      <DevNav />
      <div className="p-8 max-w-5xl mx-auto">
      <div className="flex items-center gap-3 mb-8">
        <h1 className="text-2xl font-semibold tracking-tight">Agents</h1>
        <Badge variant="secondary">{agents.length}</Badge>
        <div className="ml-auto">
          <Link href="/agents/new">
            <Button>Create Agent</Button>
          </Link>
        </div>
      </div>

      {agents.length === 0 ? (
        <div className="flex flex-col items-center justify-center py-24 text-center">
          <p className="text-muted-foreground mb-4">
            No agents yet. Create one to get started.
          </p>
          <Link href="/agents/new">
            <Button>Create Your First Agent</Button>
          </Link>
        </div>
      ) : (
        <AgentList initialAgents={agents} />
      )}
      </div>
    </div>
  )
}

```

### src/app/page.tsx

```typescript
"use client";

import { useState, useCallback, useRef } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Button } from "@/components/ui/button";
import { ChevronDownIcon, Loader2 } from 'lucide-react';
import { Skeleton } from "@/components/ui/skeleton";
import Dither from '@/components/react-bits/Dither';
import AtSign from '@/components/react-bits/AtSign';
import { ButtonGroup } from "@/components/ui/button-group";
import { toast } from "sonner";
import { FriendFocusLayout } from "@/components/friends/friend-focus-layout";
import { useBatchSimulations } from "@/hooks/use-batch-simulations";
import { SimulationRunner, type SimRunnerState } from "@/components/friends/simulation-runner";
import type { OrchestratorReport } from "@/lib/agents/types";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";

interface Friend {
  id: string;
  name: string;
  school: string;
  imageUrl?: string | null;
  linkedinUrl?: string | null;
}

interface AgentData {
  id: string;
  name: string;
  personality: string;
  backstory: string;
  imageUrl?: string | null;
}

type Emotion = 'happy' | 'excited' | 'in_love' | 'angry' | 'disappointed' | 'cautious';

interface FriendSimulation {
  simulationCount: number;
  currentEmotion: Emotion;
  sampleMessages: Array<{
    id: string;
    sender: 'user' | 'friend';
    text: string;
    emotion: Emotion;
  }>;
  isSimulating: boolean;
  turnCount: number;
  orchestratorMessage: string | null;
  energy: number;
  mood: string;
  orchestratorReport: OrchestratorReport | null;
}

export default function Home() {
  const [url, setUrl] = useState("");
  const [bio, setBio] = useState("");
  const [displayName, setDisplayName] = useState("");
  const [submitted, setSubmitted] = useState(false);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [simTurns, setSimTurns] = useState(5);

  const [userAgent, setUserAgent] = useState<AgentData | null>(null);
  const [friends, setFriends] = useState<Friend[]>([]);
  const [focusedFriendId, setFocusedFriendId] = useState<string | null>(null);

  // LinkedIn scrape state
  const [scrapeResult, setScrapeResult] = useState<{ person: { id: string; displayName: string; profilePictureUrl?: string | null }; agent: AgentData } | null>(null);
  const [scraping, setScraping] = useState(false);
  const lastScrapedSlug = useRef("");

  // X/Twitter state (non-blocking)
  const [xHandle, setXHandle] = useState("");

  // Batch simulations
  const batch = useBatchSimulations();

  // Track live state from each SimulationRunner
  const [simStates, setSimStates] = useState<Record<string, SimRunnerState>>({});
  const simStatesRef = useRef<Record<string, SimRunnerState>>({});

  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setUrl(e.target.value);
    // Reset scrape if user changes the slug
    if (scrapeResult) {
      setScrapeResult(null);
    }
  };

  const handleBioChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
    setBio(e.target.value);
  };

  // Extract clean LinkedIn slug from various input formats
  const extractSlug = (input: string): string | null => {
    const trimmed = input.trim();
    if (!trimmed) return null;
    // Handle full URLs: https://linkedin.com/in/username or linkedin.com/in/username
    const urlMatch = trimmed.match(/(?:https?:\/\/)?(?:www\.)?linkedin\.com\/in\/([^/?#]+)/i);
    if (urlMatch) return urlMatch[1].replace(/\/+$/, "");
    // Handle /in/username or in/username
    const pathMatch = trimmed.match(/^\/?in\/([^/?#]+)/i);
    if (pathMatch) return pathMatch[1].replace(/\/+$/, "");
    // Plain slug — allow alphanumeric, hyphens, tildes, dots, underscores
    const slug = trimmed.replace(/\/+$/, "").toLowerCase();
    if (slug.length < 3 || !/^[a-z0-9\-~._]+$/i.test(slug)) return null;
    return slug;
  };

  // Fire LinkedIn scrape on input blur
  const handleLinkedInBlur = useCallback(async () => {
    const slug = extractSlug(url);
    if (!slug || slug === lastScrapedSlug.current) return;

    lastScrapedSlug.current = slug;
    setScraping(true);
    setScrapeResult(null);
    setError(null);

    for (let attempt = 1; attempt <= 3; attempt++) {
      try {
        const res = await fetch("/api/onboard/scrape", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ slug }),
        });
        const data = await res.json();

        if (res.ok) {
          setScrapeResult(data);
          setScraping(false);
          if (data.existing) {
            toast.success(`Welcome back, ${data.person.displayName}`);
          } else {
            toast.success(`Profile loaded: ${data.person.displayName}`);
          }
          return;
        }
      } catch {
        // Network error — will retry
      }

      if (attempt < 3) {
        toast("Still fetching your LinkedIn profile...", { duration: 3000 });
      }
    }

    toast.error("Couldn't fetch your LinkedIn profile — check the URL");
    setScraping(false);
    lastScrapedSlug.current = ""; // Allow re-attempt
  }, [url]);
  const handleBioKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
    // Submit on Enter without Shift (Shift+Enter for new line)
    if (e.key === 'Enter' && !e.shiftKey) {
      e.preventDefault();
      const form = e.currentTarget.form;
      if (form) {
        form.requestSubmit();
      }
    }
  };

  const handleSimStateChange = useCallback((friendId: string, state: SimRunnerState) => {
    simStatesRef.current = { ...simStatesRef.current, [friendId]: state };
    setSimStates({ ...simStatesRef.current });
  }, []);

  const handleSubmit = 
[truncated — 15957 more characters]
```

### src/lib/db/index.ts

```typescript
import { drizzle } from "drizzle-orm/libsql"
import { createClient } from "@libsql/client"
import * as schema from "./schema"

const url = process.env.TURSO_DATABASE_URL ?? (
  process.env.VERCEL ? ":memory:" : "file:sqlite.db"
)

const client = createClient({
  url,
  authToken: process.env.TURSO_AUTH_TOKEN,
})

export const db = drizzle(client, { schema })

// Auto-create tables (needed for :memory: on Vercel)
const migrationSQL = `
CREATE TABLE IF NOT EXISTS agents (
  id text PRIMARY KEY NOT NULL,
  name text NOT NULL,
  personality text NOT NULL,
  backstory text NOT NULL,
  system_prompt text,
  gender text,
  attraction text,
  icon text,
  color text,
  image_url text,
  created_at integer NOT NULL
);
CREATE TABLE IF NOT EXISTS simulations (
  id text PRIMARY KEY NOT NULL,
  scenario text NOT NULL,
  status text NOT NULL,
  model_id text NOT NULL,
  max_turns integer,
  current_turn integer DEFAULT 0 NOT NULL,
  agent1_id text NOT NULL,
  agent2_id text NOT NULL,
  toolsets text,
  settings text,
  created_at integer NOT NULL
);
CREATE TABLE IF NOT EXISTS messages (
  id text PRIMARY KEY NOT NULL,
  simulation_id text NOT NULL,
  agent_id text NOT NULL,
  layer text NOT NULL,
  content text NOT NULL,
  turn_number integer NOT NULL,
  created_at integer NOT NULL
);
CREATE TABLE IF NOT EXISTS agent_states (
  id text PRIMARY KEY NOT NULL,
  simulation_id text NOT NULL,
  agent_id text NOT NULL,
  state text NOT NULL,
  turn_number integer NOT NULL,
  created_at integer NOT NULL
);
CREATE TABLE IF NOT EXISTS orchestrator_events (
  id text PRIMARY KEY NOT NULL,
  simulation_id text NOT NULL,
  type text NOT NULL,
  content text NOT NULL,
  metadata text,
  turn_number integer NOT NULL,
  created_at integer NOT NULL
);
CREATE TABLE IF NOT EXISTS people (
  id text PRIMARY KEY NOT NULL,
  email text,
  first_name text,
  last_name text,
  display_name text,
  phone text,
  other_contact text,
  github_username text,
  profile_picture_url text,
  location text,
  timezone text,
  level_of_study text,
  graduation_year text,
  skills text,
  track text,
  who_are_you text,
  idea text,
  ideal_partner text,
  ideal_team_size integer,
  favorite_tree text,
  coolest_thing text,
  code_comments text,
  song text,
  slack_id text,
  slack_title text,
  timber_id text,
  sources text,
  github_bio text,
  github_repos text,
  github_readme_snippet text,
  github_avatar_url text,
  github_followers integer,
  github_blog text,
  twitter_handle text,
  linkedin_url text,
  linkedin_headline text,
  linkedin_company text,
  linkedin_school text,
  linkedin_connections integer,
  agent_id text,
  created_at integer NOT NULL
);
CREATE TABLE IF NOT EXISTS timber_swipes (
  id text PRIMARY KEY NOT NULL,
  person_id text NOT NULL,
  target_person_id text NOT NULL,
  status text NOT NULL,
  swiped_at integer
);
CREATE TABLE IF NOT EXISTS timber_teams (
  id text PRIMARY KEY NOT NULL,
  person_id text NOT NULL,
  teammate_id text NOT NULL
);
CREATE TABLE IF NOT EXISTS global_config (
  id text PRIMARY KEY NOT NULL,
  model_id text NOT NULL,
  temperature text NOT NULL,
  max_tokens_thought integer NOT NULL,
  max_tokens_speech integer NOT NULL,
  turn_count integer NOT NULL,
  max_opponents integer NOT NULL,
  turn_delay integer NOT NULL,
  scenario_template text NOT NULL,
  updated_at integer NOT NULL
);
`

// Migrations for existing DBs — safe to re-run (uses IF NOT EXISTS / try-catch)
const alterSQL = [
  "ALTER TABLE simulations ADD COLUMN toolsets text",
  "ALTER TABLE simulations ADD COLUMN settings text",
  "ALTER TABLE simulations ADD COLUMN venue_id text",
  "ALTER TABLE agents ADD COLUMN gender text",
  "ALTER TABLE agents ADD COLUMN attraction text",
  "ALTER TABLE agents ADD COLUMN icon text",
  "ALTER TABLE agents ADD COLUMN color text",
  "ALTER TABLE simulations ADD COLUMN orchestrator_report text",
  "ALTER TABLE agents ADD COLUMN image_url text",
  "ALTER TABLE people ADD COLUMN github_bio text",
  "ALTER TABLE people ADD COLUMN github_repos text",
  "ALTER TABLE people ADD COLUMN github_readme_snippet text",
  "ALTER TABLE people ADD COLUMN github_avatar_url text",
  "ALTER TABLE people ADD COLUMN github_followers integer",
  "ALTER TABLE people ADD COLUMN github_blog text",
  "ALTER TABLE people ADD COLUMN twitter_handle text",
  "ALTER TABLE people ADD COLUMN linkedin_url text",
  "ALTER TABLE people ADD COLUMN linkedin_headline text",
  "ALTER TABLE people ADD COLUMN linkedin_company text",
  "ALTER TABLE people ADD COLUMN linkedin_school text",
  "ALTER TABLE people ADD COLUMN linkedin_connections integer",
]

let initialized = false

export async function ensureDB() {
  if (initialized) return
  // Only needed for :memory: or fresh file DBs
  for (const stmt of migrationSQL.split(";").map(s => s.trim()).filter(Boolean)) {
    await client.execute(stmt)
  }
  // Safe column additions for existing DBs
  for (const stmt of alterSQL) {
    try {
      await client.execute(stmt)
    } catch {
      // Column already exists — ignore
    }
  }
  initialized = true
}

```

### src/app/test/page.tsx

```typescript
"use client"

import { useState, useEffect } from "react"
import Link from "next/link"
import { cn } from "@/lib/utils"
import { DevNav } from "@/components/dev-nav"
import { Badge } from "@/components/ui/badge"

interface SimAgent {
  id: string
  name: string
  icon: string | null
  color: string | null
}

interface SimulationSummary {
  id: string
  scenario: string
  status: "running" | "paused" | "completed"
  modelId: string
  currentTurn: number
  durationMinutes: number
  agent1: SimAgent | null
  agent2: SimAgent | null
  messageCount: number
  compatibilityScore: number | null
  createdAt: string
}

function scoreColor(score: number): string {
  if (score >= 70) return "bg-green-500/20 text-green-400"
  if (score >= 40) return "bg-yellow-500/20 text-yellow-400"
  return "bg-red-500/20 text-red-400"
}

function statusBadge(status: string) {
  const colors: Record<string, string> = {
    running: "bg-green-900/50 text-green-400",
    paused: "bg-orange-900/50 text-orange-400",
    completed: "bg-blue-900/50 text-blue-400",
  }
  return colors[status] ?? "bg-muted text-muted-foreground"
}

function relativeTime(dateStr: string): string {
  const date = new Date(dateStr)
  const now = new Date()
  const diffMs = now.getTime() - date.getTime()
  const diffMin = Math.floor(diffMs / 60000)
  if (diffMin < 1) return "just now"
  if (diffMin < 60) return `${diffMin}m ago`
  const diffHr = Math.floor(diffMin / 60)
  if (diffHr < 24) return `${diffHr}h ago`
  const diffDay = Math.floor(diffHr / 24)
  return `${diffDay}d ago`
}

export default function HistoryPage() {
  const [simulations, setSimulations] = useState<SimulationSummary[]>([])
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    fetch("/api/simulations")
      .then((r) => r.json())
      .then((data) => setSimulations(data))
      .catch(() => {})
      .finally(() => setLoading(false))
  }, [])

  return (
    <div className="min-h-screen bg-background">
      <DevNav />
      <div className="max-w-4xl mx-auto p-6">
        <h1 className="text-lg font-bold text-foreground mb-4">Simulation History</h1>

        {loading && (
          <p className="text-sm text-muted-foreground">Loading...</p>
        )}

        {!loading && simulations.length === 0 && (
          <p className="text-sm text-muted-foreground">No simulations yet.</p>
        )}

        <div className="flex flex-col gap-3">
          {simulations.map((sim) => (
            <Link
              key={sim.id}
              href={`/simulation/${sim.id}`}
              className="block rounded-lg border border-border bg-card p-4 hover:bg-muted/50 transition-colors"
            >
              <div className="flex items-start justify-between gap-3">
                {/* Left: agent names + scenario */}
                <div className="flex-1 min-w-0">
                  <div className="flex items-center gap-2 mb-1">
                    {sim.agent1 && (
                      <span className="text-sm font-semibold text-blue-400 truncate">
                        {sim.agent1.icon ?? ""} {sim.agent1.name}
                      </span>
                    )}
                    <span className="text-xs text-muted-foreground">vs</span>
                    {sim.agent2 && (
                      <span className="text-sm font-semibold text-green-400 truncate">
                        {sim.agent2.icon ?? ""} {sim.agent2.name}
                      </span>
                    )}
                  </div>
                  <p className="text-xs text-muted-foreground truncate">
                    {sim.scenario.slice(0, 80)}{sim.scenario.length > 80 ? "..." : ""}
                  </p>
                </div>

                {/* Right: metadata */}
                <div className="flex items-center gap-2 shrink-0">
                  {/* Status */}
                  <Badge variant="outline" className={cn("text-[10px]", statusBadge(sim.status))}>
                    {sim.status}
                  </Badge>

                  {/* Turns */}
                  <span className="text-[10px] text-muted-foreground whitespace-nowrap">
                    {sim.currentTurn}/{sim.durationMinutes}
                  </span>

                  {/* Message count */}
                  <span className="text-[10px] text-muted-foreground">
                    {sim.messageCount} msg{sim.messageCount !== 1 ? "s" : ""}
                  </span>

                  {/* Compatibility score */}
                  {sim.compatibilityScore !== null && (
                    <span className={cn(
                      "text-[10px] font-bold px-1.5 py-0.5 rounded",
                      scoreColor(sim.compatibilityScore)
                    )}>
                      {sim.compatibilityScore}
                    </span>
                  )}

                  {/* Timestamp */}
                  <span className="text-[10px] text-muted-foreground/60">
                    {relativeTime(sim.createdAt)}
                  </span>
                </div>
              </div>
            </Link>
          ))}
        </div>
      </div>
    </div>
  )
}

```

### src/app/admin/page.tsx

```typescript
"use client"

import { useState, useEffect } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"

interface GlobalConfig {
  id: string
  modelId: string
  temperature: string
  maxTokensThought: number
  maxTokensSpeech: number
  turnCount: number
  maxOpponents: number
  turnDelay: number
  scenarioTemplate: string
}

export default function AdminPage() {
  const [config, setConfig] = useState<GlobalConfig | null>(null)
  const [saving, setSaving] = useState(false)
  const [status, setStatus] = useState<string | null>(null)

  useEffect(() => {
    fetch("/api/config")
      .then((res) => res.json())
      .then((data) => setConfig(data))
      .catch(() => setStatus("Failed to load config"))
  }, [])

  async function handleSave() {
    if (!config) return
    setSaving(true)
    setStatus(null)
    try {
      const res = await fetch("/api/config", {
        method: "PUT",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          modelId: config.modelId,
          temperature: config.temperature,
          maxTokensThought: config.maxTokensThought,
          maxTokensSpeech: config.maxTokensSpeech,
          turnCount: config.turnCount,
          maxOpponents: config.maxOpponents,
          turnDelay: config.turnDelay,
          scenarioTemplate: config.scenarioTemplate,
        }),
      })
      if (!res.ok) {
        const err = await res.json()
        setStatus(`Error: ${JSON.stringify(err.error)}`)
      } else {
        const updated = await res.json()
        setConfig(updated)
        setStatus("Saved")
      }
    } catch {
      setStatus("Failed to save")
    } finally {
      setSaving(false)
    }
  }

  if (!config) {
    return (
      <div className="flex min-h-screen items-center justify-center bg-[color:var(--background)]">
        <p className="text-muted-foreground">Loading config...</p>
      </div>
    )
  }

  return (
    <div className="flex min-h-screen flex-col items-center justify-center p-4 bg-[color:var(--background)]">
      <Card className="w-full max-w-xl border-border bg-card">
        <CardHeader>
          <CardTitle className="text-lg" style={{ fontFamily: "var(--font-geist-mono)" }}>
            Global Config
          </CardTitle>
        </CardHeader>
        <CardContent className="space-y-4">
          <div className="space-y-1.5">
            <Label htmlFor="modelId">Model ID</Label>
            <Input
              id="modelId"
              value={config.modelId}
              onChange={(e) => setConfig({ ...config, modelId: e.target.value })}
            />
          </div>

          <div className="grid grid-cols-2 gap-4">
            <div className="space-y-1.5">
              <Label htmlFor="temperature">Temperature</Label>
              <Input
                id="temperature"
                type="number"
                min={0}
                max={2}
                step={0.1}
                value={config.temperature}
                onChange={(e) => setConfig({ ...config, temperature: e.target.value })}
              />
            </div>
            <div className="space-y-1.5">
              <Label htmlFor="turnCount">Turn Count</Label>
              <Input
                id="turnCount"
                type="number"
                min={1}
                value={config.turnCount}
                onChange={(e) => setConfig({ ...config, turnCount: parseInt(e.target.value) || 1 })}
              />
            </div>
          </div>

          <div className="grid grid-cols-2 gap-4">
            <div className="space-y-1.5">
              <Label htmlFor="maxOpponents">Max Opponents</Label>
              <Input
                id="maxOpponents"
                type="number"
                min={1}
                value={config.maxOpponents}
                onChange={(e) => setConfig({ ...config, maxOpponents: parseInt(e.target.value) || 1 })}
              />
            </div>
            <div className="space-y-1.5">
              <Label htmlFor="turnDelay">Turn Delay (ms)</Label>
              <Input
                id="turnDelay"
                type="number"
                min={0}
                value={config.turnDelay}
                onChange={(e) => setConfig({ ...config, turnDelay: parseInt(e.target.value) || 0 })}
              />
            </div>
          </div>

          <div className="grid grid-cols-2 gap-4">
            <div className="space-y-1.5">
              <Label htmlFor="maxTokensThought">Max Tokens (Thought)</Label>
              <Input
                id="maxTokensThought"
                type="number"
                min={1}
                value={config.maxTokensThought}
                onChange={(e) => setConfig({ ...config, maxTokensThought: parseInt(e.target.value) || 100 })}
              />
            </div>
            <div className="space-y-1.5">
              <Label htmlFor="maxTokensSpeech">Max Tokens (Speech)</Label>
              <Input
                id="maxTokensSpeech"
                type="number"
                min={1}
                value={config.maxTokensSpeech}
                onChange={(e) => setConfig({ ...config, maxTokensSpeech: parseInt(e.target.value) || 100 })}
              />
            </div>
          </div>

          <div className="space-y-1.5">
            <Label htmlFor="scenarioTemplate">Scenario Template</Label>
            <Textarea
              id="scenarioTemplate"
              value={config.scenarioTemplate}
              onChange={(e) => setConfig({ ...config, scenarioTemplate: e.target.value })}
              placeholder="Use {{user}} for the user's name"
              rows={3}
            />
            <p className="text-xs text-muted-foreground">Use {"{{user}}"} as a placeho
[truncated — 489 more characters]
```

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