import { useState, useRef, useEffect, useMemo } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import './App.css';
const API = import.meta.env.VITE_API_URL ?? 'http://localhost:8000';
const PIPELINE_NODES = [
{ id: 'researcher', label: 'Researcher', icon: '⬡', color: '#6366f1', match: 'research specialist' },
{ id: 'credibility', label: 'Credibility', icon: '◈', color: '#a855f7', match: 'credibility analyst' },
{ id: 'rag', label: 'RAG Engine', icon: '✦', color: '#60a5fa', match: 'rag pipeline' },
{ id: 'synthesis', label: 'Synthesis', icon: '◉', color: '#34d399', match: 'synthesis analyst' },
];
const DEPTHS = [
{ id: 'quick', label: 'Quick', meta: '5 sources · ~30s' },
{ id: 'standard', label: 'Standard', meta: '10 sources · ~90s' },
{ id: 'deep', label: 'Deep', meta: '15 sources · ~3m' },
];
const EXAMPLES = [
'Health risks of microplastics in drinking water',
'Latest advances in large language model reasoning 2025',
'Economic impact of remote work on urban real estate',
'CRISPR gene editing applications in treating genetic diseases',
];
function detectAgentIndex(line) {
const l = line.toLowerCase();
for (let i = 0; i < PIPELINE_NODES.length; i++) {
if (l.includes(PIPELINE_NODES[i].match)) return i;
}
return -1;
}
function getLogClass(line) {
const l = line.toLowerCase();
if (l.startsWith('[error]') || l.includes('traceback') || l.includes('exception')) return 'log-error';
if (l.includes('warning') || l.includes('warn')) return 'log-warn';
if (l.includes('agent:') || l.includes('working agent') || l.includes('# agent')) return 'log-agent';
if (l.includes('final answer') || l.includes('completed') || l.includes('finished')) return 'log-success';
if (l.includes('thought:') || l.includes('action:') || l.includes('observation:')) return 'log-step';
return '';
}
function scoreColor(score) {
if (score >= 0.7) return { color: '#34d399', borderColor: 'rgba(52,211,153,0.3)' };
if (score >= 0.5) return { color: '#fbbf24', borderColor: 'rgba(251,191,36,0.3)' };
return { color: '#f87171', borderColor: 'rgba(248,113,113,0.3)' };
}
function scoreBarColor(score) {
if (score >= 0.7) return '#34d399';
if (score >= 0.5) return '#fbbf24';
return '#f87171';
}
function extractConfidence(report) {
const m = report.match(/Confidence Assessment.*?(HIGH|MEDIUM|LOW|INSUFFICIENT)/is);
return m ? m[1].toUpperCase() : null;
}
function triggerDownload(filename, content) {
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = filename; a.click();
URL.revokeObjectURL(url);
}
function PipelineDiagram({ logs, status }) {
const seen = useMemo(() => {
const s = new Set();
for (const line of logs) { const idx = detectAgentIndex(line); if (idx !== -1) s.add(idx); }
return s;
}, [logs]);
const activeIdx = useMemo(() => {
if (status !== 'running') return -1;
for (let i = logs.length - 1; i >= 0; i--) { const idx = detectAgentIndex(logs[i]); if (idx !== -1) return idx; }
return status === 'running' ? 0 : -1;
}, [logs, status]);
const items = [];
PIPELINE_NODES.forEach((node, i) => {
const isActive = activeIdx === i;
const isDone = seen.has(i) && (status === 'done' || (activeIdx !== -1 && activeIdx > i));
const isLit = isActive || isDone;
items.push(
{isDone ? '✓' : node.icon}
{node.label}
);
if (i < PIPELINE_NODES.length - 1) {
items.push(
);
}
});
return {items}
;
}
export default function App() {
const [query, setQuery] = useState('');
const [depth, setDepth] = useState('standard');
const [logs, setLogs] = useState([]);
const [report, setReport] = useState(null);
const [sources, setSources] = useState([]);
const [status, setStatus] = useState('idle');
const [activeTab, setActiveTab] = useState('logs');
const logEndRef = useRef(null);
const abortRef = useRef(null);
useEffect(() => {
if (activeTab === 'logs') logEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [logs, activeTab]);
useEffect(() => { if (report) setActiveTab('result'); }, [report]);
async function handleRun() {
if (!query.trim() || status === 'running') return;
const controller = new AbortController();
abortRef.current = controller;
setLogs([]); setReport(null); setSources([]); setActiveTab('logs'); setStatus('running');
try {
const res = await fetch(`${API}/research`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: query.trim(), depth }),
signal: controller.signal,
});
if (!res.ok) {
const j = await res.json().catch(() => ({}));
setLogs(prev => [...prev, `[ERROR] ${j.error ?? `Server ${res.status}`}`]);
setStatus('error');
return;
}
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buf = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
const lines = buf.split('\n');
buf = lines.pop();
for (const raw of lines) {
if (!raw.startsWith('data: ')) continue;
let payload;
try { payload = JSON.parse(raw.slice(6)); } catch { continue; }
if (payload === '__DONE__') { setStatus(s => s === 'running' ? 'done' : s); return; }
if (payload?.type === 'report') setReport(payload.content);
else if (payload?.type === 'sources') setSources(payload.content ?? []);
else setLogs(prev => [...prev, payload]);
}
}
setStatus('done');
} catch (err) {
if (err.name !== 'AbortError') {
setLogs(prev => [...prev, `[ERROR] ${err.message}`]);
setStatus('error');
}
}
}
function handleReset() {
setQuery(''); setLogs([]); setReport(null); setSources([]);
setStatus('idle'); setActiveTab('logs');
}
const confidence = report ? extractConfidence(report) : null;
const confClass = confidence ? `confidence-${confidence.toLowerCase()}` : '';
return (
{/* ── Left panel ────────────────────────────────────── */}
What do you want researched?
The agent pipeline searches authoritative sources, scores each one for credibility,
and synthesises a fully cited report — only trusted sources make it in.
Research Topic
Depth
{DEPTHS.map(d => (
setDepth(d.id)}
disabled={status === 'running'}>
{d.label}
{d.meta}
))}
{status === 'running' ? <> Researching…> : 'Run Research'}
{status === 'running' && (
{ abortRef.current?.abort(); setStatus('idle'); }}>Stop
)}
{(status === 'done' || status === 'error') && (
Reset
)}
Examples
{EXAMPLES.map((ex, i) => (
setQuery(ex)} disabled={status === 'running'}>
{ex}
))}
{/* ── Right panel ───────────────────────────────────── */}
setActiveTab('logs')}>
Logs {logs.length > 0 && {logs.length} }
setActiveTab('result')} disabled={!report}>
Report {report && ✓ }
setActiveTab('sources')} disabled={sources.length === 0}>
Sources {sources.length > 0 && {sources.length} }
{status !== 'idle' && (
{status === 'running' && }
{status === 'running' ? 'Live' : status === 'done' ? 'Complete' : 'Error'}
)}
{/* Logs */}
{activeTab === 'logs' && (
<>
{logs.length === 0 && status === 'idle' && (
⬡ Agent output will stream here.
)}
{logs.length === 0 && status === 'running' && (
Initializing research pipeline
)}
{logs.map((line, i) => {
const cls = getLogClass(line);
const agentIdx = detectAgentIndex(line);
if (agentIdx !== -1 && (line.toLowerCase().includes('agent:') || line.toLowerCase().includes('working agent'))) {
const node = PIPELINE_NODES[agentIdx];
return (
{node.icon}
{node.label}
);
}
return (
{String(i + 1).padStart(3, ' ')}
{line}
);
})}
{logs.length > 0 && (
{logs.length} lines
navigator.clipboard.writeText(logs.join('\n'))}>Copy all
)}
>
)}
{/* Report */}
{activeTab === 'result' && (
{!report
?
⬡ Report will appear once research completes.
: <>
REPORT
{confidence && {confidence} }
{report.length} chars · {sources.length} sources
triggerDownload(`research_${query.slice(0,30).replace(/[^\w]/g,'_')}.md`, report)}>
⬇ Download .md
>
}
)}
{/* Sources */}
{activeTab === 'sources' && (
{sources.length === 0
?
◈ Sources appear after research completes.
: <>
SOURCES
sorted by credibility
{sources.map((src, i) => {
const score = src.credibility_score ?? 0;
const { color, borderColor } = scoreColor(score);
return (
{score.toFixed(2)}
{src.title || 'Untitled'}
{src.confidence}
);
})}
{sources.filter(s => s.confidence === 'high').length} high ·{' '}
{sources.filter(s => s.confidence === 'low').length} low confidence
>
}
)}
);
}