TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
1"use client";2import * as React from "react";3import { Check, ChevronDown, Minus } from "lucide-react";4import { ProviderIcon } from "@/components/brand/provider-icon";5import { Badge } from "@/components/ui/badge";6import { BadgeChips, LifecycleChip } from "@/components/models/badge-chips";7import type { Lifecycle, ModelBadge } from "@/lib/models/badges";8import type { ProviderId } from "@/lib/ai/core/types";9import { cn } from "@/lib/utils";1011/**12 * Compact, pre-formatted row for the public catalog. The server sends only these strings/booleans13 * (≈ 200 B per model instead of the full PolyModel) and renders the first rows; the rest expands client-side.14 */15export interface CatalogRow {16 key: string;17 provider: ProviderId;18 name: string;19 id: string;20 inPrice: string;21 outPrice: string;22 context: string;23 maxOut: string;24 reasoning: boolean;25 vision: boolean;26 tools: boolean;27 json: boolean;28 lifecycle: Lifecycle;29 badges: ModelBadge[];30}3132const INITIAL = 8;3334function Mark({ on }: { on: boolean }) {35 return on ? <Check className="mx-auto size-4 text-success" aria-label="yes" /> : <Minus className="mx-auto size-4 text-border-strong" aria-label="no" />;36}3738export function PublicCatalogGroup({ rows, provider }: { rows: CatalogRow[]; provider: ProviderId }) {39 const [expanded, setExpanded] = React.useState(false);40 const shown = expanded ? rows : rows.slice(0, INITIAL);41 const hidden = rows.length - shown.length;42 return (43 <>44 {/* Desktop table */}45 <div className="mt-4 hidden overflow-hidden rounded-xl border border-border md:block">46 <table className="w-full text-[13px]">47 <thead>48 <tr className="border-b border-border bg-bg-subtle/60 text-left text-[11px] font-medium uppercase tracking-wide text-fg-subtle">49 <th className="px-3 py-2">Model</th>50 <th className="px-3 py-2 text-right">Input $/1M</th>51 <th className="px-3 py-2 text-right">Output $/1M</th>52 <th className="px-3 py-2 text-right">Context</th>53 <th className="px-3 py-2 text-right">Max out</th>54 <th className="px-3 py-2 text-center">Reasoning</th>55 <th className="px-3 py-2 text-center">Vision</th>56 <th className="px-3 py-2 text-center">Tools</th>57 <th className="px-3 py-2">Status</th>58 </tr>59 </thead>60 <tbody>61 {shown.map((m) => (62 <tr key={m.key} className="border-b border-hairline last:border-b-0">63 <td className="px-3 py-2">64 <div className="flex flex-wrap items-center gap-x-2 gap-y-0.5">65 <span className="font-medium">{m.name}</span>66 <BadgeChips badges={m.badges} max={3} size="xs" />67 </div>68 <div className="font-mono text-[11px] text-fg-subtle">{m.id}</div>69 </td>70 <td className="px-3 py-2 text-right font-mono tabular-nums text-fg-muted">{m.inPrice}</td>71 <td className="px-3 py-2 text-right font-mono tabular-nums text-fg-muted">{m.outPrice}</td>72 <td className="px-3 py-2 text-right font-mono tabular-nums text-fg-muted">{m.context}</td>73 <td className="px-3 py-2 text-right font-mono tabular-nums text-fg-muted">{m.maxOut}</td>74 <td className="px-3 py-2">75 <Mark on={m.reasoning} />76 </td>77 <td className="px-3 py-2">78 <Mark on={m.vision} />79 </td>80 <td className="px-3 py-2">81 <Mark on={m.tools} />82 </td>83 <td className="px-3 py-2">84 <LifecycleChip lifecycle={m.lifecycle} />85 </td>86 </tr>87 ))}88 </tbody>89 </table>90 </div>9192 {/* Mobile rows */}93 <ul className="mt-4 space-y-2 md:hidden">94 {shown.map((m) => (95 <li key={m.key} className="panel p-3">96 <div className="flex items-start gap-2.5">97 <ProviderIcon provider={provider} size={18} className="mt-0.5 shrink-0" />98 <div className="min-w-0 flex-1">99 <div className="flex flex-wrap items-center gap-x-1.5 gap-y-0.5">100 <span className="text-[14px] font-medium">{m.name}</span>101 <LifecycleChip lifecycle={m.lifecycle} />102 </div>103 <div className="truncate font-mono text-[11px] text-fg-subtle">{m.id}</div>104 <dl className="mt-2 grid grid-cols-3 gap-2 text-[12px]">105 <div>106 <dt className="text-[10.5px] text-fg-subtle">In / Out $/1M</dt>107 <dd className="font-mono tabular-nums">108 {m.inPrice} / {m.outPrice}109 </dd>110 </div>111 <div>112 <dt className="text-[10.5px] text-fg-subtle">Context</dt>113 <dd className="font-mono tabular-nums">{m.context}</dd>114 </div>115 <div>116 <dt className="text-[10.5px] text-fg-subtle">Max out</dt>117 <dd className="font-mono tabular-nums">{m.maxOut}</dd>118 </div>119 </dl>120 <div className="mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-[11.5px] text-fg-muted">121 <span className={m.reasoning ? "" : "line-through text-fg-subtle"}>Reasoning</span>122 <span className={m.vision ? "" : "line-through text-fg-subtle"}>Vision</span>123 <span className={m.tools ? "" : "line-through text-fg-subtle"}>Tools</span>124 <span className={m.json ? "" : "line-through text-fg-subtle"}>JSON</span>125 </div>126 <BadgeChips badges={m.badges} max={4} size="xs" className="mt-2" />127 </div>128 </div>129 </li>130 ))}131 </ul>132133 {hidden > 0 || expanded ? (134 <button type="button" onClick={() => setExpanded((v) => !v)} className={cn("tap mt-3 inline-flex h-10 w-full items-center justify-center gap-1.5 rounded-lg border border-border bg-bg-elevated text-[13px] font-medium text-fg-muted transition-colors hover:border-border-strong hover:text-fg sm:w-auto sm:px-4")} aria-expanded={expanded}>135 {expanded ? "Show fewer" : `Show all ${rows.length} models`}136 <ChevronDown className={cn("size-4 transition-transform", expanded && "rotate-180")} />137 {!expanded ? <Badge variant="default" className="ml-1">+{hidden}</Badge> : null}138 </button>139 ) : null}140 </>141 );142}143