// ====== Add-to-dataset modal (shared between Traces + Evals) ====== const { useState: dUseState, useEffect: dUseEffect } = React; // Toast manager — simple global queue const _toasts = []; const _toastListeners = new Set(); function pushToast(t) { const id = rid("toast"); const obj = { id, ...t }; _toasts.push(obj); _toastListeners.forEach((fn) => fn([..._toasts])); setTimeout(() => { const i = _toasts.findIndex((x) => x.id === id); if (i >= 0) _toasts.splice(i, 1); _toastListeners.forEach((fn) => fn([..._toasts])); }, t.duration || 5000); } function ToastContainer() { const [toasts, setToasts] = dUseState(_toasts); dUseEffect(() => { const fn = (t) => setToasts(t); _toastListeners.add(fn); return () => _toastListeners.delete(fn); }, []); return (
{toasts.map((t) => (
{t.content}
))}
); } // ====== Add to dataset modal ====== function AddToDatasetModal({ trace, onClose }) { const [datasets, setDatasets] = dUseState([]); const [selectedDsId, setSelectedDsId] = dUseState(null); const [creatingNew, setCreatingNew] = dUseState(false); const [newDsName, setNewDsName] = dUseState(""); const [loading, setLoading] = dUseState(true); const [loadError, setLoadError] = dUseState(null); // PR e2: fetch datasets from real backend on open. /v1/datasets list // doesn't include cases, so we lazy-fetch details when needed (the // sidebar shows `{cases.length} cases` — fetch detail to populate that // count). For brevity here we just show "—" if cases aren't loaded. React.useEffect(() => { let cancelled = false; (async () => { try { const items = await window.api.listDatasets(); if (cancelled) return; const detailed = await Promise.all(items.map( (ds) => window.api.getDataset(ds.dataset_id).catch(() => ({ ...ds, cases: [] })) )); const decorated = detailed.map(window.decorateDataset); setDatasets(decorated); if (decorated.length && selectedDsId === null) { setSelectedDsId(decorated[0].id); } } catch (err) { if (!cancelled) setLoadError(err.message || String(err)); } finally { if (!cancelled) setLoading(false); } })(); return () => { cancelled = true; }; }, []); // pre-extract tool names called in the trace const tracedTools = React.useMemo(() => { if (!trace) return []; const set = new Set(); trace.spans.forEach((s) => { if (s.kind === "tool" || s.kind === "mcp") { // strip "github.list_prs" -> "github" for MCP namespace, keep skills as-is const base = s.name.split(".")[0]; set.add(base); } }); return [...set]; }, [trace]); const [input, setInput] = dUseState(trace?.userInput || ""); const [expected, setExpected] = dUseState(trace?.finalOutput || ""); const [expectedTools, setExpectedTools] = dUseState(tracedTools); const [tags, setTags] = dUseState(""); // All skills/MCPs (for tool picker) const allTools = React.useMemo(() => { const t = []; SKILLS.forEach((s) => t.push({ id: s.id, name: s.name, color: s.color, initials: s.initials, kind: "skill" })); MCPS.forEach((m) => t.push({ id: m.id, name: m.name, color: m.color, initials: m.initials, kind: "mcp" })); return t; }, []); const selectedDs = datasets.find((d) => d.id === selectedDsId); const toggleTool = (id) => { setExpectedTools((cur) => cur.includes(id) ? cur.filter((x) => x !== id) : [...cur, id]); }; const handleCreateNew = async () => { if (!newDsName.trim()) return; try { const created = await window.api.createDataset({ name: newDsName.trim() }); const decorated = window.decorateDataset({ ...created, cases: [] }); setDatasets([decorated, ...datasets]); setSelectedDsId(decorated.id); setCreatingNew(false); setNewDsName(""); } catch (err) { alert("Create dataset failed: " + (err.message || err)); } }; const handleSave = async () => { if (!selectedDs) return; try { const apiCase = await window.api.addDatasetCase(selectedDs.id, { input: input.trim(), expected: expected.trim() || null, tools: expectedTools, tags: tags.split(",").map((t) => t.trim()).filter(Boolean), source: trace ? { type: "trace", traceId: trace.id, runId: trace.runId } : { type: "manual" }, }); onClose(); pushToast({ content: ( Added to {selectedDs.name} · View in Evals → ) }); } catch (err) { alert("Save case failed: " + (err.message || err)); } }; return (
{ if (e.target === e.currentTarget) onClose(); }}>
e.stopPropagation()}>

Add to dataset

Save this trace as an eval case for regression testing.
Dataset
{datasets.map((d) => (
{ setSelectedDsId(d.id); setCreatingNew(false); }} >
{d.id === selectedDsId && !creatingNew ? : }
{d.name}
{d.description || No description}
{d.cases.length} cases
))}
setCreatingNew(true)} style={{ borderTop: "1px dashed var(--border)" }} >
Create new dataset…
Start a fresh dataset for this case
{creatingNew && (
setNewDsName(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") handleCreateNew(); }} />
)}
Input — the user message