"use client"; import { useState } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { FlaskConical, Plus, Trash2, ChevronDown, ChevronRight, Database, GitBranch } from "lucide-react"; import { api } from "@/lib/client/api"; import { Button } from "@/components/ui/button"; import { Input, Textarea } from "@/components/ui/input"; import { Badge, Spinner, Empty } from "@/components/ui/primitives"; import { toast } from "@/components/ui/toast"; import { Field, Select, KVEditor, EXP_TONE } from "./ui"; import { timeAgo, cn } from "@/lib/utils"; const STATUSES = ["planned", "running", "done", "failed"]; export const expKey = (pageId: string) => ["research-experiments", pageId]; export function ExperimentsTab({ pageId }: { pageId: string }) { const qc = useQueryClient(); const [name, setName] = useState(""); const [open, setOpen] = useState>({}); const { data: exps = [], isLoading } = useQuery({ queryKey: expKey(pageId), queryFn: () => api(`/api/research/experiments?pageId=${pageId}`) }); const refresh = () => { qc.invalidateQueries({ queryKey: expKey(pageId) }); qc.invalidateQueries({ queryKey: ["research-overview"] }); }; async function create() { const n = name.trim(); if (!n) return; const e = await api("/api/research/experiments", { method: "POST", json: { pageId, name: n } }); setName(""); setOpen((o) => ({ ...o, [e.id]: true })); refresh(); } async function patch(id: string, json: any) { try { await api(`/api/research/experiments/${id}`, { method: "PATCH", json }); refresh(); } catch (e: any) { toast("Save failed", { description: e.message, tone: "danger" }); } } async function del(e: any) { if (!confirm(`Delete experiment “${e.name}”?`)) return; await api(`/api/research/experiments/${e.id}`, { method: "DELETE" }); refresh(); } return (
setName(e.target.value)} placeholder="New experiment name (e.g. Baseline hedonic, 2015–2024)" className="h-8 text-[13px]" onKeyDown={(e) => { if (e.key === "Enter") create(); }} />
{isLoading ?
: !exps.length ? } title="No experiments" hint="Track each estimation or simulation run: parameters, metrics, dataset and code repository." /> : (
{exps.map((e) => { const isOpen = open[e.id] ?? false; return (
{ if (ev.target.value.trim() && ev.target.value !== e.name) patch(e.id, { name: ev.target.value.trim() }); }} /> {e.status}
{!isOpen &&
{Object.entries(e.metrics ?? {}).slice(0, 6).map(([k, v]) => {k}={String(v)})}{!Object.keys(e.metrics ?? {}).length && {Object.keys(e.params ?? {}).length} params · no metrics yet}· updated {timeAgo(e.updatedAt)}
} {isOpen &&
patch(e.id, { params: v })} placeholderKey="alpha" placeholderValue="0.05" /> patch(e.id, { metrics: v })} placeholderKey="rmse" placeholderValue="0.123" /> Dataset}> { if ((ev.target.value || null) !== (e.datasetRef ?? null)) patch(e.id, { datasetRef: ev.target.value || null }); }} /> Repository}> { if ((ev.target.value || null) !== (e.repoUrl ?? null)) patch(e.id, { repoUrl: ev.target.value || null }); }} />