| | 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(); |
| | }, []); |
| |
|
| | |
| | const selectedDataset = useMemo( |
| | () => state.datasets.find((d) => d.id === state.selectedDatasetId) ?? null, |
| | [state.datasets, state.selectedDatasetId] |
| | ); |
| |
|
| | |
| | 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]); |
| |
|
| | |
| | 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]); |
| |
|
| | |
| | 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(""); |
| | } |
| | } |
| |
|
| | |
| | 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" |
| | > |
| | ← 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" |
| | > |
| | ← 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 → |
| | </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> |
| | ); |
| | } |
| |
|