// ====== 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 (
);
}
// ====== 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 && (
)}
Expected output — optional reference answer
Expected tool calls — what the agent should call
{allTools.map((t) => (
toggleTool(t.id)}
>
{t.initials}
{t.name}
))}
{tracedTools.length ? `Pre-selected from this trace: ${tracedTools.join(", ")}` : "No tools were called in this trace"}
Source: trace {trace?.runId.slice(-8)}
);
}
Object.assign(window, {
AddToDatasetModal,
ToastContainer,
pushToast,
});