TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
1"use client";2import * as React from "react";3import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem, SelectGroup, SelectLabel } from "@/components/ui/select";4import { ProviderIcon } from "@/components/brand/provider-icon";5import { useApp } from "@/components/app/store";6import { PROVIDERS, PROVIDER_ORDER } from "@/lib/client/providers";7import type { PolyModel, ProviderId } from "@/lib/client/types";89const NONE = "__none__";1011export interface ModelSelectProps {12 value: string | null;13 onChange: (key: string | null) => void;14 /** Only list models whose provider has a valid key (default true). */15 connectedOnly?: boolean;16 includeDeprecated?: boolean;17 allowNone?: boolean;18 noneLabel?: string;19 placeholder?: string;20 disabled?: boolean;21 id?: string;22 className?: string;23}2425/** Model picker grouped by provider, with provider glyphs. Shows a disabled hint when nothing is connected. */26export function ModelSelect({ value, onChange, connectedOnly = true, includeDeprecated = false, allowNone, noneLabel = "None", placeholder = "Choose a model", disabled, id, className }: ModelSelectProps) {27 const { models, modelsByKey, connectedProviders } = useApp();2829 const groups = React.useMemo(() => {30 const out: { provider: ProviderId; models: PolyModel[] }[] = [];31 for (const p of PROVIDER_ORDER) {32 if (connectedOnly && !connectedProviders.has(p)) continue;33 const list = models.filter((m) => m.provider === p && (includeDeprecated || m.status !== "deprecated"));34 if (list.length) out.push({ provider: p, models: list });35 }36 return out;37 }, [models, connectedProviders, connectedOnly, includeDeprecated]);3839 const selected = value ? modelsByKey.get(value) : undefined;40 const empty = groups.length === 0;41 // Keep a stale/unknown selection visible so the user understands what is stored.42 const unknownSelected = value && !selected;4344 return (45 <Select value={value ?? (allowNone ? NONE : undefined)} onValueChange={(v) => onChange(v === NONE ? null : v)} disabled={disabled || (empty && !allowNone)}>46 <SelectTrigger id={id} className={className} aria-label="Model">47 <SelectValue placeholder={empty ? "Connect a provider first" : placeholder}>48 {selected ? (49 <span className="flex items-center gap-2">50 <ProviderIcon provider={selected.provider} size={14} />51 <span className="truncate">{selected.displayName}</span>52 <span className="truncate font-mono text-[11px] text-fg-subtle">{selected.id}</span>53 </span>54 ) : unknownSelected ? (55 <span className="flex items-center gap-2 text-fg-muted">56 <ProviderIcon provider={value?.split("/")[0]} size={14} />57 <span className="truncate font-mono text-xs">{value}</span>58 </span>59 ) : allowNone && !value ? (60 <span className="text-fg-muted">{noneLabel}</span>61 ) : undefined}62 </SelectValue>63 </SelectTrigger>64 <SelectContent className="max-h-80">65 {allowNone ? <SelectItem value={NONE}>{noneLabel}</SelectItem> : null}66 {unknownSelected ? (67 <SelectItem value={value!} className="font-mono text-xs text-fg-muted">68 {value} (not available)69 </SelectItem>70 ) : null}71 {groups.map((g) => (72 <SelectGroup key={g.provider}>73 <SelectLabel className="flex items-center gap-1.5">74 <ProviderIcon provider={g.provider} size={12} /> {PROVIDERS[g.provider].name}75 </SelectLabel>76 {g.models.map((m) => (77 <SelectItem key={m.key} value={m.key}>78 <span className="flex items-center gap-2">79 <span>{m.displayName}</span>80 {m.status === "preview" ? <span className="rounded bg-info-soft px-1 text-[10px] font-medium text-info">preview</span> : null}81 {m.status === "deprecated" ? <span className="rounded bg-warning-soft px-1 text-[10px] font-medium text-warning">deprecated</span> : null}82 </span>83 </SelectItem>84 ))}85 </SelectGroup>86 ))}87 {empty && !allowNone ? <div className="px-2 py-3 text-center text-xs text-fg-subtle">No connected providers. Add a key in Settings → Providers.</div> : null}88 </SelectContent>89 </Select>90 );91}92