import React, { useState, useEffect, useMemo } from 'react'; import { format } from 'date-fns'; import { ColumnDef, ColumnFiltersState, ExpandedState, RowSelectionState, SortingState, VisibilityState, flexRender, getCoreRowModel, getExpandedRowModel, getFacetedRowModel, getFacetedUniqueValues, getFilteredRowModel, getSortedRowModel, useReactTable, } from '@tanstack/react-table'; import { motion, AnimatePresence } from 'framer-motion'; import { IconBan, IconCheck, IconX, IconTrash } from '@tabler/icons-react'; import { useTranslation } from 'react-i18next'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { TableSkeleton } from '@/components/ui/table-skeleton'; import { PermissionGuard } from '@/components/permission-guard'; import { ServerSidePagination } from '@/components/server-side-pagination'; import { useModels } from '../context/models-context'; import { Model, ModelConnection } from '../data/schema'; const MotionTableRow = motion.create(TableRow); const MotionExpandedRow = motion.create(TableRow); interface ModelsTableProps { columns: ColumnDef[]; data: Model[]; loading?: boolean; pageInfo?: ModelConnection['pageInfo']; pageSize: number; totalCount?: number; nameFilter: string; sorting: SortingState; onSortingChange: (updater: SortingState | ((prev: SortingState) => SortingState)) => void; onNextPage: () => void; onPreviousPage: () => void; onPageSizeChange: (pageSize: number) => void; onNameFilterChange: (filter: string) => void; canWrite?: boolean; } export function ModelsTable({ columns, data, loading, pageInfo, pageSize, totalCount, nameFilter, sorting, onSortingChange, onNextPage, onPreviousPage, onPageSizeChange, onNameFilterChange, canWrite = true, }: ModelsTableProps) { const { t } = useTranslation(); const { setSelectedModels, setResetRowSelection, setOpen } = useModels(); const [rowSelection, setRowSelection] = useState({}); const [expanded, setExpanded] = useState({}); const [columnVisibility, setColumnVisibility] = useState({}); const [columnFilters, setColumnFilters] = useState([]); useEffect(() => { const newColumnFilters: ColumnFiltersState = []; if (nameFilter) { newColumnFilters.push({ id: 'name', value: nameFilter }); } setColumnFilters(newColumnFilters); }, [nameFilter]); const handleColumnFiltersChange = (updater: ColumnFiltersState | ((prev: ColumnFiltersState) => ColumnFiltersState)) => { const newFilters = typeof updater === 'function' ? updater(columnFilters) : updater; setColumnFilters(newFilters); const nameFilterValue = newFilters.find((filter) => filter.id === 'name')?.value as string; const newNameFilter = nameFilterValue || ''; if (newNameFilter !== nameFilter) { onNameFilterChange(newNameFilter); } }; const table = useReactTable({ data, columns, state: { sorting, columnVisibility, rowSelection, columnFilters, expanded, }, enableRowSelection: true, getRowId: (row) => row.id, onRowSelectionChange: setRowSelection, onExpandedChange: setExpanded, onSortingChange, onColumnFiltersChange: handleColumnFiltersChange, onColumnVisibilityChange: setColumnVisibility, getCoreRowModel: getCoreRowModel(), getFilteredRowModel: getFilteredRowModel(), getSortedRowModel: getSortedRowModel(), getFacetedRowModel: getFacetedRowModel(), getFacetedUniqueValues: getFacetedUniqueValues(), getExpandedRowModel: getExpandedRowModel(), manualPagination: true, manualFiltering: true, }); const filteredSelectedRows = useMemo(() => table.getFilteredSelectedRowModel().rows, [table, rowSelection, data]); const selectedCount = filteredSelectedRows.length; useEffect(() => { const resetFn = () => { setRowSelection({}); }; setResetRowSelection(resetFn); }, [setResetRowSelection]); useEffect(() => { const selected = filteredSelectedRows.map((row) => row.original as Model); setSelectedModels(selected); }, [filteredSelectedRows, setSelectedModels]); useEffect(() => { if (selectedCount === 0) { setSelectedModels([]); } }, [selectedCount, setSelectedModels]); useEffect(() => { if (Object.keys(rowSelection).length > 0 && data.length > 0) { const dataIds = new Set(data.map((model) => model.id)); const selectedIds = Object.keys(rowSelection); const anySelectedIdMissing = selectedIds.some((id) => !dataIds.has(id)); if (anySelectedIdMissing) { setRowSelection({}); } } }, [data, rowSelection]); return (
table.getColumn('name')?.setFilterValue(event.target.value)} className='h-8 w-[150px] lg:w-[250px]' />
{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => { return ( {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} ); })} ))} {loading ? ( ) : table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => { const model = row.original; const modelCard = model.modelCard; return ( {row.getVisibleCells().map((cell) => ( {flexRender(cell.column.columnDef.cell, cell.getContext())} ))} {row.getIsExpanded() && (
{/* Top Section: Basic Info (left) + Capabilities (right) */}
{/* Basic Info */}

{t('models.expandedRow.basic')}

{t('models.columns.modelId')}: {model.modelID}
{t('models.columns.developer')}: {model.developer}
{t('models.columns.group')}: {model.group}
{t('common.columns.createdAt')}: {format(model.createdAt, 'yyyy-MM-dd HH:mm')}
{t('common.columns.updatedAt')}: {format(model.updatedAt, 'yyyy-MM-dd HH:mm')}
{model.remark && (
{t('models.columns.remark')}: {model.remark}
)}
{/* Capabilities */}

{t('models.expandedRow.capabilities')}

{t('models.modelCard.toolCall')}: {modelCard?.toolCall ? : '-'}
{t('models.modelCard.vision')}: {modelCard?.vision ? : '-'}
{t('models.modelCard.temperature')}: {modelCard?.temperature ? : '-'}
{/* Reasoning grouped */}
{t('models.modelCard.reasoning')}:
{t('models.modelCard.reasoningSupported')}: {modelCard?.reasoning?.supported ? : '-'}
{t('models.modelCard.reasoningDefault')}: {modelCard?.reasoning?.default ? : '-'}
{/* Middle Section: Modalities + Limits (left) | Cost (right) */}
{/* Left: Modalities + Limits */}
{/* Modalities */}

{t('models.modelCard.modalities')}

{t('models.modelCard.input')}:
{modelCard?.modalities?.input?.length ? modelCard.modalities.input.map((m) => ( {m} )) : '-'}
{t('models.modelCard.output')}:
{modelCard?.modalities?.output?.length ? modelCard.modalities.output.map((m) => ( {m} )) : '-'}
{/* Limits */}

{t('models.modelCard.limit')}

{t('models.modelCard.context')}: {modelCard?.limit?.context?.toLocaleString() ?? '-'}
{t('models.modelCard.output')}: {modelCard?.limit?.output?.toLocaleString() ?? '-'}
{/* Right: Cost */}

{t('models.modelCard.cost')} ($/M)

{t('models.modelCard.input')}: {modelCard?.cost?.input ?? '-'}
{t('models.modelCard.output')}: {modelCard?.cost?.output ?? '-'}
{modelCard?.cost?.cacheRead !== undefined && (
{t('models.modelCard.cacheRead')}: {modelCard.cost.cacheRead}
)} {modelCard?.cost?.cacheWrite !== undefined && (
{t('models.modelCard.cacheWrite')}: {modelCard.cost.cacheWrite}
)}
{/* Bottom Section: Dates */}

{t('models.modelCard.dates')}

{t('models.modelCard.knowledge')}: {modelCard?.knowledge || '-'}
{t('models.modelCard.releaseDate')}: {modelCard?.releaseDate || '-'}
{t('models.modelCard.lastUpdated')}: {modelCard?.lastUpdated || '-'}
)}
); }) ) : ( {t('common.noData')} )}
{selectedCount > 0 && canWrite && (
{selectedCount} {t('common.selected')}
<>
)}
); }