Spaces:
Running
Running
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) => { | |
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) => { | |
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 = ['Quality 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"> | |
<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"> | |
Quality 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 pr-4" | |
onClick={() => handleRowSort(metric)} | |
title={ | |
isSorted | |
? direction === 'asc' | |
? 'Sort descending' | |
: 'Clear sort' | |
: 'Sort by this row (sorts columns)' | |
} | |
> | |
<span className="inline-block">{metric}</span> | |
<span className="float-right"> | |
{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 | |