diff --git a/bun.lock b/bun.lock index 36ce485c6..bccc3b80b 100644 --- a/bun.lock +++ b/bun.lock @@ -32,6 +32,7 @@ "packages/web": { "name": "@models.dev/web", "dependencies": { + "@tanstack/virtual-core": "^3.14.0", "hono": "^4.8.0", "models.dev": "workspace:*", }, @@ -57,6 +58,8 @@ "@models.dev/web": ["@models.dev/web@workspace:packages/web"], + "@tanstack/virtual-core": ["@tanstack/virtual-core@3.14.0", "", {}, "sha512-JLANqGy/D6k4Ujmh8Tr25lGimuOXNiaVyXaCAZS0W+1390sADdGnyUdSWNIfd49gebtIxGMij4IktRVzrdr12Q=="], + "@tsconfig/bun": ["@tsconfig/bun@1.0.8", "", {}, "sha512-JlJaRaS4hBTypxtFe8WhnwV8blf0R+3yehLk8XuyxUYNx6VXsKCjACSCvOYEFUiqlhlBWxtYCn/zRlOb8BzBQg=="], "@types/bun": ["@types/bun@1.3.0", "", { "dependencies": { "bun-types": "1.3.0" } }, "sha512-+lAGCYjXjip2qY375xX/scJeVRmZ5cY0wyHYyCYxNcdEXrQ4AOe3gACgd4iQ8ksOslJtW4VNxBJ8llUwc3a6AA=="], diff --git a/packages/web/package.json b/packages/web/package.json index 852f51abe..db5ee052b 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -6,6 +6,7 @@ "build": "./script/build.ts" }, "dependencies": { + "@tanstack/virtual-core": "^3.14.0", "hono": "^4.8.0", "models.dev": "workspace:*" }, diff --git a/packages/web/src/index.css b/packages/web/src/index.css index 58a4a5c4e..3dd25505e 100644 --- a/packages/web/src/index.css +++ b/packages/web/src/index.css @@ -41,6 +41,8 @@ html, body { + height: 100%; + overflow: hidden; font-family: 'Rubik', sans-serif; line-height: 1.6; color: var(--color-text); @@ -205,12 +207,17 @@ header { } } +.table-viewport { + height: calc(100svh - var(--header-height)); + margin-top: var(--header-height); + overflow: auto; +} + table { border-collapse: separate; border-spacing: 0; font-size: 0.875rem; width: 100%; - margin-top: var(--header-height); } thead, @@ -218,7 +225,7 @@ tbody {} table thead th { position: sticky; - top: var(--header-height); + top: 0; border-top: 1px solid var(--color-border); border-bottom: 1px solid var(--color-border); font-size: 0.75rem; @@ -264,6 +271,7 @@ td { text-align: left; border-bottom: 1px solid var(--color-border); white-space: nowrap; + height: 48px; } tbody { @@ -319,21 +327,37 @@ tbody { gap: 0.375rem; } - .provider-cell span:first-child { - flex: 0 0 auto; - } - + .provider-cell img, .provider-cell svg { + flex: 0 0 auto; display: block; width: 1rem; height: 1rem; color: var(--color-text-secondary); } + .virtual-spacer td { + height: inherit; + padding: 0; + border-bottom: 0; + } + + .empty-row td { + padding: 2rem 0.75rem; + color: var(--color-text-tertiary); + } + + .empty-row div { + position: sticky; + left: 0; + width: calc(100vw - 1.5rem); + text-align: center; + } + .model-id-cell { display: flex; align-items: center; - justify-content: space-between; + justify-content: flex-start; gap: 0.375rem; } @@ -546,4 +570,4 @@ dialog { } } -} \ No newline at end of file +} diff --git a/packages/web/src/index.ts b/packages/web/src/index.ts index 81afb4342..29beab273 100644 --- a/packages/web/src/index.ts +++ b/packages/web/src/index.ts @@ -1,7 +1,53 @@ +import { + Virtualizer, + elementScroll, + observeElementOffset, + observeElementRect, +} from "@tanstack/virtual-core"; +import { + type TableRow, + renderRow, + escapeHtml, + booleanText, + knowledgeText, + weightsText, +} from "./shared.js"; + +declare global { + interface Window { + __TABLE_DATA__: TableRow[]; + } +} + +interface VirtualizedRow extends TableRow { + key: string; + searchText: string; + sortValues: Array; +} + +type SortDirection = "asc" | "desc"; + +const ESTIMATED_ROW_HEIGHT = 48; +const VIRTUAL_OVERSCAN = 5; + const modal = document.getElementById("modal") as HTMLDialogElement; const modalClose = document.getElementById("close")!; const help = document.getElementById("help")!; const search = document.getElementById("search")! as HTMLInputElement; +const viewport = document.getElementById("table-viewport") as HTMLElement; +const tbody = document.getElementById( + "models-table-body" +) as HTMLTableSectionElement; +const headers = Array.from(document.querySelectorAll("th.sortable")); +const columnCount = document.querySelectorAll("thead th").length; + +let isLoaded = false; +let allRows: VirtualizedRow[] = []; +let visibleRows: VirtualizedRow[] = []; +let currentSort: { column: number; direction: SortDirection } = { + column: -1, + direction: "asc", +}; ///////////////////////// // URL State Management @@ -31,10 +77,7 @@ function getColumnNameForURL(headerEl: Element): string { } function getColumnIndexByUrlName(name: string): number { - const headers = document.querySelectorAll("th.sortable"); - return Array.from(headers).findIndex( - (header) => getColumnNameForURL(header) === name - ); + return headers.findIndex((header) => getColumnNameForURL(header) === name); } ///////////////////////// @@ -63,32 +106,181 @@ modal.addEventListener("click", (e) => { }); //////////////////// -// Handle Sorting +// Row Data //////////////////// -let currentSort = { column: -1, direction: "asc" }; +function lockColumnWidths() { + const ths = document.querySelectorAll("#models-table thead th"); + const widths = Array.from(ths).map((th) => th.getBoundingClientRect().width); -function sortTable(column: number, direction: "asc" | "desc") { - const header = document.querySelectorAll("th.sortable")[column]; - const columnType = header.getAttribute("data-type"); - if (!columnType) return; + const measurementRow = tbody.querySelector('tr[aria-hidden="true"]'); + if (measurementRow) measurementRow.remove(); - // update state - currentSort = { column, direction }; - updateQueryParams({ - sort: getColumnNameForURL(header), - order: direction, - }); + const table = document.getElementById("models-table")!; + table.style.tableLayout = "fixed"; - // sort rows - const tbody = document.querySelector("table tbody")!; - const rows = Array.from( - tbody.querySelectorAll("tr") - ) as HTMLTableRowElement[]; - rows.sort((a, b) => { - const aValue = getCellValue(a.cells[column], columnType); - const bValue = getCellValue(b.cells[column], columnType); + const colgroup = document.createElement("colgroup"); + for (const width of widths) { + const col = document.createElement("col"); + col.style.width = `${width}px`; + colgroup.appendChild(col); + } + table.insertBefore(colgroup, table.firstChild); +} + +function prepareRow(row: TableRow): VirtualizedRow { + const sortValues: VirtualizedRow["sortValues"] = [ + row.providerName, + row.modelName, + row.family, + row.providerId, + row.modelId, + booleanText(row.toolCall), + booleanText(row.reasoning), + row.input.length, + row.output.length, + row.inputCost, + row.outputCost, + row.reasoningCost, + row.cacheReadCost, + row.cacheWriteCost, + row.audioInputCost, + row.audioOutputCost, + row.contextLimit, + row.inputLimit, + row.outputLimit, + row.structuredOutput === undefined + ? undefined + : booleanText(row.structuredOutput), + booleanText(row.temperature), + weightsText(row.openWeights), + row.knowledge ? knowledgeText(row.knowledge) : undefined, + row.releaseDate, + row.lastUpdated, + ]; + + const searchableValues = [ + row.providerName, + row.modelName, + row.family ?? "", + row.providerId, + row.modelId, + row.releaseDate, + row.lastUpdated, + ]; + + return { + ...row, + key: `${row.providerId}/${row.modelId}`, + searchText: searchableValues.join(" ").toLowerCase(), + sortValues, + }; +} + +//////////////////// +// Virtual Table +//////////////////// +function getVirtualizerOptions(count: number) { + return { + count, + getScrollElement: () => viewport, + estimateSize: () => ESTIMATED_ROW_HEIGHT, + getItemKey: (index: number) => visibleRows[index]?.key ?? index, + initialRect: { + width: viewport.clientWidth || window.innerWidth, + height: viewport.clientHeight || window.innerHeight, + }, + overscan: VIRTUAL_OVERSCAN, + observeElementRect, + observeElementOffset, + scrollToFn: elementScroll, + onChange: () => renderVirtualRows(), + }; +} + +const virtualizer = new Virtualizer( + getVirtualizerOptions(0) +); +const cleanupVirtualizer = virtualizer._didMount(); +virtualizer._willUpdate(); +window.addEventListener("pagehide", () => cleanupVirtualizer()); + +function renderStatusRow(message: string) { + tbody.innerHTML = `
${escapeHtml( + message + )}
`; +} + +function setVirtualizerCount(count: number, resetScroll: boolean) { + virtualizer.setOptions(getVirtualizerOptions(count)); + virtualizer._willUpdate(); + if (resetScroll) virtualizer.scrollToOffset(0); + renderVirtualRows(); +} + +function renderVirtualRows() { + if (!isLoaded) return; + if (visibleRows.length === 0) { + renderStatusRow("No models found"); + return; + } + + const virtualRows = virtualizer.getVirtualItems(); + if (virtualRows.length === 0) return; + + const firstRow = virtualRows[0]!; + const lastRow = virtualRows[virtualRows.length - 1]!; + const paddingTop = firstRow.start; + const paddingBottom = Math.max(virtualizer.getTotalSize() - lastRow.end, 0); + const html: string[] = []; + + if (paddingTop > 0) html.push(renderSpacerRow(paddingTop)); + for (const virtualRow of virtualRows) { + const row = visibleRows[virtualRow.index]; + if (row) html.push(renderRow(row, virtualRow.index)); + } + if (paddingBottom > 0) html.push(renderSpacerRow(paddingBottom)); + + tbody.innerHTML = html.join(""); + tbody.querySelectorAll("tr[data-index]").forEach((row) => + virtualizer.measureElement(row) + ); +} + +function renderSpacerRow(height: number) { + return ``; +} + +function applyRows(resetScroll = true) { + if (!isLoaded) return; + visibleRows = getRowsForDisplay(); + setVirtualizerCount(visibleRows.length, resetScroll); +} + +//////////////////// +// Sorting +//////////////////// +function getRowsForDisplay() { + const terms = search.value + .toLowerCase() + .split(",") + .map((term) => term.trim()) + .filter(Boolean); + const filteredRows = + terms.length === 0 + ? allRows + : allRows.filter((row) => + terms.some((term) => row.searchText.includes(term)) + ); + + if (currentSort.column === -1) return filteredRows; + + const columnType = headers[currentSort.column]?.getAttribute("data-type"); + if (!columnType) return filteredRows; + + return [...filteredRows].sort((a, b) => { + const aValue = a.sortValues[currentSort.column]; + const bValue = b.sortValues[currentSort.column]; - // Handle undefined values - always sort to bottom if (aValue === undefined && bValue === undefined) return 0; if (aValue === undefined) return 1; if (bValue === undefined) return -1; @@ -96,45 +288,48 @@ function sortTable(column: number, direction: "asc" | "desc") { let comparison = 0; if (columnType === "number" || columnType === "modalities") { comparison = (aValue as number) - (bValue as number); - } else if (columnType === "boolean") { - comparison = (aValue as string).localeCompare(bValue as string); } else { - comparison = (aValue as string).localeCompare(bValue as string); + comparison = String(aValue).localeCompare(String(bValue)); } - return direction === "asc" ? comparison : -comparison; + return currentSort.direction === "asc" ? comparison : -comparison; }); - rows.forEach((row) => tbody.appendChild(row)); +} - // update sort indicators - const headers = document.querySelectorAll("th.sortable"); - headers.forEach((header, i) => { - const indicator = header.querySelector(".sort-indicator")!; +function sortTable( + column: number, + direction: SortDirection, + updateURL = true +) { + const header = headers[column]; + if (!header?.getAttribute("data-type")) return; - if (i === column) { - indicator.textContent = direction === "asc" ? "↑" : "↓"; - } else { - indicator.textContent = ""; - } - }); -} + currentSort = { column, direction }; + if (updateURL) { + updateQueryParams({ + sort: getColumnNameForURL(header), + order: direction, + }); + } -function getCellValue( - cell: HTMLTableCellElement, - type: string -): string | number | undefined { - if (type === "modalities") - return cell.querySelectorAll(".modality-icon").length; + updateSortIndicators(); + applyRows(); +} - const text = cell.textContent?.trim() || ""; - if (text === "-") return; - if (type === "number") return parseFloat(text.replace(/[$,]/g, "")) || 0; - return text; +function updateSortIndicators() { + headers.forEach((header, i) => { + const indicator = header.querySelector(".sort-indicator")!; + indicator.textContent = + i === currentSort.column + ? currentSort.direction === "asc" + ? "↑" + : "↓" + : ""; + }); } -document.querySelectorAll("th.sortable").forEach((header) => { +headers.forEach((header, column) => { header.addEventListener("click", () => { - const column = Array.from(header.parentElement!.children).indexOf(header); const direction = currentSort.column === column && currentSort.direction === "asc" ? "desc" @@ -144,34 +339,19 @@ document.querySelectorAll("th.sortable").forEach((header) => { }); /////////////////// -// Handle Search +// Search /////////////////// -function filterTable(value: string) { - const lowerCaseValues = value.toLowerCase().split(",").filter(str => str.trim() !== ""); - const rows = document.querySelectorAll( - "table tbody tr" - ) as NodeListOf; - - rows.forEach((row) => { - const cellTexts = Array.from(row.cells).map((cell) => - cell.textContent!.toLowerCase() - ); - const isVisible = lowerCaseValues.length === 0 || - lowerCaseValues.some((lowerCaseValue) => cellTexts.some((text) => text.includes(lowerCaseValue))); - row.style.display = isVisible ? "" : "none"; - }); - - updateQueryParams({ search: value || null }); -} - search.addEventListener("input", () => { - filterTable(search.value); + updateQueryParams({ search: search.value || null }); + applyRows(); }); document.addEventListener("keydown", (e) => { - if ((e.metaKey || e.ctrlKey) && e.key === "k") { + const key = e.key.toLowerCase(); + if ((e.metaKey || e.ctrlKey) && (key === "k" || key === "f")) { e.preventDefault(); search.focus(); + search.select(); } }); @@ -185,10 +365,7 @@ search.addEventListener("keydown", (e) => { /////////////////////////////////// // Handle Copy model ID function /////////////////////////////////// -(window as any).copyModelId = async ( - button: HTMLButtonElement, - modelId: string -) => { +async function copyModelId(button: HTMLButtonElement, modelId: string) { try { if (navigator.clipboard) { await navigator.clipboard.writeText(modelId); @@ -209,7 +386,18 @@ search.addEventListener("keydown", (e) => { } catch (err) { console.error("Failed to copy text: ", err); } -}; +} + +document.addEventListener("click", (event) => { + if (!(event.target instanceof Element)) return; + const button = event.target.closest( + ".copy-button[data-model-id]" + ); + if (!button) return; + + const modelId = button.dataset.modelId; + if (modelId) void copyModelId(button, modelId); +}); /////////////////////////////////// // Initialize State from URL @@ -217,24 +405,37 @@ search.addEventListener("keydown", (e) => { function initializeFromURL() { const params = getQueryParams(); - (() => { - const searchQuery = params.get("search"); - if (!searchQuery) return; - search.value = searchQuery; - filterTable(searchQuery); - })(); - - (() => { - const columnName = params.get("sort"); - if (!columnName) return; + search.value = params.get("search") ?? ""; + currentSort = { column: -1, direction: "asc" }; + const columnName = params.get("sort"); + if (columnName) { const columnIndex = getColumnIndexByUrlName(columnName); - if (columnIndex === -1) return; + if (columnIndex !== -1) { + currentSort = { + column: columnIndex, + direction: params.get("order") === "desc" ? "desc" : "asc", + }; + } + } - const direction = (params.get("order") as "asc" | "desc") || "asc"; - sortTable(columnIndex, direction); - })(); + updateSortIndicators(); + applyRows(false); +} + +function loadRows() { + try { + allRows = window.__TABLE_DATA__.map(prepareRow); + lockColumnWidths(); + isLoaded = true; + initializeFromURL(); + } catch (error) { + console.error(error); + isLoaded = true; + visibleRows = []; + renderStatusRow("Failed to load model data"); + } } -document.addEventListener("DOMContentLoaded", initializeFromURL); +loadRows(); window.addEventListener("popstate", initializeFromURL); diff --git a/packages/web/src/render.tsx b/packages/web/src/render.tsx index a1bdfcc2f..cd0106f08 100644 --- a/packages/web/src/render.tsx +++ b/packages/web/src/render.tsx @@ -6,6 +6,7 @@ import { Fragment } from "hono/jsx"; import { renderToString } from "hono/jsx/dom/server"; import { existsSync } from "fs"; import path from "path"; +import { type TableRow, renderRow, getLargestRow } from "./shared.js"; export const Providers = await generate( path.join(import.meta.dir, "..", "..", "..", "providers") @@ -38,7 +39,6 @@ const loadProviderSvg = async (providerId: string): Promise => { const file = Bun.file(providerLogoPath); return await file.text(); } - // // Fall back to default logo if (existsSync(defaultLogoPath)) { const file = Bun.file(defaultLogoPath); @@ -62,122 +62,47 @@ for (const [providerId] of Object.entries(Providers)) { } } -function renderProviderLogo(providerId: string) { - const svgContent = providerLogos.get(providerId) || ""; +export const INITIAL_ROW_COUNT = 50; - return ; -} - -const getModalityIcon = (modality: string) => { - switch (modality) { - case "text": - return ( - - - - - - - - ); - case "image": - return ( - - - - - - - - ); - case "audio": - return ( - - - - - - - ); - case "video": - return ( - - - - - - - ); - case "pdf": - return ( - - - - - - - - - - ); - default: - return null; - } -}; +export const TableRows: TableRow[] = Object.entries(Providers) + .sort(([, providerA], [, providerB]) => + providerA.name.localeCompare(providerB.name) + ) + .flatMap(([providerId, provider]) => + Object.entries(provider.models) + .filter(([, model]) => model.status !== "alpha") + .sort(([, modelA], [, modelB]) => modelA.name.localeCompare(modelB.name)) + .map(([modelId, model]) => ({ + providerId, + providerName: provider.name, + providerLogoSvg: providerLogos.get(providerId) || "", + modelId, + modelName: model.name, + family: model.family, + toolCall: model.tool_call, + reasoning: model.reasoning, + input: model.modalities.input, + output: model.modalities.output, + inputCost: model.cost?.input, + outputCost: model.cost?.output, + reasoningCost: model.cost?.reasoning, + cacheReadCost: model.cost?.cache_read, + cacheWriteCost: model.cost?.cache_write, + audioInputCost: model.cost?.input_audio, + audioOutputCost: model.cost?.output_audio, + contextLimit: model.limit.context, + inputLimit: model.limit.input, + outputLimit: model.limit.output, + structuredOutput: model.structured_output, + temperature: model.temperature ?? false, + openWeights: model.open_weights, + knowledge: model.knowledge, + releaseDate: model.release_date, + lastUpdated: model.last_updated, + })) + ); -const renderCost = (cost?: number) => { - return cost === undefined ? "-" : `$${cost.toFixed(2)}`; -}; +const largestRow = getLargestRow(TableRows); export const Rendered = renderToString( @@ -213,7 +138,8 @@ export const Rendered = renderToString( - +
+
- - {Object.entries(Providers) - .sort(([, providerA], [, providerB]) => - providerA.name.localeCompare(providerB.name) - ) - .flatMap(([providerId, provider]) => - Object.entries(provider.models) - .filter(([, model]) => model.status !== "alpha") - .sort(([, modelA], [, modelB]) => - modelA.name.localeCompare(modelB.name) - ) - .map(([modelId, model]) => ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - )) - )} - -
@@ -342,120 +268,12 @@ export const Rendered = renderToString(
-
- {renderProviderLogo(providerId)} - {provider.name} -
-
{model.name}{model.family ?? "-"}{providerId} -
- {modelId} - -
-
{model.tool_call ? "Yes" : "No"}{model.reasoning ? "Yes" : "No"} -
- {model.modalities.input.map((modality) => - getModalityIcon(modality) - )} -
-
-
- {model.modalities.output.map((modality) => - getModalityIcon(modality) - )} -
-
{renderCost(model.cost?.input)}{renderCost(model.cost?.output)}{renderCost(model.cost?.reasoning)}{renderCost(model.cost?.cache_read)}{renderCost(model.cost?.cache_write)}{renderCost(model.cost?.input_audio)}{renderCost(model.cost?.output_audio)}{model.limit.context.toLocaleString()}{model.limit.input?.toLocaleString() ?? "-"}{model.limit.output.toLocaleString()} - {model.structured_output === undefined - ? "-" - : model.structured_output - ? "Yes" - : "No"} - {model.temperature ? "Yes" : "No"}{model.open_weights ? "Open" : "Closed"} - {model.knowledge ? model.knowledge.substring(0, 7) : "-"} - {model.release_date}{model.last_updated}
+ renderRow(row, i)).join('') + + renderRow(largestRow, -1).replace('
); diff --git a/packages/web/src/shared.ts b/packages/web/src/shared.ts new file mode 100644 index 000000000..153705937 --- /dev/null +++ b/packages/web/src/shared.ts @@ -0,0 +1,176 @@ +export interface TableRow { + providerId: string; + providerName: string; + providerLogoSvg: string; + modelId: string; + modelName: string; + family?: string; + toolCall: boolean; + reasoning: boolean; + input: string[]; + output: string[]; + inputCost?: number; + outputCost?: number; + reasoningCost?: number; + cacheReadCost?: number; + cacheWriteCost?: number; + audioInputCost?: number; + audioOutputCost?: number; + contextLimit: number; + inputLimit?: number; + outputLimit: number; + structuredOutput?: boolean; + temperature: boolean; + openWeights: boolean; + knowledge?: string; + releaseDate: string; + lastUpdated: string; +} + +const MODALITY_ICONS: Record = { + text: ``, + image: ``, + audio: ``, + video: ``, + pdf: ``, +}; + +export function escapeHtml(value: string | number) { + return String(value).replace(/[&<>'"]/g, (char) => { + switch (char) { + case "&": + return "&"; + case "<": + return "<"; + case ">": + return ">"; + case "'": + return "'"; + case '"': + return """; + default: + return char; + } + }); +} + +export function booleanText(value: boolean) { + return value ? "Yes" : "No"; +} + +export function optionalBooleanText(value?: boolean) { + return value === undefined ? "-" : booleanText(value); +} + +export function formatCost(cost?: number) { + return cost === undefined ? "-" : `$${cost.toFixed(2)}`; +} + +export function formatNumber(value?: number) { + return value === undefined ? "-" : value.toLocaleString(); +} + +export function knowledgeText(value?: string) { + return value ? value.substring(0, 7) : "-"; +} + +export function weightsText(value: boolean) { + return value ? "Open" : "Closed"; +} + +export function renderModalityIcon(modality: string) { + const label = + modality === "pdf" + ? "PDF" + : modality[0]!.toUpperCase() + modality.slice(1); + const icon = MODALITY_ICONS[modality]; + if (!icon) return ""; + return `${icon}`; +} + +export function renderModalities(modalities: string[]) { + return `
${modalities + .map(renderModalityIcon) + .join("")}
`; +} + +export function renderCopyButton(modelId: string) { + const escapedModelId = escapeHtml(modelId); + return ``; +} + +export function renderRow(row: TableRow, index: number) { + return ` +
${row.providerLogoSvg}${escapeHtml( + row.providerName + )}
+ ${escapeHtml(row.modelName)} + ${escapeHtml(row.family ?? "-")} + ${escapeHtml(row.providerId)} +
${escapeHtml( + row.modelId + )}${renderCopyButton(row.modelId)}
+ ${booleanText(row.toolCall)} + ${booleanText(row.reasoning)} + ${renderModalities(row.input)} + ${renderModalities(row.output)} + ${formatCost(row.inputCost)} + ${formatCost(row.outputCost)} + ${formatCost(row.reasoningCost)} + ${formatCost(row.cacheReadCost)} + ${formatCost(row.cacheWriteCost)} + ${formatCost(row.audioInputCost)} + ${formatCost(row.audioOutputCost)} + ${formatNumber(row.contextLimit)} + ${formatNumber(row.inputLimit)} + ${formatNumber(row.outputLimit)} + ${optionalBooleanText(row.structuredOutput)} + ${booleanText(row.temperature)} + ${weightsText(row.openWeights)} + ${knowledgeText(row.knowledge)} + ${escapeHtml(row.releaseDate)} + ${escapeHtml(row.lastUpdated)} + `; +} + +export function getLargestRow(rows: TableRow[]): TableRow { + const worst: TableRow = { + providerId: "", providerName: "", providerLogoSvg: "", modelId: "", modelName: "", + toolCall: true, reasoning: true, + input: [], output: [], + contextLimit: 0, outputLimit: 0, + structuredOutput: true, temperature: true, openWeights: false, + releaseDate: "", lastUpdated: "", + }; + + for (const row of rows) { + if (row.providerName.length > worst.providerName.length) worst.providerName = row.providerName; + if (row.modelName.length > worst.modelName.length) worst.modelName = row.modelName; + if ((row.family ?? "").length > (worst.family ?? "").length) worst.family = row.family; + if (row.providerId.length > worst.providerId.length) worst.providerId = row.providerId; + if (row.modelId.length > worst.modelId.length) worst.modelId = row.modelId; + if ((row.knowledge ?? "").length > (worst.knowledge ?? "").length) worst.knowledge = row.knowledge; + if (row.releaseDate.length > worst.releaseDate.length) worst.releaseDate = row.releaseDate; + if (row.lastUpdated.length > worst.lastUpdated.length) worst.lastUpdated = row.lastUpdated; + if (row.input.length > worst.input.length) worst.input = row.input; + if (row.output.length > worst.output.length) worst.output = row.output; + + const costWider = (a: number | undefined, b: number | undefined) => + b !== undefined && (a === undefined || formatCost(b).length > formatCost(a).length); + if (costWider(worst.inputCost, row.inputCost)) worst.inputCost = row.inputCost; + if (costWider(worst.outputCost, row.outputCost)) worst.outputCost = row.outputCost; + if (costWider(worst.reasoningCost, row.reasoningCost)) worst.reasoningCost = row.reasoningCost; + if (costWider(worst.cacheReadCost, row.cacheReadCost)) worst.cacheReadCost = row.cacheReadCost; + if (costWider(worst.cacheWriteCost, row.cacheWriteCost)) worst.cacheWriteCost = row.cacheWriteCost; + if (costWider(worst.audioInputCost, row.audioInputCost)) worst.audioInputCost = row.audioInputCost; + if (costWider(worst.audioOutputCost, row.audioOutputCost)) worst.audioOutputCost = row.audioOutputCost; + + const numWider = (a: number | undefined, b: number | undefined) => + b !== undefined && (a === undefined || formatNumber(b).length > formatNumber(a).length); + if (numWider(worst.contextLimit as number | undefined, row.contextLimit)) worst.contextLimit = row.contextLimit; + if (numWider(worst.inputLimit, row.inputLimit)) worst.inputLimit = row.inputLimit; + if (numWider(worst.outputLimit as number | undefined, row.outputLimit)) worst.outputLimit = row.outputLimit; + } + + return worst; +}