Insights.tsx (5296B)
1 import { useState, useMemo } from "react"; 2 import type { Run } from "../lib/types"; 3 import { computeMainEffects, computeInteraction, groupIntoCells } from "../lib/analysis"; 4 import { modelSortOrder } from "../lib/colors"; 5 import TornadoChart from "./TornadoChart"; 6 import Heatmap from "./Heatmap"; 7 import ModelSelector from "./ModelSelector"; 8 9 interface InsightsProps { 10 runs: Run[]; 11 } 12 13 const METRICS = [ 14 { key: "score", label: "Outcome" }, 15 { key: "gameplay", label: "Gameplay" }, 16 { key: "sonarqube", label: "Code Quality" }, 17 { key: "quality", label: "Build Quality" }, 18 { key: "structural", label: "Structural" }, 19 { key: "code_quality", label: "Code Analysis" }, 20 { key: "transcript", label: "Agent Eff." }, 21 { key: "cost", label: "Cost" }, 22 { key: "turns", label: "Turns" }, 23 { key: "wall_time", label: "Time" }, 24 ]; 25 26 export default function Insights({ runs }: InsightsProps) { 27 const [metric, setMetric] = useState("score"); 28 const [axisA, setAxisA] = useState(""); 29 const [axisB, setAxisB] = useState(""); 30 31 // Model filter 32 const allModels = useMemo(() => { 33 const models = new Set<string>(); 34 for (const run of runs) { 35 models.add(run.meta.actual_model || run.meta.model); 36 } 37 return [...models].sort((a, b) => modelSortOrder(a) - modelSortOrder(b) || a.localeCompare(b)); 38 }, [runs]); 39 40 const [selectedModels, setSelectedModels] = useState<Set<string>>(() => new Set(allModels)); 41 42 const filteredRuns = useMemo( 43 () => runs.filter((r) => selectedModels.has(r.meta.actual_model || r.meta.model)), 44 [runs, selectedModels] 45 ); 46 47 const effects = useMemo( 48 () => computeMainEffects(filteredRuns, metric), 49 [filteredRuns, metric] 50 ); 51 52 const filteredCells = useMemo(() => groupIntoCells(filteredRuns), [filteredRuns]); 53 54 // Auto-pick top 2 axes for interaction if not selected 55 const topAxes = useMemo(() => effects.slice(0, 6).map((e) => e.axis), [effects]); 56 57 const interaction = useMemo(() => { 58 const a = axisA || topAxes[0] || ""; 59 const b = axisB || topAxes[1] || ""; 60 if (!a || !b || a === b) return null; 61 return computeInteraction(filteredRuns, a, b, metric); 62 }, [filteredRuns, axisA, axisB, metric, topAxes]); 63 64 return ( 65 <div style={{ display: "flex", flexDirection: "column", gap: "24px" }}> 66 {/* Overall sample size subtitle */} 67 <div style={{ fontSize: "10px", fontFamily: "'JetBrains Mono', monospace", color: "var(--text-muted, hsl(213 14% 65%))" }}> 68 (n={filteredRuns.length} runs across {filteredCells.length} cells) 69 </div> 70 71 {/* Metric selector */} 72 <div style={{ display: "flex", gap: "8px", alignItems: "center", flexWrap: "wrap" }}> 73 <span style={{ fontSize: "0.8rem", color: "var(--text-muted)" }}> 74 Metric: 75 </span> 76 {METRICS.map((m) => ( 77 <button 78 key={m.key} 79 onClick={() => setMetric(m.key)} 80 style={{ 81 padding: "4px 12px", 82 borderRadius: "4px", 83 border: 84 metric === m.key 85 ? "1px solid var(--accent)" 86 : "1px solid var(--border)", 87 background: 88 metric === m.key ? "rgba(99, 102, 241, 0.15)" : "transparent", 89 color: metric === m.key ? "var(--accent)" : "var(--text-muted)", 90 cursor: "pointer", 91 fontSize: "0.8rem", 92 }} 93 > 94 {m.label} 95 </button> 96 ))} 97 </div> 98 99 {/* Model filter */} 100 <div style={{ display: "flex", gap: "8px", alignItems: "center" }}> 101 <span style={{ fontSize: "0.8rem", color: "var(--text-muted)" }}> 102 Models: 103 </span> 104 <ModelSelector 105 allModels={allModels} 106 selectedModels={selectedModels} 107 onChange={setSelectedModels} 108 /> 109 </div> 110 111 {/* Tornado chart */} 112 <TornadoChart effects={effects} metric={metric} totalRuns={filteredRuns.length} totalCells={filteredCells.length} runs={filteredRuns} /> 113 114 {/* Interaction explorer */} 115 <div className="card"> 116 <h3 style={{ marginBottom: "12px" }}>Interaction Explorer</h3> 117 <div style={{ display: "flex", gap: "12px", marginBottom: "16px" }}> 118 <div className="filter-group"> 119 <label>Axis A</label> 120 <select 121 value={axisA || topAxes[0] || ""} 122 onChange={(e) => setAxisA(e.target.value)} 123 > 124 {topAxes.map((a) => ( 125 <option key={a} value={a}> 126 {a} 127 </option> 128 ))} 129 </select> 130 </div> 131 <div className="filter-group"> 132 <label>Axis B</label> 133 <select 134 value={axisB || topAxes[1] || ""} 135 onChange={(e) => setAxisB(e.target.value)} 136 > 137 {topAxes 138 .filter((a) => a !== (axisA || topAxes[0])) 139 .map((a) => ( 140 <option key={a} value={a}> 141 {a} 142 </option> 143 ))} 144 </select> 145 </div> 146 </div> 147 148 {interaction && <Heatmap data={interaction} metric={metric} totalRuns={filteredRuns.length} totalCells={filteredCells.length} />} 149 </div> 150 </div> 151 ); 152 }