"use client"; import { useEffect, useState } from "react"; import { useTranslations } from "next-intl"; import Select from "./Select"; import Input from "./Input"; import { cn } from "@/shared/utils/cn"; import { isVisionModelId } from "@/shared/constants/visionModels"; export interface ApiModel { provider: string; model: string; fullModel?: string; type?: string; subtype?: string; supportsVision?: boolean; } export interface ModelSelectFieldProps { value: string; onChange: (value: string) => void; disabled?: boolean; label?: React.ReactNode; placeholder?: string; ariaLabel?: string; /** Render a plain text fallback (custom option / off-catalog) — default true. */ allowCustom?: boolean; /** Let operators select the empty-value placeholder (for Auto/default semantics). */ allowEmpty?: boolean; /** Optional catalog predicate, e.g. restrict the picker to STT models. */ modelFilter?: (model: ApiModel) => boolean; /** Model API to read. The unified catalog includes specialty audio/video surfaces. */ modelSource?: "available" | "catalog"; /** * Render an editable text input (with a of catalog suggestions) * instead of a plain onChange(e.target.value)} placeholder={placeholder} disabled={disabled} aria-label={ariaLabel} className={className} /> ); } const hasKnownValue = value === "" || state.options.some((o) => o.value === value); const options = !hasKnownValue && allowCustom ? [{ value, label: `${value} (${t("custom")})` }, ...state.options] : state.options; // Editable custom-input mode (#10703): an operator may need a vision model // that is not (yet) in the catalog (self-hosted Ollama/vLLM, unlisted // provider). Keep the filtered ), while the custom field's // `onChange` lets operators save any string. if (allowCustomInput && state.status === "ready") { const customInputId = `${testId ?? "model-select-field"}-custom-input`; // Known means either the empty placeholder, a catalog option, or the // injected "(custom)" entry — so an off-catalog saved value stays visible // as the selected option instead of falling back to the placeholder. const knownValue = value === "" || options.some((o) => o.value === value); return (
onChange(e.target.value)} className="w-full py-2 px-3 text-sm text-text-main bg-surface border border-black/10 dark:border-white/10 rounded-control focus:ring-1 focus:ring-accent/30 focus:border-accent/50 focus:outline-none transition-all disabled:opacity-50 disabled:cursor-not-allowed text-[16px] sm:text-sm" />
); } return (