SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
13 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
6.5 KB · 143 lines tsx
Raw Blame History
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