# Project export: Resonance

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 personalized, AI-powered research soundboard, centralizing papers across the web while providing streamlined frameworks for synthesis, analysis, and development of ideas.
- Devpost: https://devpost.com/software/asdf-596pqz
- GitHub: https://github.com/twosodium/resonance
- Video: https://www.youtube.com/embed/m_F5pL9B64A?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 2 GitHub contributor(s) — twosodium (15 commits), Advay Aravind (7 commits)

## Devpost submission (written by the team)

### Inspiration

Novel research is fundamental to societal, technological, and economic development on a global scale. Unfortunately, research papers are often highly decentralized, and there exist a scarcity of tools for consolidating and interacting deeply with existing research. A quick web-based search often only reveals highly cited, public research, often sweeping intriguing but underviewed research aside. Whether it’s researchers looking for more comprehensive topical literature reviews, VCs looking for immediate, actionable areas of R&D investment, or anyone interested in deeply interacting with an idea of interest, Resonance streamlines the process of finding any and all intriguing research with the power of agentic and non-agentic AI.

### What it does

Whether it’s researchers looking for more comprehensive topical literature reviews, VCs looking for immediate, actionable areas of R&D investment, or anyone interested in deeply interacting with an idea of interest, Resonance streamlines the process of finding any and all intriguing research with the power of agentic and non-agentic AI. Users can search for a topic of interest in keyword or paragraph form. Resonance then scrapes the web, with the power of Browserbase and available APIs, for recent papers from a selection of arXiv, bioRxiv, OpenAlex, Semantic Scholar, Google Scholar, and Google search which match your needs. Following intelligent filtering, key papers of research are selected and added to the user’s query dashboard. Our agentic system then reads through the full text of each paper to rate the level of topicality with the user’s request, flagging papers of special interest. The user can navigate to the research paper, or converse with another agent about the paper and its relevance to their needs. We also have a custom mindmap, where your top researched ideas are dynamically and graphically represented, connected by strands of similarity, so users can effectively build upon previous research ideas, identifying key trends or underexplored areas. There’s more! Resonance can be connected to your phone via Poke, so rather than logging in via a web-based server, one can equivalently text the Poke agent their interests and specifications. Poke then automatically connects to the user’s dashboard, pursuing and reporting the relevant queries.

### How we built it

The project backend was built in python, with the frontend in HTML/CSS/Javascript. The web-scraping algorithm utilized APIs of various research databases, utilizing Browserbase Stagehand significantly to select relevant sources and navigate to the full-text for text-scraping. Poke was key in connecting the user's messaging platform directly with the research harness, for quick results via text!

### Challenges we ran into

Integrating the various APIs of the research databases, and especially navigating and scraping the fulltext via Browserbase Stagehand, was surprisingly difficult but rewarding. Other significant challenges involved designing the intricate AI stack for maximum result utility (diagrammed in slideshow), as well as effectively streamlining Poke to connect to the research harness and drive the integration process with mobile.

### Accomplishments we're proud of

We are excited to have created an AI-powered application with a real impact -- making interaction with research more accessible by centralizing research databases and designing numerous ways users can seamlessly interact with research and build upon previous ideas.

### What's next

The ultimate goal is for the world of research to be your soundboard with Resonance, the AI-integrated system for finding, filtering, and building upon global research.

## README (from the GitHub repository)

# 🌿 Resonance

**Spot tomorrow's breakthroughs today.**

Resonance is an AI-powered research discovery platform that scrapes papers from multiple academic sources, runs multi-agent debates to evaluate each paper's promise, and presents results in an interactive dashboard with a connected mind map of your best ideas. Built at **TreeHacks 2026**.

---

## How It Works

```
 User enters a topic
        │
        ▼
 ┌──────────────┐     arXiv API ──────────┐
 │  Research     │     OpenAlex API ───────┤
 │  Harness      │──▶  Semantic Scholar ───┼──▶ Papers stored in Supabase
 │  (scraper)    │     bioRxiv (Browser) ──┤      (papers table)
 └──────────────┘     Internet (Browser) ──┘
        │
        ▼
 ┌──────────────┐     🔍 Scout Agent ──────┐
 │  Multi-Agent  │     ✅ Advocate Agent ───┼──▶ Debate rounds
 │  Debate       │     ❌ Skeptic Agent ────┤
 │  (agents.py)  │     ⚖️  Moderator ───────┘──▶ Verdict + confidence
 └──────────────┘                                stored in Supabase
        │                                        (debates table)
        ▼
 ┌──────────────┐
 │  Dashboard    │──▶  Paper list with verdicts, confidence, topicality
 │  (frontend)   │──▶  Per-paper chat with Claude
 │               │──▶  Top Ideas mind map (connected graph)
 └──────────────┘
        │
        ▼
 ┌──────────────┐
 │  Poke MCP     │──▶  Text-based agent via iMessage / SMS / Slack
 │  (optional)   │     Queries sync to your dashboard
 └──────────────┘
```

### Pipeline

1. **Scraping** — `research_harness.py` queries arXiv, OpenAlex, and Semantic Scholar via their REST APIs. bioRxiv and internet search use Browserbase/Stagehand (optional, toggleable). Papers are filtered for relevance by Claude, then stored in the Supabase `papers` table.

2. **Debating** — `agents.py` runs a multi-agent debate on each paper. A **Scout** evaluates novelty, an **Advocate** argues for the paper's promise, and a **Skeptic** challenges it. After configurable rounds, a **Moderator** synthesizes a verdict (Promising / Interesting / Uncertain / Weak), confidence score, topicality score, key strengths, risks, big ideas, and follow-up questions. Results are stored in the Supabase `debates` table.

