# Project export: Deliberate

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

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2026
- Tagline: Turns debates, meetings, and decisions into a visual map
- Devpost: https://devpost.com/software/deliberate-r7tboc
- GitHub: https://github.com/matthewcabrera/deliberate
- Demo: http://usedeliberate.com/
- Video: https://www.youtube.com/embed/GbZqKvT8SeM?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — matthewcabrera (4 commits)

## Devpost submission (written by the team)

### Inspiration

I was on my high school's debate team for three years, and during that time I fell in love with debate. I love the complexity that debate can bring, but debate can also be hard to follow, both for non-debaters watching from the outside and for experienced debaters in high-level rounds. I wanted to build a tool that could quickly break down a debate into a visual format so people could understand it more clearly after it happened. The content and flow of a debate are often forgotten if no one writes them down in the moment, but with a recording, Deliberate can turn that debate into a fully fledged flow map.

### What it does

Deliberate turns a debate into a map you can actually read. Upload an MP4 or audio file, point it at a YouTube video, browse to an article or PDF, or just talk, and Deliberate converts the argument into an Issue-Based Information System map. The map shows the central question, the competing positions, and the pros, cons, and evidence connected to each one. Every node is grounded in its source, whether that is a transcript quote, a timestamp, or a captured page, so you can trace any claim back to who said it and where it came from. Deliberate supports four modes: Upload mode turns an MP4 or audio file into a diarized, timestamped transcript and then a map. Browse mode turns a URL or PDF into a captured source with a screenshot and replay trail, then a map. Observe mode listens to a live debate through your mic and grows the map in real time. Argue mode lets you debate a spoken AI opponent that pushes back out loud, and the whole exchange becomes a map. The result is a living argument map that makes the shape of a disagreement legible at a glance. How I built it We built Deliberate with Next.js and React Flow for the app and editable 2D canvas, including pan, zoom, drag-to-connect, and IBIS node grammar. Deepgram powers the voice layer. We use Nova-3 for diarized, timestamped transcription across prerecorded and live audio, and Aura TTS for the AI opponent's spoken rebuttals. Anthropic Claude powers the reasoning layer. Claude Haiku extracts the IBIS graph through structured outputs, and Claude Sonnet acts as a judge that scores each map. A deterministic validation-and-repair layer enforces the IBIS grammar so every map is structurally correct by construction. Browserbase handles source capture. A recorded cloud browser pulls page text and PDF bytes, along with a screenshot and replayable session, so the map has an evidence and trust trail. Arize gives us observability. OpenTelemetry traces every pipeline run, including tokens, cost, latency, node counts, and evaluator results. An online evaluator automatically grades each map for IBIS correctness. Challenges I ran into One of the hardest parts was making an LLM obey strict IBIS structure. Models often want to attach a con directly to the central question instead of attaching it to a position. We built a deterministic repair pass that re-links every pro and con to its correct parent, giving us strict IBIS by construction instead of hoping the model gets it right. Another challenge was making the product feel useful instead of generic. Early versions looked and felt like a standard AI demo, but I had a specific vision for a clean, visual workspace that felt more like a debate flow tool than a chatbot. It took a lot of iteration to get the interface to that state. We also had to account for messy real-world inputs: unclear audio, multiple speakers, imperfect transcripts, long videos, PDFs, web pages, and live conversations. The app needed fallbacks, validation, and visible evidence links so users could trust but verify the generated map. Accomplishments that I'm proud of Built a clean UI and UX experience around visual debate mapping. Shipped four working input modes: upload, browse, observe, and argue. Created a live spoken debate mode where an AI opponent talks back out loud. Grounded every node in evidence, from claim to quote to timestamp or source. Added full observability so every run is traced and automatically evaluated. Enforced valid IBIS maps by construction rather than best effort. Shipped the project to production at usedeliberate.com with multiple sponsor technologies integrated. What I learned I learned that building a large project with AI still requires a lot of taste, direction, and iteration. The first versions had a very generic AI-generated feel, but I had a clear vision for how the app should look and feel, so I kept steering it toward that. I also learned that transcription alone is not the product. The valuable part is turning speech into something people can inspect, edit, and reason about. A debate map is only useful if every generated claim stays connected to the original evidence. Finally, I learned that IBIS is a strong format for AI-assisted debate mapping because it is structured enough to validate but lightweight enough for people to understand and edit.

### What's next

Next, I want to turn Deliberate from a single-session demo into a real workspace. The biggest step is adding an account system with Supabase so users can sign in, keep a library of their maps, and return to debates they have already analyzed. I also want people to be able to save and share maps with others. A user should be able to generate a map from a debate, clean it up, and send a link to a teammate, classmate, or community so everyone can inspect the same claims, evidence, timestamps, and objections. Longer term, shared maps could support permissions, comments, collaborative editing, and public read-only views for making complex disagreements easier to understand together.

