diff --git a/src/components/results/ComparisonGrid.tsx b/src/components/results/ComparisonGrid.tsx index 91ae188..577c6c9 100644 --- a/src/components/results/ComparisonGrid.tsx +++ b/src/components/results/ComparisonGrid.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useState, useRef, useCallback, useEffect } from 'react' import { ChevronDown, ChevronRight, Loader2 } from 'lucide-react' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { ScrollArea } from '@/components/ui/scroll-area' @@ -11,11 +11,125 @@ interface ComparisonGridProps { run: RunResult } +const MIN_COLUMN_WIDTH = 120 +const MIN_ROW_HEIGHT = 40 + export function ComparisonGrid({ run }: ComparisonGridProps) { const { testSuites } = useTestSuiteStore() const testSuite = testSuites.find((s) => s.id === run.testSuiteId) const [expandedRows, setExpandedRows] = useState>(new Set()) + const containerRef = useRef(null) + const [columnWidths, setColumnWidths] = useState([]) + const [rowHeights, setRowHeights] = useState>({}) + const [isResizingColumn, setIsResizingColumn] = useState(null) + const [isResizingRow, setIsResizingRow] = useState(null) + const resizeStartRef = useRef<{ x: number; y: number; initialSize: number }>({ x: 0, y: 0, initialSize: 0 }) + + const totalColumns = run.models.length + 1 + + // Initialize column widths evenly based on container width + useEffect(() => { + const updateColumnWidths = () => { + if (containerRef.current) { + const containerWidth = containerRef.current.clientWidth + const evenWidth = Math.max(MIN_COLUMN_WIDTH, Math.floor(containerWidth / totalColumns)) + setColumnWidths(Array(totalColumns).fill(evenWidth)) + } + } + + updateColumnWidths() + + const resizeObserver = new ResizeObserver(updateColumnWidths) + if (containerRef.current) { + resizeObserver.observe(containerRef.current) + } + + return () => resizeObserver.disconnect() + }, [totalColumns]) + + // Column resize handlers + const handleColumnResizeStart = useCallback((e: React.MouseEvent, columnIndex: number) => { + e.preventDefault() + e.stopPropagation() + setIsResizingColumn(columnIndex) + resizeStartRef.current = { x: e.clientX, y: 0, initialSize: columnWidths[columnIndex] || MIN_COLUMN_WIDTH } + }, [columnWidths]) + + const handleColumnResizeMove = useCallback((e: MouseEvent) => { + if (isResizingColumn === null) return + + const delta = e.clientX - resizeStartRef.current.x + const newWidth = Math.max(MIN_COLUMN_WIDTH, resizeStartRef.current.initialSize + delta) + + setColumnWidths(prev => { + const updated = [...prev] + updated[isResizingColumn] = newWidth + return updated + }) + }, [isResizingColumn]) + + const handleColumnResizeEnd = useCallback(() => { + setIsResizingColumn(null) + }, []) + + // Row resize handlers + const handleRowResizeStart = useCallback((e: React.MouseEvent, rowId: string, currentHeight: number) => { + e.preventDefault() + e.stopPropagation() + setIsResizingRow(rowId) + resizeStartRef.current = { x: 0, y: e.clientY, initialSize: currentHeight } + }, []) + + const handleRowResizeMove = useCallback((e: MouseEvent) => { + if (isResizingRow === null) return + + const delta = e.clientY - resizeStartRef.current.y + const newHeight = Math.max(MIN_ROW_HEIGHT, resizeStartRef.current.initialSize + delta) + + setRowHeights(prev => ({ + ...prev, + [isResizingRow]: newHeight + })) + }, [isResizingRow]) + + const handleRowResizeEnd = useCallback(() => { + setIsResizingRow(null) + }, []) + + // Global mouse event listeners for resize + useEffect(() => { + if (isResizingColumn !== null) { + document.addEventListener('mousemove', handleColumnResizeMove) + document.addEventListener('mouseup', handleColumnResizeEnd) + document.body.style.cursor = 'col-resize' + document.body.style.userSelect = 'none' + + return () => { + document.removeEventListener('mousemove', handleColumnResizeMove) + document.removeEventListener('mouseup', handleColumnResizeEnd) + document.body.style.cursor = '' + document.body.style.userSelect = '' + } + } + }, [isResizingColumn, handleColumnResizeMove, handleColumnResizeEnd]) + + useEffect(() => { + if (isResizingRow !== null) { + document.addEventListener('mousemove', handleRowResizeMove) + document.addEventListener('mouseup', handleRowResizeEnd) + document.body.style.cursor = 'row-resize' + document.body.style.userSelect = 'none' + + return () => { + document.removeEventListener('mousemove', handleRowResizeMove) + document.removeEventListener('mouseup', handleRowResizeEnd) + document.body.style.cursor = '' + document.body.style.userSelect = '' + } + } + }, [isResizingRow, handleRowResizeMove, handleRowResizeEnd]) + if (!testSuite) { return ( @@ -51,126 +165,175 @@ export function ComparisonGrid({ run }: ComparisonGridProps) { ) } + const getColumnWidth = (index: number) => columnWidths[index] || MIN_COLUMN_WIDTH + const getRowHeight = (rowId: string) => rowHeights[rowId] + + // Resize handle component for columns + const ColumnResizeHandle = ({ columnIndex }: { columnIndex: number }) => ( +
handleColumnResizeStart(e, columnIndex)} + /> + ) + + // Resize handle component for rows + const RowResizeHandle = ({ rowId, currentHeight }: { rowId: string; currentHeight: number }) => ( +
handleRowResizeStart(e, rowId, currentHeight)} + /> + ) + return ( Comparison Grid - +
{/* Header Row */} -
-
+
+
Test Case +
- {run.models.map((modelId) => ( + {run.models.map((modelId, idx) => (
{modelId.split('/').pop()} +
))}
{/* Data Rows */} - {testSuite.testCases.map((testCase, index) => ( -
- {/* Summary Row */} -
toggleRow(testCase.id)} - > -
- {expandedRows.has(testCase.id) ? ( - - ) : ( - - )} - #{index + 1} - - {testCase.prompt.slice(0, 30)}... - + {testSuite.testCases.map((testCase, index) => { + const rowId = `row-${testCase.id}` + const customHeight = getRowHeight(rowId) + + return ( +
+ {/* Summary Row */} +
toggleRow(testCase.id)} + style={customHeight ? { minHeight: customHeight } : undefined} + > +
+ {expandedRows.has(testCase.id) ? ( + + ) : ( + + )} + #{index + 1} + + {testCase.prompt} + +
+ {run.models.map((modelId, idx) => { + const result = getResultForCell(testCase.id, modelId) + return ( +
+ {result?.status === 'running' ? ( + + ) : result?.status === 'failed' ? ( + Failed + ) : result?.score ? ( +
+
+ + {(result.score.score * 100).toFixed(0)}% + +
+ ) : result?.status === 'completed' ? ( + No score + ) : ( + Pending + )} +
+ ) + })} +
- {run.models.map((modelId) => { - const result = getResultForCell(testCase.id, modelId) - return ( -
- {result?.status === 'running' ? ( - - ) : result?.status === 'failed' ? ( - Failed - ) : result?.score ? ( -
-
- - {(result.score.score * 100).toFixed(0)}% - + + {/* Expanded Content */} + {expandedRows.has(testCase.id) && ( +
+ {/* Prompt */} +
+
+ Prompt +
+
+ {testCase.prompt} +
+ {testCase.expectedOutput && ( +
+
+ Expected Output +
+
+ {testCase.expectedOutput} +
- ) : result?.status === 'completed' ? ( - No score - ) : ( - Pending )}
- ) - })} -
- {/* Expanded Content */} - {expandedRows.has(testCase.id) && ( -
- {/* Prompt */} -
-
- Prompt -
-
- {testCase.prompt} -
- {testCase.expectedOutput && ( -
-
- Expected Output -
-
- {testCase.expectedOutput} -
+ {/* Responses */} +
+
+ Responses
- )} -
- - {/* Responses */} -
-
- Responses + {run.models.map((modelId, idx) => { + const result = getResultForCell(testCase.id, modelId) + const expandedRowId = `expanded-${testCase.id}` + const expandedHeight = getRowHeight(expandedRowId) + return ( +
+ +
+ ) + })} +
- {run.models.map((modelId) => { - const result = getResultForCell(testCase.id, modelId) - return ( -
- -
- ) - })}
-
- )} -
- ))} + )} +
+ ) + })}