+ {/* Presets */}
+
+
Presets
+ {presets.length === 0 ? (
+
No presets saved
+ ) : (
+ <>
+ {presets.length > 6 && (
+
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-purple-500 focus:outline-none"
+ />
+ )}
+
+ {presets
+ .filter(p => !presetSearch || p.name.toLowerCase().includes(presetSearch.toLowerCase()))
+ .map(p => (
+
+
onLoadPreset(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.split ?? "train"})`}
+ >
+ {p.name}
+
+
+ onDeletePreset(p.id)}
+ className="px-1.5 py-0.5 text-[10px] bg-red-900 hover:bg-red-800 rounded text-red-300"
+ >
+ Delete
+
+
+
+ ))}
+
+ >
+ )}
+
+
+ {/* Env selector */}
+ {allEnvIds.length > 1 && (
+
+
Environments
+
+ {allEnvIds.map(envId => {
+ const color = getEnvColor(envId);
+ const isActive = envId === currentEnvId;
+ return (
+ onSetCurrentEnv(envId)}
+ className={`w-full flex items-center gap-2 px-2 py-1.5 rounded text-xs transition-colors ${
+ isActive ? "bg-gray-800 text-gray-200" : "text-gray-500 hover:bg-gray-800/50"
+ }`}
+ >
+
+ {envId}
+ {isActive && viewing }
+
+ );
+ })}
+
+
+ )}
+
+ {/* Datasets */}
+
+
Loaded Repos
+ {datasets.length === 0 ? (
+
No repos loaded. Add one below.
+ ) : (
+
+ {datasets.map(ds => (
+
+
{
+ if (ds.presetId) {
+ setEditingDatasetId(editingDatasetId === ds.id ? null : ds.id);
+ setEditPresetName(ds.presetName || "");
+ }
+ }}
+ className={`flex items-center gap-2 px-2 py-1.5 rounded text-sm transition-colors ${
+ ds.active ? "bg-gray-800" : "bg-gray-900 opacity-60"
+ } ${ds.presetId ? "cursor-pointer" : ""}`}
+ >
+
onToggleDataset(ds.id)}
+ onClick={e => e.stopPropagation()}
+ className="rounded border-gray-600 bg-gray-800 text-purple-500 focus:ring-purple-500 focus:ring-offset-0"
+ />
+
+
+ {ds.presetName || ds.name}
+
+
+ {ds.model_name} | {ds.n_rows} eps | W:{ds.stats.wins} L:{ds.stats.losses} E:{ds.stats.errors}
+
+
+
{ e.stopPropagation(); setSavingPresetForId(savingPresetForId === ds.id ? null : ds.id); setPresetName(""); }}
+ className={`transition-colors shrink-0 ${savingPresetForId === ds.id ? "text-purple-400" : "text-gray-600 hover:text-purple-400"}`}
+ title="Save as preset"
+ >
+
+
+
+
+
{ e.stopPropagation(); onRemoveDataset(ds.id); }}
+ className="text-gray-600 hover:text-red-400 transition-colors shrink-0"
+ title="Remove"
+ >
+
+
+
+
+
+ {savingPresetForId === ds.id && (
+
+ 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-purple-500 focus:outline-none"
+ autoFocus
+ />
+ handleSavePresetForRepo(ds)} className="px-2 py-1 text-xs bg-purple-600 hover:bg-purple-500 rounded text-white">Save
+
+ )}
+
+ ))}
+
+ )}
+
+
+ {/* Preset edit panel */}
+ {editingDatasetId && (() => {
+ const editDs = datasets.find(d => d.id === editingDatasetId);
+ if (!editDs?.presetId) return null;
+ return (
+
+
Edit Preset
+
setEditPresetName(e.target.value)}
+ onKeyDown={e => {
+ if (e.key === "Enter" && editPresetName.trim()) { onUpdatePreset(editDs.presetId!, editDs.id, { name: editPresetName.trim() }); setEditingDatasetId(null); }
+ 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-purple-500 focus:outline-none"
+ autoFocus
+ />
+
+ { if (editPresetName.trim()) { onUpdatePreset(editDs.presetId!, editDs.id, { name: editPresetName.trim() }); setEditingDatasetId(null); } }}
+ disabled={!editPresetName.trim()} className="flex-1 px-2 py-1 text-xs bg-purple-600 hover:bg-purple-500 disabled:bg-gray-700 disabled:text-gray-500 rounded text-white transition-colors">Save
+ { onDeletePreset(editDs.presetId!); setEditingDatasetId(null); }}
+ className="px-2 py-1 text-xs bg-red-900 hover:bg-red-800 rounded text-red-300 transition-colors">Delete
+ setEditingDatasetId(null)}
+ className="px-2 py-1 text-xs bg-gray-700 hover:bg-gray-600 rounded text-gray-300 transition-colors">Cancel
+
+
+ );
+ })()}
+
+ {/* Add repo */}
+
+ {!showAddModal ? (
+
{ setEditingDatasetId(null); setShowAddModal(true); setRepoInput(""); setSplitInput("train"); }}
+ className="w-full px-3 py-2 text-sm bg-purple-600 hover:bg-purple-500 rounded text-white font-medium transition-colors"
+ >
+ + Add Repo
+
+ ) : (
+
+
setRepoInput(e.target.value)}
+ onKeyDown={e => e.key === "Enter" && handleAdd()}
+ placeholder="org/dataset-name"
+ className="w-full px-2 py-1.5 text-sm bg-gray-800 border border-gray-600 rounded text-gray-200 placeholder-gray-500 focus:border-purple-500 focus:outline-none"
+ autoFocus
+ />
+
+ setSplitInput(e.target.value)}
+ placeholder="Split"
+ className="w-20 px-2 py-1 text-xs bg-gray-800 border border-gray-600 rounded text-gray-200 placeholder-gray-500 focus:border-purple-500 focus:outline-none"
+ />
+
+
+
+ {loading[repoInput.trim()] ? "Loading..." : "Load"}
+
+ setShowAddModal(false)}
+ className="px-3 py-1.5 text-sm bg-gray-700 hover:bg-gray-600 rounded text-gray-300 transition-colors">Cancel
+
+
+ )}
+
+
+ );
+}
diff --git a/frontend/src/arena/components/TranscriptPanel.tsx b/frontend/src/arena/components/TranscriptPanel.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..1dbfa142b3d0f13df2b2099e68b91f4736382f0c
--- /dev/null
+++ b/frontend/src/arena/components/TranscriptPanel.tsx
@@ -0,0 +1,198 @@
+import { useState } from "react";
+import type { EpisodeData, TranscriptTurn } from "../types";
+import { highlightTrace } from "../utils/traceHighlight";
+
+export interface DragHandleProps {
+ draggable: true;
+ onDragStart: (e: React.DragEvent) => void;
+ onDragEnd: (e: React.DragEvent) => void;
+}
+
+interface TranscriptPanelProps {
+ datasetName: string;
+ repoName?: string;
+ data: EpisodeData | undefined;
+ dragHandleProps?: DragHandleProps;
+}
+
+const OUTCOME_STYLES = {
+ win: { bg: "bg-green-900", text: "text-green-300", label: "WIN" },
+ loss: { bg: "bg-red-900", text: "text-red-300", label: "LOSS" },
+ error: { bg: "bg-yellow-900", text: "text-yellow-300", label: "ERROR" },
+ unknown: { bg: "bg-gray-700", text: "text-gray-300", label: "?" },
+};
+
+const PLAYER_COLORS: Record