| | 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(""); |
| |
|
| | |
| | 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 (!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 { |
| | |
| | } |
| | 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 { |
| | |
| | } |
| | 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> |
| |
|
| | {} |
| | <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> |
| |
|
| | {} |
| | {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> |
| | ); |
| | })()} |
| |
|
| | {} |
| | <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> |
| | ); |
| | } |
| |
|