import { useState, useCallback } from "react"; import type { Artifact, ExperimentDetail as ExperimentDetailType, ExperimentNote } from "../types"; import { HF_ORG } from "../../config"; import { deriveDisplayStatus, statusBadgeColor } from "../types"; import { navigateTo, replaceRoute, parseHash } from "../../hashRouter"; import Markdown from "./Markdown"; import TimelineTab from "./TimelineTab"; import ArtifactsTab from "./ArtifactsTab"; type Tab = "overview" | "artifacts" | "notes" | "live" | "timeline" | "red_team_brief"; const LIVE_JOB_STATUS_COLORS: Record = { pending: "text-gray-400", running: "text-yellow-400", completed: "text-green-400", failed: "text-red-400", blocked: "text-red-400", }; const TIMELINE_DOT_COLORS: Record = { blocked: "bg-red-500", failed: "bg-red-500", error: "bg-red-500", completed: "bg-green-500", started: "bg-yellow-500", submitted: "bg-blue-500", }; interface Props { experiment: ExperimentDetailType; onBack: () => void; onSelectNote: (noteId: string) => void; onRefresh: () => void; } /** Group notes by their directory path */ function groupNotesByDir(notes: ExperimentNote[]): Map { const groups = new Map(); for (const note of notes) { const relPath = note.relative_path || note.filename || ""; const dir = relPath.includes("/") ? relPath.substring(0, relPath.lastIndexOf("/")) : "(root)"; if (!groups.has(dir)) groups.set(dir, []); groups.get(dir)!.push(note); } // Sort directories return new Map([...groups.entries()].sort(([a], [b]) => a.localeCompare(b))); } const VALID_TABS = new Set(["overview", "artifacts", "notes", "live", "timeline", "red_team_brief"]); function getInitialTab(): Tab { const route = parseHash(); const t = route.params.get("tab"); if (t && VALID_TABS.has(t as Tab)) return t as Tab; return "overview"; } export default function ExperimentDetail({ experiment, onBack, onSelectNote, onRefresh }: Props) { const [tab, _setTab] = useState(getInitialTab); const setTab = useCallback((t: Tab) => { _setTab(t); const route = parseHash(); const params = new URLSearchParams(route.params); if (t === "overview") { params.delete("tab"); } else { params.set("tab", t); } replaceRoute({ params }); }, []); const liveJobCount = Object.keys(experiment.live_jobs || {}).length; const isFinished = !!experiment.zayne_findings; const TABS: { id: Tab; label: string; count?: number }[] = [ { id: "overview", label: "Overview" }, ...(experiment.red_team_brief ? [{ id: "red_team_brief" as Tab, label: "Red Team Brief" }] : []), ...(experiment.live_status || liveJobCount > 0 ? [{ id: "live" as Tab, label: "Live Jobs", count: liveJobCount }] : []), { id: "timeline", label: "Timeline", count: experiment.activity_log?.length || 0 }, { id: "artifacts", label: "Artifacts", count: experiment.artifacts?.length || experiment.hf_repos?.length || 0 }, { id: "notes", label: "Files", count: experiment.experiment_notes?.length || 0 }, ]; return (
{/* Header */}

{experiment.name}

{isFinished && ( Finished )} {experiment.live_status && !isFinished && ( {deriveDisplayStatus(experiment).replace("_", " ")} )}
{experiment.live_message && (

{experiment.live_message}

)} {experiment.zayne_summary ? (
Researcher's Summary
) : experiment.hypothesis?.statement ? (

{experiment.hypothesis.statement}

) : null}
{/* Detail tabs */}
{TABS.map((t) => ( ))}
{/* Tab content */}
{tab === "overview" && (
{/* FINDINGS (above readme, only when filled) */} {experiment.zayne_findings && (
Findings
)} {/* Researcher's README */} {experiment.zayne_readme && (
Researcher's README
)} {/* DECISIONS (below readme, only when filled) */} {experiment.zayne_decisions && (
Decisions
)} {/* Agent Notes (EXPERIMENT_README) - always shown */} {experiment.notes && (
Experiment Notes
)}
)} {tab === "artifacts" && ( { const INLINE_TYPES = new Set(["table", "yaml_config", "plotly", "image"]); if (!artifact.visualizer_type || INLINE_TYPES.has(artifact.visualizer_type)) { return; } const vizTabMap: Record = { model_trace: "model", }; const vizTab = vizTabMap[artifact.visualizer_type]; if (vizTab) { const fullName = artifact.dataset_name.includes("/") ? artifact.dataset_name : `${HF_ORG}/${artifact.dataset_name}`; navigateTo({ page: "viz", tab: vizTab, params: new URLSearchParams({ repos: fullName, from_exp: experiment.id, }), }); } }} /> )} {tab === "red_team_brief" && experiment.red_team_brief && (
Claude Code: Red Team Brief
)} {tab === "notes" && (

Project Files

{experiment.experiment_notes?.length || 0} files
{(experiment.experiment_notes || []).length === 0 ? (

No project files found.

) : (
{[...groupNotesByDir(experiment.experiment_notes)].map(([dir, files]) => (
{dir}/ ({files.length})
{files.map((note) => ( ))}
))}
)}
)} {tab === "timeline" && ( console.log("artifact clicked:", datasetName)} /> )} {tab === "live" && (
{/* Unreachable clusters warning */} {experiment.unreachable_clusters && Object.keys(experiment.unreachable_clusters).length > 0 && (

Unreachable Clusters

{Object.entries(experiment.unreachable_clusters).map(([cluster, info]) => (
{cluster} {info.reason} (since {new Date(info.since).toLocaleString()})
))}
)} {/* Jobs table */}

Active Jobs

{liveJobCount === 0 ? (

No live jobs tracked.

) : (
{Object.entries(experiment.live_jobs || {}).map(([jobId, job]) => ( ))}
ID Cluster GPUs Status Message ETA
{jobId} {job.slurm_job_id && ( ({job.slurm_job_id}) )} {job.cluster} {job.partition && ( /{job.partition} )} {job.gpus} {job.status} {job.blocker && ( ({job.blocker.reason}) )} {job.message || "-"} {job.estimated_completion ? new Date(job.estimated_completion).toLocaleString() : "-"}
)}
{/* Job metrics summary */} {liveJobCount > 0 && (

Job Metrics

{Object.entries(experiment.live_jobs || {}).map(([jobId, job]) => Object.keys(job.metrics || {}).length > 0 ? (
{jobId}
{Object.entries(job.metrics).map(([k, v]) => (
{k} {typeof v === "number" ? v.toFixed(3) : v}
))}
) : null )}
)} {/* Timeline */}

Event Timeline

{(!experiment.live_history || experiment.live_history.length === 0) ? (

No timeline events recorded.

) : (
{experiment.live_history.slice(-10).reverse().map((entry, i) => { const eventLower = entry.event.toLowerCase(); const dotColor = TIMELINE_DOT_COLORS[eventLower] || (eventLower.includes("block") || eventLower.includes("fail") || eventLower.includes("error") ? "bg-red-500" : "bg-gray-500"); return (
{entry.event} {entry.cluster && ( {entry.cluster} )} {entry.job_id && ( {entry.job_id} )}
{entry.message && (

{entry.message}

)} {new Date(entry.timestamp).toLocaleString()}
); })}
)}
{/* Timestamps */} {(experiment.live_started_at || experiment.live_updated_at) && (
{experiment.live_started_at && ( Started: {new Date(experiment.live_started_at).toLocaleString()} )} {experiment.live_updated_at && ( Last update: {new Date(experiment.live_updated_at).toLocaleString()} )}
)}
)}
); }