## README (from the GitHub repository)

# Deliberate

Deliberate turns debates into editable, evidence-linked argument maps.

It takes spoken debate, uploaded media, pasted transcripts, or captured web sources and converts them into an Issue-Based Information System (IBIS) map. The goal is not to declare a winner. The goal is to make the shape of a disagreement clear: the central question, the competing positions, the reasons for and against each position, and the evidence behind each claim.

**Live app:** https://usedeliberate.com  
**Repository:** https://github.com/matthewcabrera/deliberate

<p>
  <img src="public/brand/deliberate-mark-paper.png" alt="Deliberate mark" width="180" />
</p>

## Why It Exists

Debates move quickly. Even experienced debaters can lose track of how each argument relates to the others, and non-debaters often hear a fast exchange without seeing the underlying structure.

Deliberate was built to preserve that structure. With a recording, transcript, or source URL, it creates a visual flow map that can be inspected after the debate is over. Each generated claim stays connected to its source evidence, so the map is auditable instead of being an opaque AI summary.

## What It Does

Deliberate converts messy argument sources into a clean IBIS map:

- **Issues:** central questions or decision points
- **Positions:** possible answers or proposals
- **Pros:** reasons supporting a position
- **Cons:** objections or risks against a position
- **Evidence:** source-backed references, quotes, timestamps, or page captures
- **Decisions and notes:** useful context or outcomes when present

Every node is grounded in the original source where possible:

- speaker
- transcript quote
- timestamp
- source URL or captured page
- confidence and review state

## Input Modes

### Upload

Upload an audio or video file. Deliberate sends the media to Deepgram for diarized, timestamped transcription, then converts the transcript into an editable IBIS map.

### Browse

Paste a URL or PDF. Deliberate uses Browserbase to capture the source in a recorded cloud browser session, extracts readable text, stores a screenshot/replay trail, and maps the argument.

### Observe

Use the microphone to observe a live debate. Deliberate listens through the mic and grows the argument map as the debate unfolds.

### Argue

Debate a spoken AI opponent. The AI pushes back out loud, and the full exchange becomes a map you can inspect afterward.

### Transcript Paste

Paste a written transcript directly. This is the fastest path for testing the IBIS extraction pipeline without media ingestion.

## How It Works

```text
source input
  -> transcript or text capture
  -> normalized utterances
  -> Claude IBIS extraction
  -> deterministic validation and repair
  -> editable React Flow canvas
  -> evidence-linked transcript/source inspection
  -> optional Arize tracing and evaluation
```

The map is not just a drawing. It is a structured graph with typed nodes and typed edges. A repair layer enforces core IBIS rules, such as attaching pros and cons to positions rather than directly to the central issue.

## Tech Stack

| Area | Technology |
| --- | --- |
| App framework | Next.js, React, TypeScript |
| Canvas | React Flow / `@xyflow/react` |
| Voice transcription | Deepgram Nova |
| Text-to-speech | Deepgram Aura |
| Reasoning and extraction | Anthropic Claude |
| Web source capture | Browserbase, Playwright Core |
| PDF text extraction | `pdf-parse` |
| Observability | OpenTelemetry, Arize |
| Deployment | Vercel |

## Architecture

```text
src/app/page.tsx
  Main client app, menu flow, upload/browse/observe/argue screens

src/components/Workspace.tsx
  Editable IBIS canvas, transcript dock, source/video panel, node editing

src/components/Observe.tsx
  Live microphone observation mode

src/components/Argue.tsx
  Spoken debate mode against an AI opponent

src/app/api/transcribe/route.ts
  Upload and YouTube transcription endpoint

src/app/api/extract-ibis/route.ts
  Transcript to IBIS graph endpoint

src/app/api/ingest-url/route.ts
  Browserbase URL/PDF capture endpoint

src/app/api/tts/route.ts
  Text-to-speech endpoint for spoken replies

src/lib/ibis-extract.ts
  Claude prompt, schema, graph extraction, validation, and repair

src/lib/ibis-eval.ts
  Deterministic and judge-based map quality evaluation

src/lib/deepgram.ts
  Deepgram transcription normalization

src/lib/browserbase.ts
  Source capture, PDF parsing, screenshot/replay metadata

src/lib/contracts.ts
  Shared source, transcript, speaker, and IBIS graph contracts

src/lib/trace.ts
  OpenTelemetry span helpers and Arize-compatible metadata
```

## Environment Variables

