HeatmapMatrix.tsx (10406B)
1 import { useState, useMemo } from "react"; 2 import type { Run, AxisName } from "../lib/types"; 3 import { AXIS_NAMES } from "../lib/types"; 4 import { groupIntoCells } from "../lib/analysis"; 5 6 interface HeatmapMatrixProps { 7 runs: Run[]; 8 } 9 10 const AXIS_LABELS: Record<AxisName, string> = { 11 model: "Model", 12 effort: "Effort", 13 prompt_style: "Prompt Style", 14 language: "Language", 15 human_language: "Human Lang", 16 tool_read: "Tool: Read", 17 tool_write: "Tool: Write", 18 tool_edit: "Tool: Edit", 19 tool_glob: "Tool: Glob", 20 tool_grep: "Tool: Grep", 21 linter: "Linter", 22 playwright: "Playwright", 23 context_file: "Context File", 24 web_search: "Web Search", 25 max_budget: "Max Budget", 26 tests_provided: "Tests Provided", 27 strategy: "Strategy", 28 design_guidance: "Design Guidance", 29 architecture: "Architecture", 30 error_checking: "Error Checking", 31 context_noise: "Context Noise", 32 renderer: "Renderer", 33 provider: "Provider", 34 }; 35 36 interface CellData { 37 totalScore: number; 38 count: number; 39 } 40 41 function scoreToColor(pct: number): string { 42 // red (0%) -> yellow (50%) -> green (100%) 43 // Using the CSS variable HSL values directly for interpolation 44 if (pct <= 50) { 45 // red to yellow 46 const t = pct / 50; 47 const h = 355 + t * (40 - 355 + 360); // wrap around hue 48 const s = 52 + t * (71 - 52); 49 const l = 64 + t * (73 - 64); 50 return `hsl(${h % 360} ${s}% ${l}%)`; 51 } else { 52 // yellow to green 53 const t = (pct - 50) / 50; 54 const h = 40 + t * (92 - 40); 55 const s = 71 + t * (28 - 71); 56 const l = 73 + t * (65 - 73); 57 return `hsl(${h} ${s}% ${l}%)`; 58 } 59 } 60 61 function cellBackground(pct: number): string { 62 const color = scoreToColor(pct); 63 // Use the color at low opacity for the cell background 64 return color.replace("hsl(", "hsla(").replace(")", " / 0.18)"); 65 } 66 67 export default function HeatmapMatrix({ runs }: HeatmapMatrixProps) { 68 const [rowAxis, setRowAxis] = useState<AxisName>("model"); 69 const [colAxis, setColAxis] = useState<AxisName>("prompt_style"); 70 71 const { rowValues, colValues, cells } = useMemo(() => { 72 const analysisCells = groupIntoCells(runs); 73 const cellMap: Record<string, Record<string, CellData>> = {}; 74 const rowSet = new Set<string>(); 75 const colSet = new Set<string>(); 76 77 for (const cell of analysisCells) { 78 // Skip cells where no run has a score 79 const hasScore = cell.runs.some((r) => r.eval_results?.score != null); 80 if (!hasScore) continue; 81 // Use the cell's average score as a single data point 82 const cellAvg = cell.score.avg; 83 84 const rv = String(cell.meta[rowAxis]); 85 const cv = String(cell.meta[colAxis]); 86 87 rowSet.add(rv); 88 colSet.add(cv); 89 90 if (!cellMap[rv]) cellMap[rv] = {}; 91 if (!cellMap[rv][cv]) cellMap[rv][cv] = { totalScore: 0, count: 0 }; 92 93 cellMap[rv][cv].totalScore += cellAvg; 94 cellMap[rv][cv].count += 1; 95 } 96 97 return { 98 rowValues: Array.from(rowSet).sort(), 99 colValues: Array.from(colSet).sort(), 100 cells: cellMap, 101 }; 102 }, [runs, rowAxis, colAxis]); 103 104 const selectorStyle: React.CSSProperties = { 105 background: "var(--surface-2)", 106 border: "1px solid var(--border)", 107 borderRadius: 0, 108 color: "var(--text)", 109 fontFamily: "var(--font-mono)", 110 fontSize: "var(--text-ui)", 111 padding: "6px 10px", 112 textTransform: "uppercase" as const, 113 letterSpacing: "0.5px", 114 }; 115 116 const labelStyle: React.CSSProperties = { 117 fontSize: "var(--text-label)", 118 color: "var(--text-muted)", 119 textTransform: "uppercase" as const, 120 letterSpacing: "1px", 121 fontWeight: 500, 122 fontFamily: "var(--font-mono)", 123 }; 124 125 return ( 126 <div 127 style={{ 128 background: "var(--surface-1)", 129 border: "1px solid var(--border)", 130 borderRadius: 0, 131 padding: "20px", 132 }} 133 > 134 <h3 style={{ margin: "0 0 16px" }}>Configuration Heatmap</h3> 135 136 {/* Axis selectors */} 137 <div 138 style={{ 139 display: "flex", 140 gap: "24px", 141 marginBottom: "20px", 142 flexWrap: "wrap", 143 alignItems: "flex-end", 144 }} 145 > 146 <div style={{ display: "flex", flexDirection: "column", gap: "4px" }}> 147 <label style={labelStyle}>Row Axis</label> 148 <select 149 value={rowAxis} 150 onChange={(e) => setRowAxis(e.target.value as AxisName)} 151 style={selectorStyle} 152 > 153 {AXIS_NAMES.map((axis) => ( 154 <option key={axis} value={axis}> 155 {AXIS_LABELS[axis]} 156 </option> 157 ))} 158 </select> 159 </div> 160 <div style={{ display: "flex", flexDirection: "column", gap: "4px" }}> 161 <label style={labelStyle}>Column Axis</label> 162 <select 163 value={colAxis} 164 onChange={(e) => setColAxis(e.target.value as AxisName)} 165 style={selectorStyle} 166 > 167 {AXIS_NAMES.map((axis) => ( 168 <option key={axis} value={axis}> 169 {AXIS_LABELS[axis]} 170 </option> 171 ))} 172 </select> 173 </div> 174 </div> 175 176 {/* Heatmap table */} 177 {rowValues.length === 0 || colValues.length === 0 ? ( 178 <div 179 style={{ 180 textAlign: "center", 181 padding: "40px", 182 color: "var(--text-muted)", 183 fontFamily: "var(--font-mono)", 184 }} 185 > 186 No scored cells available for this axis combination. 187 </div> 188 ) : ( 189 <div style={{ overflowX: "auto" }}> 190 <table 191 style={{ 192 borderCollapse: "collapse", 193 width: "auto", 194 fontFamily: "var(--font-mono)", 195 }} 196 > 197 <thead> 198 <tr> 199 <th 200 style={{ 201 padding: "8px 12px", 202 fontSize: "var(--text-label)", 203 textTransform: "uppercase", 204 letterSpacing: "1px", 205 fontWeight: 500, 206 color: "var(--text-muted)", 207 background: "var(--surface-2)", 208 border: "1px solid var(--border)", 209 borderRadius: 0, 210 textAlign: "left", 211 }} 212 > 213 {AXIS_LABELS[rowAxis]} \ {AXIS_LABELS[colAxis]} 214 </th> 215 {colValues.map((col) => ( 216 <th 217 key={col} 218 style={{ 219 padding: "8px 12px", 220 fontSize: "var(--text-label)", 221 textTransform: "uppercase", 222 letterSpacing: "1px", 223 fontWeight: 500, 224 color: "var(--text-muted)", 225 background: "var(--surface-2)", 226 border: "1px solid var(--border)", 227 borderRadius: 0, 228 textAlign: "center", 229 fontFamily: "var(--font-mono)", 230 }} 231 > 232 {col} 233 </th> 234 ))} 235 </tr> 236 </thead> 237 <tbody> 238 {rowValues.map((row) => ( 239 <tr key={row}> 240 <td 241 style={{ 242 padding: "8px 12px", 243 fontSize: "var(--text-label)", 244 textTransform: "uppercase", 245 letterSpacing: "1px", 246 fontWeight: 600, 247 fontFamily: "var(--font-mono)", 248 color: "var(--text)", 249 background: "var(--surface-2)", 250 border: "1px solid var(--border)", 251 borderRadius: 0, 252 whiteSpace: "nowrap", 253 }} 254 > 255 {row} 256 </td> 257 {colValues.map((col) => { 258 const cell = cells[row]?.[col]; 259 if (!cell) { 260 return ( 261 <td 262 key={col} 263 style={{ 264 padding: "10px 16px", 265 textAlign: "center", 266 color: "var(--text-muted)", 267 fontFamily: "var(--font-mono)", 268 fontSize: "var(--text-ui)", 269 border: "1px solid var(--border)", 270 borderRadius: 0, 271 background: "var(--surface-0)", 272 }} 273 > 274 - 275 </td> 276 ); 277 } 278 279 const avg = cell.totalScore / cell.count; 280 const pct = avg * 100; 281 282 return ( 283 <td 284 key={col} 285 style={{ 286 padding: "10px 16px", 287 textAlign: "center", 288 fontFamily: "var(--font-mono)", 289 border: "1px solid var(--border)", 290 borderRadius: 0, 291 background: cellBackground(pct), 292 }} 293 > 294 <div 295 style={{ 296 fontSize: "var(--text-ui)", 297 fontWeight: 700, 298 color: scoreToColor(pct), 299 lineHeight: 1.3, 300 }} 301 > 302 {pct.toFixed(0)}% 303 </div> 304 <div 305 style={{ 306 fontSize: "var(--text-label)", 307 fontWeight: 400, 308 color: "var(--text-muted)", 309 lineHeight: 1.3, 310 }} 311 > 312 {cell.count} {cell.count === 1 ? "cell" : "cells"} 313 </div> 314 </td> 315 ); 316 })} 317 </tr> 318 ))} 319 </tbody> 320 </table> 321 </div> 322 )} 323 </div> 324 ); 325 }