| 1 | import { type ProviderDriverKind, type ProviderInstanceId } from "@t3tools/contracts"; |
| 2 | import { memo } from "react"; |
| 3 | import { CheckIcon, StarIcon } from "lucide-react"; |
| 4 | import { |
| 5 | getDisplayModelName, |
| 6 | getProviderRowLabel, |
| 7 | getTriggerDisplayModelLabel, |
| 8 | type ModelEsque, |
| 9 | } from "./providerIconUtils"; |
| 10 | import { ComboboxItem } from "../ui/combobox"; |
| 11 | import { Button } from "../ui/button"; |
| 12 | import { Badge } from "../ui/badge"; |
| 13 | import { Kbd } from "../ui/kbd"; |
| 14 | import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; |
| 15 | import { cn } from "~/lib/utils"; |
| 16 | import { modelPickerModelKey } from "./modelPickerKeys"; |
| 17 | import { ProviderInstanceIcon } from "./ProviderInstanceIcon"; |
| 18 | |
| 19 | export const ModelListRow = memo(function ModelListRow(props: { |
| 20 | index: number; |
| 21 | model: ModelEsque; |
| 22 | /** Instance the model belongs to — the routing key used in combobox values. */ |
| 23 | instanceId: ProviderInstanceId; |
| 24 | /** Driver kind of the instance — used for the provider icon glyph. */ |
| 25 | driverKind: ProviderDriverKind; |
| 26 | /** |
| 27 | * Display name to show in the secondary line (provider footer). Usually |
| 28 | * the instance's configured `displayName` so custom instances like |
| 29 | * "Codex Personal" render with their user-authored label. |
| 30 | */ |
| 31 | providerDisplayName: string; |
| 32 | providerAccentColor?: string | undefined; |
| 33 | acpRegistryAgentId?: string | undefined; |
| 34 | acpRegistryIconUrl?: string | undefined; |
| 35 | isFavorite: boolean; |
| 36 | isSelected: boolean; |
| 37 | showSelection?: boolean; |
| 38 | showProvider: boolean; |
| 39 | preferShortName?: boolean; |
| 40 | useTriggerLabel?: boolean; |
| 41 | showNewBadge?: boolean; |
| 42 | unavailable?: boolean; |
| 43 | jumpLabel?: string | null; |
| 44 | disabledReason?: string | null; |
| 45 | onToggleFavorite: () => void; |
| 46 | }) { |
| 47 | const providerLabel = getProviderRowLabel(props.providerDisplayName, props.model.subProvider); |
| 48 | |
| 49 | const row = ( |
| 50 | <ComboboxItem |
| 51 | hideIndicator |
| 52 | index={props.index} |
| 53 | value={modelPickerModelKey(props.instanceId, props.model.slug)} |
| 54 | disabled={Boolean(props.disabledReason)} |
| 55 | className={cn( |
| 56 | "group relative w-full !min-w-0 max-w-full cursor-pointer", |
| 57 | props.disabledReason && |
| 58 | "data-disabled:pointer-events-auto data-disabled:cursor-not-allowed", |
| 59 | )} |
| 60 | > |
| 61 | <div className="min-w-0 flex-1 text-left"> |
| 62 | <div className="flex min-w-0 items-center gap-2"> |
| 63 | <div className="min-w-0 truncate text-xs font-medium leading-snug"> |
| 64 | {props.useTriggerLabel |
| 65 | ? getTriggerDisplayModelLabel(props.model) |
| 66 | : getDisplayModelName( |
| 67 | props.model, |
| 68 | props.preferShortName ? { preferShortName: true } : undefined, |
| 69 | )} |
| 70 | </div> |
| 71 | {props.showNewBadge ? ( |
| 72 | <span |
| 73 | className="shrink-0 rounded border border-update/35 bg-update/15 px-0.5 py-px text-3xs font-bold uppercase leading-none tracking-wide text-update-foreground" |
| 74 | aria-label="New model" |
| 75 | > |
| 76 | New |
| 77 | </span> |
| 78 | ) : null} |
| 79 | {props.unavailable ? ( |
| 80 | <Badge variant="outline" size="sm"> |
| 81 | Unavailable |
| 82 | </Badge> |
| 83 | ) : null} |
| 84 | </div> |
| 85 | {props.showProvider && ( |
| 86 | <div className="mt-1 flex items-center gap-1.5"> |
| 87 | <ProviderInstanceIcon |
| 88 | driverKind={props.driverKind} |
| 89 | displayName={props.providerDisplayName} |
| 90 | acpRegistryAgentId={props.acpRegistryAgentId} |
| 91 | acpRegistryIconUrl={props.acpRegistryIconUrl} |
| 92 | className="size-3" |
| 93 | iconClassName="size-3" |
| 94 | /> |
| 95 | <span className="truncate text-xs font-normal leading-snug text-muted-foreground/70"> |
| 96 | {providerLabel} |
| 97 | </span> |
| 98 | </div> |
| 99 | )} |
| 100 | </div> |
| 101 | |
| 102 | <div className="flex shrink-0 items-center gap-1.5"> |
| 103 | {props.showSelection && props.isSelected ? ( |
| 104 | <CheckIcon className="size-3.5" aria-hidden="true" /> |
| 105 | ) : null} |
| 106 | {props.jumpLabel ? <Kbd>{props.jumpLabel}</Kbd> : null} |
| 107 | <Tooltip> |
| 108 | <TooltipTrigger |
| 109 | render={ |
| 110 | <Button |
| 111 | size="icon-xs" |
| 112 | variant="ghost-muted" |
| 113 | className="-mr-1 shrink-0" |
| 114 | onClick={(event) => { |
| 115 | event.stopPropagation(); |
| 116 | props.onToggleFavorite(); |
| 117 | }} |
| 118 | onKeyDown={(event) => { |
| 119 | event.stopPropagation(); |
| 120 | }} |
| 121 | disabled={Boolean(props.disabledReason)} |
| 122 | aria-label={props.isFavorite ? "Remove from favorites" : "Add to favorites"} |
| 123 | > |
| 124 | <StarIcon |
| 125 | className={cn( |
| 126 | "size-3.5 sm:size-3", |
| 127 | props.isFavorite && "fill-current text-warning", |
| 128 | )} |
| 129 | /> |
| 130 | </Button> |
| 131 | } |
| 132 | /> |
| 133 | <TooltipPopup side="top" align="center"> |
| 134 | {props.isFavorite ? "Remove from favorites" : "Add to favorites"} |
| 135 | </TooltipPopup> |
| 136 | </Tooltip> |
| 137 | </div> |
| 138 | </ComboboxItem> |
| 139 | ); |
| 140 | |
| 141 | if (!props.disabledReason) { |
| 142 | return row; |
| 143 | } |
| 144 | |
| 145 | return ( |
| 146 | <Tooltip> |
| 147 | <TooltipTrigger render={row} /> |
| 148 | <TooltipPopup side="left" align="center"> |
| 149 | {props.disabledReason} |
| 150 | </TooltipPopup> |
| 151 | </Tooltip> |
| 152 | ); |
| 153 | }); |