Copy `.env.example` to `.env.local` for local development:

```bash
cp .env.example .env.local
```

Required for core AI functionality:

| Variable | Purpose |
| --- | --- |
| `DEEPGRAM_API_KEY` | Speech transcription and TTS |
| `ANTHROPIC_API_KEY` | IBIS graph extraction and argue mode |

Optional:

| Variable | Purpose |
| --- | --- |
| `ANTHROPIC_MODEL` | Override the extraction model |
| `ANTHROPIC_ARGUE_MODEL` | Override the spoken opponent model |
| `DEEPGRAM_TTS_MODEL` | Override the Deepgram voice |
| `ARIZE_API_KEY` | Enable tracing export to Arize |
| `ARIZE_SPACE_ID` | Arize space for tracing |
| `ARIZE_PROJECT_NAME` | Arize project name |
| `ARIZE_COLLECTOR_ENDPOINT` | OTLP endpoint, defaults to Arize |
| `BROWSERBASE_API_KEY` | Enable browse mode source capture |
| `BROWSERBASE_PROJECT_ID` | Browserbase project for sessions |
| `BROWSERBASE_PROXIES` | Optional paid proxy support for blocked sites |

## Local Development

Install dependencies:

```bash
npm install
```

Start the dev server:

```bash
npm run dev
```

Open:

```text
http://localhost:3000
```

Build for production:

```bash
npm run build
```

Run lint:

```bash
npm run lint
```

Run the IBIS evaluation script:

```bash
npm run eval
```

## Deployment

The project is deployed on Vercel:

```text
https://usedeliberate.com
```

Production needs the same environment variables configured in the Vercel project settings. Without the API keys, the static UI can load, but transcription, extraction, browse capture, TTS, and tracing features will not fully work.

## Current Status

Implemented:

- production Next.js app
- branded Deliberate interface
- upload flow for media files
- pasted transcript flow
- URL/PDF browse capture through Browserbase
- Deepgram transcription
- Claude IBIS extraction
- deterministic graph validation and repair
- editable React Flow workspace
- source evidence display
- live observe mode
- spoken argue mode
- Arize/OpenTelemetry tracing hooks
- Vercel deployment and custom domain

Known limits:

- YouTube ingestion can be unreliable on cloud hosts because YouTube may block serverless/cloud requests.
- Account persistence is not implemented yet.
- Maps are currently session-oriented rather than stored in a user library.
- Shared map links and collaboration are planned but not implemented yet.

## Roadmap

Next, Deliberate should become a real workspace rather than a single-session demo:

- add Supabase authentication
- save maps to user accounts
- build a personal map library
- support shareable map links
- add public read-only map views
- add permissions for private and team maps
- support comments and review workflows
- add collaborative editing
- improve speaker identity inference
- deepen Arize evaluation loops
- make live observation more robust for longer debates

## Project Story

Deliberate started from a debate problem: arguments are often rich and complex, but the structure disappears as soon as the round ends. A traditional debate flow sheet solves this for trained debaters, but it is manual and hard for outsiders to read.

This project uses AI to create that flow automatically. The hard part is not just summarizing the debate. The hard part is preserving structure while keeping every generated claim traceable to evidence.

## License

No open-source license has been added yet. 

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 53 recognized source files, 289 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- HTML (language) — detected in the code
- Next.js (technology) — detected in the code
- React (technology) — detected in the code
- SQL (language) — detected in the code
- Supabase (technology) — detected in the code
- TypeScript (language) — detected in the code
- AI coding agent: Claude Code — evidence: commit authorship or trailers

## Codebase structure (from repository index)

### Files (62 of 62)

```
.env.example
.gitignore
.vercelignore
AGENT.md
CLAUDE_ATTENTION_BEFORE_STAGE_5.md
eslint.config.mjs
gtm/copy-pack.md
gtm/playbook.html
gtm/pricing-and-offer.md
gtm/target-list.md
HUMAN_SETUP_CHECKLIST.md
LOCAL_SETUP_VALUES.template.md
next-env.d.ts
next.config.ts
package.json
PROGRESS.md
public/brand/README.md
README.md
research/arize-phoenix-plan.md
research/browserbase-plan.md
research/deepgram-integration.md
research/ibis-notation.md
research/miro-workspace-ui.md
research/README.md
research/ui-ux-plan.md
scripts/eval-ibis.mjs
scripts/otel-probe.mjs
src/app/api/argue/route.ts
src/app/api/extract-ibis/route.ts
src/app/api/transcribe-chunk/route.ts
src/app/api/transcribe/route.ts
src/app/api/tts/route.ts
src/app/auth/callback/route.ts
src/app/globals.css
src/app/layout.tsx
src/app/page.tsx
src/components/Argue.tsx
src/components/AuthPanel.tsx
src/components/Observe.tsx
src/components/VideoPanel.tsx
src/components/Workspace.tsx
src/lib/contracts.ts
src/lib/deepgram.ts
src/lib/demo-data.ts
src/lib/demo-map.json
src/lib/ibis-eval.ts
src/lib/ibis-extract.ts
src/lib/job-storage.ts
src/lib/layout.ts
src/lib/live-transcript.ts
src/lib/rounds.ts
src/lib/supabase/client.ts
src/lib/supabase/middleware.ts
src/lib/supabase/server.ts
src/lib/trace.ts
src/lib/use-mic.ts
src/lib/use-user.ts
src/lib/youtube-audio.ts
src/proxy.ts
supabase/collab-sharing.sql
supabase/schema.sql
tsconfig.json
```

