loop-benchmarking

Controlled experiments across agentic coding configurations. Same task, one variable, what actually works.
git clone https://git.shiptheloop.com/loop-benchmarking.git
Log | Files | Refs | README

RunDetail.tsx (27732B)


      1 import type { Run, AxisName } from "../lib/types";
      2 import TranscriptViewer from "./TranscriptViewer";
      3 
      4 const REPO_URL = "https://git.statagroup.com/research/loop-benchmarking";
      5 
      6 function Stat({ label, value }: { label: string; value: string | number | boolean }) {
      7   return (
      8     <div style={{ display: "flex", justifyContent: "space-between" }}>
      9       <span style={{ color: "var(--text-muted)" }}>{label}</span>
     10       <span style={{ fontFamily: "var(--font-mono)" }}>{String(value)}</span>
     11     </div>
     12   );
     13 }
     14 
     15 interface RunDetailProps {
     16   run: Run;
     17   transcriptLines: string[];
     18   axisValues: Record<AxisName, string[]>;
     19   contextContent?: string;
     20 }
     21 
     22 const EXIT_CODES: Record<number, string> = {
     23   0: "Success",
     24   1: "Error",
     25   2: "Misuse of shell command",
     26   124: "Timeout (exceeded time limit)",
     27   125: "Command failed to execute",
     28   126: "Command not executable",
     29   127: "Command not found",
     30   130: "Interrupted (SIGINT)",
     31   137: "Killed (SIGKILL / OOM)",
     32   143: "Terminated (SIGTERM)",
     33 };
     34 
     35 const AXIS_CONFIG: Array<{ key: string; label: string }> = [
     36   { key: "model", label: "Model" },
     37   { key: "provider", label: "Provider" },
     38   { key: "effort", label: "Effort" },
     39   { key: "prompt_style", label: "Prompt" },
     40   { key: "language", label: "Language" },
     41   { key: "human_language", label: "Human Lang" },
     42   { key: "strategy", label: "Strategy" },
     43   { key: "tests_provided", label: "Tests Provided" },
     44   { key: "design_guidance", label: "Design Guidance" },
     45   { key: "architecture", label: "Architecture" },
     46   { key: "error_checking", label: "Error Checking" },
     47   { key: "renderer", label: "Renderer" },
     48   { key: "context_noise", label: "Context Noise" },
     49   { key: "tool_read", label: "Read" },
     50   { key: "tool_write", label: "Write" },
     51   { key: "tool_edit", label: "Edit" },
     52   { key: "tool_glob", label: "Glob" },
     53   { key: "tool_grep", label: "Grep" },
     54   { key: "linter", label: "Linter" },
     55   { key: "playwright", label: "Playwright" },
     56   { key: "context_file", label: "Context" },
     57   { key: "web_search", label: "Web Search" },
     58   { key: "max_budget", label: "Budget" },
     59 ];
     60 
     61 function ConfigPills({
     62   label,
     63   activeValue,
     64   allValues,
     65 }: {
     66   label: string;
     67   activeValue: string;
     68   allValues: string[];
     69 }) {
     70   return (
     71     <div style={{ display: "flex", alignItems: "center", gap: "8px", marginBottom: "4px" }}>
     72       <div style={{ width: "80px", fontSize: "0.65rem", color: "var(--text-muted)", textTransform: "uppercase", letterSpacing: "0.03em", textAlign: "right", flexShrink: 0 }}>
     73         {label}
     74       </div>
     75       <div style={{ display: "flex", gap: "3px", flexWrap: "wrap" }}>
     76         {allValues.map((val) => (
     77           <span key={val} style={{
     78             padding: "1px 6px",
     79             borderRadius: "3px",
     80             fontSize: "0.65rem",
     81             fontFamily: "var(--font-mono)",
     82             background: val === activeValue ? "rgba(255, 255, 255, 0.1)" : "transparent",
     83             color: val === activeValue ? "#fff" : "rgba(255, 255, 255, 0.2)",
     84             border: val === activeValue ? "1px solid rgba(255, 255, 255, 0.3)" : "1px solid rgba(255, 255, 255, 0.05)",
     85           }}>
     86             {val}
     87           </span>
     88         ))}
     89       </div>
     90     </div>
     91   );
     92 }
     93 
     94 function ExitCodeBadge({ code }: { code: number | undefined }) {
     95   if (code === undefined || code === null)
     96     return <span style={{ color: "var(--text-muted)" }}>?</span>;
     97   const label = EXIT_CODES[code] || `Exit ${code}`;
     98   const isOk = code === 0;
     99   return (
    100     <div>
    101       <span style={{ fontFamily: "var(--font-mono)", fontWeight: 700, fontSize: "1.75rem", color: isOk ? "var(--green)" : "var(--red)" }}>
    102         {code}
    103       </span>
    104       <div style={{ fontSize: "0.7rem", color: isOk ? "var(--green)" : "var(--red)", opacity: 0.8 }}>
    105         {label}
    106       </div>
    107       <div style={{ fontSize: "0.6rem", color: "var(--text-muted)", marginTop: "2px" }}>
    108         exit code
    109       </div>
    110     </div>
    111   );
    112 }
    113 
    114 function ScoreBar({ label, score }: { label: string; score: number | null | undefined }) {
    115   if (score === null || score === undefined) {
    116     return (
    117       <div style={{ marginBottom: "6px" }}>
    118         <div style={{ display: "flex", justifyContent: "space-between", fontSize: "0.75rem", marginBottom: "2px" }}>
    119           <span>{label}</span>
    120           <span style={{ color: "var(--text-muted)" }}>N/A</span>
    121         </div>
    122       </div>
    123     );
    124   }
    125   const pct = Math.round(score * 100);
    126   const color = pct >= 70 ? "var(--green)" : pct >= 40 ? "var(--yellow)" : "var(--red)";
    127   return (
    128     <div style={{ marginBottom: "6px" }}>
    129       <div style={{ display: "flex", justifyContent: "space-between", fontSize: "0.75rem", marginBottom: "2px" }}>
    130         <span>{label}</span>
    131         <span style={{ fontFamily: "var(--font-mono)", fontWeight: 600, color }}>{pct}%</span>
    132       </div>
    133       <div style={{ background: "var(--bg)", borderRadius: "3px", height: "4px", overflow: "hidden" }}>
    134         <div style={{ width: `${pct}%`, height: "100%", background: color, borderRadius: "3px" }} />
    135       </div>
    136     </div>
    137   );
    138 }
    139 
    140 export default function RunDetail({ run, transcriptLines, axisValues, contextContent }: RunDetailProps) {
    141   const { meta, eval_results, claude_output } = run;
    142 
    143   // Check if this run has an artifact to preview (tetris games, web apps)
    144   const hasArtifact = meta.task === "tetris" || meta.task === "bookmarks-api";
    145   const artifactUrl = hasArtifact ? `/artifacts/${meta.run_id}/index.html` : null;
    146 
    147   return (
    148     <div style={{ display: "flex", flexDirection: "column", gap: "20px" }}>
    149       {/* Top section: stats + config + scores side by side */}
    150       <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: "16px" }}>
    151         {/* Stats */}
    152         <div className="card" style={{ padding: "16px" }}>
    153           <h3 style={{ marginBottom: "12px", fontSize: "0.85rem" }}>Metrics</h3>
    154           {(() => {
    155             const cost = claude_output?.total_cost_usd ?? 0;
    156             const budget = meta.max_budget_usd ?? 0;
    157             const hitBudget = budget > 0 && cost >= budget * 0.95;
    158             const hitTimeout = meta.exit_code === 124;
    159             if (hitBudget || hitTimeout) {
    160               return (
    161                 <div style={{
    162                   background: "rgba(234, 179, 8, 0.1)", border: "1px solid var(--yellow)",
    163                   padding: "6px 10px", marginBottom: "12px", fontSize: "0.7rem",
    164                   color: "var(--yellow)", textTransform: "uppercase", letterSpacing: "0.5px",
    165                   display: "flex", gap: "8px", alignItems: "center",
    166                 }}>
    167                   {hitBudget && <span>Budget limit reached (${cost.toFixed(2)} / ${budget.toFixed(2)})</span>}
    168                   {hitTimeout && <span>Timeout (exceeded time limit)</span>}
    169                   <span style={{ color: "var(--text-muted)" }}>Results may be incomplete</span>
    170                 </div>
    171               );
    172             }
    173             return null;
    174           })()}
    175           <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "12px" }}>
    176             <div>
    177               <ExitCodeBadge code={meta.exit_code} />
    178             </div>
    179             <div>
    180               <div style={{ fontFamily: "var(--font-mono)", fontWeight: 700, fontSize: "1.75rem" }}>
    181                 {meta.wall_time_seconds != null
    182                   ? meta.wall_time_seconds < 60
    183                     ? `${meta.wall_time_seconds}s`
    184                     : `${Math.floor(meta.wall_time_seconds / 60)}m${meta.wall_time_seconds % 60}s`
    185                   : "-"}
    186               </div>
    187               <div style={{ fontSize: "0.7rem", color: "var(--text-muted)" }}>Wall Time</div>
    188             </div>
    189             <div>
    190               <div style={{ fontFamily: "var(--font-mono)", fontWeight: 700, fontSize: "1.75rem" }}>
    191                 {claude_output?.total_cost_usd != null ? `$${claude_output.total_cost_usd.toFixed(2)}` : "-"}
    192               </div>
    193               <div style={{ fontSize: "0.7rem", color: "var(--text-muted)" }}>Cost</div>
    194             </div>
    195             <div>
    196               <div style={{ fontFamily: "var(--font-mono)", fontWeight: 700, fontSize: "1.75rem" }}>
    197                 {claude_output?.num_turns ?? "-"}
    198               </div>
    199               <div style={{ fontSize: "0.7rem", color: "var(--text-muted)" }}>Turns</div>
    200             </div>
    201           </div>
    202           {meta.claude_version && (
    203             <div style={{ marginTop: "12px", fontSize: "0.65rem", color: "var(--text-muted)" }}>
    204               Claude {meta.claude_version}
    205             </div>
    206           )}
    207           <div style={{ marginTop: "8px", display: "flex", flexWrap: "wrap", gap: "8px", fontSize: "0.65rem" }}>
    208             <a href={`/c/${meta.short_cell_id || meta.cell_id}`} style={{ color: "var(--accent)" }}>
    209               cell
    210             </a>
    211             <a href={`${REPO_URL}/src/branch/main/results/runs/${meta.run_id}`} target="_blank" rel="noopener noreferrer" style={{ color: "var(--accent)" }}>
    212               raw data
    213             </a>
    214             <a href={`${REPO_URL}/src/branch/main/tasks/${meta.task}/prompts/${meta.prompt_style}.${meta.human_language}.md`} target="_blank" rel="noopener noreferrer" style={{ color: "var(--accent)" }}>
    215               prompt
    216             </a>
    217             <a href={`${REPO_URL}/src/branch/main/tasks/${meta.task}/eval`} target="_blank" rel="noopener noreferrer" style={{ color: "var(--accent)" }}>
    218               eval suite
    219             </a>
    220             <a href={`${REPO_URL}/src/branch/main/tasks/${meta.task}/scoring.yaml`} target="_blank" rel="noopener noreferrer" style={{ color: "var(--accent)" }}>
    221               scoring
    222             </a>
    223           </div>
    224         </div>
    225 
    226         {/* Config */}
    227         <div className="card" style={{ padding: "16px" }}>
    228           <h3 style={{ marginBottom: "10px", fontSize: "0.85rem" }}>Configuration</h3>
    229           {AXIS_CONFIG.map(({ key, label }) => {
    230             const active = String((meta as Record<string, unknown>)[key] ?? "");
    231             const all = (axisValues as Record<string, string[]>)[key] || [active];
    232             if (!active) return null;
    233             return <ConfigPills key={key} label={label} activeValue={active} allValues={all} />;
    234           })}
    235           {contextContent && (
    236             <div style={{ marginTop: "10px", borderTop: "1px solid var(--border)", paddingTop: "8px" }}>
    237               <div style={{ fontSize: "0.7rem", color: "var(--text-muted)", marginBottom: "4px", textTransform: "uppercase", letterSpacing: "0.5px" }}>Context file provided</div>
    238               <pre style={{ fontSize: "0.65rem", color: "var(--text-muted)", whiteSpace: "pre-wrap", lineHeight: 1.5, maxHeight: "150px", overflow: "auto" }}>{contextContent}</pre>
    239             </div>
    240           )}
    241         </div>
    242 
    243         {/* Scores */}
    244         <div className="card" style={{ padding: "16px" }}>
    245           {eval_results && (
    246             <>
    247               {/* OUTCOME */}
    248               <div style={{ fontSize: "0.6rem", fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.08em", color: "var(--text-muted)", marginBottom: "8px" }}>
    249                 Outcome
    250               </div>
    251               {(() => {
    252                 const pct = eval_results.score != null ? Math.round(eval_results.score * 100) : null;
    253                 const color = pct != null
    254                   ? pct >= 70 ? "var(--green)" : pct >= 40 ? "var(--yellow)" : "var(--red)"
    255                   : "var(--text-muted)";
    256                 return (
    257                   <div style={{ textAlign: "center", marginBottom: "12px" }}>
    258                     <div style={{ fontFamily: "var(--font-mono)", fontWeight: 700, fontSize: "2.25rem", color }}>
    259                       {pct != null ? `${pct}%` : "-"}
    260                     </div>
    261                     <div style={{ fontSize: "0.65rem", color: "var(--text-muted)" }}>overall score</div>
    262                   </div>
    263                 );
    264               })()}
    265               <ScoreBar label="Gameplay" score={(eval_results as Record<string, any>).gameplay_bot?.score} />
    266               <ScoreBar label="Code Quality (SonarQube)" score={(eval_results as Record<string, any>).sonarqube?.score} />
    267 
    268               {/* Separator */}
    269               <div style={{ borderTop: "1px solid var(--border)", margin: "10px 0" }} />
    270 
    271               {/* OUTPUT METRICS */}
    272               <div style={{ fontSize: "0.6rem", fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.08em", color: "var(--text-muted)", marginBottom: "8px" }}>
    273                 Output Metrics
    274               </div>
    275               <ScoreBar label="Build Quality" score={eval_results.quality?.score} />
    276               <ScoreBar label="Structural" score={eval_results.structural?.score} />
    277               <ScoreBar label="Code Analysis" score={(eval_results as Record<string, any>).code_analysis?.score} />
    278               <ScoreBar label="Transcript" score={(eval_results as Record<string, any>).transcript_analysis?.score} />
    279             </>
    280           )}
    281         </div>
    282       </div>
    283 
    284       {/* Detail cards row */}
    285       <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: "16px" }}>
    286         {/* Structural checks */}
    287         {eval_results?.structural?.checks && (
    288           <div className="card" style={{ padding: "16px" }}>
    289             <h4 style={{ fontSize: "0.8rem", color: "var(--text-muted)", marginBottom: "8px" }}>Structural Checks</h4>
    290             {eval_results.structural.checks.map(
    291               (check: { pass: boolean; name: string; detail: string }, i: number) => (
    292                 <div key={i} style={{ display: "flex", gap: "6px", fontSize: "0.7rem", marginBottom: "2px" }}>
    293                   <span style={{ color: check.pass ? "var(--green)" : "var(--red)", flexShrink: 0 }}>
    294                     {check.pass ? "+" : "-"}
    295                   </span>
    296                   <span style={{ fontFamily: "var(--font-mono)" }}>{check.name}</span>
    297                 </div>
    298               )
    299             )}
    300           </div>
    301         )}
    302 
    303         {/* Code analysis details */}
    304         {(eval_results as Record<string, any>)?.code_analysis && !(eval_results as Record<string, any>).code_analysis.error && (
    305           <div className="card" style={{ padding: "16px" }}>
    306             <h4 style={{ fontSize: "0.8rem", color: "var(--text-muted)", marginBottom: "8px" }}>Code Analysis</h4>
    307             {(() => {
    308               const ca = (eval_results as Record<string, any>).code_analysis;
    309               return (
    310                 <div style={{ fontSize: "0.7rem", display: "flex", flexDirection: "column", gap: "3px" }}>
    311                   <Stat label="Files" value={`${ca.files?.code ?? "?"} code, ${ca.files?.unnecessary ?? 0} unnecessary`} />
    312                   <Stat label="Lines of code" value={ca.lines_of_code} />
    313                   <Stat label="Dependencies" value={ca.dependencies?.total ?? 0} />
    314                   <Stat label="Complexity" value={ca.complexity} />
    315                   <Stat label="Functions" value={`${ca.function_length?.count ?? "?"} (avg ${ca.function_length?.average ?? "?"}  lines, max ${ca.function_length?.max ?? "?"})`} />
    316                   <Stat label="Max nesting" value={`${ca.max_nesting_depth} levels`} />
    317                   <Stat label="Naming" value={`${ca.naming?.dominant_style} (${ca.naming?.consistency_pct}% consistent)`} />
    318                   <Stat label="Comments" value={`${ca.comments?.ratio_pct ?? 0}% of source`} />
    319                   <Stat label="Separation" value={ca.separation_of_concerns?.verdict} />
    320                   <Stat label="Console.logs" value={ca.console_logs} />
    321                   <Stat label="Duplication" value={`${ca.duplication_percentage ?? 0}%`} />
    322                   {ca.html_validation && <Stat label="HTML valid" value={ca.html_validation.valid ? "yes" : `no (${ca.html_validation.errors} errors)`} />}
    323                 </div>
    324               );
    325             })()}
    326           </div>
    327         )}
    328 
    329         {/* Transcript analysis details */}
    330         {(eval_results as Record<string, any>)?.transcript_analysis && !(eval_results as Record<string, any>).transcript_analysis.error && (
    331           <div className="card" style={{ padding: "16px" }}>
    332             <h4 style={{ fontSize: "0.8rem", color: "var(--text-muted)", marginBottom: "8px" }}>Agent Process</h4>
    333             {(() => {
    334               const ta = (eval_results as Record<string, any>).transcript_analysis;
    335               return (
    336                 <div style={{ fontSize: "0.7rem", display: "flex", flexDirection: "column", gap: "3px" }}>
    337                   <Stat label="Tool calls" value={ta.tool_calls?.total ?? 0} />
    338                   <Stat label="Bash" value={ta.tool_calls?.bash ?? 0} />
    339                   <Stat label="Write/Edit" value={`${ta.tool_calls?.write ?? 0} / ${ta.tool_calls?.edit ?? 0}`} />
    340                   <Stat label="Wasted turns" value={`${ta.wasted_turns?.total ?? 0} (${ta.wasted_turns?.docs ?? 0} docs, ${ta.wasted_turns?.ascii_art ?? 0} ascii, ${ta.wasted_turns?.server_starts ?? 0} server)`} />
    341                   <Stat label="Errors hit" value={ta.errors_encountered ?? 0} />
    342                   <Stat label="Productivity" value={`${((ta.productivity_ratio ?? 0) * 100).toFixed(0)}%`} />
    343                   <Stat label="Self-tested" value={ta.self_tested ? "yes" : "no"} />
    344                   <Stat label="Thinking blocks" value={ta.thinking_blocks ?? 0} />
    345                 </div>
    346               );
    347             })()}
    348           </div>
    349         )}
    350 
    351         {/* Gameplay Bot details */}
    352         {(eval_results as Record<string, any>)?.gameplay_bot && (
    353           <div className="card" style={{ padding: "16px" }}>
    354             <h4 style={{ fontSize: "0.8rem", color: "var(--text-muted)", marginBottom: "8px" }}>Gameplay Bot</h4>
    355             {(() => {
    356               const gb = (eval_results as Record<string, any>).gameplay_bot;
    357               const report = gb.report;
    358               const tests = report?.tests as Array<{ name: string; pass: boolean; detail: string }> | undefined;
    359               const impl = report?.implementation;
    360               const gameplay = report?.gameplay;
    361               return (
    362                 <div style={{ fontSize: "0.7rem", display: "flex", flexDirection: "column", gap: "3px" }}>
    363                   {tests && tests.map((t, i) => (
    364                     <div key={i} style={{ display: "flex", gap: "6px", marginBottom: "1px" }}>
    365                       <span style={{ color: t.pass ? "var(--green)" : "var(--red)", flexShrink: 0 }}>
    366                         {t.pass ? "+" : "-"}
    367                       </span>
    368                       <span style={{ fontFamily: "var(--font-mono)", flexShrink: 0 }}>{t.name}</span>
    369                       <span style={{ color: "var(--text-muted)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{t.detail}</span>
    370                     </div>
    371                   ))}
    372                   {(impl || gameplay) && (
    373                     <div style={{ borderTop: "1px solid var(--border)", marginTop: "6px", paddingTop: "6px", display: "flex", flexDirection: "column", gap: "3px" }}>
    374                       {impl?.renderer && <Stat label="Renderer" value={impl.renderer} />}
    375                       {impl?.start_mechanism && <Stat label="Start" value={impl.start_mechanism} />}
    376                       {impl?.controls && <Stat label="Controls" value={Object.entries(impl.controls).map(([k, v]) => `${k}:${v}`).join(", ")} />}
    377                       {gameplay?.pieces_placed != null && <Stat label="Pieces placed" value={gameplay.pieces_placed} />}
    378                       {gameplay?.lines_cleared != null && <Stat label="Lines cleared" value={gameplay.lines_cleared} />}
    379                       {gameplay?.max_score_observed != null && <Stat label="Max score" value={gameplay.max_score_observed} />}
    380                     </div>
    381                   )}
    382                   {!tests && !report && (
    383                     <Stat label="Score" value={`${Math.round((gb.score ?? 0) * 100)}%`} />
    384                   )}
    385                 </div>
    386               );
    387             })()}
    388           </div>
    389         )}
    390 
    391         {/* SonarQube details */}
    392         {(eval_results as Record<string, any>)?.sonarqube && !(eval_results as Record<string, any>).sonarqube.error && (
    393           <div className="card" style={{ padding: "16px" }}>
    394             <h4 style={{ fontSize: "0.8rem", color: "var(--text-muted)", marginBottom: "8px" }}>SonarQube</h4>
    395             {(() => {
    396               const sq = (eval_results as Record<string, any>).sonarqube;
    397               const ratingColor = (r: string) => r === "A" ? "var(--green)" : r === "B" ? "var(--yellow)" : "var(--red)";
    398               return (
    399                 <div style={{ fontSize: "0.7rem", display: "flex", flexDirection: "column", gap: "3px" }}>
    400                   <div style={{ display: "flex", gap: "12px", marginBottom: "4px" }}>
    401                     {["maintainability", "reliability", "security"].map((k) => sq[k] && (
    402                       <div key={k} style={{ textAlign: "center" }}>
    403                         <div style={{ fontFamily: "var(--font-mono)", fontWeight: 700, fontSize: "1.1rem", color: ratingColor(sq[k]) }}>{sq[k]}</div>
    404                         <div style={{ fontSize: "0.55rem", color: "var(--text-muted)", textTransform: "capitalize" }}>{k}</div>
    405                       </div>
    406                     ))}
    407                   </div>
    408                   <Stat label="Bugs" value={sq.bugs ?? 0} />
    409                   <Stat label="Vulnerabilities" value={sq.vulnerabilities ?? 0} />
    410                   <Stat label="Code smells" value={sq.code_smells ?? 0} />
    411                   <Stat label="Cognitive complexity" value={sq.cognitive_complexity ?? "-"} />
    412                   <Stat label="Duplication" value={`${sq.duplication_pct ?? 0}%`} />
    413                   <Stat label="Tech debt" value={sq.tech_debt_minutes != null ? `${sq.tech_debt_minutes} min` : "-"} />
    414                   <Stat label="Lines analyzed" value={sq.lines_of_code ?? "-"} />
    415                 </div>
    416               );
    417             })()}
    418           </div>
    419         )}
    420 
    421         {/* Quality details */}
    422         {eval_results?.quality && (
    423           <div className="card" style={{ padding: "16px" }}>
    424             <h4 style={{ fontSize: "0.8rem", color: "var(--text-muted)", marginBottom: "8px" }}>Quality</h4>
    425             {(() => {
    426               const q = eval_results.quality as Record<string, any>;
    427               const lint = q.lint;
    428               const tc = q.typecheck;
    429               const perf = q.performance;
    430               return (
    431                 <div style={{ fontSize: "0.7rem", display: "flex", flexDirection: "column", gap: "3px" }}>
    432                   {lint && (
    433                     <>
    434                       <div style={{ display: "flex", gap: "6px", marginBottom: "1px" }}>
    435                         <span style={{ color: lint.pass ? "var(--green)" : "var(--red)", flexShrink: 0 }}>
    436                           {lint.pass ? "+" : "-"}
    437                         </span>
    438                         <span style={{ fontFamily: "var(--font-mono)" }}>lint</span>
    439                         <span style={{ color: "var(--text-muted)" }}>
    440                           {lint.errors > 0 ? `${lint.errors} errors` : ""}
    441                           {lint.errors > 0 && lint.warnings > 0 ? ", " : ""}
    442                           {lint.warnings > 0 ? `${lint.warnings} warnings` : ""}
    443                           {lint.errors === 0 && lint.warnings === 0 ? "clean" : ""}
    444                         </span>
    445                       </div>
    446                     </>
    447                   )}
    448                   {tc && (
    449                     <div style={{ display: "flex", gap: "6px", marginBottom: "1px" }}>
    450                       <span style={{ color: tc.pass ? "var(--green)" : "var(--red)", flexShrink: 0 }}>
    451                         {tc.pass ? "+" : "-"}
    452                       </span>
    453                       <span style={{ fontFamily: "var(--font-mono)" }}>typecheck</span>
    454                       <span style={{ color: "var(--text-muted)" }}>
    455                         {tc.errors ? `${tc.errors} errors` : tc.pass ? "clean" : "failed"}
    456                       </span>
    457                     </div>
    458                   )}
    459                   {perf && (
    460                     <div style={{ borderTop: "1px solid var(--border)", marginTop: "6px", paddingTop: "6px", display: "flex", flexDirection: "column", gap: "3px" }}>
    461                       <Stat label="Bundle size" value={perf.bundle_size_bytes != null ? `${(perf.bundle_size_bytes / 1024).toFixed(1)} KB` : "N/A"} />
    462                       {perf.size_under_512kb != null && <Stat label="Under 512 KB" value={perf.size_under_512kb ? "yes" : "no"} />}
    463                     </div>
    464                   )}
    465                 </div>
    466               );
    467             })()}
    468           </div>
    469         )}
    470 
    471         {/* Functional details */}
    472         {eval_results?.functional && (
    473           <div className="card" style={{ padding: "16px" }}>
    474             <h4 style={{ fontSize: "0.8rem", color: "var(--text-muted)", marginBottom: "8px" }}>Functional</h4>
    475             {(() => {
    476               const fn = eval_results.functional as Record<string, any>;
    477               return (
    478                 <div style={{ fontSize: "0.7rem", display: "flex", flexDirection: "column", gap: "3px" }}>
    479                   <div style={{ display: "flex", gap: "6px", marginBottom: "1px" }}>
    480                     <span style={{ color: fn.pass ? "var(--green)" : "var(--red)", flexShrink: 0 }}>
    481                       {fn.pass ? "+" : "-"}
    482                     </span>
    483                     <span style={{ fontFamily: "var(--font-mono)" }}>
    484                       {fn.pass ? "passed" : "failed"}
    485                     </span>
    486                   </div>
    487                   {fn.error && (
    488                     <div style={{ color: "var(--text-muted)", fontStyle: "italic" }}>{fn.error}</div>
    489                   )}
    490                   {fn.total != null && (
    491                     <Stat label="Tests" value={`${fn.passed ?? 0}/${fn.total} passed`} />
    492                   )}
    493                   {fn.tests && (fn.tests as Array<{ name: string; pass: boolean; detail?: string }>).map((t, i) => (
    494                     <div key={i} style={{ display: "flex", gap: "6px", marginBottom: "1px" }}>
    495                       <span style={{ color: t.pass ? "var(--green)" : "var(--red)", flexShrink: 0 }}>
    496                         {t.pass ? "+" : "-"}
    497                       </span>
    498                       <span style={{ fontFamily: "var(--font-mono)" }}>{t.name}</span>
    499                       {t.detail && <span style={{ color: "var(--text-muted)" }}>{t.detail}</span>}
    500                     </div>
    501                   ))}
    502                 </div>
    503               );
    504             })()}
    505           </div>
    506         )}
    507       </div>
    508 
    509       {/* Bottom: transcript + artifact preview */}
    510       <div style={{
    511         display: "grid",
    512         gridTemplateColumns: artifactUrl ? "1fr 1fr" : "1fr",
    513         gap: "20px",
    514         alignItems: "start",
    515       }}>
    516         <TranscriptViewer lines={transcriptLines} />
    517 
    518         {artifactUrl && (
    519           <div className="card" style={{ padding: "0", overflow: "hidden", position: "sticky", top: "16px" }}>
    520             <div style={{ padding: "12px 16px", borderBottom: "1px solid var(--border)", fontSize: "0.8rem", fontWeight: 600, display: "flex", justifyContent: "space-between", alignItems: "center" }}>
    521               <span>Result Preview</span>
    522               <a href={artifactUrl} target="_blank" rel="noopener noreferrer" style={{ fontSize: "0.7rem", color: "var(--accent)" }}>
    523                 Open standalone
    524               </a>
    525             </div>
    526             <iframe
    527               src={artifactUrl}
    528               style={{
    529                 width: "100%",
    530                 height: "70vh",
    531                 border: "none",
    532                 background: "#fff",
    533               }}
    534               sandbox="allow-scripts"
    535               title="Result preview"
    536             />
    537           </div>
    538         )}
    539       </div>
    540     </div>
    541   );
    542 }

Impressum · Datenschutz