import type { ReactNode } from 'react'; import { CompareButton } from '@/components/compare/compare-button'; import { CompareTrayBar } from '@/components/compare/compare-tray-bar'; import { FilterBar, type FilterField } from '@/components/listing/filters'; import { DataTable, EmptyRow, Td, Th } from '@/components/ui/data-table'; import { Pagination, withParams } from '@/components/ui/pagination'; import { Container, Note, PageHeader } from '@/components/ui/section'; import { EmptyState, Unavailable } from '@/components/ui/unavailable'; import { safe } from '@/lib/api'; import { fmtInt } from '@/lib/format'; import type { EntitySummary, Page } from '@/lib/types'; /* Intelligence listing: the typed table used by /agents /tools /datasets /frameworks. Same URL contract as `TypedListing` (q · org · sort · offset, GET FilterBar) but with an optional per-page `enrich(items)` step that fetches extra facts for the rows shown (relations, metric history…) and passes them to the column renderers, plus honest empty states: no filters → "No X recorded yet — connectors: …"; filters → "No X match these filters". */ export type IntelColumn = { key: string; label: string; num?: boolean; primary?: boolean; wide?: boolean; hideStack?: boolean; className?: string; render: (e: EntitySummary, extra: X | undefined) => ReactNode; }; const LIMIT = 40; const PARAM_KEYS = ['q', 'org', 'sort', 'order', 'category', 'kind', 'since', 'until', 'manufacturer', 'offset']; export async function IntelListing({ title, eyebrow, lede, basePath, searchParams, fetch, columns, sorts, filters = [], enrich, connectors, emptyHint, note, compare = false, headerAside, children, caption, }: { title: string; eyebrow: string; lede?: ReactNode; basePath: string; searchParams: Record; fetch: (q: Record) => Promise>; columns: IntelColumn[]; sorts: { value: string; label: string }[]; filters?: FilterField[]; /** Fetch extra facts for the rows on this page (keyed by slug). Failures per row are tolerated (undefined). */ enrich?: (items: EntitySummary[]) => Promise>; /** Connector names that feed this type (for the honest empty state). */ connectors?: string[]; emptyHint?: ReactNode; note?: ReactNode; compare?: boolean; headerAside?: ReactNode; children?: ReactNode; caption?: string; }) { const current: Record = {}; for (const k of PARAM_KEYS) if (searchParams[k]) current[k] = searchParams[k]; const offset = Math.max(0, Number(current.offset) || 0); const sort = current.sort ?? sorts[0]?.value; const page = await safe(fetch({ ...current, sort, limit: LIMIT, offset })); const extras = page && enrich && page.items.length ? await enrich(page.items).catch(() => new Map()) : new Map(); const href = (patch: Record) => withParams(basePath, current, patch); const filtered = Object.keys(current).some((k) => !['sort', 'order', 'offset'].includes(k)); const cols = columns.length + (compare ? 1 : 0); const lower = title.toLowerCase(); return ( {page &&

{fmtInt(page.total)} total

}{headerAside} : undefined}>
{children}
{!page ? ( ) : page.items.length === 0 && !offset ? ( filtered ? ( {emptyHint ?? 'Remove a filter or search for another name.'} ) : ( {emptyHint} {connectors && connectors.length > 0 && ( Connectors that will populate this type: {connectors.join(' · ')}. Nothing is listed until a source has been crawled. )} ) ) : ( <>
{columns.map((c) => ( {c.label} ))} {compare && } {page.items.length === 0 && No rows on this page.} {page.items.map((e) => ( {columns.map((c) => ( {c.render(e, extras.get(e.slug))} ))} {compare && ( )} ))}
href({ offset: o || undefined })} className="mt-4" /> {note && {note}} )}
{compare && }
); } export function Dash() { return —; } export function str(v: unknown): string | null { return typeof v === 'string' && v.trim() ? v : null; } export function list(v: unknown): string[] { return Array.isArray(v) ? v.filter((x) => x !== null && x !== undefined).map(String) : []; }