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 = { 0: { bubble: "bg-purple-900/60 border-purple-700", label: "text-purple-400", name: "Player 0" }, 1: { bubble: "bg-orange-900/60 border-orange-700", label: "text-orange-400", name: "Player 1" }, 2: { bubble: "bg-purple-900/60 border-purple-700", label: "text-purple-400", name: "Player 2" }, 3: { bubble: "bg-teal-900/60 border-teal-700", label: "text-teal-400", name: "Player 3" }, }; function getPlayerColor(playerId: number) { return PLAYER_COLORS[playerId] || PLAYER_COLORS[0]; } export default function TranscriptPanel({ datasetName, repoName, data, dragHandleProps }: TranscriptPanelProps) { if (!data) { return (
No data
); } const outcomeStyle = OUTCOME_STYLES[data.outcome]; const borderColor = data.outcome === "win" ? "border-green-600" : data.outcome === "loss" ? "border-red-600" : data.outcome === "error" ? "border-yellow-600" : "border-gray-700"; return (
{/* Header */}
{datasetName} {outcomeStyle.label}
{dragHandleProps && ( )}
{data.model} {data.num_turns} turns {data.reward !== null && reward: {data.reward}} {data.opponent_model && vs {data.opponent_model}}
{/* Error banner */} {data.error && (
Error: {data.error}
)} {/* Transcript chat */}
{/* System prompt */} {data.system_prompt && ( )} {data.transcript.map((turn, i) => ( ))}
); } function SystemPromptBubble({ text }: { text: string }) { const [expanded, setExpanded] = useState(true); return (
{expanded && (
            {text}
          
)}
); } interface TurnBubbleProps { turn: TranscriptTurn; turnIndex: number; allTurns: TranscriptTurn[]; systemPrompt: string | null; hasMultiplePlayers: boolean; } function buildRawPrompt( turnIndex: number, allTurns: TranscriptTurn[], systemPrompt: string | null, ): object[] { const messages: object[] = []; if (systemPrompt) { messages.push({ role: "system", content: systemPrompt }); } for (let i = 0; i < turnIndex; i++) { messages.push({ role: "user", content: allTurns[i].observation }); messages.push({ role: "assistant", content: allTurns[i].action }); } messages.push({ role: "user", content: allTurns[turnIndex].observation }); return messages; } function TurnBubble({ turn, turnIndex, allTurns, systemPrompt, hasMultiplePlayers }: TurnBubbleProps) { const [thinkExpanded, setThinkExpanded] = useState(false); const [rawPromptExpanded, setRawPromptExpanded] = useState(false); const playerColor = getPlayerColor(turn.player_id); const thinkSegments = highlightTrace(turn.think_text); return (
{/* Turn number marker */}
Turn {turn.turn}
{/* Observation (environment message) — left aligned */} {turn.observation && (
ENV
                {turn.observation}
              
)} {/* Action (model response) — right aligned */}
{hasMultiplePlayers ? `${playerColor.name} (${turn.player_id === 0 ? "model" : "opponent"})` : "Model"}
{/* Raw Prompt — collapsible, shows full messages sent to API */}
{rawPromptExpanded && (
                  {JSON.stringify(buildRawPrompt(turnIndex, allTurns, systemPrompt), null, 2)}
                
)}
{/* Think section — collapsible */} {turn.think_text && (
{thinkExpanded && (
                    {thinkSegments.map((seg, i) => (
                      {seg.text}
                    ))}
                  
)}
)} {/* Action text (the actual game move) */}
              {turn.action_text || turn.action || "(no action)"}
            
); }