omnisealbench / frontend /src /components /QualityMetricsTable.tsx
Mark Duppenthaler
Updated table. No more individual rows, separate tabs for leaderboard type, export tables
7f6ef8f
raw
history blame
6.86 kB
import React, { useState } from 'react'
import { ArrowDownTrayIcon } from '@heroicons/react/24/outline'
interface Row {
metric: string
[key: string]: string | number
}
interface QualityMetricsTableProps {
qualityMetrics: string[]
tableHeader: string[]
selectedModels: Set<string>
tableRows: Row[]
}
const QualityMetricsTable: React.FC<QualityMetricsTableProps> = ({
qualityMetrics,
tableHeader,
selectedModels,
tableRows,
}) => {
// Sorting state
const [rowSort, setRowSort] = useState<{ metric: string; direction: 'asc' | 'desc' } | null>(null)
const [columnSort, setColumnSort] = useState<{ model: string; direction: 'asc' | 'desc' } | null>(
null
)
// Handle row sort (sort columns by this metric)
const handleRowSort = (metric: string) => {
setColumnSort(null) // Only one sort active at a time
setRowSort((prev) => {
if (!prev || prev.metric !== metric) return { metric, direction: 'asc' }
if (prev.direction === 'asc') return { metric, direction: 'desc' }
return null // Remove sort
})
}
// Handle column sort (sort rows by this model)
const handleColumnSort = (model: string) => {
setRowSort(null) // Only one sort active at a time
setColumnSort((prev) => {
if (!prev || prev.model !== model) return { model, direction: 'asc' }
if (prev.direction === 'asc') return { model, direction: 'desc' }
return null // Remove sort
})
}
// Sort models (columns)
let sortedModels = tableHeader.filter((model) => selectedModels.has(model))
if (rowSort) {
// Sort columns by the value in the selected metric row
sortedModels = [...sortedModels].sort((a, b) => {
const row = tableRows.find((r) => r.metric === rowSort.metric)
if (!row) return 0
const valA = Number(row[a])
const valB = Number(row[b])
if (isNaN(valA) && isNaN(valB)) return 0
if (isNaN(valA)) return 1
if (isNaN(valB)) return -1
return rowSort.direction === 'asc' ? valA - valB : valB - valA
})
}
// Sort metrics (rows)
let sortedMetrics = [...qualityMetrics]
if (columnSort) {
// Sort rows by the value in the selected model column
sortedMetrics = [...sortedMetrics].sort((a, b) => {
const rowA = tableRows.find((r) => r.metric === a)
const rowB = tableRows.find((r) => r.metric === b)
if (!rowA || !rowB) return 0
const valA = Number(rowA[columnSort.model])
const valB = Number(rowB[columnSort.model])
if (isNaN(valA) && isNaN(valB)) return 0
if (isNaN(valA)) return 1
if (isNaN(valB)) return -1
return columnSort.direction === 'asc' ? valA - valB : valB - valA
})
}
// CSV export logic
function exportToCSV() {
// Build header row
const header = ['Metric', ...sortedModels]
// Build data rows
const rows = sortedMetrics
.map((metric) => {
const row = tableRows.find((r) => r.metric === metric)
if (!row) return null
return [
metric,
...sortedModels.map((col) => {
const cell = row[col]
// Format as displayed
return !isNaN(Number(cell)) ? Number(Number(cell).toFixed(3)) : cell
}),
]
})
.filter((row): row is (string | number)[] => !!row)
// Combine
const csv = [header, ...rows]
.map((row) => row.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(','))
.join('\n')
// Download
const blob = new Blob([csv], { type: 'text/csv' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'quality_metrics.csv'
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
}
if (qualityMetrics.length === 0) return null
return (
<div className="overflow-x-auto max-h-[80vh] overflow-y-auto">
<div className="flex justify-end mb-2">
<button className="btn btn-ghost btn-circle" title="Export CSV" onClick={exportToCSV}>
<ArrowDownTrayIcon className="h-6 w-6" />
</button>
</div>
<table className="table w-full min-w-max border-gray-700 border">
<thead>
<tr>
<th className="sticky left-0 top-0 bg-base-100 z-20 border-gray-700 border">Metric</th>
{sortedModels.map((model) => {
const isSorted = columnSort && columnSort.model === model
const direction = isSorted ? columnSort.direction : undefined
return (
<th
key={`quality-${model}`}
className="sticky top-0 bg-base-100 z-10 text-center text-xs border-gray-700 border cursor-pointer select-none"
onClick={() => handleColumnSort(model)}
title={
isSorted
? direction === 'asc'
? 'Sort descending'
: 'Clear sort'
: 'Sort by this column'
}
>
{model}
<span className="ml-1">{isSorted ? (direction === 'asc' ? '↑' : '↓') : 'β‡…'}</span>
</th>
)
})}
</tr>
</thead>
<tbody>
{sortedMetrics.map((metric) => {
const row = tableRows.find((r) => r.metric === metric)
if (!row) return null
const isSorted = rowSort && rowSort.metric === metric
const direction = isSorted ? rowSort.direction : undefined
return (
<tr key={`quality-${metric}`} className="hover:bg-base-100">
<td
className="sticky left-0 bg-base-100 z-10 border-gray-700 border cursor-pointer select-none"
onClick={() => handleRowSort(metric)}
title={
isSorted
? direction === 'asc'
? 'Sort descending'
: 'Clear sort'
: 'Sort by this row (sorts columns)'
}
>
{metric}
<span className="ml-1">{isSorted ? (direction === 'asc' ? '↑' : '↓') : 'β‡…'}</span>
</td>
{sortedModels.map((col) => {
const cell = row[col]
return (
<td
key={`quality-${metric}-${col}`}
className="text-center border-gray-700 border"
>
{!isNaN(Number(cell)) ? Number(Number(cell).toFixed(3)) : cell}
</td>
)
})}
</tr>
)
})}
</tbody>
</table>
</div>
)
}
export default QualityMetricsTable