mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-06-29 15:31:05 +08:00
Fix: Fix some model provider-related UI issues (#15884)
### What problem does this PR solve? Fix: Fix some model provider-related UI issues ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue)
This commit is contained in:
@@ -74,6 +74,9 @@ function ProviderCard({
|
||||
) => void;
|
||||
}) {
|
||||
const { data: instances } = useFetchProviderInstances(provider.name);
|
||||
if (!instances || instances.length <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -90,7 +93,6 @@ function ProviderCard({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Instances */}
|
||||
{instances.length > 0 && (
|
||||
<div className="border-t border-border-button">
|
||||
|
||||
@@ -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<AddCustomModelDialogFields[]>(() => {
|
||||
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<AddCustomModelDialogFields[]>(
|
||||
() =>
|
||||
MODEL_FIELD_SCHEMA.map((field) => ({
|
||||
...field,
|
||||
label: t(field.label),
|
||||
options: field.options?.map((opt) => ({
|
||||
value: opt.value,
|
||||
label: t(opt.label),
|
||||
})),
|
||||
})),
|
||||
[t],
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -42,6 +42,7 @@ const ProviderModal = ({
|
||||
const { instanceNameSet } = useFetchInstanceNameSet(llmFactory);
|
||||
const hideWhenInstanceExists = useHideWhenInstanceExists(instanceNameSet);
|
||||
const [verifyResult, setVerifyResult] = useState<VerifyResult | null>(null);
|
||||
const scrollAnchorRef = useRef<HTMLDivElement>(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<HTMLElement>('.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 = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
<VerifyButton
|
||||
onVerify={handleVerify}
|
||||
verifyCallback={(result: VerifyResult | null) => {
|
||||
setVerifyResult(result);
|
||||
}}
|
||||
/>
|
||||
<div ref={scrollAnchorRef}>
|
||||
<VerifyButton
|
||||
onVerify={handleVerify}
|
||||
verifyCallback={(result: VerifyResult | null) => {
|
||||
setVerifyResult(result);
|
||||
}}
|
||||
className={cn({
|
||||
'!flex flex-col ![position:inherit] ':
|
||||
verifyResult && docLinkText && config.docLink,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
docLinkText
|
||||
|
||||
Reference in New Issue
Block a user