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%
2.9 KB · 68 lines tsx
Raw Blame History
1"use client";2import * as React from "react";3import { Trophy } from "lucide-react";4import { ProviderIcon } from "@/components/brand/provider-icon";5import type { ColumnStatus } from "./types";6import { StatusDot } from "./metrics-strip";7import { BlindAvatar } from "./blind";8import { cn } from "@/lib/utils";910export interface ModelTabItem {11  key: string;12  name: string;13  provider: string | null;14  status: ColumnStatus;15  tokensPerSecond: number | null;16  isWinner?: boolean;17  /** Blind: identity hidden. */18  hidden?: boolean;19  blindIndex: number;20}2122/**23 * Sticky model tab bar for the phone carousel (Segmented-like): provider icon (or blind letter),24 * status dot, name and live tok/s. Selecting a tab scrolls the carousel; swiping updates the tab.25 */26export function ModelTabs({ items, active, onSelect, className }: { items: ModelTabItem[]; active: number; onSelect: (index: number) => void; className?: string }) {27  const ref = React.useRef<HTMLDivElement>(null);28  // Keep the active tab in view while swiping.29  React.useEffect(() => {30    const el = ref.current?.children[active] as HTMLElement | undefined;31    el?.scrollIntoView({ block: "nearest", inline: "center", behavior: "smooth" });32  }, [active]);33  const onKey = (e: React.KeyboardEvent) => {34    if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;35    e.preventDefault();36    const next = (active + (e.key === "ArrowLeft" ? -1 : 1) + items.length) % items.length;37    onSelect(next);38    (ref.current?.children[next] as HTMLElement | undefined)?.focus();39  };40  return (41    <div ref={ref} role="tablist" aria-label="Models" onKeyDown={onKey} className={cn("flex h-11 w-full items-center gap-0.5 overflow-x-auto rounded-lg bg-bg-muted p-1 scrollbar-none", className)}>42      {items.map((it, i) => {43        const on = i === active;44        return (45          <button46            key={it.key}47            role="tab"48            type="button"49            aria-selected={on}50            tabIndex={on ? 0 : -1}51            onClick={() => onSelect(i)}52            className={cn(53              "inline-flex h-9 min-w-0 flex-1 shrink-0 basis-[44%] items-center justify-center gap-1.5 rounded-md px-2 text-[12.5px] font-medium transition-[background-color,color,box-shadow] duration-150 sm:basis-auto",54              on ? "bg-bg-elevated text-fg shadow-xs" : "text-fg-muted",55            )}56          >57            {it.hidden ? <BlindAvatar index={it.blindIndex} size={16} /> : <ProviderIcon provider={it.provider} size={13} />}58            <span className="truncate">{it.name}</span>59            <StatusDot status={it.status} />60            {it.tokensPerSecond ? <span className="hidden shrink-0 text-[10.5px] tabular-nums text-fg-subtle min-[400px]:inline">{it.tokensPerSecond}/s</span> : null}61            {it.isWinner ? <Trophy className="size-3 shrink-0 text-accent" aria-label="Winner" /> : null}62          </button>63        );64      })}65    </div>66  );67}68