3. **Dashboard** — The frontend shows your search history, paper details, debate verdicts, and lets you chat with Claude about any individual paper. The **Top Ideas** page renders a physics-based mind map of your most promising discoveries across all searches, with AI-generated connection labels.

4. **Poke** (optional) — A Poke MCP server lets you interact with Resonance via text message. Ask it to research a topic, check status, or browse results — all synced to your dashboard.

---

## Tech Stack

| Layer | Technology |
|-------|-----------|
| **Frontend** | Vanilla HTML/CSS/JS, Supabase JS client |
| **Backend API** | Flask (Python), Flask-CORS |
| **Agents** | Anthropic Claude (Haiku for speed, Sonnet for quality) |
| **Scraping** | arXiv API, OpenAlex API, Semantic Scholar API, Browserbase/Stagehand |
| **Database** | Supabase (PostgreSQL) with RLS |
| **Auth** | Supabase Auth (email/password) |
| **Messaging** | Poke MCP + Poke Python SDK |
| **Config** | `config.json` + `.env` |

---

## Setup

### 1. Clone & install dependencies

```bash
git clone https://github.com/your-org/treehacks26.git
cd treehacks26

python3 -m venv venv
source venv/bin/activate   # macOS/Linux
pip install -r requirements.txt
```

### 2. Configure environment variables

Create a `.env` file in the project root:

```env
# Required
ANTHROPIC_API_KEY=sk-ant-...
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_SERVICE_ROLE_KEY=eyJ...
SUPABASE_KEY=eyJ...                          # anon key (for frontend)

# Optional — Browserbase (for bioRxiv + internet scraping)
BROWSERBASE_API_KEY=...
BROWSERBASE_PROJECT_ID=...
SKIP_BROWSERBASE=1                            # set to 1 to disable Browserbase

# Optional — higher rate limits
OPENALEX_MAILTO=you@example.com
SEMANTIC_SCHOLAR_API_KEY=...

# Optional — Poke integration
POKE_API_KEY=pk_...
```

### 3. Supabase tables

Run the following SQL in the Supabase SQL editor to create the required tables:

```sql
-- Papers table
CREATE TABLE IF NOT EXISTS papers (
  id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  topic text,
  paper_name text NOT NULL,
  paper_authors jsonb DEFAULT '[]',
  published text,
  journal text,
  abstract text,
  fulltext text,
  url text NOT NULL,
  user_id uuid REFERENCES auth.users(id),
  created_at timestamptz DEFAULT now(),
  CONSTRAINT papers_url_user_key UNIQUE (url, user_id)
);

-- Debates table
CREATE TABLE IF NOT EXISTS debates (
  id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  paper_id bigint REFERENCES papers(id),
  verdict text,
  confidence real DEFAULT 0,
  topicality real DEFAULT 0,
  one_liner text DEFAULT '',
  key_strengths text DEFAULT '[]',
  key_risks text DEFAULT '[]',
  big_ideas text DEFAULT '[]',
  follow_up_questions text DEFAULT '[]',
  debate_log text DEFAULT '[]',
  raw_verdict text DEFAULT '',
  created_at timestamptz DEFAULT now(),
  user_id uuid REFERENCES auth.users(id),
  topic text
);

-- Profiles table
CREATE TABLE IF NOT EXISTS profiles (
  id uuid PRIMARY KEY REFERENCES auth.users(id),
  first_name text DEFAULT '',
  last_name text DEFAULT '',
  role text DEFAULT '',
  bio text DEFAULT '',
  link_token text,
  link_token_expires timestamptz,
  poke_api_key text DEFAULT ''
);

-- Auto-create profile on signup
CREATE OR REPLACE FUNCTION handle_new_user()
RETURNS TRIGGER
SECURITY DEFINER
SET search_path = public
AS $$
BEGIN
  INSERT INTO profiles (id, first_name, last_name, role, bio)
  VALUES (
    NEW.id,
    COALESCE(NEW.raw_user_meta_data->>'first_name', ''),
    COALESCE(NEW.raw_user_meta_data->>'last_name', ''),
    COALESCE(NEW.raw_user_meta_data->>'role', ''),
    COALESCE(NEW.raw_user_meta_data->>'bio', '')
  );
  RETURN NEW;
EXCEPTION WHEN OTHERS THEN
  RAISE LOG 'Profile creation failed for user %: %', NEW.id, SQLERRM;
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users;
CREATE TRIGGER on_auth_user_created
  AFTER INSERT ON auth.users
  FOR EACH ROW EXECUTE FUNCTION handle_new_user();

-- RLS policies
ALTER TABLE papers ENABLE ROW LEVEL SECURITY;
ALTER TABLE debates ENABLE ROW LEVEL SECURITY;
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Users see own papers" ON papers FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Users insert own papers" ON papers FOR INSERT WITH CHECK (true);
CREATE POLICY "Users delete own papers" ON papers FOR DELETE USING (auth.uid() = user_id);

CREATE POLICY "Users see own debates" ON debates FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Allow insert debates" ON debates FOR INSERT WITH CHECK (true);

CREATE POLICY "Users see own profile" ON profiles FOR SELECT USING (auth.uid() = id);
CREATE POLICY "Users update own profile" ON profiles FOR UPDATE USING (auth.uid() = id);
```

### 4. Update `frontend/supabase.js`

Make sure the Supabase URL and anon key in `frontend/supabase.js` match your project.

---

## Running Locally

You need **three terminals** to run everything:

### Terminal 1 — Frontend (static file server)

```bash
cd frontend
python3 -m http.server 8080
```

