"use client"; import { ArrowDownIcon, ArrowUpIcon, EyeIcon, EyeOffIcon, InfoIcon, PlusIcon, StarIcon, XIcon,} from "lucide-react";import { useMemo, useRef, useState } from "react";import { ProviderDriverKind, type ProviderInstanceId, type ServerProviderModel,} from "@t3tools/contracts";import { normalizeCustomModelSlug } from "@t3tools/shared/model"; import { cn } from "../../lib/utils";import { sortModelsForProviderInstance } from "../../modelOrdering";import { MAX_CUSTOM_MODEL_LENGTH } from "../../modelSelection";import { Button } from "../ui/button";import { Input } from "../ui/input";import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; /** * Placeholder text for the "add a custom model" input, keyed by driver * kind. Mirrors the prior hardcoded switch in `SettingsPanels.tsx` so the * UX is unchanged — only the owning component has moved. */const CUSTOM_MODEL_PLACEHOLDER_BY_KIND: Partial<Record<ProviderDriverKind, string>> = { [ProviderDriverKind.make("codex")]: "gpt-6.7-codex-ultra-preview", [ProviderDriverKind.make("claudeAgent")]: "claude-sonnet-5", [ProviderDriverKind.make("cursor")]: "claude-sonnet-4-6", [ProviderDriverKind.make("opencode")]: "openai/gpt-5",}; interface ProviderModelsSectionProps { /** Identifier used to namespace input ids within the DOM. */ readonly instanceId: ProviderInstanceId; /** * Driver kind for slug normalization + input placeholder. `null` when * the section is rendered without enough provider metadata. */ readonly driverKind: ProviderDriverKind | null; /** * The live model list to display. Includes both built-in (probe-reported) * and custom entries, distinguished by `isCustom`. */ readonly models: ReadonlyArray<ServerProviderModel>; /** * The persisted custom-model slug list for this instance. Drives dedup, * and is the array we hand back verbatim (with the new slug appended / * removed) via `onChange`. */ readonly customModels: ReadonlyArray<string>; /** Server-returned model slugs hidden from the model picker. */ readonly hiddenModels: ReadonlyArray<string>; /** Model slugs favorited for this provider instance. */ readonly favoriteModels: ReadonlyArray<string>; /** Explicit user-authored model ordering for this provider instance. */ readonly modelOrder: ReadonlyArray<string>; /** * Commit the new custom-model list. Caller is responsible for routing the * write to the correct storage (legacy `settings.providers[kind]` vs. * `providerInstances[id].config`). */ readonly onChange: (next: ReadonlyArray<string>) => void; readonly onHiddenModelsChange: (next: ReadonlyArray<string>) => void; readonly onFavoriteModelsChange: (next: ReadonlyArray<string>) => void; readonly onModelOrderChange: (next: ReadonlyArray<string>) => void;} /** * Shared "Models" section rendered on both the built-in default and custom * provider-instance cards. Owns its own input + error local state so two * cards on screen don't fight over the input value. * * Validation mirrors the pre-consolidation logic in `SettingsPanels`: * - empty / whitespace → "Enter a model slug." * - duplicate of a non-custom (probe-reported) slug → "already built in" * - exceeds `MAX_CUSTOM_MODEL_LENGTH` → length error * - duplicate of an already-saved custom slug → already-saved error */export function ProviderModelsSection({ instanceId, driverKind, models, customModels, hiddenModels, favoriteModels, modelOrder, onChange, onHiddenModelsChange, onFavoriteModelsChange, onModelOrderChange,}: ProviderModelsSectionProps) { const [input, setInput] = useState(""); const [error, setError] = useState<string | null>(null); const listRef = useRef<HTMLDivElement | null>(null); const hiddenModelSet = useMemo(() => new Set(hiddenModels), [hiddenModels]); const favoriteModelSet = useMemo(() => new Set(favoriteModels), [favoriteModels]); const orderedModels = useMemo(() => { return sortModelsForProviderInstance(models, { favoriteModels: favoriteModelSet, groupFavorites: true, modelOrder, }); }, [favoriteModelSet, modelOrder, models]); const handleAdd = () => { const normalized = normalizeCustomModelSlug(input); if (!normalized) { setError("Enter a model slug."); return; } if (models.some((model) => !model.isCustom && model.slug === normalized)) { setError("That model is already built in."); return; } if (normalized.length > MAX_CUSTOM_MODEL_LENGTH) { setError(`Model slugs must be ${MAX_CUSTOM_MODEL_LENGTH} characters or less.`); return; } if (customModels.includes(normalized)) { setError("That custom model is already saved."); return; } onChange([...customModels, normalized]); setInput(""); setError(null); // Scroll the new row into view once the DOM reflects the commit. // `MutationObserver` handles the one-frame gap between `onChange` and // the `models` prop update; the `requestAnimationFrame` covers the // common case where the parent updates synchronously. const el = listRef.current; if (!el) return; const scrollToEnd = () => el.scrollTo({ top: el.scrollHeight, behavior: "smooth" }); requestAnimationFrame(scrollToEnd); const observer = new MutationObserver(() => { scrollToEnd(); observer.disconnect(); }); observer.observe(el, { childList: true, subtree: true }); setTimeout(() => observer.disconnect(), 2_000); }; const handleRemove = (slug: string) => { onChange(customModels.filter((model) => model !== slug)); onModelOrderChange(modelOrder.filter((model) => model !== slug)); onFavoriteModelsChange(favoriteModels.filter((model) => model !== slug)); setError(null); }; const handleToggleHidden = (slug: string) => { if (hiddenModelSet.has(slug)) { onHiddenModelsChange(hiddenModels.filter((model) => model !== slug)); return; } onHiddenModelsChange([...hiddenModels, slug]); }; const handleToggleFavorite = (slug: string) => { if (favoriteModelSet.has(slug)) { onFavoriteModelsChange(favoriteModels.filter((model) => model !== slug)); return; } onFavoriteModelsChange([...favoriteModels, slug]); }; const handleMove = (slug: string, direction: -1 | 1) => { const slugs = orderedModels.map((model) => model.slug); const index = slugs.indexOf(slug); const nextIndex = index + direction; if (index < 0 || nextIndex < 0 || nextIndex >= slugs.length) { return; } const next = [...slugs]; [next[index], next[nextIndex]] = [next[nextIndex]!, next[index]!]; onModelOrderChange(next); }; return ( <div> <div className="text-xs font-medium text-foreground">Models</div> <div className="mt-1 text-xs text-muted-foreground"> {models.length} model{models.length === 1 ? "" : "s"} available. </div> <div ref={listRef} className="mt-2 max-h-40 overflow-y-auto pb-1"> {orderedModels.map((model, index) => { const caps = model.capabilities; const capLabels: string[] = []; const isHidden = !model.isCustom && hiddenModelSet.has(model.slug); const isFavorite = favoriteModelSet.has(model.slug); const previousModel = orderedModels[index - 1]; const nextModel = orderedModels[index + 1]; const canMoveUp = previousModel !== undefined && favoriteModelSet.has(previousModel.slug) === isFavorite; const canMoveDown = nextModel !== undefined && favoriteModelSet.has(nextModel.slug) === isFavorite; const descriptors = caps?.optionDescriptors ?? []; if (descriptors.some((descriptor) => descriptor.id === "fastMode")) { capLabels.push("Fast mode"); } if (descriptors.some((descriptor) => descriptor.id === "thinking")) { capLabels.push("Thinking"); } if ( descriptors.some( (descriptor) => descriptor.type === "select" && (descriptor.id === "reasoningEffort" || descriptor.id === "effort" || descriptor.id === "reasoning" || descriptor.id === "variant"), ) ) { capLabels.push("Reasoning"); } const hasDetails = capLabels.length > 0 || model.name !== model.slug; return ( <div key={`${instanceId}:${model.slug}`} className={cn( "grid min-h-7 grid-cols-[minmax(0,1fr)_auto] items-center gap-2 py-1", isHidden && "text-muted-foreground", )} > <div className="flex min-w-0 items-center gap-1"> <span className={cn( "min-w-0 truncate text-xs", isHidden ? "text-muted-foreground line-through" : "text-foreground/90", )} > {model.name} </span> {hasDetails ? ( <Tooltip> <TooltipTrigger render={ <Button size="icon-xs" variant="ghost" className="size-5 rounded-sm p-0 text-muted-foreground/60 hover:text-muted-foreground" aria-label={`Details for ${model.name}`} /> } > <InfoIcon className="size-3" /> </TooltipTrigger> <TooltipPopup side="top" className="max-w-56"> <div className="space-y-1"> <code className="block text-[11px] text-foreground">{model.slug}</code> {capLabels.length > 0 ? ( <div className="flex flex-wrap gap-x-2 gap-y-0.5"> {capLabels.map((label) => ( <span key={label} className="text-[10px] text-muted-foreground"> {label} </span> ))} </div> ) : null} </div> </TooltipPopup> </Tooltip> ) : null} {isHidden ? ( <span className="text-[10px] text-muted-foreground">hidden</span> ) : null} {model.isCustom ? ( <span className="text-[10px] text-muted-foreground">custom</span> ) : null} </div> <div className="flex shrink-0 items-center gap-0.5"> <Tooltip> <TooltipTrigger render={ <Button size="icon-xs" variant="ghost" className={cn( "size-5 rounded-sm p-0 text-muted-foreground hover:text-foreground", isFavorite && "text-yellow-500 hover:text-yellow-600", )} onClick={() => handleToggleFavorite(model.slug)} aria-label={`${isFavorite ? "Remove" : "Add"} ${model.name} ${ isFavorite ? "from" : "to" } favorites`} /> } > <StarIcon className={cn("size-3", isFavorite && "fill-current")} /> </TooltipTrigger> <TooltipPopup side="top"> {isFavorite ? "Remove from favorites" : "Add to favorites"} </TooltipPopup> </Tooltip> <Tooltip> <TooltipTrigger render={ <Button size="icon-xs" variant="ghost" className="size-5 rounded-sm p-0 text-muted-foreground hover:text-foreground" disabled={!canMoveUp} onClick={() => handleMove(model.slug, -1)} aria-label={`Move ${model.name} up`} /> } > <ArrowUpIcon className="size-3" /> </TooltipTrigger> <TooltipPopup side="top">Move up</TooltipPopup> </Tooltip> <Tooltip> <TooltipTrigger render={ <Button size="icon-xs" variant="ghost" className="size-5 rounded-sm p-0 text-muted-foreground hover:text-foreground" disabled={!canMoveDown} onClick={() => handleMove(model.slug, 1)} aria-label={`Move ${model.name} down`} /> } > <ArrowDownIcon className="size-3" /> </TooltipTrigger> <TooltipPopup side="top">Move down</TooltipPopup> </Tooltip> {!model.isCustom ? ( <Tooltip> <TooltipTrigger render={ <Button size="icon-xs" variant="ghost" className="size-5 rounded-sm p-0 text-muted-foreground hover:text-foreground" onClick={() => handleToggleHidden(model.slug)} aria-label={`${isHidden ? "Show" : "Hide"} ${model.name}`} /> } > {isHidden ? ( <EyeIcon className="size-3" /> ) : ( <EyeOffIcon className="size-3" /> )} </TooltipTrigger> <TooltipPopup side="top"> {isHidden ? "Show in picker" : "Hide from picker"} </TooltipPopup> </Tooltip> ) : null} {model.isCustom ? ( <Tooltip> <TooltipTrigger render={ <Button size="icon-xs" variant="ghost" className="size-5 rounded-sm p-0 text-muted-foreground hover:text-foreground" aria-label={`Remove ${model.slug}`} onClick={() => handleRemove(model.slug)} /> } > <XIcon className="size-3" /> </TooltipTrigger> <TooltipPopup side="top">Remove custom model</TooltipPopup> </Tooltip> ) : null} </div> </div> ); })} </div> <div className="mt-3 flex flex-col gap-2 sm:flex-row"> <Input id={`provider-instance-${instanceId}-custom-model`} value={input} onChange={(event) => { setInput(event.target.value); if (error) setError(null); }} onKeyDown={(event) => { if (event.key !== "Enter") return; event.preventDefault(); handleAdd(); }} placeholder={driverKind ? CUSTOM_MODEL_PLACEHOLDER_BY_KIND[driverKind] : "model-slug"} spellCheck={false} /> <Button className="shrink-0" variant="outline" onClick={handleAdd}> <PlusIcon className="size-3.5" /> Add </Button> </div> {error ? <p className="mt-2 text-xs text-destructive">{error}</p> : null} </div> );}
"use client";
import {
ArrowDownIcon,
ArrowUpIcon,
EyeIcon,
EyeOffIcon,
InfoIcon,
PlusIcon,
StarIcon,
XIcon,
} from "lucide-react";
import { useMemo, useRef, useState } from "react";
import {
ProviderDriverKind,
type ProviderInstanceId,
type ServerProviderModel,
} from "@t3tools/contracts";
import { normalizeCustomModelSlug } from "@t3tools/shared/model";
import { cn } from "../../lib/utils";
import { sortModelsForProviderInstance } from "../../modelOrdering";
import { MAX_CUSTOM_MODEL_LENGTH } from "../../modelSelection";
import { Button } from "../ui/button";
import { Input } from "../ui/input";
import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
/**
* Placeholder text for the "add a custom model" input, keyed by driver
* kind. Mirrors the prior hardcoded switch in `SettingsPanels.tsx` so the
* UX is unchanged — only the owning component has moved.
*/
const CUSTOM_MODEL_PLACEHOLDER_BY_KIND: Partial<Record<ProviderDriverKind, string>> = {
[ProviderDriverKind.make("codex")]: "gpt-6.7-codex-ultra-preview",
[ProviderDriverKind.make("claudeAgent")]: "claude-sonnet-5",
[ProviderDriverKind.make("cursor")]: "claude-sonnet-4-6",
[ProviderDriverKind.make("opencode")]: "openai/gpt-5",
};
interface ProviderModelsSectionProps {
/** Identifier used to namespace input ids within the DOM. */
readonly instanceId: ProviderInstanceId;
/**
* Driver kind for slug normalization + input placeholder. `null` when
* the section is rendered without enough provider metadata.
*/
readonly driverKind: ProviderDriverKind | null;
/**
* The live model list to display. Includes both built-in (probe-reported)
* and custom entries, distinguished by `isCustom`.
*/
readonly models: ReadonlyArray<ServerProviderModel>;
/**
* The persisted custom-model slug list for this instance. Drives dedup,
* and is the array we hand back verbatim (with the new slug appended /
* removed) via `onChange`.
*/
readonly customModels: ReadonlyArray<string>;
/** Server-returned model slugs hidden from the model picker. */
readonly hiddenModels: ReadonlyArray<string>;
/** Model slugs favorited for this provider instance. */
readonly favoriteModels: ReadonlyArray<string>;
/** Explicit user-authored model ordering for this provider instance. */
readonly modelOrder: ReadonlyArray<string>;
/**
* Commit the new custom-model list. Caller is responsible for routing the
* write to the correct storage (legacy `settings.providers[kind]` vs.
* `providerInstances[id].config`).
*/
readonly onChange: (next: ReadonlyArray<string>) => void;
readonly onHiddenModelsChange: (next: ReadonlyArray<string>) => void;
readonly onFavoriteModelsChange: (next: ReadonlyArray<string>) => void;
readonly onModelOrderChange: (next: ReadonlyArray<string>) => void;
}
/**
* Shared "Models" section rendered on both the built-in default and custom
* provider-instance cards. Owns its own input + error local state so two
* cards on screen don't fight over the input value.
*
* Validation mirrors the pre-consolidation logic in `SettingsPanels`:
* - empty / whitespace → "Enter a model slug."
* - duplicate of a non-custom (probe-reported) slug → "already built in"
* - exceeds `MAX_CUSTOM_MODEL_LENGTH` → length error
* - duplicate of an already-saved custom slug → already-saved error
*/
export function ProviderModelsSection({
instanceId,
driverKind,
models,
customModels,
hiddenModels,
favoriteModels,
modelOrder,
onChange,
onHiddenModelsChange,
onFavoriteModelsChange,
onModelOrderChange,
}: ProviderModelsSectionProps) {
const [input, setInput] = useState("");
const [error, setError] = useState<string | null>(null);
const listRef = useRef<HTMLDivElement | null>(null);
const hiddenModelSet = useMemo(() => new Set(hiddenModels), [hiddenModels]);
const favoriteModelSet = useMemo(() => new Set(favoriteModels), [favoriteModels]);
const orderedModels = useMemo(() => {
return sortModelsForProviderInstance(models, {
favoriteModels: favoriteModelSet,
groupFavorites: true,
modelOrder,
});
}, [favoriteModelSet, modelOrder, models]);
const handleAdd = () => {
const normalized = normalizeCustomModelSlug(input);
if (!normalized) {
setError("Enter a model slug.");
return;
}
if (models.some((model) => !model.isCustom && model.slug === normalized)) {
setError("That model is already built in.");
return;
}
if (normalized.length > MAX_CUSTOM_MODEL_LENGTH) {
setError(`Model slugs must be ${MAX_CUSTOM_MODEL_LENGTH} characters or less.`);
return;
}
if (customModels.includes(normalized)) {
setError("That custom model is already saved.");
return;
}
onChange([...customModels, normalized]);
setInput("");
setError(null);
// Scroll the new row into view once the DOM reflects the commit.
// `MutationObserver` handles the one-frame gap between `onChange` and
// the `models` prop update; the `requestAnimationFrame` covers the
// common case where the parent updates synchronously.
const el = listRef.current;
if (!el) return;
const scrollToEnd = () => el.scrollTo({ top: el.scrollHeight, behavior: "smooth" });
requestAnimationFrame(scrollToEnd);
const observer = new MutationObserver(() => {
scrollToEnd();
observer.disconnect();
});
observer.observe(el, { childList: true, subtree: true });
setTimeout(() => observer.disconnect(), 2_000);
};
const handleRemove = (slug: string) => {
onChange(customModels.filter((model) => model !== slug));
onModelOrderChange(modelOrder.filter((model) => model !== slug));
onFavoriteModelsChange(favoriteModels.filter((model) => model !== slug));
setError(null);
};
const handleToggleHidden = (slug: string) => {
if (hiddenModelSet.has(slug)) {
onHiddenModelsChange(hiddenModels.filter((model) => model !== slug));
return;
}
onHiddenModelsChange([...hiddenModels, slug]);
};
const handleToggleFavorite = (slug: string) => {
if (favoriteModelSet.has(slug)) {
onFavoriteModelsChange(favoriteModels.filter((model) => model !== slug));
return;
}
onFavoriteModelsChange([...favoriteModels, slug]);
};
const handleMove = (slug: string, direction: -1 | 1) => {
const slugs = orderedModels.map((model) => model.slug);
const index = slugs.indexOf(slug);
const nextIndex = index + direction;
if (index < 0 || nextIndex < 0 || nextIndex >= slugs.length) {
return;
}
const next = [...slugs];
[next[index], next[nextIndex]] = [next[nextIndex]!, next[index]!];
onModelOrderChange(next);
};
return (
<div>
<div className="text-xs font-medium text-foreground">Models</div>
<div className="mt-1 text-xs text-muted-foreground">
{models.length} model{models.length === 1 ? "" : "s"} available.
</div>
<div ref={listRef} className="mt-2 max-h-40 overflow-y-auto pb-1">
{orderedModels.map((model, index) => {
const caps = model.capabilities;
const capLabels: string[] = [];
const isHidden = !model.isCustom && hiddenModelSet.has(model.slug);
const isFavorite = favoriteModelSet.has(model.slug);
const previousModel = orderedModels[index - 1];
const nextModel = orderedModels[index + 1];
const canMoveUp =
previousModel !== undefined && favoriteModelSet.has(previousModel.slug) === isFavorite;
const canMoveDown =
nextModel !== undefined && favoriteModelSet.has(nextModel.slug) === isFavorite;
const descriptors = caps?.optionDescriptors ?? [];
if (descriptors.some((descriptor) => descriptor.id === "fastMode")) {
capLabels.push("Fast mode");
}
if (descriptors.some((descriptor) => descriptor.id === "thinking")) {
capLabels.push("Thinking");
}
if (
descriptors.some(
(descriptor) =>
descriptor.type === "select" &&
(descriptor.id === "reasoningEffort" ||
descriptor.id === "effort" ||
descriptor.id === "reasoning" ||
descriptor.id === "variant"),
)
) {
capLabels.push("Reasoning");
}
const hasDetails = capLabels.length > 0 || model.name !== model.slug;
return (
<div
key={`${instanceId}:${model.slug}`}
className={cn(
"grid min-h-7 grid-cols-[minmax(0,1fr)_auto] items-center gap-2 py-1",
isHidden && "text-muted-foreground",
)}
>
<div className="flex min-w-0 items-center gap-1">
<span
className={cn(
"min-w-0 truncate text-xs",
isHidden ? "text-muted-foreground line-through" : "text-foreground/90",
)}
>
{model.name}
</span>
{hasDetails ? (
<Tooltip>
<TooltipTrigger
render={
<Button
size="icon-xs"
variant="ghost"
className="size-5 rounded-sm p-0 text-muted-foreground/60 hover:text-muted-foreground"
aria-label={`Details for ${model.name}`}
/>
}
>
<InfoIcon className="size-3" />
</TooltipTrigger>
<TooltipPopup side="top" className="max-w-56">
<div className="space-y-1">
<code className="block text-[11px] text-foreground">{model.slug}</code>
{capLabels.length > 0 ? (
<div className="flex flex-wrap gap-x-2 gap-y-0.5">
{capLabels.map((label) => (
<span key={label} className="text-[10px] text-muted-foreground">
{label}
</span>
))}
</div>
) : null}
</div>
</TooltipPopup>
</Tooltip>
) : null}
{isHidden ? (
<span className="text-[10px] text-muted-foreground">hidden</span>
) : null}
{model.isCustom ? (
<span className="text-[10px] text-muted-foreground">custom</span>
) : null}
</div>
<div className="flex shrink-0 items-center gap-0.5">
<Tooltip>
<TooltipTrigger
render={
<Button
size="icon-xs"
variant="ghost"
className={cn(
"size-5 rounded-sm p-0 text-muted-foreground hover:text-foreground",
isFavorite && "text-yellow-500 hover:text-yellow-600",
)}
onClick={() => handleToggleFavorite(model.slug)}
aria-label={`${isFavorite ? "Remove" : "Add"} ${model.name} ${
isFavorite ? "from" : "to"
} favorites`}
/>
}
>
<StarIcon className={cn("size-3", isFavorite && "fill-current")} />
</TooltipTrigger>
<TooltipPopup side="top">
{isFavorite ? "Remove from favorites" : "Add to favorites"}
</TooltipPopup>
</Tooltip>
<Tooltip>
<TooltipTrigger
render={
<Button
size="icon-xs"
variant="ghost"
className="size-5 rounded-sm p-0 text-muted-foreground hover:text-foreground"
disabled={!canMoveUp}
onClick={() => handleMove(model.slug, -1)}
aria-label={`Move ${model.name} up`}
/>
}
>
<ArrowUpIcon className="size-3" />
</TooltipTrigger>
<TooltipPopup side="top">Move up</TooltipPopup>
</Tooltip>
<Tooltip>
<TooltipTrigger
render={
<Button
size="icon-xs"
variant="ghost"
className="size-5 rounded-sm p-0 text-muted-foreground hover:text-foreground"
disabled={!canMoveDown}
onClick={() => handleMove(model.slug, 1)}
aria-label={`Move ${model.name} down`}
/>
}
>
<ArrowDownIcon className="size-3" />
</TooltipTrigger>
<TooltipPopup side="top">Move down</TooltipPopup>
</Tooltip>
{!model.isCustom ? (
<Tooltip>
<TooltipTrigger
render={
<Button
size="icon-xs"
variant="ghost"
className="size-5 rounded-sm p-0 text-muted-foreground hover:text-foreground"
onClick={() => handleToggleHidden(model.slug)}
aria-label={`${isHidden ? "Show" : "Hide"} ${model.name}`}
/>
}
>
{isHidden ? (
<EyeIcon className="size-3" />
) : (
<EyeOffIcon className="size-3" />
)}
</TooltipTrigger>
<TooltipPopup side="top">
{isHidden ? "Show in picker" : "Hide from picker"}
</TooltipPopup>
</Tooltip>
) : null}
{model.isCustom ? (
<Tooltip>
<TooltipTrigger
render={
<Button
size="icon-xs"
variant="ghost"
className="size-5 rounded-sm p-0 text-muted-foreground hover:text-foreground"
aria-label={`Remove ${model.slug}`}
onClick={() => handleRemove(model.slug)}
/>
}
>
<XIcon className="size-3" />
</TooltipTrigger>
<TooltipPopup side="top">Remove custom model</TooltipPopup>
</Tooltip>
) : null}
</div>
</div>
);
})}
</div>
<div className="mt-3 flex flex-col gap-2 sm:flex-row">
<Input
id={`provider-instance-${instanceId}-custom-model`}
value={input}
onChange={(event) => {
setInput(event.target.value);
if (error) setError(null);
}}
onKeyDown={(event) => {
if (event.key !== "Enter") return;
event.preventDefault();
handleAdd();
}}
placeholder={driverKind ? CUSTOM_MODEL_PLACEHOLDER_BY_KIND[driverKind] : "model-slug"}
spellCheck={false}
/>
<Button className="shrink-0" variant="outline" onClick={handleAdd}>
<PlusIcon className="size-3.5" />
Add
</Button>
</div>
{error ? <p className="mt-2 text-xs text-destructive">{error}</p> : null}
</div>
);
}