import { useState, useRef, useEffect, useMemo } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import {
BarChart, Bar,
LineChart, Line,
PieChart, Pie,
ScatterChart, Scatter, ZAxis,
FunnelChart, Funnel, LabelList,
RadarChart, Radar, PolarGrid, PolarAngleAxis, PolarRadiusAxis,
Cell, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer,
} from 'recharts';
import './App.css';
const API = import.meta.env.VITE_API_URL ?? 'http://localhost:8000';
const CHART_COLORS = [
'#6366f1',
'#34d399',
'#f472b6',
'#fbbf24',
'#60a5fa',
'#a78bfa',
'#fb923c',
];
const HISTORY_KEY = 'dataflow_history';
const MAX_HISTORY = 5;
const PIPELINE_NODES = [
{ id: 'context', label: 'Context', match: 'analysis directive specialist', icon: '◈', color: '#6366f1' },
{ id: 'cleaner', label: 'Cleaner', match: 'data quality inspector', icon: '✧', color: '#60a5fa' },
{ id: 'prompt', label: 'Prompt Eng', match: 'data analysis prompt engineer', icon: '✦', color: '#8b5cf6' },
{ id: 'analyst', label: 'Analyst', match: 'senior data analyst', icon: '⬡', color: '#34d399' },
{ id: 'formatter', label: 'Formatter', match: 'structured output specialist', icon: '◉', color: '#f472b6' },
{ id: 'qa', label: 'QA Critic', match: 'analysis quality critic', icon: '◎', color: '#fb923c' },
];
// ── Helpers ───────────────────────────────────────────────────────────────────
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 loadHistory() {
try {
return JSON.parse(localStorage.getItem(HISTORY_KEY) || '[]');
} catch {
return [];
}
}
function saveHistory(entries) {
try {
localStorage.setItem(HISTORY_KEY, JSON.stringify(entries));
} catch {}
}
function timeAgo(ts) {
const diff = Date.now() - new Date(ts).getTime();
const m = Math.floor(diff / 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`;
return `${Math.floor(h / 24)}d ago`;
}
function getLogClass(line) {
const l = line.toLowerCase();
if (l.startsWith('[rotate]') || l.startsWith('[retry]')) return 'log-rotate';
if (
l.startsWith('[error]') ||
l.includes('traceback') ||
l.includes('exception')
)
return 'log-error';
if (l.includes('warning') || l.includes('warn')) return 'log-warn';
if (
detectAgentIndex(line) !== -1 &&
(l.includes('agent:') ||
l.includes('# agent') ||
l.includes('working agent'))
)
return 'log-agent-header';
if (
l.includes('agent:') ||
l.includes('task:') ||
l.includes('> entering') ||
l.includes('crew')
)
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 encodeShare(str) {
try { return btoa(unescape(encodeURIComponent(str))); } catch { return ''; }
}
function decodeShare(str) {
try { return decodeURIComponent(escape(atob(str))); } catch { return null; }
}
function parseResult(content) {
try {
const parsed = JSON.parse(content.trim());
const STRUCTURED_TYPES = ['chart', 'report', 'code', 'table', 'metrics'];
if (STRUCTURED_TYPES.includes(parsed.output_type))
return { type: 'structured', data: parsed };
return { type: 'json', data: parsed };
} catch {}
const lines = content
.trim()
.split('\n')
.filter((l) => l.trim());
if (lines.length >= 2) {
const cols = lines[0].split(',').length;
if (
cols > 1 &&
lines.slice(1, 5).every((l) => l.split(',').length === cols)
)
return { type: 'csv', data: content };
}
return { type: 'text', data: content };
}
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);
}
// ── Typewriter hook ───────────────────────────────────────────────────────────
function useTypewriter(text, speed = 10) {
const [displayed, setDisplayed] = useState('');
const [done, setDone] = useState(false);
const prevRef = useRef('');
useEffect(() => {
if (!text) {
setDisplayed('');
setDone(false);
return;
}
if (text === prevRef.current) return;
prevRef.current = text;
setDisplayed('');
setDone(false);
let i = 0;
const id = setInterval(() => {
i++;
setDisplayed(text.slice(0, i));
if (i >= text.length) {
setDone(true);
clearInterval(id);
}
}, speed);
return () => clearInterval(id);
}, [text, speed]);
return { displayed, done };
}
// ── Pipeline diagram ──────────────────────────────────────────────────────────
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}
;
}
// ── Download popup ────────────────────────────────────────────────────────────
function DownloadPopup({ parsed, onClose }) {
const options = [];
if (parsed.type === 'structured') {
const d = parsed.data;
const reportMd = [
`# ${d.chart_title || 'Analysis Report'}`,
`\n## Summary\n${d.summary}`,
`\n## Findings\n${d.findings.map((f) => `- ${f}`).join('\n')}`,
`\n## Recommendations\n${d.recommendations.map((r) => `- ${r}`).join('\n')}`,
].join('\n');
options.push({ icon: '{}', label: 'Full JSON', ext: 'json', content: JSON.stringify(d, null, 2) });
options.push({ icon: '📝', label: 'Report (Markdown)', ext: 'md', content: reportMd });
if (d.data_points?.length) {
const csv = ['label,value,category,x_value', ...d.data_points.map((p) => `${p.label},${p.value},${p.category ?? ''},${p.x_value ?? ''}`)];
options.push({ icon: '⊞', label: 'Chart Data (CSV)', ext: 'csv', content: csv.join('\n') });
}
if (d.code_blocks?.length) {
const ext = { python: 'py', sql: 'sql', bash: 'sh', r: 'r', javascript: 'js' };
d.code_blocks.forEach((b, i) => {
options.push({ icon: '{ }', label: `Code: ${b.title}`, ext: ext[b.language] ?? 'txt', content: b.code });
});
}
if (d.table_headers?.length) {
const csv = [d.table_headers.join(','), ...d.table_rows.map((r) => r.join(','))];
options.push({ icon: '⊞', label: 'Table (CSV)', ext: 'csv', content: csv.join('\n') });
}
if (d.metrics?.length) {
const csv = ['label,value,unit,trend,change,context', ...d.metrics.map((m) => `${m.label},${m.value},${m.unit ?? ''},${m.trend ?? ''},${m.change ?? ''},${m.context ?? ''}`)];
options.push({ icon: '◈', label: 'Metrics (CSV)', ext: 'csv', content: csv.join('\n') });
}
if (d.comparison_rows?.length) {
const csv = [`metric,${d.comparison_a_label || 'A'},${d.comparison_b_label || 'B'},winner`,
...d.comparison_rows.map((r) => `${r.metric},${r.value_a},${r.value_b},${r.winner ?? ''}`)];
options.push({ icon: '⇌', label: 'Comparison (CSV)', ext: 'csv', content: csv.join('\n') });
}
if (d.heatmap_values?.length) {
const header = ['', ...(d.heatmap_col_labels ?? [])].join(',');
const dataRows = (d.heatmap_row_labels ?? []).map((r, i) =>
[r, ...(d.heatmap_values[i] ?? [])].join(',')
);
options.push({ icon: '▦', label: 'Heatmap (CSV)', ext: 'csv', content: [header, ...dataRows].join('\n') });
}
} else if (parsed.type === 'json') {
options.push({
icon: '{}',
label: 'JSON',
ext: 'json',
content: JSON.stringify(parsed.data, null, 2),
});
} else if (parsed.type === 'csv') {
options.push({ icon: '⊞', label: 'CSV', ext: 'csv', content: parsed.data });
}
options.push({
icon: '📄',
label: 'Plain Text',
ext: 'txt',
content:
typeof parsed.data === 'string'
? parsed.data
: JSON.stringify(parsed.data, null, 2),
});
return (
e.stopPropagation()}>
Download Result
{options.map((opt, i) => (
))}
);
}
// ── Chart renderer ────────────────────────────────────────────────────────────
function ChartView({ data, overrideType }) {
const points = data.data_points ?? [];
const chartType = overrideType || data.chart_type;
const chartData = points.map((p) => ({
name: p.label,
value: p.value,
x: p.x_value ?? null,
category: p.category,
}));
const tooltipStyle = {
background: '#13141a',
border: '1px solid #1e2030',
borderRadius: 8,
fontSize: 12,
};
if (chartType === 'pie') {
return (
`${name} ${(percent * 100).toFixed(0)}%`
}
labelLine={false}>
{chartData.map((_, i) => (
|
))}
);
}
if (chartType === 'scatter') {
const scatterData = chartData.map((p) => ({
x: p.x ?? 0,
y: p.value,
name: p.name,
}));
return (
{
if (!payload?.length) return null;
const d = payload[0].payload;
return (
{d.name}
{data.x_axis_label}: {d.x}
{data.y_axis_label}: {d.y}
);
}}
/>
);
}
if (chartType === 'line') {
return (
);
}
if (chartType === 'funnel') {
const funnelData = chartData.map((d, i) => ({
...d,
fill: CHART_COLORS[i % CHART_COLORS.length],
}));
return (
);
}
if (chartType === 'radar') {
const radarData = points.map((p) => ({
subject: p.label,
value: p.value,
value2: p.value2 ?? null,
}));
const hasB = radarData.some((d) => d.value2 != null);
return (
{hasB && (
)}
);
}
// Default: bar
return (
{chartData.map((_, i) => (
|
))}
);
}
// ── Data table for chart points ───────────────────────────────────────────────
function DataPointTable({ data }) {
const points = data.data_points ?? [];
const hasCategory = points.some((p) => p.category);
const hasX = points.some((p) => p.x_value != null);
return (
| {data.x_axis_label || 'Label'} |
{hasX && X Value | }
{data.y_axis_label || 'Value'} |
{hasCategory && Category | }
{points.map((p, i) => (
| {p.label} |
{hasX && {p.x_value?.toLocaleString() ?? '—'} | }
{p.value.toLocaleString()} |
{hasCategory && {p.category ?? '—'} | }
))}
);
}
// ── Result views ──────────────────────────────────────────────────────────────
function StructuredReport({ data }) {
const { displayed: sumDisplayed, done: sumDone } = useTypewriter(data.summary, 10);
const hasChart = data.output_type === 'chart' && data.data_points?.length > 0;
const hasCode = data.output_type === 'code' && data.code_blocks?.length > 0;
const hasMetrics = data.output_type === 'metrics' && data.metrics?.length > 0;
const hasTable = data.output_type === 'table' && data.table_headers?.length > 0;
const hasComparison = data.output_type === 'comparison' && data.comparison_rows?.length > 0;
const hasHeatmap = data.output_type === 'heatmap' && data.heatmap_values?.length > 0;
return (
{hasChart && (
{data.chart_title &&
{data.chart_title}
}
)}
{hasMetrics && (
)}
{hasCode && (
)}
{hasTable && (
)}
{hasComparison && (
)}
{hasHeatmap && (
)}
{sumDisplayed}
{!sumDone && }
{data.findings?.length > 0 && (
Findings
{data.findings.map((f, i) => (
-
{f}
))}
)}
{data.recommendations?.length > 0 && (
Recommendations
{data.recommendations.map((r, i) => (
-
{r}
))}
)}
);
}
function ResultView({ parsed }) {
if (parsed.type === 'structured')
return ;
if (parsed.type === 'json')
return (
{JSON.stringify(parsed.data, null, 2)}
);
if (parsed.type === 'csv') {
const lines = parsed.data
.trim()
.split('\n')
.filter((l) => l.trim());
const headers = lines[0]
.split(',')
.map((h) => h.trim().replace(/^"|"$/g, ''));
const rows = lines
.slice(1)
.map((l) => l.split(',').map((c) => c.trim().replace(/^"|"$/g, '')));
return (
{headers.map((h, i) => (
| {h} |
))}
{rows.map((row, i) => (
{row.map((c, j) => (
| {c} |
))}
))}
);
}
return (
{parsed.data}
);
}
// ── History drawer ────────────────────────────────────────────────────────────
const TYPE_COLORS = {
chart: '#a78bfa',
report: '#34d399',
json: '#60a5fa',
csv: '#fbbf24',
text: '#94a3b8',
};
function HistoryDrawer({ history, onSelect, onClose }) {
return (
e.stopPropagation()}>
Session History
{history.length === 0 ? (
No saved sessions yet. Complete an analysis to save it.
) : (
history.map((entry) => {
const c = TYPE_COLORS[entry.resultType] || '#94a3b8';
return (
);
})
)}
);
}
// ── Code view ─────────────────────────────────────────────────────────────────
const LANG_COLORS = {
python: '#3b82f6',
sql: '#f59e0b',
bash: '#34d399',
r: '#a78bfa',
javascript: '#fbbf24',
};
function CodeView({ data }) {
const blocks = data.code_blocks ?? [];
const [copied, setCopied] = useState(null);
function copyBlock(code, i) {
navigator.clipboard.writeText(code).then(() => {
setCopied(i);
setTimeout(() => setCopied(null), 1500);
});
}
return (
{blocks.map((block, i) => (
{block.language}
{block.title}
{block.code}
))}
);
}
// ── Metrics view ──────────────────────────────────────────────────────────────
const TREND_ICON = { up: '↑', down: '↓', neutral: '→' };
const TREND_COLOR = { up: '#34d399', down: '#f87171', neutral: '#6b7280' };
function MetricsView({ data }) {
const items = data.metrics ?? [];
return (
{items.map((m, i) => {
const tc = TREND_COLOR[m.trend] ?? '#6b7280';
const ti = TREND_ICON[m.trend] ?? '';
return (
{m.label}
{m.value}
{m.unit && {m.unit}}
{(m.trend || m.change) && (
{ti && {ti}}
{m.change && {m.change}}
{m.context && {m.context}}
)}
);
})}
);
}
// ── Table output view ─────────────────────────────────────────────────────────
function TableOutputView({ data }) {
const headers = data.table_headers ?? [];
const rows = data.table_rows ?? [];
return (
{headers.map((h, i) => | {h} | )}
{rows.map((row, i) => (
{row.map((cell, j) => | {cell} | )}
))}
);
}
// ── Comparison view ───────────────────────────────────────────────────────────
function ComparisonView({ data }) {
const rows = data.comparison_rows ?? [];
const aLabel = data.comparison_a_label || 'A';
const bLabel = data.comparison_b_label || 'B';
const aWins = rows.filter((r) => r.winner === 'a').length;
const bWins = rows.filter((r) => r.winner === 'b').length;
return (
{rows.map((row, i) => (
{row.metric}
{row.winner === 'a' && ▲}
{row.value_a}
{row.winner === 'b' && ▲}
{row.value_b}
))}
{rows.length > 0 && (
{aLabel}: {aWins} wins
{bLabel}: {bWins} wins
{rows.length - aWins - bWins > 0 && (
{rows.length - aWins - bWins} ties
)}
)}
);
}
// ── Heatmap view ──────────────────────────────────────────────────────────────
function HeatmapView({ data }) {
const rows = data.heatmap_row_labels ?? [];
const cols = data.heatmap_col_labels ?? [];
const values = data.heatmap_values ?? [];
const flat = values.flat().filter((v) => typeof v === 'number');
const min = flat.length ? Math.min(...flat) : 0;
const max = flat.length ? Math.max(...flat) : 1;
function colorFor(v) {
const t = max === min ? 0.5 : (v - min) / (max - min);
const r = Math.round(60 + t * 190);
const g = Math.round(70 - t * 30);
const b = Math.round(241 - t * 180);
return `rgba(${r},${g},${b},${0.3 + t * 0.6})`;
}
return (
{data.heatmap_title &&
{data.heatmap_title}
}
{cols.map((c, j) => (
{c}
))}
{rows.map((r, i) => (
<>
{r}
{cols.map((_, j) => {
const v = values[i]?.[j] ?? 0;
return (
{v.toLocaleString(undefined, { maximumFractionDigits: 1 })}
);
})}
>
))}
);
}
// ── Artifacts panel (full chart view + type switcher + data table) ────────────
const CHART_TYPES = [
{ id: 'bar', label: '▬ Bar' },
{ id: 'line', label: '╱ Line' },
{ id: 'pie', label: '◔ Pie' },
{ id: 'scatter', label: '⁘ Scatter' },
{ id: 'funnel', label: '⯆ Funnel' },
{ id: 'radar', label: '⬡ Radar' },
];
function ArtifactsPanel({ parsed, onDownload }) {
const data = parsed.data;
const otype = data.output_type;
// chart-specific state
const [viewType, setViewType] = useState(data.chart_type || 'bar');
const [artifactTab, setArtifactTab] = useState(
otype === 'code' ? 'code'
: otype === 'metrics' ? 'metrics'
: otype === 'table' ? 'table'
: otype === 'comparison' ? 'comparison'
: otype === 'heatmap' ? 'heatmap'
: 'chart'
);
const hasScatterData = data.data_points?.some((p) => p.x_value != null);
const hasRadarB = data.data_points?.some((p) => p.value2 != null);
const availableTypes = CHART_TYPES.filter((t) => {
if (t.id === 'scatter') return hasScatterData;
if (t.id === 'radar') return (data.data_points?.length ?? 0) >= 3;
if (t.id === 'pie') return (data.data_points?.length ?? 0) <= 6;
if (t.id === 'funnel') return (data.data_points?.length ?? 0) >= 2;
return true;
});
const badgeLabel = {
chart: 'CHART', code: 'CODE', table: 'TABLE',
metrics: 'METRICS', comparison: 'COMPARE', heatmap: 'HEATMAP',
}[otype] ?? otype.toUpperCase();
const footerInfo = otype === 'chart' ? `${data.data_points?.length} data points · ${viewType} view`
: otype === 'code' ? `${data.code_blocks?.length} block(s)`
: otype === 'table' ? `${data.table_rows?.length} rows · ${data.table_headers?.length} cols`
: otype === 'metrics' ? `${data.metrics?.length} metrics`
: otype === 'comparison' ? `${data.comparison_rows?.length} metrics compared`
: otype === 'heatmap' ? `${data.heatmap_row_labels?.length} × ${data.heatmap_col_labels?.length} matrix`
: '';
return (
{badgeLabel}
{otype === 'chart' && (
{availableTypes.map((t) => (
))}
)}
{otype === 'chart' && (
{['chart', 'table', 'stats'].map((tab) => (
))}
)}
{otype === 'code' &&
}
{otype === 'metrics' &&
}
{otype === 'table' &&
}
{otype === 'comparison' &&
}
{otype === 'heatmap' &&
}
{otype === 'chart' && (
<>
{artifactTab === 'chart' && (
<>
{data.chart_title &&
{data.chart_title}
}
{data.x_axis_label &&
X: {data.x_axis_label} · Y: {data.y_axis_label}
}
>
)}
{artifactTab === 'table' &&
}
{artifactTab === 'stats' && (
{data.data_points.map((p, i) => (
{p.label}
{p.value.toLocaleString()}
{p.category && {p.category}}
))}
)}
>
)}
{footerInfo}
);
}
// ── File preview panel ────────────────────────────────────────────────────────
function FilePreviewPanel({ previews, files }) {
const [expanded, setExpanded] = useState(true);
const entries = files.filter((f) => previews[f.name]);
if (entries.length === 0) return null;
return (
setExpanded((v) => !v)}>
File Preview
{expanded ? '▾' : '▸'}
{expanded && entries.map((f, i) => (
{f.name}
{previews[f.name]}
))}
);
}
// ── Main app ──────────────────────────────────────────────────────────────────
export default function App() {
const [context, setContext] = useState('');
const [files, setFiles] = useState([]);
const [logs, setLogs] = useState([]);
const [result, setResult] = useState(null);
const [status, setStatus] = useState('idle');
const [showLog, setShowLog] = useState(true);
const [activeTab, setActiveTab] = useState('logs');
const [showDownload, setShowDownload] = useState(false);
const [showHistory, setShowHistory] = useState(false);
const [history, setHistory] = useState(() => loadHistory());
const [filePreviews, setFilePreviews] = useState({});
const [shareCopied, setShareCopied] = useState(false);
const logEndRef = useRef(null);
const fileInputRef = useRef(null);
const abortRef = useRef(null);
const analyzeRef = useRef(null);
useEffect(() => {
if (activeTab === 'logs')
logEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [logs, activeTab]);
// Load shared result from URL hash on mount
useEffect(() => {
const hash = window.location.hash;
if (hash.startsWith('#r=')) {
const decoded = decodeShare(hash.slice(3));
if (decoded) {
setResult(decoded);
setStatus('done');
setActiveTab('result');
}
}
}, []);
// Read file previews when files change
useEffect(() => {
if (files.length === 0) { setFilePreviews({}); return; }
files.forEach((f) => {
if (filePreviews[f.name]) return;
if (f.type === 'application/pdf') {
setFilePreviews((p) => ({ ...p, [f.name]: '(PDF — preview not available)' }));
return;
}
f.text()
.then((text) => {
const lines = text.split('\n').slice(0, 8).join('\n');
const preview = lines.length > 500 ? lines.slice(0, 500) + '…' : lines;
setFilePreviews((p) => ({ ...p, [f.name]: preview }));
})
.catch(() => setFilePreviews((p) => ({ ...p, [f.name]: '(preview unavailable)' })));
});
}, [files]); // eslint-disable-line
// Keyboard shortcuts
useEffect(() => {
function onKeyDown(e) {
if (e.key === 'Escape') {
setShowDownload(false);
setShowHistory(false);
}
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
e.preventDefault();
analyzeRef.current?.();
}
}
window.addEventListener('keydown', onKeyDown);
return () => window.removeEventListener('keydown', onKeyDown);
}, []);
useEffect(() => {
if (result) setActiveTab('result');
}, [result]);
// Save completed run to history
useEffect(() => {
if (status !== 'done' || !result) return;
const p = parseResult(result);
const entry = {
id: Date.now(),
timestamp: new Date().toISOString(),
context,
fileName: files.length > 0 ? files.map((f) => f.name).join(', ') : null,
result,
logCount: logs.length,
resultType: p.type === 'structured' ? p.data.output_type : p.type,
};
const updated = [entry, ...history].slice(0, MAX_HISTORY);
setHistory(updated);
saveHistory(updated);
}, [status]); // eslint-disable-line
const parsed = result ? parseResult(result) : null;
const hasChart =
parsed?.type === 'structured' &&
parsed.data?.output_type === 'chart' &&
parsed.data?.data_points?.length > 0;
const hasArtifacts =
parsed?.type === 'structured' &&
['chart', 'code', 'table', 'metrics', 'comparison', 'heatmap'].includes(parsed.data?.output_type);
async function handleAnalyze(isAutoRetry = false) {
if (!context.trim() || (status === 'running' && !isAutoRetry)) return;
const controller = new AbortController();
abortRef.current = controller;
if (isAutoRetry) {
setLogs((prev) => [
...prev,
'[RETRY] All providers rate-limited. Auto-retrying in 10s…',
]);
await new Promise((r) => setTimeout(r, 10000));
if (controller.signal.aborted) return;
}
setLogs(isAutoRetry ? (prev) => [...prev] : []);
setResult(null);
setActiveTab('logs');
setStatus('running');
setShowLog(true);
const form = new FormData();
form.append('context', context);
files.forEach((f) => form.append('files', f));
let gotResult = false;
try {
const res = await fetch(`${API}/analyze`, {
method: 'POST',
body: form,
signal: controller.signal,
});
if (!res.ok) {
let msg = `Server returned ${res.status}`;
try {
const j = await res.json();
if (j.error) msg = j.error;
} catch {}
setLogs((prev) => [...prev, `[ERROR] ${msg}`]);
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__') {
if (!gotResult && !isAutoRetry) {
// All rotations exhausted — auto-retry once
handleAnalyze(true);
} else {
setStatus(gotResult ? 'done' : 'error');
}
return;
}
if (payload?.type === 'result') {
gotResult = true;
setResult(payload.content);
} else {
setLogs((prev) => [...prev, payload]);
}
}
}
setStatus('done');
} catch (err) {
if (err.name !== 'AbortError') {
setLogs((prev) => [...prev, `[ERROR] ${err.message}`]);
setStatus('error');
}
}
}
// Keep ref in sync so keyboard shortcut always calls latest version
analyzeRef.current = handleAnalyze;
function shareResult() {
if (!result) return;
const encoded = encodeShare(result);
const base = window.location.href.split('#')[0];
const url = base + '#r=' + encoded;
window.location.hash = 'r=' + encoded;
navigator.clipboard.writeText(url).then(() => {
setShareCopied(true);
setTimeout(() => setShareCopied(false), 2000);
});
}
function handleReset() {
setLogs([]);
setResult(null);
setStatus('idle');
setActiveTab('logs');
setFiles([]);
setFilePreviews({});
setContext('');
if (fileInputRef.current) fileInputRef.current.value = '';
window.location.hash = '';
}
function handleHistorySelect(entry) {
setContext(entry.context);
setResult(entry.result);
setLogs([]);
setStatus('done');
setActiveTab('result');
setFiles([]);
setShowHistory(false);
}
return (
What do you want to analyze?
Describe your dataset and the question you want answered. The agent
pipeline will interpret your request, clean the data, and extract
insights.
0 ? ' dropzone--has-file' : ''}`}
onDrop={(e) => {
e.preventDefault();
const dropped = Array.from(e.dataTransfer.files).slice(0, 3);
setFiles((prev) => [...prev, ...dropped].slice(0, 3));
}}
onDragOver={(e) => e.preventDefault()}
onClick={() => fileInputRef.current?.click()}
role='button'
tabIndex={0}
onKeyDown={(e) => e.key === 'Enter' && fileInputRef.current?.click()}>
{
const picked = Array.from(e.target.files).slice(0, 3);
setFiles((prev) => [...prev, ...picked].slice(0, 3));
e.target.value = '';
}}
style={{ display: 'none' }}
/>
{files.length > 0 ? (
{files.map((f, i) => (
📄
{f.name}
))}
{files.length < 3 && (
+ add more
)}
) : (
Drop files here or click to upload
CSV · JSON · TXT · PDF · XML · up to 3 files
)}
{status === 'running' && (
)}
{(status === 'done' || status === 'error') && (
<>
>
)}
Ctrl+Enter
{hasArtifacts && (
)}
{status !== 'idle' && (
{status === 'running' && }
{status === 'running'
? 'Live'
: status === 'done'
? 'Complete'
: 'Error'}
)}
{activeTab === 'logs' && (
<>
{logs.length === 0 && status === 'idle' && (
⬡
Agent output will stream here in real-time.
)}
{logs.length === 0 && status === 'running' && (
Initializing agent pipeline
)}
{logs.map((line, i) => {
const cls = getLogClass(line);
if (cls === 'log-agent-header') {
const agentIdx = detectAgentIndex(line);
if (agentIdx !== -1) {
const node = PIPELINE_NODES[agentIdx];
return (
{node.icon}
{node.label}
);
}
}
return (
{String(i + 1).padStart(3, ' ')}
{line}
);
})}
{logs.length > 0 && (
{logs.length} lines
)}
>
)}
{activeTab === 'result' && (
{!parsed ? (
⬡
Result will appear once analysis completes.
) : (
<>
{parsed.type === 'structured'
? parsed.data.output_type.toUpperCase()
: parsed.type.toUpperCase()}
{parsed.type === 'structured' && parsed.data.chart_type && (
{parsed.data.chart_type} chart
)}
{parsed.type === 'structured' && parsed.data.quality_score != null && (
= 8 ? '#34d399'
: parsed.data.quality_score >= 5 ? '#fbbf24'
: '#f87171',
}}>
Quality {parsed.data.quality_score}/10
)}
{parsed.type === 'structured' && parsed.data.quality_verdict && (
{parsed.data.quality_verdict}
)}
{result.length} chars
>
)}
)}
{activeTab === 'artifacts' && hasArtifacts && (
setShowDownload(true)}
/>
)}
{showDownload && parsed && (
setShowDownload(false)}
/>
)}
{showHistory && (
setShowHistory(false)}
/>
)}
);
}