Then open [http://localhost:8080](http://localhost:8080) in your browser.

### Terminal 2 — Backend API

```bash
cd /path/to/treehacks26
source venv/bin/activate
python3 api.py
```

This starts the Flask API on port **5000**. The frontend calls this for search, status, settings, chat, and ideas endpoints.

### Terminal 3 — Poke MCP Server (optional)

```bash
lsof -ti:8765 | xargs kill -9 2>/dev/null
cd /path/to/treehacks26/poke-mcp && python3 server.py
```

This starts the MCP server on port **8765**. Then in a **fourth terminal**, expose it via Poke's tunnel:

```bash
npx poke tunnel http://localhost:8765/mcp -n "Resonance"
```

This outputs a tunnel URL like:
```
Tunnel URL: https://tunnel.poke.com/xxxxxxxx-x

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 18 recognized source files, 336 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- Flask (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- OpenAI (technology) — detected in the code
- Python (language) — detected in the code
- Supabase (technology) — detected in the code
- Java (language) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (23 of 23)

```
.DS_Store
.gitignore
agents.py
api.py
config.json
frontend/app.js
frontend/dashboard.html
frontend/how-it-works.html
frontend/ideas.html
frontend/index.html
frontend/login.html
frontend/settings.html
frontend/styles.css
frontend/supabase.js
frontend/theme.js
pipeline.py
poke-mcp/requirements.txt
poke-mcp/server.py
README.md
requirements.txt
research_harness.py
tests/__init__.py
tests/test_research_harness.py
```

### Dependencies

- poke-mcp/requirements.txt: anthropic@>=0.40.0, fastmcp@>=2.0.0, poke@>=0.1.1, python-dotenv@>=1.0.0, supabase@>=2.0.0
- requirements.txt: anthropic@>=0.40.0, flask@>=3.0.0, flask-cors@>=4.0.0, httpx@>=0.27.0, openai@>=1.0.0, poke@>=0.1.1, pymupdf@>=1.24.0, pypdf@>=4.0.0, pytest@>=7.0.0, pytest-asyncio@>=0.21.0, python-dotenv@>=1.0.0, requests@>=2.31.0, stagehand@>=3.0.0, supabase@>=2.0.0

### Recent commits (newest first)

- change branding
- Merge branch 'main' of https://github.com/twosodium/treehacks26
- revert
- Delete .env
- Merge branch 'main' of https://github.com/twosodium/treehacks25
- Merge Final
- merge branch 'main' of https://github.com/twosodium/treehacks26
- poke changes
- Merge 5
- h
- m
- poke
- Merge4 (Broken)
- fast working
- merg
- integrate
- Merge4
- Merge 3
- Merge branch 'main' of https://github.com/twosodium/treehacks26
- requirements

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

### requirements.txt

```
anthropic>=0.40.0
httpx>=0.27.0
supabase>=2.0.0
python-dotenv>=1.0.0
requests>=2.31.0
stagehand>=3.0.0
openai>=1.0.0
pymupdf>=1.24.0
pypdf>=4.0.0
flask>=3.0.0
flask-cors>=4.0.0
pytest>=7.0.0
pytest-asyncio>=0.21.0
poke>=0.1.1
```

### poke-mcp/requirements.txt

```
fastmcp>=2.0.0
anthropic>=0.40.0
supabase>=2.0.0
python-dotenv>=1.0.0
poke>=0.1.1
```

### poke-mcp/server.py

```python
"""
Resonance MCP Server — exposes Resonance tools to Poke.

When `npx poke` runs, it starts this server on port 8765 and opens a tunnel
so Poke's cloud AI can call these @mcp.tool() functions from group chats,
iMessage, Slack, etc.

Each function you decorate with @mcp.tool() becomes a "tool" that Poke's
AI agent can decide to call based on what the user asks in the chat.
"""

from __future__ import annotations

import json
import os
import sys
import logging
import traceback
import threading

# ── Make sure the parent project is importable ──────────────────────────
PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, PROJECT_DIR)

from dotenv import load_dotenv

load_dotenv(os.path.join(PROJECT_DIR, ".env"))

from fastmcp import FastMCP

# ── Verbose logging ─────────────────────────────────────────────────────
# Only show DEBUG for our own logger; silence noisy libraries
logging.basicConfig(
    level=logging.WARNING,                       # default: quiet
    format="%(asctime)s %(levelname)s [%(name)s] %(message)s",
    datefmt="%H:%M:%S",
)
logger = logging.getLogger("poke-mcp")
logger.setLevel(logging.DEBUG)                   # our logs: verbose
logging.getLogger("docket").setLevel(logging.WARNING)
logging.getLogger("fakeredis").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("httpcore").setLevel(logging.WARNING)
# Silence noisy MCP transport errors (ClientDisconnect from tunnel timeouts)
logging.getLogger("mcp.server.streamable_http").setLevel(logging.CRITICAL)
logging.getLogger("mcp.server.lowlevel.server").setLevel(logging.CRITICAL)

# ── Startup diagnostics ────────────────────────────────────────────────
logger.info("=" * 60)
logger.info("Resonance MCP Server starting")
logger.info("PROJECT_DIR = %s", PROJECT_DIR)
logger.info("Python       = %s", sys.executable)
logger.info("=" * 60)

# Check critical env vars
_REQUIRED_ENV = [
    "SUPABASE_URL",
    "SUPABASE_SERVICE_ROLE_KEY",
    "ANTHROPIC_API_KEY",
]
_OPTIONAL_ENV = [
    "SUPABASE_KEY",
    "BROWSERBASE_API_KEY",
    "BROWSERBASE_PROJECT_ID",
    "SKIP_BROWSERBASE",
    "RESONANCE_API_BASE",
]
for var in _REQUIRED_ENV:
    val = os.environ.get(var, "")
    status = "✅ SET" if val.strip() else "❌ MISSING"
    # Show first 8 chars only for security
    preview = val[:8] + "…" if len(val) > 8 else val
    logger.info("  env %-30s %s  (%s)", var, status, preview if val else "")
