Spaces:
Running
Running
import React, { useState, useEffect, useMemo } from 'react'; | |
import { useTable, useSortBy, useFilters, usePagination } from 'react-table'; | |
import api from '../api'; | |
// Default filter UI for columns | |
function DefaultColumnFilter({ | |
column: { filterValue, preFilteredRows, setFilter }, | |
}) { | |
const count = preFilteredRows.length; | |
return ( | |
<input | |
value={filterValue || ''} | |
onChange={e => { | |
setFilter(e.target.value || undefined); | |
}} | |
placeholder={`Search ${count} records...`} | |
className="px-2 py-1 text-sm border rounded focus:outline-none focus:ring-2 focus:ring-primary/50 w-full" | |
/> | |
); | |
} | |
// Dummy data for when API fails | |
const dummyData = [ | |
{ | |
Model: 'WatermarkA', | |
'Success Rate': '95%', | |
'Attack Resistance': 'High', | |
'Visual Quality': '9.2', | |
'Compute Requirements': 'Medium' | |
}, | |
{ | |
Model: 'WatermarkB', | |
'Success Rate': '92%', | |
'Attack Resistance': 'Medium', | |
'Visual Quality': '9.5', | |
'Compute Requirements': 'Low' | |
}, | |
{ | |
Model: 'WatermarkC', | |
'Success Rate': '98%', | |
'Attack Resistance': 'Very High', | |
'Visual Quality': '8.8', | |
'Compute Requirements': 'High' | |
} | |
]; | |
// Dummy columns for when API fails | |
const dummyColumns = [ | |
{ Header: 'Model', accessor: 'Model' }, | |
{ Header: 'Success Rate', accessor: 'Success Rate' }, | |
{ Header: 'Attack Resistance', accessor: 'Attack Resistance' }, | |
{ Header: 'Visual Quality', accessor: 'Visual Quality' }, | |
{ Header: 'Compute Requirements', accessor: 'Compute Requirements' } | |
]; | |
function Leaderboard({ benchmark }) { | |
const [data, setData] = useState([]); | |
const [columns, setColumns] = useState([]); | |
const [loading, setLoading] = useState(true); | |
const [error, setError] = useState(null); | |
const [searchQuery, setSearchQuery] = useState(''); | |
useEffect(() => { | |
// Fetch leaderboard data and columns when the benchmark changes | |
async function fetchData() { | |
try { | |
setLoading(true); | |
// Fetch available columns first | |
const columnsResponse = await api.getColumns(benchmark); | |
if (!columnsResponse.success) { | |
throw new Error(columnsResponse.error || 'Failed to fetch columns'); | |
} | |
// Format columns for react-table | |
const tableColumns = columnsResponse.columns.map(column => ({ | |
Header: column, | |
accessor: column, | |
Filter: DefaultColumnFilter, | |
})); | |
setColumns(tableColumns); | |
// Fetch leaderboard data | |
const dataResponse = await api.getLeaderboard(benchmark); | |
if (!dataResponse.success) { | |
throw new Error(dataResponse.error || 'Failed to fetch leaderboard data'); | |
} | |
setData(dataResponse.data); | |
setError(null); | |
} catch (err) { | |
console.error('Leaderboard error:', err); | |
setError(err.message || 'An error occurred'); | |
// Set dummy data so UI doesn't show error | |
setData(dummyData); | |
setColumns(dummyColumns.map(col => ({ | |
...col, | |
Filter: DefaultColumnFilter | |
}))); | |
} finally { | |
setLoading(false); | |
} | |
} | |
fetchData(); | |
}, [benchmark]); | |
// Filter data based on search query | |
const filteredData = useMemo(() => { | |
if (!searchQuery) return data; | |
return data.filter(row => { | |
return Object.values(row).some(value => | |
String(value).toLowerCase().includes(searchQuery.toLowerCase()) | |
); | |
}); | |
}, [data, searchQuery]); | |
// Set up react-table | |
const { | |
getTableProps, | |
getTableBodyProps, | |
headerGroups, | |
page, | |
prepareRow, | |
canPreviousPage, | |
canNextPage, | |
pageOptions, | |
pageCount, | |
gotoPage, | |
nextPage, | |
previousPage, | |
setPageSize, | |
state: { pageIndex, pageSize }, | |
} = useTable( | |
{ | |
columns, | |
data: filteredData, | |
initialState: { pageIndex: 0, pageSize: 10 }, | |
}, | |
useFilters, | |
useSortBy, | |
usePagination | |
); | |
if (loading) { | |
return ( | |
<div className="flex justify-center items-center p-8"> | |
<div className="animate-pulse text-primary font-medium">Loading leaderboard data...</div> | |
</div> | |
); | |
} | |
return ( | |
<div className="leaderboard"> | |
<h2 className="text-2xl font-display font-medium text-secondary mb-6"> | |
{benchmark.charAt(0).toUpperCase() + benchmark.slice(1)} Watermark Benchmark | |
</h2> | |
<div className="mb-4"> | |
<input | |
type="text" | |
placeholder="Search across all columns..." | |
value={searchQuery} | |
onChange={e => setSearchQuery(e.target.value)} | |
className="w-full px-4 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50" | |
/> | |
</div> | |
<div className="overflow-x-auto rounded-md shadow"> | |
<table {...getTableProps()} className="data-table w-full"> | |
<thead> | |
{headerGroups.map(headerGroup => ( | |
<tr {...headerGroup.getHeaderGroupProps()}> | |
{headerGroup.headers.map(column => ( | |
<th | |
{...column.getHeaderProps(column.getSortByToggleProps())} | |
className="bg-gray-100 text-left px-4 py-3 font-semibold text-gray-700" | |
> | |
<div className="flex items-center space-x-1"> | |
<span>{column.render('Header')}</span> | |
<span> | |
{column.isSorted | |
? column.isSortedDesc | |
? ' ↓' | |
: ' ↑' | |
: ''} | |
</span> | |
</div> | |
<div className="mt-2">{column.canFilter ? column.render('Filter') : null}</div> | |
</th> | |
))} | |
</tr> | |
))} | |
</thead> | |
<tbody {...getTableBodyProps()}> | |
{page.map(row => { | |
prepareRow(row); | |
return ( | |
<tr {...row.getRowProps()} className="hover:bg-gray-50"> | |
{row.cells.map(cell => ( | |
<td | |
{...cell.getCellProps()} | |
className="border-t px-4 py-3" | |
> | |
{cell.render('Cell')} | |
</td> | |
))} | |
</tr> | |
); | |
})} | |
</tbody> | |
</table> | |
</div> | |
<div className="mt-4 flex flex-wrap items-center justify-between"> | |
<div className="flex space-x-2 mb-2 sm:mb-0"> | |
<button | |
onClick={() => gotoPage(0)} | |
disabled={!canPreviousPage} | |
className="px-3 py-1 rounded border bg-white disabled:opacity-50" | |
> | |
{'<<'} | |
</button> | |
<button | |
onClick={() => previousPage()} | |
disabled={!canPreviousPage} | |
className="px-3 py-1 rounded border bg-white disabled:opacity-50" | |
> | |
{'<'} | |
</button> | |
<button | |
onClick={() => nextPage()} | |
disabled={!canNextPage} | |
className="px-3 py-1 rounded border bg-white disabled:opacity-50" | |
> | |
{'>'} | |
</button> | |
<button | |
onClick={() => gotoPage(pageCount - 1)} | |
disabled={!canNextPage} | |
className="px-3 py-1 rounded border bg-white disabled:opacity-50" | |
> | |
{'>>'} | |
</button> | |
</div> | |
<span className="text-sm"> | |
Page{' '} | |
<strong> | |
{pageIndex + 1} of {pageOptions.length} | |
</strong> | |
</span> | |
<div className="flex items-center space-x-2"> | |
<span className="text-sm">Go to page:</span> | |
<input | |
type="number" | |
defaultValue={pageIndex + 1} | |
onChange={e => { | |
const page = e.target.value ? Number(e.target.value) - 1 : 0; | |
gotoPage(page); | |
}} | |
className="w-16 px-2 py-1 border rounded" | |
/> | |
<select | |
value={pageSize} | |
onChange={e => { | |
setPageSize(Number(e.target.value)); | |
}} | |
className="px-2 py-1 border rounded" | |
> | |
{[10, 20, 30, 40, 50].map(pageSize => ( | |
<option key={pageSize} value={pageSize}> | |
Show {pageSize} | |
</option> | |
))} | |
</select> | |
</div> | |
</div> | |
</div> | |
); | |
} | |
export default Leaderboard; | |