diff --git a/web/src/pages/user-setting/setting-model/components/used-model.tsx b/web/src/pages/user-setting/setting-model/components/used-model.tsx index 23d2f21d6a..fbc13ac1a6 100644 --- a/web/src/pages/user-setting/setting-model/components/used-model.tsx +++ b/web/src/pages/user-setting/setting-model/components/used-model.tsx @@ -74,6 +74,9 @@ function ProviderCard({ ) => void; }) { const { data: instances } = useFetchProviderInstances(provider.name); + if (!instances || instances.length <= 0) { + return null; + } return (
- {/* Instances */} {instances.length > 0 && (
diff --git a/web/src/pages/user-setting/setting-model/modal/provider-modal/components/use-custom-model-fields.tsx b/web/src/pages/user-setting/setting-model/modal/provider-modal/components/use-custom-model-fields.tsx index 01b1d05b69..e96833c192 100644 --- a/web/src/pages/user-setting/setting-model/modal/provider-modal/components/use-custom-model-fields.tsx +++ b/web/src/pages/user-setting/setting-model/modal/provider-modal/components/use-custom-model-fields.tsx @@ -1,127 +1,73 @@ import { useTranslate } from '@/hooks/common-hooks'; -import { IProviderModelItem } from '@/interfaces/request/llm'; import { useMemo } from 'react'; import type { AddCustomModelDialogFields } from './add-custom-model-dialog'; -/** - * Allowed values for `IProviderModelItem['model_types']`. Kept in sync - * with the backend model registry; `LabelKey` matches the i18n namespace - * the descriptor below uses for each option. - */ -const MODEL_TYPE_VALUES = [ - 'chat', - 'embedding', - 'rerank', - 'tts', - 'image2text', - 'speech2text', -] as const; - -/** - * Allowed values for `IProviderModelItem['features']`. - */ -const FEATURE_VALUES = ['tool_call'] as const; - -/** - * Descriptor for a single IProviderModelItem property. Each descriptor - * tells the dialog how to render the form input for that property. - * - * - `type` : the form input type - * - `required` : whether the field must be non-empty - * - `min` : for `number` types, the minimum allowed value - * - `options` : for select-style types, the allowed value set - * - `labelKey` : i18n key for the field label - * - `optionLabelKey` : i18n key prefix for each option's label - */ -type ModelFieldDescriptor = { - type: AddCustomModelDialogFields['type']; - required: boolean; - min?: number; - options?: readonly string[]; - labelKey: string; - optionLabelKey?: (value: string) => string; -}; - /** * Single source of truth for the custom-model dialog schema. Mirrors * the shape of `IProviderModelItem` 1:1 — adding a new property to the * interface means adding an entry here, and the dialog auto-adapts. + * + * `label` and each option's `label` are i18n keys (under the `setting` + * namespace). `useCustomModelFields` resolves them via `t()`. */ -const MODEL_FIELD_SCHEMA: Record< - keyof IProviderModelItem, - ModelFieldDescriptor -> = { - name: { +export const MODEL_FIELD_SCHEMA: AddCustomModelDialogFields[] = [ + { + name: 'name', + label: 'modelName', type: 'text', required: true, - labelKey: 'modelName', + defaultValue: '', }, - model_types: { + { + name: 'model_types', + label: 'modelType', type: 'multi-select', required: false, - options: MODEL_TYPE_VALUES, - labelKey: 'modelType', - optionLabelKey: (v) => `setting.modelTypes.${v}`, + defaultValue: [], + options: [ + { value: 'chat', label: 'modelTypes.chat' }, + { value: 'embedding', label: 'modelTypes.embedding' }, + { value: 'rerank', label: 'modelTypes.rerank' }, + { value: 'tts', label: 'modelTypes.tts' }, + { value: 'image2text', label: 'modelTypes.image2text' }, + { value: 'speech2text', label: 'modelTypes.speech2text' }, + ], }, - max_tokens: { + { + name: 'max_tokens', + label: 'modelMaxTokens', type: 'number', required: false, min: 0, - labelKey: 'modelMaxTokens', + defaultValue: 0, }, - features: { + { + name: 'features', + label: 'modelFeatures', type: 'switch-group', required: false, - options: FEATURE_VALUES, - labelKey: 'modelFeatures', - optionLabelKey: (v) => - v === 'tool_call' - ? 'modelFeatureToolCall' - : v === 'function_call' - ? 'modelFeatureFunctionCall' - : v, + defaultValue: [], + options: [{ value: 'is_tools', label: 'modelFeatureToolCall' }], }, -}; +]; /** - * Dialog field schema for adding a custom model. Derived from - * `IProviderModelItem` via `MODEL_FIELD_SCHEMA`, so the form is in - * lockstep with the model interface. + * Dialog field schema for adding a custom model. Returns + * `MODEL_FIELD_SCHEMA` with i18n keys resolved. */ export const useCustomModelFields = (): AddCustomModelDialogFields[] => { const { t } = useTranslate('setting'); - return useMemo(() => { - return ( - Object.entries(MODEL_FIELD_SCHEMA) as Array< - [keyof IProviderModelItem, ModelFieldDescriptor] - > - ).map(([prop, desc]) => { - const defaultValue = - desc.type === 'number' - ? 0 - : desc.type === 'multi-select' || desc.type === 'switch-group' - ? [] - : ''; - - return { - name: String(prop), - label: t(desc.labelKey), - type: desc.type, - required: desc.required, - defaultValue, - ...(desc.min !== undefined ? { min: desc.min } : {}), - ...(desc.options - ? { - options: desc.options.map((value) => ({ - value, - label: t( - desc.optionLabelKey ? desc.optionLabelKey(value) : value, - ), - })), - } - : {}), - }; - }); - }, [t]); + return useMemo( + () => + MODEL_FIELD_SCHEMA.map((field) => ({ + ...field, + label: t(field.label), + options: field.options?.map((opt) => ({ + value: opt.value, + label: t(opt.label), + })), + })), + [t], + ); }; diff --git a/web/src/pages/user-setting/setting-model/modal/provider-modal/hooks/use-list-models-picker.ts b/web/src/pages/user-setting/setting-model/modal/provider-modal/hooks/use-list-models-picker.ts index 15f5d2ca9f..004c7c16b0 100644 --- a/web/src/pages/user-setting/setting-model/modal/provider-modal/hooks/use-list-models-picker.ts +++ b/web/src/pages/user-setting/setting-model/modal/provider-modal/hooks/use-list-models-picker.ts @@ -18,7 +18,7 @@ const getIsToolsFromFeatures = ( features: IProviderModelItem['features'], ): boolean | undefined => { if (!Array.isArray(features)) return undefined; - return features.includes('tool_call') || features.includes('function_call'); + return features.includes('is_tools'); }; // Map a fetched list-model item to the request-side IModelInfo shape. diff --git a/web/src/pages/user-setting/setting-model/modal/provider-modal/index.tsx b/web/src/pages/user-setting/setting-model/modal/provider-modal/index.tsx index dacb400f83..53f5cb3ea9 100644 --- a/web/src/pages/user-setting/setting-model/modal/provider-modal/index.tsx +++ b/web/src/pages/user-setting/setting-model/modal/provider-modal/index.tsx @@ -42,6 +42,7 @@ const ProviderModal = ({ const { instanceNameSet } = useFetchInstanceNameSet(llmFactory); const hideWhenInstanceExists = useHideWhenInstanceExists(instanceNameSet); const [verifyResult, setVerifyResult] = useState(null); + const scrollAnchorRef = useRef(null); useEffect(() => { setVerifyResult(null); return () => { @@ -49,6 +50,26 @@ const ProviderModal = ({ }; }, [visible]); + // When a verify result comes back, the VerifyButton renders new log + // content below the existing form. Scroll the modal's scrollable area + // to the bottom so the user actually sees the result. We walk up the + // DOM from a ref inside the scrollable container (the Modal renders + // it via a Radix Portal) and use rAF to wait for the new content to + // be laid out before measuring scrollHeight. + useEffect(() => { + if (!verifyResult || !scrollAnchorRef.current) { + return; + } + const scrollContainer = + scrollAnchorRef.current.closest('.overflow-y-auto'); + if (!scrollContainer) { + return; + } + requestAnimationFrame(() => { + scrollContainer.scrollTop = scrollContainer.scrollHeight; + }); + }, [verifyResult]); + // Field config, default values, doc link, and the LIST_MODEL_PROVIDERS // flag are all derived from the current llmFactory / mode / initialValues. // `baseUrlRegionMaps` is forwarded to the actions hook so the modal can @@ -123,9 +144,7 @@ const ProviderModal = ({ max_tokens: item.max_tokens, extra: item.features ? { - is_tools: - item.features.includes('tool_call') || - item.features.includes('function_call'), + is_tools: item.features.includes('is_tools'), } : undefined, }); @@ -203,12 +222,18 @@ const ProviderModal = ({ /> )} - { - setVerifyResult(result); - }} - /> +
+ { + setVerifyResult(result); + }} + className={cn({ + '!flex flex-col ![position:inherit] ': + verifyResult && docLinkText && config.docLink, + })} + /> +