for var in _OPTIONAL_ENV:
    val = os.environ.get(var, "")
    status = "SET" if val.strip() else "not set"
    logger.info("  env %-30s %s", var, status)

# Check that key imports work
try:
    from supabase import create_client
    logger.info("  import supabase        ✅")
except ImportError as e:
    logger.error("  import supabase        ❌  %s", e)

try:
    from anthropic import Anthropic
    logger.info("  import anthropic       ✅")
except ImportError as e:
    logger.error("  import anthropic       ❌  %s", e)

try:
    import pipeline  # noqa: F401
    logger.info("  import pipeline        ✅")
except Exception as e:
    logger.error("  import pipeline        ❌  %s", e)

try:
    import agents  # noqa: F401
    logger.info("  import agents          ✅")
except Exception as e:
    logger.error("  import agents          ❌  %s", e)

logger.info("=" * 60)


# ── FastMCP app ─────────────────────────────────────────────────────────
mcp = FastMCP(
    "Resonance Research",
    instructions=(
        "You are Resonance, an AI research-scouting assistant. "
        "You help users discover promising new research papers and debate "
        "their merits. You can search for papers, run multi-agent debates, "
        "look up past results, and brainstorm follow-up ideas.\n\n"
        "ACCOUNT LINKING (CRITICAL):\n"
        "- On your VERY FIRST message in EVERY new conversation, ALWAYS call `whoami` to check.\n"
        "- If `whoami` says an account IS linked, greet the user by name and confirm: "
        "'Hey [name]! I'm connected to your Resonance account. Is this you, or would you like to "
        "switch accounts?'\n"
        "- If the user says they are someone else or wants to switch, call `unlink_account()` first, "
        "then guide them through linking.\n"
        "- If NOT linked, tell the user exactly this: 'To get started, please link your Resonance account:\n"
        "  1. Open your Resonance dashboard (the website where you signed up)\n"
        "  2. Go to Settings (gear icon in the sidebar)\n"
        "  3. Scroll to the Poke Integration section\n"
        "  4. Click Generate link token\n"
        "  5. Copy the token and paste it here'\n"
        "- Do NOT invent URLs, links, or authentication pages. There is NO external auth URL.\n"
        "- The ONLY way to link is with a token from the Resonance Settings page.\n"
        "- Wait for the user to provide the token, then call `link_account(token)`.\n\n"
        "RESEARCH WORKFLOW:\n"
        "- When the user asks you to research a topic, call `research_topic(topic)`. "
        "It runs in the background.\n"
        "- After starting research, PROACTIVELY call `check_research_status(topic)` "
        "after about 60-90 seconds to see if it's done.\n"
        "- When research is complete, `check_research_status` returns the top results "
        "with paper links — share these with the user immediately.\n"
        "- Results from Poke queries are automatically saved to the user's Resonance "
        "dashboard — mention this so they know.\n\n"
        "OTHER RULES:\n"
        "- NEVER make up or assume any data. Only report what tools actually return.\n"
        "- NEVER invent URLs or links. If you don't know a URL, say so.\n"
        "- If a tool returns an error, show the error to the user.\n"
        "- You can use `brainstorm` without a linked account for general questions."
    ),
)


# ── Session state: linked user ──────────────────────────────────────────
# Single-user-at-a-time: the
[truncated — 24132 more characters]
```

### frontend/app.js

```javascript
/* ============================================================
   app.js — Dashboard interactivity
   Pulls real data from Supabase via helpers in supabase.js.
   Triggers the backend pipeline when the user submits a topic.
   ============================================================ */

// ---------------------------------------------------------------------------
// State
// ---------------------------------------------------------------------------

let topics = [];          // [{ topic, last_run, paper_count, debate_count }]
let activeTopic = null;   // currently viewed topic string
let activeDebates = [];   // debates for the active topic
let activePanel = null;   // currently open debate object
let _pollTimer = null;    // polling interval id

// Chat state
let _chatPaperId = null;
let _chatPaperData = null;
let _chatHistory = [];    // [{role, content}]
let _chatSuggestions = [];


// ---------------------------------------------------------------------------
// Init — check auth, then set up the page
// ---------------------------------------------------------------------------

document.addEventListener('DOMContentLoaded', async () => {
  const user = await requireAuth();
  if (!user) return;

  const { data: profile } = await getProfile(user.id);
  const firstName = profile?.first_name
    || user.user_metadata?.first_name
    || user.email?.split('@')[0]
    || 'there';

  setGreeting(firstName);
  setupSearch();
  setupSidebarToggle();
  await loadDashboardSources();
  setupDashboardSourceToggles();
  await loadDashboardModelAndDate();
  setupDashboardModelAndDate();

  // Load real topics from DB
  await loadTopics();
});


// ---------------------------------------------------------------------------
// Greeting
// ---------------------------------------------------------------------------

function setGreeting(name) {
  const hour = new Date().getHours();
  const timeOfDay = hour < 12 ? 'morning' : hour < 18 ? 'afternoon' : 'evening';

  const greetEl = document.getElementById('greeting');
  greetEl.querySelector('h1').textContent = `Good ${timeOfDay}, ${name}`;

  const avatar = document.getElementById('avatar');
  avatar.textContent = name.charAt(0).toUpperCase();
}


// ---------------------------------------------------------------------------
// Load topics from Supabase
// ---------------------------------------------------------------------------