### Dependencies

- package.json: @anthropic-ai/sdk@^0.105.0, @supabase/ssr@^0.12.0, @supabase/supabase-js@^2.108.2, @types/node@latest, @types/react@latest, @types/react-dom@latest, @xyflow/react@^12.11.0, eslint@latest, eslint-config-next@latest, lucide-react@latest, next@latest, react@latest, react-dom@latest, typescript@latest

### Recent commits (newest first)

- Add auto-organize button to tidy the map
- Harden auth callback: open-redirect guard + OAuth error surfacing
- Add go-to-market sales assets
- Keyboard-first argument mapping + workspace cleanup
- Stage 8 accounts + autosave, sharing, build mode, multi-speech upload, fixes
- Real "see an example" map; new default YouTube URL
- Add back arrow to top-left crumb; deliberate is now a label
- Remove Browserbase + Arize; add PROGRESS.md
- Serverless transcription + browse-by-topic
- Add complete GitHub README
- Use branded favicon
- Fix Vercel function packaging
- Initial deliberate app

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

### LOCAL_SETUP_VALUES.template.md

```markdown
# Local Setup Values

Copy this file to `LOCAL_SETUP_VALUES.md` and paste your real values there.

Do not commit the filled-in file. It may contain API keys and account details.

Recommended command:

```bash
cp LOCAL_SETUP_VALUES.template.md LOCAL_SETUP_VALUES.md
```

Then fill in `LOCAL_SETUP_VALUES.md`.

## Status

Mark each line when ready:

- [ x ] Deepgram ready
- [ ] Anthropic ready
- [ ] Arize ready
- [ ] Browserbase ready
- [ ] Local media tools ready
- [ ] Demo media ready

## Environment Variables

Paste the exact values below. Leave unknown values blank.

```bash
DEEPGRAM_API_KEY=
ANTHROPIC_API_KEY=
ARIZE_API_KEY=
ARIZE_COLLECTOR_ENDPOINT=
BROWSERBASE_API_KEY=
BROWSERBASE_PROJECT_ID=
```

## Deepgram

```text
Account email: matthewcabrera2700@gmail.com
Project name: calhacks
API key created: yes
Billing or credits active: $200 free credits for organizer
Notes:
```

## Anthropic

```text
Account email:
API key created: yes/no
Billing or credits active: yes/no/unknown
Preferred model, if known:
Notes:
```

## Arize / Phoenix

```text
Account email:
Project name:
Phoenix API key created: yes/no
Collector endpoint:
Dashboard URL:
Notes:
```

## Browserbase

```text
Account email:
Project name:
API key created: yes/no
Project ID:
Dashboard URL:
Notes:
```

## Local Tools

Paste command outputs here.

```bash
node -v

npm -v

ffmpeg -version

yt-dlp --version
```

## Demo Video

Primary demo URL:

```text
https://www.youtube.com/watch?v=lu0ic3L3Z3k
```

Permission status:

```text
Allowed to process for hackathon demo: yes/no/unknown
Reason or source of permission:
```

Backup media:

```text
Backup MP4/audio file path:
Backup transcript file path:
Notes:
```

## Prize Proof Links and Screenshots

Fill these in as they are created.

```text
Deepgram screenshot path:
Deepgram demo clip path:

Arize trace screenshot path:
Arize evaluator screenshot path:
Arize before/after note path:

Browserbase source capture screenshot path:
Browserbase replay/live-view screenshot path:
```

## Extra Notes For Claude

Paste anything Claude should know before integrating:

```text

```


```

### HUMAN_SETUP_CHECKLIST.md

