Zayne Rea Sprague
feat: add AdaEvolve visualizer tab for reasoning stickiness analysis
e6cfd0f
import { useEffect, useState, useMemo } from "react";
import { useAppState } from "./store";
import type { IterationSummary } from "./types";
const ADAPTATION_COLORS: Record<string, { bg: string; text: string; bar: string }> = {
L1_explore: { bg: "bg-blue-900/40", text: "text-blue-400", bar: "bg-blue-500" },
L1_exploit: { bg: "bg-green-900/40", text: "text-green-400", bar: "bg-green-500" },
L2_migrate: { bg: "bg-amber-900/40", text: "text-amber-400", bar: "bg-amber-500" },
L3_meta: { bg: "bg-red-900/40", text: "text-red-400", bar: "bg-red-500" },
};
const DEFAULT_COLOR = { bg: "bg-gray-900/40", text: "text-gray-400", bar: "bg-gray-500" };
function getAdaptationColor(type: string) {
return ADAPTATION_COLORS[type] || DEFAULT_COLOR;
}
type SortKey = "iteration" | "score" | "best_score" | "delta" | "island_id" | "adaptation_type";
type SortDir = "asc" | "desc";
export default function AdaevolveApp() {
const store = useAppState();
const { state } = store;
const [repoInput, setRepoInput] = useState("");
const [sortKey, setSortKey] = useState<SortKey>("iteration");
const [sortDir, setSortDir] = useState<SortDir>("asc");
useEffect(() => {
store.loadPresets();
}, []);
// Get the currently selected dataset
const selectedDataset = useMemo(
() => state.datasets.find((d) => d.id === state.selectedDatasetId) ?? null,
[state.datasets, state.selectedDatasetId]
);
// Filter iterations by adaptation type
const filteredIterations = useMemo(() => {
if (!selectedDataset) return [];
let iters = selectedDataset.iterations;
if (state.filterAdaptation) {
iters = iters.filter((it) => it.adaptation_type === state.filterAdaptation);
}
return iters;
}, [selectedDataset, state.filterAdaptation]);
// Sort iterations
const sortedIterations = useMemo(() => {
const sorted = [...filteredIterations];
sorted.sort((a, b) => {
const av = a[sortKey];
const bv = b[sortKey];
if (typeof av === "number" && typeof bv === "number") {
return sortDir === "asc" ? av - bv : bv - av;
}
const sa = String(av);
const sb = String(bv);
return sortDir === "asc" ? sa.localeCompare(sb) : sb.localeCompare(sa);
});
return sorted;
}, [filteredIterations, sortKey, sortDir]);
// Get all unique adaptation types across selected dataset
const adaptationTypes = useMemo(() => {
if (!selectedDataset) return [];
const types = new Set(selectedDataset.iterations.map((it) => it.adaptation_type));
return Array.from(types).sort();
}, [selectedDataset]);
function handleSort(key: SortKey) {
if (sortKey === key) {
setSortDir((d) => (d === "asc" ? "desc" : "asc"));
} else {
setSortKey(key);
setSortDir("asc");
}
}
function handleLoad() {
if (repoInput.trim()) {
store.loadDataset(repoInput.trim());
setRepoInput("");
}
}
// Compute max score for bar chart scaling
const maxScore = useMemo(() => {
if (!filteredIterations.length) return 1;
return Math.max(...filteredIterations.map((it) => Math.abs(it.score)), 0.001);
}, [filteredIterations]);
const sortIndicator = (key: SortKey) => {
if (sortKey !== key) return "";
return sortDir === "asc" ? " ^" : " v";
};
return (
<div className="flex h-full overflow-hidden">
{/* Sidebar */}
<div className="w-72 border-r border-gray-800 bg-gray-900 flex flex-col overflow-hidden shrink-0">
{/* Repo input */}
<div className="p-3 border-b border-gray-800">
<label className="text-xs text-gray-500 mb-1 block">HuggingFace Repo</label>
<div className="flex gap-1">
<input
type="text"
value={repoInput}
onChange={(e) => setRepoInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleLoad()}
placeholder="org/dataset-name"
className="flex-1 bg-gray-800 text-sm px-2 py-1.5 rounded border border-gray-700 focus:border-rose-500 focus:outline-none text-gray-200"
/>
<button
onClick={handleLoad}
disabled={state.loading || !repoInput.trim()}
className="bg-rose-600 hover:bg-rose-500 disabled:opacity-50 text-white text-sm px-3 py-1.5 rounded"
>
Load
</button>
</div>
</div>
{/* Presets */}
<div className="p-3 border-b border-gray-800">
<div className="text-xs text-gray-500 mb-2">Presets</div>
{state.presets.length === 0 && (
<div className="text-xs text-gray-600">No presets available</div>
)}
<div className="space-y-1 max-h-32 overflow-y-auto">
{state.presets.map((p) => (
<div
key={p.id}
className="flex items-center justify-between group"
>
<button
onClick={() => store.loadPreset(p)}
className="text-xs text-rose-400 hover:text-rose-300 truncate flex-1 text-left"
title={p.repo}
>
{p.name}
</button>
<button
onClick={() => store.deletePreset(p.id)}
className="text-gray-600 hover:text-red-400 text-xs opacity-0 group-hover:opacity-100 ml-1"
>
x
</button>
</div>
))}
</div>
</div>
{/* Loaded datasets */}
<div className="flex-1 overflow-y-auto p-3">
<div className="text-xs text-gray-500 mb-2">
Loaded Datasets ({state.datasets.length})
</div>
{state.datasets.map((ds) => (
<div
key={ds.id}
className={`mb-3 p-2 rounded border cursor-pointer ${
state.selectedDatasetId === ds.id
? "border-rose-500 bg-rose-900/20"
: "border-gray-700 hover:border-gray-600"
}`}
onClick={() => store.selectDataset(ds.id)}
>
<div className="flex items-center justify-between">
<span className="text-sm text-gray-200 truncate" title={ds.repo}>
{ds.name}
</span>
<button
onClick={(e) => {
e.stopPropagation();
store.unloadDataset(ds.id);
}}
className="text-gray-600 hover:text-red-400 text-xs ml-1"
>
x
</button>
</div>
<div className="text-xs text-gray-500 mt-1">
{ds.n_iterations} iterations | {ds.summary_stats.n_islands} islands
</div>
<div className="text-xs text-gray-500">
Best: {ds.summary_stats.global_best.toFixed(4)}
</div>
{/* Adaptation breakdown */}
<div className="mt-1 space-y-0.5">
{Object.entries(ds.summary_stats.adaptation_counts).map(([type, count]) => {
const color = getAdaptationColor(type);
return (
<div key={type} className="flex items-center text-xs gap-1">
<span className={`w-2 h-2 rounded-full ${color.bar}`} />
<span className={color.text}>{type}</span>
<span className="text-gray-600 ml-auto">{count}</span>
</div>
);
})}
</div>
</div>
))}
</div>
{/* Filter by adaptation type */}
{selectedDataset && (
<div className="p-3 border-t border-gray-800">
<div className="text-xs text-gray-500 mb-1">Filter by Adaptation</div>
<select
value={state.filterAdaptation}
onChange={(e) => store.setFilterAdaptation(e.target.value)}
className="w-full bg-gray-800 text-sm px-2 py-1 rounded border border-gray-700 text-gray-200"
>
<option value="">All</option>
{adaptationTypes.map((t) => (
<option key={t} value={t}>
{t}
</option>
))}
</select>
</div>
)}
</div>
{/* Main content */}
<div className="flex-1 flex flex-col overflow-hidden">
{state.error && (
<div className="bg-red-900/50 border-b border-red-700 px-4 py-2 text-sm text-red-200 flex justify-between items-center">
<span>{state.error}</span>
<button
onClick={() => store.setError(null)}
className="text-red-400 hover:text-red-200 ml-4"
>
dismiss
</button>
</div>
)}
{state.loading && (
<div className="bg-rose-900/30 border-b border-rose-800 px-4 py-1.5 text-xs text-rose-300">
Loading...
</div>
)}
{/* Timeline view */}
{state.viewMode === "timeline" && selectedDataset && (
<div className="flex-1 flex flex-col overflow-hidden">
{/* Bar chart */}
<div className="border-b border-gray-800 p-4 shrink-0">
<div className="text-sm text-gray-400 mb-2">
Score Timeline ({filteredIterations.length} iterations)
</div>
<div className="flex items-end gap-px h-32 overflow-x-auto">
{filteredIterations.map((it) => {
const height = Math.max((Math.abs(it.score) / maxScore) * 100, 2);
const color = getAdaptationColor(it.adaptation_type);
return (
<div
key={it.index}
className="flex flex-col items-center justify-end flex-shrink-0 cursor-pointer group"
style={{ minWidth: Math.max(4, Math.min(20, 800 / filteredIterations.length)) }}
onClick={() => store.selectIteration(selectedDataset.id, it.index)}
title={`iter ${it.iteration} | score ${it.score.toFixed(4)} | ${it.adaptation_type}`}
>
<div
className={`w-full rounded-t ${color.bar} group-hover:opacity-80 transition-opacity`}
style={{ height: `${height}%` }}
/>
</div>
);
})}
</div>
{/* Legend */}
<div className="flex gap-4 mt-2">
{Object.entries(ADAPTATION_COLORS).map(([type, color]) => (
<div key={type} className="flex items-center gap-1 text-xs">
<span className={`w-2 h-2 rounded-full ${color.bar}`} />
<span className={color.text}>{type}</span>
</div>
))}
</div>
</div>
{/* Iteration table */}
<div className="flex-1 overflow-auto">
<table className="w-full text-sm">
<thead className="sticky top-0 bg-gray-900 border-b border-gray-800">
<tr>
{(
[
["iteration", "Iter"],
["island_id", "Island"],
["score", "Score"],
["best_score", "Best"],
["delta", "Delta"],
["adaptation_type", "Adaptation"],
] as [SortKey, string][]
).map(([key, label]) => (
<th
key={key}
onClick={() => handleSort(key)}
className="px-3 py-2 text-left text-gray-400 font-medium cursor-pointer hover:text-gray-200 select-none"
>
{label}
{sortIndicator(key)}
</th>
))}
<th className="px-3 py-2 text-left text-gray-400 font-medium">Valid</th>
<th className="px-3 py-2 text-left text-gray-400 font-medium">Task</th>
</tr>
</thead>
<tbody>
{sortedIterations.map((it) => {
const color = getAdaptationColor(it.adaptation_type);
return (
<tr
key={it.index}
onClick={() => store.selectIteration(selectedDataset.id, it.index)}
className="border-b border-gray-800/50 hover:bg-gray-800/50 cursor-pointer"
>
<td className="px-3 py-1.5 text-gray-300">{it.iteration}</td>
<td className="px-3 py-1.5 text-gray-300">{it.island_id}</td>
<td className="px-3 py-1.5 text-gray-200 font-mono">
{it.score.toFixed(4)}
</td>
<td className="px-3 py-1.5 text-gray-200 font-mono">
{it.best_score.toFixed(4)}
</td>
<td
className={`px-3 py-1.5 font-mono ${
it.delta > 0
? "text-green-400"
: it.delta < 0
? "text-red-400"
: "text-gray-500"
}`}
>
{it.delta > 0 ? "+" : ""}
{it.delta.toFixed(4)}
</td>
<td className="px-3 py-1.5">
<span
className={`px-1.5 py-0.5 rounded text-xs ${color.bg} ${color.text}`}
>
{it.adaptation_type}
</span>
</td>
<td className="px-3 py-1.5">
{it.is_valid ? (
<span className="text-green-400 text-xs">yes</span>
) : (
<span className="text-red-400 text-xs">no</span>
)}
</td>
<td className="px-3 py-1.5 text-gray-500 text-xs truncate max-w-[150px]">
{it.task_id}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
)}
{/* Detail view */}
{state.viewMode === "detail" && state.iterationDetail && (
<div className="flex-1 flex flex-col overflow-hidden">
{/* Detail header */}
<div className="border-b border-gray-800 px-4 py-2 flex items-center gap-4 shrink-0">
<button
onClick={() => store.backToTimeline()}
className="text-rose-400 hover:text-rose-300 text-sm"
>
&larr; Back to timeline
</button>
<div className="text-sm text-gray-300">
Iteration {state.iterationDetail.iteration} | Island{" "}
{state.iterationDetail.island_id}
</div>
<div className="flex items-center gap-2 ml-auto">
<span
className={`px-2 py-0.5 rounded text-xs ${
getAdaptationColor(state.iterationDetail.adaptation_type).bg
} ${getAdaptationColor(state.iterationDetail.adaptation_type).text}`}
>
{state.iterationDetail.adaptation_type}
</span>
<span className="text-gray-400 text-sm font-mono">
score: {state.iterationDetail.score.toFixed(4)}
</span>
<span
className={`text-sm font-mono ${
state.iterationDetail.delta > 0
? "text-green-400"
: state.iterationDetail.delta < 0
? "text-red-400"
: "text-gray-500"
}`}
>
({state.iterationDetail.delta > 0 ? "+" : ""}
{state.iterationDetail.delta.toFixed(4)})
</span>
{state.iterationDetail.is_valid ? (
<span className="text-green-400 text-xs border border-green-800 rounded px-1">
valid
</span>
) : (
<span className="text-red-400 text-xs border border-red-800 rounded px-1">
invalid
</span>
)}
</div>
</div>
{/* Detail grid */}
<div className="flex-1 overflow-auto p-4">
<div className="grid grid-cols-2 gap-4 h-full">
{/* Meta info */}
<div className="col-span-2 grid grid-cols-4 gap-3">
<div className="bg-gray-900 rounded border border-gray-800 p-3">
<div className="text-xs text-gray-500 mb-1">Task ID</div>
<div className="text-sm text-gray-200 break-all">
{state.iterationDetail.task_id || "-"}
</div>
</div>
<div className="bg-gray-900 rounded border border-gray-800 p-3">
<div className="text-xs text-gray-500 mb-1">Exploration Intensity</div>
<div className="text-sm text-gray-200 font-mono">
{state.iterationDetail.exploration_intensity.toFixed(4)}
</div>
</div>
<div className="bg-gray-900 rounded border border-gray-800 p-3">
<div className="text-xs text-gray-500 mb-1">Meta-Guidance Tactic</div>
<div className="text-sm text-gray-200">
{state.iterationDetail.meta_guidance_tactic || "-"}
</div>
</div>
<div className="bg-gray-900 rounded border border-gray-800 p-3">
<div className="text-xs text-gray-500 mb-1">Tactic Approach</div>
<div className="text-sm text-gray-200">
{state.iterationDetail.tactic_approach_type || "-"}
</div>
</div>
</div>
{/* Prompt */}
<div className="bg-gray-900 rounded border border-gray-800 flex flex-col overflow-hidden">
<div className="px-3 py-2 border-b border-gray-800 text-xs text-rose-400 font-medium shrink-0">
Prompt
</div>
<pre className="flex-1 overflow-auto p-3 text-xs text-gray-300 whitespace-pre-wrap font-mono">
{state.iterationDetail.prompt_text || "(empty)"}
</pre>
</div>
{/* Reasoning trace */}
<div className="bg-gray-900 rounded border border-gray-800 flex flex-col overflow-hidden">
<div className="px-3 py-2 border-b border-gray-800 text-xs text-rose-400 font-medium shrink-0">
Reasoning Trace
</div>
<pre className="flex-1 overflow-auto p-3 text-xs text-gray-300 whitespace-pre-wrap font-mono">
{state.iterationDetail.reasoning_trace || "(empty)"}
</pre>
</div>
{/* Generated code */}
<div className="col-span-2 bg-gray-900 rounded border border-gray-800 flex flex-col overflow-hidden max-h-96">
<div className="px-3 py-2 border-b border-gray-800 text-xs text-rose-400 font-medium shrink-0">
Generated Code
</div>
<pre className="flex-1 overflow-auto p-3 text-xs text-gray-300 whitespace-pre-wrap font-mono">
{state.iterationDetail.program_code || "(empty)"}
</pre>
</div>
</div>
</div>
{/* Navigation */}
{selectedDataset && (
<div className="border-t border-gray-800 px-4 py-2 flex items-center justify-between shrink-0">
<button
onClick={() => {
const idx = state.iterationDetail!.index;
if (idx > 0) store.selectIteration(selectedDataset.id, idx - 1);
}}
disabled={state.iterationDetail.index <= 0}
className="text-sm text-rose-400 hover:text-rose-300 disabled:opacity-30 disabled:cursor-default"
>
&larr; Previous
</button>
<span className="text-xs text-gray-500">
{state.iterationDetail.index + 1} / {selectedDataset.n_iterations}
</span>
<button
onClick={() => {
const idx = state.iterationDetail!.index;
if (idx < selectedDataset.n_iterations - 1)
store.selectIteration(selectedDataset.id, idx + 1);
}}
disabled={
state.iterationDetail.index >= selectedDataset.n_iterations - 1
}
className="text-sm text-rose-400 hover:text-rose-300 disabled:opacity-30 disabled:cursor-default"
>
Next &rarr;
</button>
</div>
)}
</div>
)}
{/* Empty state */}
{state.datasets.length === 0 && state.viewMode === "timeline" && (
<div className="flex-1 flex items-center justify-center text-gray-500">
<div className="text-center">
<div className="text-lg mb-2">No datasets loaded</div>
<div className="text-sm">
Load a HuggingFace repo from the sidebar or select a preset
</div>
</div>
</div>
)}
{state.datasets.length > 0 && !selectedDataset && state.viewMode === "timeline" && (
<div className="flex-1 flex items-center justify-center text-gray-500">
<div className="text-center">
<div className="text-lg mb-2">Select a dataset</div>
<div className="text-sm">Click a dataset in the sidebar to view its iterations</div>
</div>
</div>
)}
</div>
</div>
);
}