async function loadTopics() {
  topics = await fetchTopics();
  renderSidebar();
}


// ---------------------------------------------------------------------------
// Sidebar toggle
// ---------------------------------------------------------------------------

function setupSidebarToggle() {
  const stored = localStorage.getItem('resonance-sidebar-collapsed');
  if (stored === 'true') {
    document.querySelector('.dashboard').classList.add('sidebar-collapsed');
  }
}

function toggleSidebar() {
  const dash = document.querySelector('.dashboard');
  dash.classList.toggle('sidebar-collapsed');
  localStorage.setItem('resonance-sidebar-collapsed', dash.classList.contains('sidebar-collapsed'));
}

// ---------------------------------------------------------------------------
// Dashboard source toggles (under search bar)
// ---------------------------------------------------------------------------

const DASHBOARD_SOURCE_IDS = ['arxiv', 'biorxiv', 'openalex', 'semantic_scholar', 'internet'];

async function loadDashboardSources() {
  const el = document.getElementById('search-sources');
  if (!el) return;
  try {
    const s = await getSettings();
    const sources = s.sources || DASHBOARD_SOURCE_IDS;
    DASHBOARD_SOURCE_IDS.forEach(name => {
      const cb = document.getElementById('ds-source-' + name);
      if (cb) cb.checked = sources.includes(name);
    });
  } catch (_) {
    DASHBOARD_SOURCE_IDS.forEach(name => {
      const cb = document.getElementById('ds-source-' + name);
      if (cb) cb.checked = true;
    });
  }
}

let _sourceSaveTimer = null;
function getEnabledSourceNames() {
  const labels = { arxiv: 'arXiv', biorxiv: 'bioRxiv', openalex: 'OpenAlex', semantic_scholar: 'Semantic Scholar', internet: 'Internet' };
  return DASHBOARD_SOURCE_IDS
    .filter(name => document.getElementById('ds-source-' + name)?.checked)
    .map(name => labels[name] || name);
}

function getEnabledSourceIds() {
  return DASHBOARD_SOURCE_IDS.filter(name => {
    const cb = document.getElementById('ds-source-' + name);
    return cb && cb.checked;
  });
}

/** Save current source checkboxes to config.json and return immediately. */
async function saveSourcesNow() {
  const sources = getEnabledSourceIds();
  try {
    await putSettings({ sources: sources.length ? sources : DASHBOARD_SOURCE_IDS });
  } catch (_) { /* best-effort */ }
}

function setupDashboardSourceToggles() {
  const el = document.getElementById('search-sources');
  if (!el) return;
  DASHBOARD_SOURCE_IDS.forEach(name => {
    const cb = document.getElementById('ds-source-' + name);
    if (cb) {
      cb.addEventListener('change', () => {
        clearTimeout(_sourceSaveTimer);
        _sourceSaveTimer = setTimeout(saveSourcesNow, 400);
      });
    }
  });
}


// ---------------------------------------------------------------------------
// Dashboard model toggle & earliest-date (under search bar)
// ---------------------------------------------------------------------------

let _modelDateSaveTimer = null;

