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 }