```markdown
# Human Setup Checklist

This is the work an agent usually cannot complete for you because it needs account access, billing approval, API keys, local permissions, or judgment about what media you are allowed to process.

## 1. Required Accounts and API Keys

Create accounts and generate API keys for:

- [ ] **Deepgram**
  - Needed for transcription, diarization, timestamps, confidence, and utterances.
  - Create a project/API key.
  - Confirm the key has access to prerecorded transcription.

- [ ] **Anthropic**
  - Needed for transcript-to-IBIS extraction and graph repair.
  - Create an API key.
  - Confirm billing/credits are active.

- [ ] **Arize Phoenix / Arize**
  - Needed for tracing, evaluator runs, and prize documentation.
  - Create an account/project.
  - Get the Phoenix API key.
  - Get the collector endpoint.

- [ ] **Browserbase**
  - Needed for source-page capture, screenshots, session metadata, and prize story.
  - Create a project.
  - Get the Browserbase API key.
  - Get the project ID if required by the SDK.

## 2. Environment File

Create `.env.local` in the project root with real values:

```bash
DEEPGRAM_API_KEY=
ANTHROPIC_API_KEY=
PHOENIX_API_KEY=
PHOENIX_COLLECTOR_ENDPOINT=
BROWSERBASE_API_KEY=
BROWSERBASE_PROJECT_ID=
```

Keep this file private. Do not commit it.

## 3. Local Machine Setup

Confirm these are installed:

- [ ] Node.js works:

```bash
node -v
npm -v
```

- [ ] ffmpeg is installed:

```bash
ffmpeg -version
```

- [ ] yt-dlp is installed if using YouTube demo audio:

```bash
yt-dlp --version
```

Install options on macOS:

```bash
brew install ffmpeg yt-dlp
```

## 4. Demo Media Readiness

For the curated YouTube demo:

- [ ] Confirm you are allowed to process the video/audio for a hackathon demo.
- [ ] Keep the URL handy:

```text
https://www.youtube.com/watch?v=lu0ic3L3Z3k
```

- [ ] Download or prepare a backup MP4/audio file in case YouTube extraction fails.
- [ ] Prepare a backup pasted transcript in case both YouTube and upload fail.
- [ ] Keep the demo video short, ideally 2-5 minutes.

## 5. Deepgram Prize Prep

Prepare to explain why voice is fundamental:

- [ ] Every IBIS node links to transcript utterance IDs.
- [ ] Every important node has speaker, quote, timestamp, and confidence.
- [ ] Clicking a node should replay or seek to the relevant audio span.
- [ ] Low-confidence transcript spans become review warnings.

Collect proof during implementation:

- [ ] Screenshot of Deepgram transcript response or normalized transcript.
- [ ] Screenshot of map node linked to timestamped evidence.
- [ ] Short clip showing node click -> audio evidence.

## 6. Arize Prize Prep

Prepare the Arize story:

- [ ] Create an Arize/Phoenix project named something like `ibis-video-map`.
- [ ] Confirm traces appear in the dashboard.
- [ ] Create or document one LLM judge/evaluator for IBIS quality.
- [ ] Save one before/after example showing prompt/schema improvement.

Screenshots to collect:

- [ ] Full pipeline trace.
-
[truncated — 1381 more characters]
```

### package.json

```
{
  "name": "deliberate",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint",
    "eval": "node scripts/eval-ibis.mjs"
  },
  "dependencies": {
    "@anthropic-ai/sdk": "^0.105.0",
    "@supabase/ssr": "^0.12.0",
    "@supabase/supabase-js": "^2.108.2",
    "@xyflow/react": "^12.11.0",
    "lucide-react": "latest",
    "next": "latest",
    "react": "latest",
    "react-dom": "latest"
  },
  "devDependencies": {
    "@types/node": "latest",
    "@types/react": "latest",
    "@types/react-dom": "latest",
    "eslint": "latest",
    "eslint-config-next": "latest",
    "typescript": "latest"
  }
}

```

### src/app/layout.tsx

```typescript
import type { Metadata } from "next";
import type { ReactNode } from "react";
import "./globals.css";

export const metadata: Metadata = {
  title: "deliberate",
  description: "Turn debate into an editable, evidence-linked map.",
  icons: {
    icon: [{ url: "/brand/deliberate-mark-paper.png", type: "image/png" }],
    apple: [{ url: "/brand/deliberate-mark-paper.png", type: "image/png" }],
  },
};

export default function RootLayout({
  children,
}: Readonly<{
  children: ReactNode;
}>) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body suppressHydrationWarning>
        {/* Paper fiber grain over the whole surface */}
        <svg className="paper-texture" aria-hidden="true" preserveAspectRatio="none">
          <filter id="paperGrain">
            <feTurbulence type="fractalNoise" baseFrequency="0.62" numOctaves="3" stitchTiles="stitch" />
            <feColorMatrix
              type="matrix"
              values="0 0 0 0 0.28  0 0 0 0 0.24  0 0 0 0 0.19  0 0 0 0.18 0"
            />
          </filter>
          <rect width="100%" height="100%" filter="url(#paperGrain)" />
        </svg>
        {children}
      </body>
    </html>
  );
}

```

