import type { CompiledFilter, CompiledQuery2 } from '@/lib/types'; /* Search compiler v2 helpers (server-safe, pure). Two ways to "drop a chip": - re-query: remove the chip's `source_span` from the text and search again (works for every filter); - structured: map the remaining compiled filters to `/models?…` parameters when the compiled type is `model`. Filters the listing API cannot express are reported, never silently dropped. */ export function withoutSpan(text: string, span: string | null | undefined): string { if (!span) return text; const i = text.toLowerCase().indexOf(span.toLowerCase()); if (i < 0) return text; return `${text.slice(0, i)} ${text.slice(i + span.length)}`.replace(/\s{2,}/g, ' ').trim(); } export type Mapped = { params: URLSearchParams; unmapped: { filter: CompiledFilter; why: string }[]; links: { label: string; href: string }[] }; const SORT_MAP: Record = { newest: { sort: 'release' }, largest: { sort: 'params' }, smallest: { sort: 'params', order: 'asc' }, cheapest: { sort: 'cheapest' }, best: { sort: 'quality' } }; /** Map compiled filters to `/models` query parameters (docs/API.md `/models`). */ export function toModelsParams(compiled: CompiledFilter[], residual?: string | null): Mapped { const p = new URLSearchParams(); const unmapped: Mapped['unmapped'] = []; const links: Mapped['links'] = []; const slugOf = (v: unknown) => (v && typeof v === 'object' && 'slug' in (v as Record) ? String((v as { slug: unknown }).slug) : typeof v === 'string' ? v : null); for (const f of compiled) { const v = f.value; switch (f.filter) { case 'entity_type': break; case 'params_min': p.set('min_params', String(v)); break; case 'params_max': p.set('max_params', String(v)); break; case 'params_range': if (Array.isArray(v) && v.length === 2) { p.set('min_params', String(v[0])); p.set('max_params', String(v[1])); } break; case 'context_min': p.set('min_context', String(v)); break; case 'year': p.set('year_from', String(v)); p.set('year_to', String(v)); break; case 'year_from': p.set('year_from', String(v)); break; case 'year_to': p.set('year_to', String(v)); break; case 'license': p.set('license', String(v)); break; case 'modality': p.set('modality', String(v)); break; case 'openness': p.set('openness', String(v)); break; case 'organization': { const s = slugOf(v); if (s) p.set('org', s); else unmapped.push({ filter: f, why: 'organization not resolved to a slug' }); break; } case 'family': { const s = slugOf(v); if (s) p.set('family', s); break; } case 'reasoning': p.set('reasoning', v ? '1' : '0'); break; case 'status': p.set('status', String(v)); break; case 'sort': { const m = SORT_MAP[String(v)]; if (m) { p.set('sort', m.sort); if (m.order) p.set('order', m.order); } else unmapped.push({ filter: f, why: `sort “${String(v)}” has no /models equivalent` }); break; } case 'benchmark': { const s = slugOf(v); if (s) links.push({ label: `${f.label} → leaderboard`, href: `/benchmarks/${encodeURIComponent(s)}` }); break; } case 'provider': { const s = slugOf(v); if (s) links.push({ label: `${f.label} → provider page`, href: `/providers/${encodeURIComponent(s)}` }); break; } case 'max_input_price': case 'max_output_price': unmapped.push({ filter: f, why: '/models has no price filter — use Prices sorted cheapest' }); links.push({ label: 'Prices, cheapest first', href: `/prices?sort=${f.filter === 'max_input_price' ? 'input' : 'output'}` }); break; case 'memory_gb': unmapped.push({ filter: f, why: 'an ESTIMATED parameter bound — use Run locally for a real fit' }); links.push({ label: `Run locally with ${String(v)} GB`, href: `/run-locally?memory_gb=${encodeURIComponent(String(v))}` }); break; case 'days_back': unmapped.push({ filter: f, why: '/models filters by release year, not by a rolling window' }); break; case 'commercial_use': unmapped.push({ filter: f, why: 'licence permissions are not a /models filter — see Licenses' }); links.push({ label: 'Licenses (commercial use)', href: '/licenses' }); break; default: unmapped.push({ filter: f, why: 'no /models equivalent' }); } } if (residual && residual.trim()) p.set('q', residual.trim()); return { params: p, unmapped, links }; } /** Which model columns the query implies (always Model · Org; the rest depends on the compiled filters). */ export function impliedColumns(compiled: CompiledFilter[], sort?: string | null): Set { const cols = new Set(['params', 'context', 'openness', 'released']); for (const f of compiled) { if (/params|memory_gb/.test(f.filter)) cols.add('params'); if (/context/.test(f.filter)) cols.add('context'); if (/price/.test(f.filter)) cols.add('price'); if (/openness|license|commercial/.test(f.filter)) cols.add('openness'); if (/year|days_back/.test(f.filter)) cols.add('released'); if (f.filter === 'reasoning') cols.add('reasoning'); if (f.filter === 'modality') cols.add('modalities'); } if (sort === 'cheapest') cols.add('price'); return cols; } export function compiledType(q: CompiledQuery2 | undefined): string | null { const t = (q?.entity_type ?? q?.type) as string | null | undefined; return t ?? null; }