Zayne Rea Sprague
new tab
b630916
import { useState } from "react";
import type { DatasetInfo, Preset } from "../types";
import { api } from "../api";
interface SidebarProps {
datasets: DatasetInfo[];
presets: Preset[];
setPresets: (p: Preset[]) => void;
loading: Record<string, boolean>;
onAddDataset: (repo: string, config?: string, split?: string, presetId?: string, presetName?: string) => void;
onRemoveDataset: (id: string) => void;
onToggleDataset: (id: string) => void;
onSelectDataset: (id: string) => void;
onUpdateDatasetPresetName: (dsId: string, name: string) => void;
onClearDatasetPreset: (dsId: string) => void;
}
export default function Sidebar({
datasets,
presets,
setPresets,
loading,
onAddDataset,
onRemoveDataset,
onToggleDataset,
onSelectDataset,
onUpdateDatasetPresetName,
onClearDatasetPreset,
}: SidebarProps) {
const [showAddForm, setShowAddForm] = useState(false);
const [repo, setRepo] = useState("");
const [config, setConfig] = useState("rlm_call_traces");
const [split, setSplit] = useState("train");
const [presetSearch, setPresetSearch] = useState("");
// Inline preset saving
const [savingPresetForId, setSavingPresetForId] = useState<string | null>(null);
const [presetName, setPresetName] = useState("");
// Preset editing panel
const [editingDatasetId, setEditingDatasetId] = useState<string | null>(null);
const [editPresetName, setEditPresetName] = useState("");
const handleAdd = () => {
if (!repo.trim()) return;
onAddDataset(repo.trim(), config, split);
setRepo("");
setShowAddForm(false);
};
const handleLoadPreset = (p: Preset) => {
onAddDataset(p.repo, p.config, p.split || "train", p.id, p.name);
};
const handleSavePresetForRepo = async (ds: DatasetInfo) => {
if (!presetName.trim()) return;
try {
const preset = (await api.createPreset({
name: presetName.trim(),
repo: ds.repo,
config: ds.config,
split: ds.split,
})) as unknown as Preset;
setPresets([...presets, preset]);
onUpdateDatasetPresetName(ds.id, presetName.trim());
} catch {
/* ignore */
}
setPresetName("");
setSavingPresetForId(null);
};
const handleUpdatePreset = async (presetId: string, dsId: string) => {
if (!editPresetName.trim()) return;
try {
await api.updatePreset(presetId, { name: editPresetName.trim() });
setPresets(
presets.map((p) => (p.id === presetId ? { ...p, name: editPresetName.trim() } : p))
);
onUpdateDatasetPresetName(dsId, editPresetName.trim());
} catch {
/* ignore */
}
setEditingDatasetId(null);
};
const handleDeletePreset = async (id: string, dsId?: string) => {
await api.deletePreset(id).catch(() => {});
setPresets(presets.filter((p) => p.id !== id));
if (dsId) {
onClearDatasetPreset(dsId);
}
setEditingDatasetId(null);
};
const filteredPresets = presetSearch
? presets.filter(
(p) =>
p.name.toLowerCase().includes(presetSearch.toLowerCase()) ||
p.repo.toLowerCase().includes(presetSearch.toLowerCase())
)
: presets;
return (
<div className="w-64 min-w-64 bg-gray-900 border-r border-gray-700 flex flex-col h-full overflow-hidden">
{/* Header */}
<div className="p-3 border-b border-gray-700">
<h1 className="text-sm font-bold tracking-wide text-gray-200">RLM Eval Visualizer</h1>
</div>
{/* Presets section */}
<div className="p-3 border-b border-gray-700">
<div className="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2">
Presets
</div>
{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-emerald-500 focus:outline-none"
/>
)}
<div className="flex flex-wrap gap-1 max-h-32 overflow-y-auto">
{filteredPresets.map((p) => (
<div key={p.id} className="group relative">
<button
onClick={() => handleLoadPreset(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.config}, ${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={() => handleDeletePreset(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>
{/* Loaded Experiments */}
<div className="flex-1 overflow-y-auto p-3">
<div className="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2">
Loaded Datasets
</div>
{datasets.length === 0 ? (
<p className="text-xs text-gray-500 italic">No datasets loaded</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 || "");
setShowAddForm(false);
}
onSelectDataset(ds.id);
}}
className={`flex items-center gap-2 px-2 py-1.5 rounded text-sm transition-colors cursor-pointer ${
ds.active ? "bg-gray-800" : "bg-gray-900 opacity-60"
} ${editingDatasetId === ds.id ? "ring-1 ring-emerald-500" : "hover:bg-gray-800"}`}
>
<input
type="checkbox"
checked={ds.active}
onChange={() => onToggleDataset(ds.id)}
onClick={(e) => e.stopPropagation()}
className="accent-emerald-500 shrink-0"
/>
<div className="flex-1 min-w-0">
<div
className="text-xs font-medium text-gray-200 truncate"
title={ds.presetName ? `${ds.presetName}\n${ds.repo}` : ds.repo}
>
{ds.presetName || ds.name}
</div>
<div className="text-[10px] text-gray-500">
{ds.metadata.model} | {ds.n_examples} examples
</div>
</div>
{/* Save as preset bookmark */}
<button
onClick={(e) => {
e.stopPropagation();
setSavingPresetForId(savingPresetForId === ds.id ? null : ds.id);
setPresetName(ds.presetName || ds.name);
}}
className={`transition-colors shrink-0 ${
savingPresetForId === ds.id
? "text-emerald-400"
: ds.presetId
? "text-emerald-500"
: "text-gray-600 hover:text-emerald-400"
}`}
title={ds.presetId ? "Saved as preset" : "Save as preset"}
>
<svg
className="w-3.5 h-3.5"
fill={ds.presetId ? "currentColor" : "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>
{/* Remove */}
<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>
{/* Inline preset name input */}
{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-emerald-500 focus:outline-none"
autoFocus
/>
<button
onClick={() => handleSavePresetForRepo(ds)}
className="px-2 py-1 text-xs bg-emerald-600 hover:bg-emerald-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()) {
handleUpdatePreset(editDs.presetId!, editDs.id);
}
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-emerald-500 focus:outline-none"
autoFocus
/>
<div className="flex gap-2">
<button
onClick={() => handleUpdatePreset(editDs.presetId!, editDs.id)}
disabled={!editPresetName.trim()}
className="flex-1 px-2 py-1 text-xs bg-emerald-600 hover:bg-emerald-500 disabled:bg-gray-700 disabled:text-gray-500 rounded text-white transition-colors"
>
Save
</button>
<button
onClick={() => handleDeletePreset(editDs.presetId!, editDs.id)}
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 Dataset Form */}
<div className="p-3 border-t border-gray-700">
{showAddForm ? (
<div className="space-y-2">
<input
className="w-full bg-gray-800 text-sm text-gray-200 rounded px-2 py-1.5 border border-gray-600 focus:border-emerald-500 outline-none"
placeholder="org/repo-name"
value={repo}
onChange={(e) => setRepo(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleAdd()}
autoFocus
/>
<div className="flex gap-2">
<input
className="flex-1 bg-gray-800 text-xs text-gray-200 rounded px-2 py-1 border border-gray-600 focus:border-emerald-500 outline-none"
placeholder="Config"
value={config}
onChange={(e) => setConfig(e.target.value)}
/>
<input
className="w-16 bg-gray-800 text-xs text-gray-200 rounded px-2 py-1 border border-gray-600 focus:border-emerald-500 outline-none"
placeholder="Split"
value={split}
onChange={(e) => setSplit(e.target.value)}
/>
</div>
<div className="flex gap-2">
<button
className="flex-1 px-2 py-1.5 text-sm bg-emerald-600 hover:bg-emerald-500 disabled:bg-gray-700 disabled:text-gray-500 rounded text-white transition-colors"
onClick={handleAdd}
disabled={!repo.trim() || !!loading[repo.trim()]}
>
{loading[repo.trim()] ? "Loading..." : "Load"}
</button>
<button
className="px-3 py-1.5 text-sm bg-gray-700 hover:bg-gray-600 rounded text-gray-300 transition-colors"
onClick={() => setShowAddForm(false)}
>
Cancel
</button>
</div>
</div>
) : (
<button
className="w-full px-3 py-2 text-sm bg-emerald-600 hover:bg-emerald-500 rounded text-white font-medium transition-colors"
onClick={() => {
setEditingDatasetId(null);
setShowAddForm(true);
setRepo("");
setConfig("rlm_call_traces");
setSplit("train");
}}
>
+ Add Dataset
</button>
)}
</div>
</div>
);
}