### src/lib/supabase/server.ts

```typescript
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";

// Server-side Supabase client bound to the request's cookies (App Router).
// Use in route handlers / server components to read the signed-in coach.
export async function createClient() {
  const cookieStore = await cookies();
  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll();
        },
        setAll(cookiesToSet) {
          try {
            cookiesToSet.forEach(({ name, value, options }) => cookieStore.set(name, value, options));
          } catch {
            // called from a Server Component without a response to mutate; safe to ignore
          }
        },
      },
    },
  );
}

export const supabaseEnabled = Boolean(
  process.env.NEXT_PUBLIC_SUPABASE_URL && process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
);

```

### src/app/page.tsx

```typescript
"use client";

import { useCallback, useEffect, useRef, useState } from "react";
import { ArrowLeft } from "lucide-react";
import Workspace from "@/components/Workspace";
import Observe from "@/components/Observe";
import Argue from "@/components/Argue";
import AuthPanel from "@/components/AuthPanel";
import type { MediaSource } from "@/components/VideoPanel";
import { demoGraph, demoTranscript, demoYoutubeUrl } from "@/lib/demo-data";
import type { IbisGraph, NormalizedTranscript, SpeakerProfile } from "@/lib/contracts";
import { createClient, supabaseEnabled } from "@/lib/supabase/client";
import { useUser } from "@/lib/use-user";
import {
  deleteRound,
  getOrCreateShareLink,
  listRounds,
  loadRound,
  redeemShare,
  renameRound,
  saveRound,
  updateRound,
  type RoundSummary,
} from "@/lib/rounds";
import deliberateLogo from "../../deliberate logo no bg.png";

function youtubeId(url: string): string | null {
  const m = url.match(/(?:youtu\.be\/|youtube\.com\/(?:watch\?v=|embed\/|shorts\/|v\/))([\w-]{11})/);
  return m ? m[1] : null;
}

type Screen = "menu" | "upload" | "processing" | "workspace" | "observe" | "argue" | "library";
type UploadMode = "youtube" | "media" | "transcript";

// YouTube ingestion now works on serverless via the RapidAPI audio path
// (with local yt-dlp as a dev fallback), so it's enabled everywhere.
const YOUTUBE_ENABLED = true;

// Prefilled in the YouTube field. ("see an example" stays on the LBC debate via demoYoutubeUrl.)
const DEFAULT_YOUTUBE_URL = "https://youtu.be/5rysVKetHYU?si=e7nY1X09muRGlmxu";

type Phase = "transcribing" | "mapping" | "done" | "error";

// observe / argue exist but aren't advertised yet — menu shows upload + build.
const menuItems: { key: Screen; word: string }[] = [{ key: "upload", word: "upload" }];


function InkMark() {
  return (
    <div className="ink-mark" aria-hidden="true">
      <svg width="100%" height="100%" viewBox="0 0 1000 1000" preserveAspectRatio="xMidYMid slice">
        <g fill="none" stroke="rgba(27,23,19,0.06)" strokeWidth="2">
          <path d="M120 720 C 300 600, 360 760, 520 660 S 760 560, 900 660" />
          <path d="M90 780 C 280 690, 380 820, 560 720 S 800 650, 940 720" />
          <path d="M160 660 C 320 560, 420 700, 600 600" strokeWidth="1.4" />
        </g>
        <g fill="rgba(27,23,19,0.05)">
          <circle cx="250" cy="250" r="2.2" />
          <circle cx="780" cy="200" r="1.8" />
          <circle cx="680" cy="820" r="2.4" />
        </g>
      </svg>
    </div>
  );
}

// Parse a pasted transcript ("Name: line") into a minimal normalized transcript
// so the IBIS extractor can run without audio.
function transcriptFromText(text: string): NormalizedTranscript {
  const lines = text
    .split(/\r?\n+/)
    .map((l) => l.trim())
    .filter(Boolean);

  const labelToId = new Map<string, string>();
  let cursor = 0;
  const utterances = lines.map((line, index) => {
    const match = line.match(/^([^:]{1,40}):\s*(.+)$/);
    const label = match ? match[1].trim() : "Speaker 1";
    const body = match ? match[2].trim() : line;
    if (!labelToId.has(label)) labelToId.set(label, `speaker_${labelToId.size}`);
    const start = cursor;
    cursor += Math.max(3, Math.round(body.split(/\s+/).length / 2.5));
    return {
      id: `u${index + 1}`,
      speakerId: labelToId.get(label)!,
      speakerLabel: label,
      startSec: start,
      endSec: cursor,
      confidence: 1,
      text: body,
    };
  });

  const speakers: SpeakerProfile[] = [...labelToId.entries()].map(([label, id]) => ({
    id,
    label,
    inferredName: /^speaker/i.test(label) ? undefined : label,
    confidence: /^speaker/i.test(label) ? "low" : "high",
  }));

  return {
    type: "normalized_transcript",
    version: 1,
    source: { id: "src_paste", inputUrl: "pasted-transcript", sourceType: "transcript", title: "Pasted transcript" },
    durationSec: cursor,
    speakers,
    utterances,
    fullText: lines.join("\n"),
  };
}

// Stitch per-speech transcripts into one round: offset each speech's timestamps
// by the running total, and label every speech as its own speaker.
function combineTranscripts(parts: NormalizedTranscript[], names: string[]): NormalizedTranscript {
  let offset = 0;
  let idx = 1;
  const utterances: NormalizedTranscript["utterances"] = [];
  const speakers: SpeakerProfile[] = [];
  parts.forEach((t, i) => {
    const sid = `speech_${i}`;
    const label = (names[i] || `Speech ${i + 1}`).replace(/\.[^.]+$/, "");
    speakers.push({ id: sid, label, inferredName: label, confidence: "high" });
    for (const u of t.utterances) {
      utterances.push({
        ...u,
        id: `u${idx++}`,
        speakerId: sid,
        speakerLabel: label,
        startSec: u.startSec + offset,
        endSec: (u.endSec ?? u.startSec) + offset,
      });
    }
    offset += t.durationSec || (t.utterances.at(-1)?.endSec ?? 0);
  });

  return {
    type: "normalized_transcript",
    version: 1,
    source: {
      id: "src_round",
      inputUrl: "upload",
      sourceType: "upload",
      title: `Debate round (${parts.length} speeches)`,
      fetchedAt: new Date().toISOString(),
    },
    durationSec: offset,
    speakers,
    utterances,
    fullText: utterances.map((u) => u.text).join("\n"),
  };
}

function relativeTime(iso: string): string {
  const ms = Date.now() - new Date(iso).getTime();
  const m = Math.floor(ms / 60000);
  if (m < 1) return "just now";
  if (m < 60) return `${m}m ago`;
  const h = Math.floor(m / 60);
  if (h < 24) return `${h}h ago`;
  const d = Math.floor(h / 24);
  if (d < 7) return `${d}d ago`;
  return new Date(iso).toLocaleDateString();
}

// A blank map for prep — one issue node to start; build the rest with the tool rail.
function blankGraph(): IbisGraph {
  return {
    type: "ibis_graph",
    version: 1,
    title: "Prep map",
    sourceId: "src_blank",
    nodes: [
      {
        id: "n1",
        type: "issue",
        label: "",
        transcriptSpanIds: [],
     
[truncated — 23663 more characters]
```