async function loadDashboardModelAndDate() {
  try {
    const s = await getSettings();
    const modelDeep = (s.filter_llm_model || '').includes('sonnet');
    const fastRadio = document.getElementById('ds-model-fast');
    const deepRadio = document.getElementById('ds-model-deep');
    if (fastRadio) fastRadio.checked = !modelDeep;
    if (deepRadio) deepRadio.checked = modelDeep;

    const dateInput = document.getElementById('ds-earliest-date');
    if (dateInput && s.earliest_date) dateInput.value = s.earliest_date.slice(0, 10);
    // Highlight mat
[truncated — 33401 more characters]
```

### agents.py

```python
"""
agents.py — Multi-agent debate system for research paper analysis.

Three agents (Scout, Advocate, Skeptic) debate in rounds, then a
Moderator synthesises a final verdict.  Uses the Anthropic Python SDK

you can also run `python agents.py` with a ANTHROPIC_API_KEY env
var to see a demo debate on a sample paper
"""

from __future__ import annotations

import json
import os
import re
from dataclasses import dataclass, field
from typing import Any

from anthropic import Anthropic

MODEL = "claude-haiku-4-5-20251001"
MAX_TOKENS = 1024

# ---------------------------------------------------------------------------
# System prompts
# ---------------------------------------------------------------------------

SCOUT_SYSTEM = """\
You are the **Scout** — an expert at scanning new research and spotting
hidden gems.  Your job:

• Identify what is *genuinely novel* about the paper (method, dataset,
  result, or framing).
• Estimate the *potential impact* — academic and commercial.
• **Evaluate topicality** — how closely does this paper relate to the
  user's search topic?  Is it core, adjacent, or only tangentially related?
• Suggest the *big ideas* this paper could inspire — new research
  directions, applications, paradigm shifts, or cross-domain connections.
• Be specific: cite numbers, comparisons, or prior work when possible.

{user_context}
Keep your response concise and under 120 words.
"""

ADVOCATE_SYSTEM = """\
You are the **Advocate** — an enthusiastic but rigorous champion of
promising research.  Your job:

• Build on the Scout's analysis and *strengthen* the case for this paper.
• Identify real-world applications, potential products, or new research
  directions that could emerge from this work.
• Draw connections to adjacent fields or trends.
• Comment on how relevant the paper is to the search topic — if it's
  adjacent, explain *why* it still matters.
• Rebut the Skeptic's concerns point-by-point when they arise.

{user_context}
Stay grounded in evidence — never resort to empty hype.
Keep your response concise and under 120 words.
"""

SKEPTIC_SYSTEM = """\
You are the **Skeptic** — a sharp, fair, but tough critic.  Your job:

• Stress-test the paper's claims: methodology, statistical rigour,
  dataset quality, reproducibility.
• Identify *risks* — technical barriers, timing, ethical issues,
  regulatory headwinds.
• Assess **topicality** — if the paper is only loosely related to the
  search topic, flag it.  A technically sound paper that's off-topic is
  less useful.
• Call out when the Scout or Advocate are over-extrapolating from the
  evidence.
• Suggest what *additional evidence* would be needed to convince you.

{user_context}
Be concise and specific.  Aim for constructive criticism, not dismissal.
Keep your response under 120 words.
"""

MODERATOR_SYSTEM = """\
You are the **Moderator**.  You have just observed a multi-round debate
between a Scout, an Advocate and a Skeptic about a research paper.

Produce a **structured JSON** verdict with exactly these keys:

{{
  "verdict": "PROMISING" | "INTERESTING" | "UNCERTAIN" | "WEAK",
  "confidence": <float 0-1>,
  "topicality": <float 0-1, how relevant the paper is to the search topic>,
  "one_liner": "<1-sentence summary>",
  "key_strengths": ["...", "..."],
  "key_risks": ["...", "..."],
  "big_ideas": ["<broad ideas / directions this paper could inspire>", "..."],
  "follow_up_questions": ["...", "..."]
}}

IMPORTANT RULES:
• **Always** populate one_liner, key_strengths, key_risks, big_ideas — even when
  the verdict is UNCERTAIN or WEAK.  There is always *something* to say.
• topicality: 1.0 = core to the search topic, 0.5 = adjacent, 0.0 = unrelated.
• Return ONLY valid JSON — no markdown fences, no commentary outside the JSON.
"""


# ---------------------------------------------------------------------------
# Agent class
# ---------------------------------------------------------------------------

@dataclass
class Agent:
    """Thin wrapper around a Claude conversation with a fixed system prompt."""

    name: str
    system_prompt: str
    client: Anthropic
    model: str = MODEL
    history: list[dict[str, str]] = field(default_factory=list)

    # ---- public API -------------------------------------------------------

    def say(self, user_message: str) -> str:
        """Send *user_message* and return the assistant's reply."""
        self.history.append({"role": "user", "content": user_message})
        response = self.client.messages.create(
            model=self.model,
            max_tokens=MAX_TOKENS,
            system=self.system_prompt,
            messages=self.history,
        )
        text = response.content[0].text
        self.history.append({"role": "assistant", "content": text})
        return text

    def reset(self) -> None:
        self.history.clear()


# ---------------------------------------------------------------------------
# Debate orchestrator
# ---------------------------------------------------------------------------

@dataclass
class DebateResult:
    paper: dict[str, Any]
    rounds: list[dict[str, str]]
    verdict: dict[str, Any]
    raw_verdict: str


def _format_paper(paper: dict[str, Any]) -> str:
    """Turn a paper dict (matching the Supabase ``papers`` schema) into a
    readable prompt block.

    Expected keys (from DB): paper_name, paper_authors (JSON list),
    published (date string), abstract, journal, fulltext, url, topic.
    Also tolerates the generic keys title/authors/abstract for testing.
    """
    title = paper.get("paper_name") or paper.get("title", "Unknown")
    authors = paper.get("paper_authors") or paper.get("authors", "Unknown")
    if isinstance(authors, list):
        authors = ", ".join(authors)
    abstract = paper.get("abstract", "")
    date = paper.get("published") or paper.get("date", "")
    url = paper.get("url", "")
    topic = paper.get("topic", "")
    journal = paper.get("journal", "")
    fulltext = paper.get("fulltext", "")

    line
[truncated — 8514 more characters]
```

### pipeline.py

```python
"""
pipeline.py — End-to-end research pipeline.

Phases
------
1. **Scrape** — Use ``research_harness.run_harness()`` to fetch papers
   from arXiv / bioRxiv / web, then store in Supabase ``papers`` table.
2. **Debate** — Pull papers from DB, run multi-agent debate on each,
   store verdicts in Supabase ``debates`` table.
3. **full_pipeline** — Scrape ➜ Debate in one call (used by the API).

Run standalone::

    python pipeline.py --scrape --topic "protein folding"
    python pipeline.py --topic "protein folding"     # debate only
    python pipeline.py --full --topic "protein folding"   # both
"""

from __future__ import annotations

import argparse
import json
import logging
import concurrent.futures
import os
import threading
import re
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Callable

from dotenv import load_dotenv

_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
load_dotenv(dotenv_path=os.path.join(_SCRIPT_DIR, ".env"))

from supabase import Client as SupabaseClient, create_client  # noqa: E402

from agents import DebateResult, run_debate  # noqa: E402

logger = logging.getLogger("pipeline")
logging.basicConfig(level=logging.INFO, format="%(levelname)s [%(name)s] %(message)s")

# ---------------------------------------------------------------------------
# Config — loaded from config.json (dashboard writes this file)
# ---------------------------------------------------------------------------

CONFIG_PATH = Path(__file__).parent / "config.json"


def load_config() -> dict:
    """Read config.json, falling back to sensible defaults."""
    defaults = {
        "topic": "",
        "multiplier": 3,
        "candidate_count": 8,
        "top_k": 5,
        "debate_rounds": 2,
        "sources": ["arxiv", "biorxiv", "openalex", "semantic_scholar", "internet"],
    }
    if CONFIG_PATH.exists():
        with open(CONFIG_PATH) as f:
            cfg = json.load(f)
        return {**defaults, **cfg}
    return defaults


def save_config(cfg: dict) -> None:
    """Persist config back to config.json (called by the dashboard later)."""
    with open(CONFIG_PATH, "w") as f:
        json.dump(cfg, f, indent=2)


# ---------------------------------------------------------------------------
# Supabase client  (prefers service-role key for backend writes)
# ---------------------------------------------------------------------------

SUPABASE_URL = os.getenv("SUPABASE_URL", "")
SUPABASE_KEY = (
    os.getenv("SUPABASE_SERVICE_ROLE_KEY")
    or os.getenv("SUPABASE_KEY")
    or ""
)


def _get_supabase() -> SupabaseClient:
    """Return a Supabase client.  Raises if creds are missing."""
    if not SUPABASE_URL or not SUPABASE_KEY:
        raise RuntimeError(
            "Set SUPABASE_URL and SUPABASE_KEY (or SUPABASE_SERVICE_ROLE_KEY) "
            "env vars (grab them from your Supabase project → Settings → API)."
        )
    return create_client(SUPABASE_URL, SUPABASE_KEY)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _sanitize(s: str | None) -> str | None:
    """Remove null bytes that PostgreSQL text columns reject."""
    if s is None:
        return None
    if not isinstance(s, str):
        return s
    return "".join(c for c in s if c != "\x00" and (ord(c) >= 32 or c in "\n\r\t"))


# ---------------------------------------------------------------------------
# A.  SCRAPE workflow — uses research_harness
# ---------------------------------------------------------------------------

def summarize_query_to_topic(query: str) -> str:
    """Turn a user synthesis query or paragraph into a short search topic via Claude."""
    from research_harness import _summarize_paragraph_to_topic
    return (_summarize_paragraph_to_topic(query) or query).strip() or query


def scrape_and_store(
    topic: str,
    *,
    user_id: str | None = None,
    candidate_count: int = 50,
    top_k: int = 20,
    max_age_months: int = 0,
    fast: bool = True,
    sources: list[str] | None = None,
) -> list[dict]:
    """
    Fetch papers via research_harness (fast=API-only by default), convert to DB
    schema dicts, and upsert into the ``papers`` table.
    """
    from research_harness import paper_to_dict, run_harness

    logger.info("Scraping papers for topic=%r  (candidates=%d, top_k=%d, fast=%s, sources=%s)", topic, candidate_count, top_k, fast, sources)
    papers = run_harness(
        prompt=topic,
        candidate_count=candidate_count,
        top_k=top_k,
        max_age_months=max_age_months,
        sources=set(sources) if sources else None,
        fast=fast,
    )
    logger.info("Harness returned %d papers", len(papers))

    if not papers:
        return []

    # Convert Paper dataclass → dict (matching the DB schema)
    rows: list[dict] = []
    for p in papers:
        row = paper_to_dict(p, topic=topic)
        if user_id:
            row["user_id"] = user_id
        rows.append(row)

    # Upsert into Supabase
    sb = _get_supabase()
    try:
        resp = sb.table("papers").upsert(rows, on_conflict="url").execute()
        stored = resp.data if resp.data else rows
    except Exception as exc:
        err = str(exc)
        # If upsert fails because there's no unique constraint on url,
        # fall back to plain insert.
        if "42P10" in err or "unique or exclusion constraint" in err.lower():
            logger.warning("No UNIQUE on url — falling back to INSERT.")
            try:
                resp = sb.table("papers").insert(rows).execute()
                stored = resp.data if resp.data else rows
            except Exception as exc2:
                logger.error("Insert also failed: %s", exc2)
                stored = rows
        else:
            # Retry without columns that might not exist in the table
            col_match = re.search(r"Could not find the ['\"](\w+)['\"] column", err)
      
[truncated — 11431 more characters]
```

### frontend/theme.js

```javascript
/* Apply theme from localStorage (dark-mode: "1" = dark). Run early to avoid flash. */
(function () {
  var isDark = localStorage.getItem('resonance-dark-mode') === '1';
  document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
})();
function applyTheme(isDark) {
  document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
  localStorage.setItem('resonance-dark-mode', isDark ? '1' : '0');
}

```

### frontend/index.html

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Resonance — Your Research Soundboard</title>
  <link rel="icon" type="image/png" href="papermint_logo.png" />
  <link rel="stylesheet" href="styles.css" />
  <script src="theme.js"></script>
</head>
<body>
  <div class="landing">
    <!-- Nav -->
    <nav class="landing-nav">
      <div class="landing-logo"><img src="papermint_logo.png" alt="" class="logo-icon" /> resonance</div>
      <div class="landing-nav-links">
        <a href="login.html" class="btn btn-ghost">Log in</a>
        <a href="login.html#signup" class="btn btn-primary">Get started</a>
      </div>
    </nav>

    <!-- Hero -->
    <main class="landing-hero fade-in">
      <div class="landing-badge">
        <span class="dot"></span>
        TreeHacks 2026
      </div>

      <h1>Spot tomorrow's breakthroughs&nbsp;today</h1>
      <p class="landing-subtitle" style="font-size:1.1rem; color:var(--accent); font-weight:500; margin-bottom:4px;">Your Research Soundboard</p>

      <p>
        AI agents scout, debate, stress-test, and chat about the latest research papers, surfacing the ideas that matter most for researchers, investors, and builders.
      </p>

      <div class="landing-cta">
        <a href="login.html#signup" class="btn btn-primary">Start exploring →</a>
        <a href="how-it-works.html" class="btn btn-outline">How it works</a>
      </div>
    </main>

    <!-- Footer -->
    <footer class="landing-footer">
      Built at TreeHacks 2026 · Powered by Claude, Browserbase &amp; Poke
    </footer>
  </div>
</body>
</html>


```

### api.py

```python
"""
api.py — Flask backend API for Resonance.

Endpoints
---------
POST /api/search   — Kick off scrape + debate pipeline for a topic.
GET  /api/status    — Check the status of a running pipeline job.
"""

from __future__ import annotations

import logging
import os
import secrets
import threading
import traceback
from datetime import datetime, timezone, timedelta

from dotenv import load_dotenv, set_key
from flask import Flask, jsonify, request
from flask_cors import CORS

# Load .env from project root
_ENV_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".env")
load_dotenv(_ENV_PATH)

from pipeline import full_pipeline, load_config, save_config  # noqa: E402 (must be after dotenv)
from supabase import create_client as _create_client  # noqa: E402

logger = logging.getLogger("api")
logging.basicConfig(level=logging.INFO, format="%(levelname)s [%(name)s] %(message)s")

# Anthropic client for paper chat — rebuilt whenever the API key changes
_chat_client = None
_chat_client_key: str | None = None  # tracks which key the client was built with

def _get_chat_client():
    global _chat_client, _chat_client_key
    current_key = os.environ.get("ANTHROPIC_API_KEY", "")
    if _chat_client is None or current_key != _chat_client_key:
        from anthropic import Anthropic
        _chat_client = Anthropic(api_key=current_key)
        _chat_client_key = current_key
    return _chat_client

# In-memory paper chat histories: { paper_id: [ {role, content} ] }
_paper_chats: dict[int, list[dict]] = {}
_paper_chats_lock = threading.Lock()

app = Flask(__name__)
CORS(app)  # allow frontend on any origin during dev


def _get_sb():
    """Return a Supabase client for profile lookups."""
    return _create_client(
        os.environ.get("SUPABASE_URL", ""),
        os.environ.get("SUPABASE_SERVICE_ROLE_KEY") or os.environ.get("SUPABASE_KEY", ""),
    )


def _build_user_context(user_id: str | None) -> str:
    """Fetch user profile and return a short context string for the agents."""
    if not user_id:
        return ""
    try:
        sb = _get_sb()
        resp = sb.table("profiles").select("role, bio").eq("id", user_id).single().execute()
        profile = resp.data if resp.data else {}
    except Exception:
        return ""

    parts = []
    role = (profile.get("role") or "").strip()
    bio = (profile.get("bio") or "").strip()
    if role:
        parts.append(f"Role: {role}")
    if bio:
        parts.append(f"Bio: {bio}")
    return "\n".join(parts)

# ---------------------------------------------------------------------------
# In-memory job tracker  (topic -> status dict)
# For a hackathon this is fine; production would use Redis / DB.
# ---------------------------------------------------------------------------
_jobs: dict[str, dict] = {}
_jobs_lock = threading.Lock()


def _set_job(key: str, status: str, **extra):
    with _jobs_lock:
        _jobs[key] = {"status": status, "updated_at": datetime.now(timezone.utc).isoformat(), **extra}


def _get_job(key: str) -> dict:
    with _jobs_lock:
        return dict(_jobs.get(key, {"status": "unknown"}))


_cancel_requested: dict[str, bool] = {}


# ---------------------------------------------------------------------------
# POST /api/search/cancel — request cancel of running pipeline
# ---------------------------------------------------------------------------

@app.route("/api/search/cancel", methods=["POST"])
def search_cancel():
    """Set cancel flag for the job so the pipeline stops after current phase."""
    data = request.get_json(silent=True) or {}
    topic = (data.get("topic") or "").strip()
    user_id = (data.get("user_id") or "").strip() or None
    if not topic:
        return jsonify({"error": "topic is required"}), 400
    job_key = f"{user_id or 'anon'}:{topic}"
    with _jobs_lock:
        _cancel_requested[job_key] = True
    _set_job(job_key, "cancelled")
    return jsonify({"ok": True, "status": "cancelled"})


# ---------------------------------------------------------------------------
# POST /api/search
# Body: { "topic": "...", "user_id": "..." }
# ---------------------------------------------------------------------------

@app.route("/api/search", methods=["POST"])
def search():
    """Start pipeline: user synthesis query (or topic) → Claude summarizes → fast scrape → Supabase → debate."""
    data = request.get_json(silent=True) or {}
    topic = (data.get("topic") or data.get("query") or "").strip()
    user_id = (data.get("user_id") or "").strip() or None

    if not topic:
        return jsonify({"error": "topic or query is required"}), 400

    job_key = f"{user_id or 'anon'}:{topic}"

    # Don't start duplicate jobs
    current = _get_job(job_key)
    if current.get("status") in ("scraping", "debating"):
        return jsonify({"status": current["status"], "topic": topic, "message": "Pipeline already running."})

    _set_job(job_key, "scraping")

    # Build user context once before spawning the thread
    user_ctx = _build_user_context(user_id)

    def _run():
        from pipeline import full_pipeline
        try:
            _set_job(job_key, "scraping")
            cancel_check = lambda: _cancel_requested.get(job_key)
            result = full_pipeline(
                topic=topic,
                user_id=user_id,
                user_context=user_ctx,
                on_phase=lambda phase, **kw: _set_job(job_key, phase, **kw),
                cancel_check=cancel_check,
            )
            with _jobs_lock:
                _cancel_requested.pop(job_key, None)
            if _get_job(job_key).get("status") == "cancelled":
                return
            if result is None:
                _set_job(job_key, "cancelled")
                return
            _set_job(
                job_key, "complete",
                papers_count=result.get("papers_count", 0),
                debates_count=result.get("debates_count", 0),
            )
        except Exception as exc:
            logge
[truncated — 17663 more characters]
```

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