TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
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