### src/app/api/tts/route.ts

```typescript
import { NextResponse, type NextRequest } from "next/server";

export const runtime = "nodejs";

// Deepgram Aura — the opponent's spoken voice in argue mode.
const MODEL = process.env.DEEPGRAM_TTS_MODEL || "aura-2-thalia-en";

export async function POST(req: NextRequest) {
  const key = process.env.DEEPGRAM_API_KEY;
  if (!key) return NextResponse.json({ error: "DEEPGRAM_API_KEY not set." }, { status: 500 });

  const { text } = (await req.json()) as { text?: string };
  if (!text?.trim()) return NextResponse.json({ error: "No text to speak." }, { status: 400 });

  const dg = await fetch(`https://api.deepgram.com/v1/speak?model=${MODEL}`, {
    method: "POST",
    headers: { Authorization: `Token ${key}`, "Content-Type": "application/json" },
    body: JSON.stringify({ text: text.slice(0, 1800) }),
  });

  if (!dg.ok) {
    const detail = await dg.text();
    return NextResponse.json({ error: `Aura TTS error ${dg.status}: ${detail.slice(0, 200)}` }, { status: 502 });
  }

  const audio = Buffer.from(await dg.arrayBuffer());
  return new NextResponse(audio as unknown as BodyInit, {
    headers: { "Content-Type": "audio/mpeg", "Cache-Control": "no-store" },
  });
}

