Zayne Rea Sprague
Initial deploy: aggregate trace visualizer
8b41737
import { useState } from "react";
import type { DatasetInfo, Preset } from "../types";
const ENV_COLORS = [
{ bg: "bg-purple-500", border: "border-purple-500", text: "text-purple-400", label: "text-purple-300" },
{ bg: "bg-emerald-500", border: "border-emerald-500", text: "text-emerald-400", label: "text-emerald-300" },
{ bg: "bg-amber-500", border: "border-amber-500", text: "text-amber-400", label: "text-amber-300" },
{ bg: "bg-purple-500", border: "border-purple-500", text: "text-purple-400", label: "text-purple-300" },
{ bg: "bg-rose-500", border: "border-rose-500", text: "text-rose-400", label: "text-rose-300" },
{ bg: "bg-cyan-500", border: "border-cyan-500", text: "text-cyan-400", label: "text-cyan-300" },
];
interface SidebarProps {
datasets: DatasetInfo[];
presets: Preset[];
loading: Record<string, boolean>;
allEnvIds: string[];
currentEnvId: string | null;
onAddDataset: (repo: string, split?: string) => void;
onRemoveDataset: (id: string) => void;
onToggleDataset: (id: string) => void;
onSetCurrentEnv: (envId: string) => void;
onLoadPreset: (preset: Preset) => void;
onSavePreset: (name: string, repo: string, split?: string) => void;
onDeletePreset: (id: string) => void;
onUpdatePreset: (presetId: string, datasetId: string, updates: { name?: string }) => void;
}
export default function Sidebar({
datasets, presets, loading, allEnvIds, currentEnvId,
onAddDataset, onRemoveDataset, onToggleDataset, onSetCurrentEnv,
onLoadPreset, onSavePreset, onDeletePreset, onUpdatePreset,
}: SidebarProps) {
const [showAddModal, setShowAddModal] = useState(false);
const [repoInput, setRepoInput] = useState("");
const [splitInput, setSplitInput] = useState("train");
const [presetSearch, setPresetSearch] = useState("");
const [savingPresetForId, setSavingPresetForId] = useState<string | null>(null);
const [presetName, setPresetName] = useState("");
const [editingDatasetId, setEditingDatasetId] = useState<string | null>(null);
const [editPresetName, setEditPresetName] = useState("");
const handleAdd = () => {
if (!repoInput.trim()) return;
onAddDataset(repoInput.trim(), splitInput.trim() || undefined);
setRepoInput("");
setShowAddModal(false);
};
const handleSavePresetForRepo = (ds: DatasetInfo) => {
if (!presetName.trim()) return;
onSavePreset(presetName.trim(), ds.repo, ds.split);
setPresetName("");
setSavingPresetForId(null);
};
const getEnvColor = (envId: string) => {
const idx = allEnvIds.indexOf(envId);
return ENV_COLORS[idx % ENV_COLORS.length];
};
return (
<div className="w-72 min-w-72 bg-gray-900 border-r border-gray-700 flex flex-col h-full">
{/* Presets */}
<div className="p-3 border-b border-gray-700">
<h3 className="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2">Presets</h3>
{presets.length === 0 ? (
<p className="text-xs text-gray-500 italic">No presets saved</p>
) : (
<>
{presets.length > 6 && (
<input
type="text"
value={presetSearch}
onChange={(e) => setPresetSearch(e.target.value)}
placeholder="Search presets..."
className="w-full px-2 py-1 mb-2 text-xs bg-gray-800 border border-gray-600 rounded text-gray-200 placeholder-gray-500 focus:border-purple-500 focus:outline-none"
/>
)}
<div className="flex flex-wrap gap-1 max-h-32 overflow-y-auto">
{presets
.filter(p => !presetSearch || p.name.toLowerCase().includes(presetSearch.toLowerCase()))
.map(p => (
<div key={p.id} className="group relative">
<button
onClick={() => onLoadPreset(p)}
className="px-2 py-1 text-xs bg-gray-800 hover:bg-gray-700 rounded border border-gray-600 text-gray-300 transition-colors"
title={`${p.repo} (${p.split ?? "train"})`}
>
{p.name}
</button>
<div className="hidden group-hover:flex absolute top-full left-0 mt-1 z-10 gap-1">
<button
onClick={() => onDeletePreset(p.id)}
className="px-1.5 py-0.5 text-[10px] bg-red-900 hover:bg-red-800 rounded text-red-300"
>
Delete
</button>
</div>
</div>
))}
</div>
</>
)}
</div>
{/* Env selector */}
{allEnvIds.length > 1 && (
<div className="p-3 border-b border-gray-700">
<h3 className="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2">Environments</h3>
<div className="space-y-1">
{allEnvIds.map(envId => {
const color = getEnvColor(envId);
const isActive = envId === currentEnvId;
return (
<button
key={envId}
onClick={() => onSetCurrentEnv(envId)}
className={`w-full flex items-center gap-2 px-2 py-1.5 rounded text-xs transition-colors ${
isActive ? "bg-gray-800 text-gray-200" : "text-gray-500 hover:bg-gray-800/50"
}`}
>
<span className={`w-2 h-2 rounded-full ${color.bg} shrink-0`} />
<span className="truncate">{envId}</span>
{isActive && <span className="text-[9px] text-gray-600 ml-auto">viewing</span>}
</button>
);
})}
</div>
</div>
)}
{/* Datasets */}
<div className="flex-1 overflow-y-auto p-3">
<h3 className="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2">Loaded Repos</h3>
{datasets.length === 0 ? (
<p className="text-xs text-gray-500 italic">No repos loaded. Add one below.</p>
) : (
<div className="space-y-1">
{datasets.map(ds => (
<div key={ds.id}>
<div
onClick={() => {
if (ds.presetId) {
setEditingDatasetId(editingDatasetId === ds.id ? null : ds.id);
setEditPresetName(ds.presetName || "");
}
}}
className={`flex items-center gap-2 px-2 py-1.5 rounded text-sm transition-colors ${
ds.active ? "bg-gray-800" : "bg-gray-900 opacity-60"
} ${ds.presetId ? "cursor-pointer" : ""}`}
>
<input
type="checkbox"
checked={ds.active}
onChange={() => onToggleDataset(ds.id)}
onClick={e => e.stopPropagation()}
className="rounded border-gray-600 bg-gray-800 text-purple-500 focus:ring-purple-500 focus:ring-offset-0"
/>
<div className="flex-1 min-w-0">
<div className="text-gray-200 truncate text-xs font-medium" title={ds.repo}>
{ds.presetName || ds.name}
</div>
<div className="text-[10px] text-gray-500">
{ds.model_name} | {ds.n_rows} eps | W:{ds.stats.wins} L:{ds.stats.losses} E:{ds.stats.errors}
</div>
</div>
<button
onClick={e => { e.stopPropagation(); setSavingPresetForId(savingPresetForId === ds.id ? null : ds.id); setPresetName(""); }}
className={`transition-colors shrink-0 ${savingPresetForId === ds.id ? "text-purple-400" : "text-gray-600 hover:text-purple-400"}`}
title="Save as preset"
>
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />
</svg>
</button>
<button
onClick={e => { e.stopPropagation(); onRemoveDataset(ds.id); }}
className="text-gray-600 hover:text-red-400 transition-colors shrink-0"
title="Remove"
>
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{savingPresetForId === ds.id && (
<div className="flex gap-1 mt-1 ml-6">
<input
type="text"
value={presetName}
onChange={e => setPresetName(e.target.value)}
onKeyDown={e => { if (e.key === "Enter") handleSavePresetForRepo(ds); if (e.key === "Escape") setSavingPresetForId(null); }}
placeholder="Preset name..."
className="flex-1 px-2 py-1 text-xs bg-gray-800 border border-gray-600 rounded text-gray-200 placeholder-gray-500 focus:border-purple-500 focus:outline-none"
autoFocus
/>
<button onClick={() => handleSavePresetForRepo(ds)} className="px-2 py-1 text-xs bg-purple-600 hover:bg-purple-500 rounded text-white">Save</button>
</div>
)}
</div>
))}
</div>
)}
</div>
{/* Preset edit panel */}
{editingDatasetId && (() => {
const editDs = datasets.find(d => d.id === editingDatasetId);
if (!editDs?.presetId) return null;
return (
<div className="p-3 border-t border-gray-700 space-y-2">
<div className="text-[10px] text-gray-500 uppercase font-semibold tracking-wider">Edit Preset</div>
<input
type="text" value={editPresetName} onChange={e => setEditPresetName(e.target.value)}
onKeyDown={e => {
if (e.key === "Enter" && editPresetName.trim()) { onUpdatePreset(editDs.presetId!, editDs.id, { name: editPresetName.trim() }); setEditingDatasetId(null); }
if (e.key === "Escape") setEditingDatasetId(null);
}}
placeholder="Preset name..."
className="w-full px-2 py-1 text-xs bg-gray-800 border border-gray-600 rounded text-gray-200 placeholder-gray-500 focus:border-purple-500 focus:outline-none"
autoFocus
/>
<div className="flex gap-2">
<button onClick={() => { if (editPresetName.trim()) { onUpdatePreset(editDs.presetId!, editDs.id, { name: editPresetName.trim() }); setEditingDatasetId(null); } }}
disabled={!editPresetName.trim()} className="flex-1 px-2 py-1 text-xs bg-purple-600 hover:bg-purple-500 disabled:bg-gray-700 disabled:text-gray-500 rounded text-white transition-colors">Save</button>
<button onClick={() => { onDeletePreset(editDs.presetId!); setEditingDatasetId(null); }}
className="px-2 py-1 text-xs bg-red-900 hover:bg-red-800 rounded text-red-300 transition-colors">Delete</button>
<button onClick={() => setEditingDatasetId(null)}
className="px-2 py-1 text-xs bg-gray-700 hover:bg-gray-600 rounded text-gray-300 transition-colors">Cancel</button>
</div>
</div>
);
})()}
{/* Add repo */}
<div className="p-3 border-t border-gray-700">
{!showAddModal ? (
<button
onClick={() => { setEditingDatasetId(null); setShowAddModal(true); setRepoInput(""); setSplitInput("train"); }}
className="w-full px-3 py-2 text-sm bg-purple-600 hover:bg-purple-500 rounded text-white font-medium transition-colors"
>
+ Add Repo
</button>
) : (
<div className="space-y-2">
<input
type="text" value={repoInput} onChange={e => setRepoInput(e.target.value)}
onKeyDown={e => e.key === "Enter" && handleAdd()}
placeholder="org/dataset-name"
className="w-full px-2 py-1.5 text-sm bg-gray-800 border border-gray-600 rounded text-gray-200 placeholder-gray-500 focus:border-purple-500 focus:outline-none"
autoFocus
/>
<div className="flex gap-2">
<input
type="text" value={splitInput} onChange={e => setSplitInput(e.target.value)}
placeholder="Split"
className="w-20 px-2 py-1 text-xs bg-gray-800 border border-gray-600 rounded text-gray-200 placeholder-gray-500 focus:border-purple-500 focus:outline-none"
/>
</div>
<div className="flex gap-2">
<button onClick={handleAdd} disabled={!repoInput.trim() || loading[repoInput.trim()]}
className="flex-1 px-2 py-1.5 text-sm bg-purple-600 hover:bg-purple-500 disabled:bg-gray-700 disabled:text-gray-500 rounded text-white transition-colors">
{loading[repoInput.trim()] ? "Loading..." : "Load"}
</button>
<button onClick={() => setShowAddModal(false)}
className="px-3 py-1.5 text-sm bg-gray-700 hover:bg-gray-600 rounded text-gray-300 transition-colors">Cancel</button>
</div>
</div>
)}
</div>
</div>
);
}