```

### src/app/api/transcribe-chunk/route.ts

```typescript
import { NextResponse, type NextRequest } from "next/server";
import { transcribeWithDeepgram } from "@/lib/deepgram";
import { flushTraces, traced } from "@/lib/trace";

export const runtime = "nodejs";
export const maxDuration = 60;

// Live modes (observe/argue) stream short audio segments here. The Deepgram key
// stays server-side; we return just the recognized text for that segment.
export async function POST(req: NextRequest) {
  try {
    const buf = Buffer.from(await req.arrayBuffer());
    if (!buf.length) return NextResponse.json({ error: "Empty audio." }, { status: 400 });
    const contentType = req.headers.get("content-type") || "audio/webm";

    const text = await traced("deepgram.live_chunk", { "audio.bytes": buf.length }, async (span) => {
      const dg = await transcribeWithDeepgram(new Uint8Array(buf), contentType);
      const recognized = (
        dg.results?.utterances?.map((u) => u.transcript).join(" ") ||
        dg.results?.channels?.[0]?.alternatives?.[0]?.transcript ||
        ""
      ).trim();
      span.setAttributes({ "transcript.chars": recognized.length });
      return recognized;
    });

    return NextResponse.json({ text });
  } catch (err) {
    return NextResponse.json(
      { error: err instanceof Error ? err.message : "Transcription failed." },
      { status: 500 },
    );
  } finally {
    await flushTraces();
  }
}

```

### src/app/auth/callback/route.ts

```typescript
import { NextResponse, type NextRequest } from "next/server";
import { createClient } from "@/lib/supabase/server";

export const runtime = "nodejs";

// Magic-link / OAuth land here; exchange the code for a session, then redirect.
export async function GET(req: NextRequest) {
  const { searchParams, origin } = new URL(req.url);
  const code = searchParams.get("code");

  // Only allow same-origin relative redirects (guard against open redirects).
  const nextParam = searchParams.get("next") ?? "/";
  const next = nextParam.startsWith("/") && !nextParam.startsWith("//") ? nextParam : "/";

  // Google / Supabase can bounce back with an error instead of a code
  // (user denied consent, provider misconfigured, expired link, …).
  const oauthError = searchParams.get("error_description") ?? searchParams.get("error");
  if (oauthError) {
    return NextResponse.redirect(`${origin}/?auth_error=${encodeURIComponent(oauthError)}`);
  }

  if (code) {
    const supabase = await createClient();
    const { error } = await supabase.auth.exchangeCodeForSession(code);
    if (error) {
      return NextResponse.redirect(`${origin}/?auth_error=${encodeURIComponent(error.message)}`);
    }
    return NextResponse.redirect(`${origin}${next}`);
  }

  // No code and no error — nothing to exchange; send them home.
  return NextResponse.redirect(`${origin}/?auth_error=${encodeURIComponent("Missing sign-in code")}`);
}

```

### src/app/api/extract-ibis/route.ts

```typescript
import { randomUUID } from "node:crypto";
import { mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { NextResponse, type NextRequest } from "next/server";
import { extractIbisGraph } from "@/lib/ibis-extract";
import { evaluateIbis } from "@/lib/ibis-eval";
import { jobDir } from "@/lib/job-storage";
import { flushTraces, traced } from "@/lib/trace";
import type { NormalizedTranscript } from "@/lib/contracts";

export const runtime = "nodejs";
export const maxDuration = 300;

export async function POST(req: NextRequest) {
  try {
    const body = (await req.json()) as { transcript?: NormalizedTranscript; jobId?: string; judge?: boolean };
    const transcript = body.transcript;
    if (!transcript?.utterances?.length) {
      return NextResponse.json({ error: "Missing transcript with utterances." }, { status: 400 });
    }

    const jobId = body.jobId || `job_${randomUUID().slice(0, 8)}`;

    // Run extraction + evaluation under one trace so Arize shows the whole job.
    const { graph, quality } = await traced(
      "ibis.pipeline",
      { job_id: jobId, "source.type": transcript.source.sourceType, "transcript.utterances": transcript.utterances.length },
      async () => {
        const g = await extractIbisGraph(transcript);
        const q = await evaluateIbis(g, transcript, { judge: Boolean(body.judge) });
        return { graph: g, quality: q };
      },
    );

    const dir = jobDir(jobId);
    await mkdir(dir, { recursive: true });
    await writeFile(join(dir, "graph.json"), JSON.stringify(graph, null, 2));

    return NextResponse.json({ jobId, graph, quality });
  } catch (err) {
    const message = err instanceof Error ? err.message : "Unknown extraction error.";
    return NextResponse.json({ error: message }, { status: 500 });
  } finally {
    await flushTraces();
  }
}

```

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