Refactor(setting-model): Refactor the model management interface and optimize the component structure. #10703 (#10905)

### What problem does this PR solve?

Refactor(setting-model): Refactor the model management interface and
optimize the component structure. #10703

### Type of change

- [x] Refactoring
This commit is contained in:
chanx
2025-10-31 09:27:30 +08:00
committed by GitHub
parent ff2365b146
commit 5a830ea68b
11 changed files with 1191 additions and 338 deletions

View File

@ -31,9 +31,16 @@ export function TemplateCard({ data, showModal }: IProps) {
avatar={data.avatar ? data.avatar : 'https://github.com/shadcn.png'}
name={data?.title[language] || 'CN'}
></RAGFlowAvatar>
<div className="text-[18px] font-bold break-words hyphens-auto overflow-hidden"lang={language}>{data?.title[language]}</div>
<div
className="text-[18px] font-bold break-words hyphens-auto overflow-hidden"
lang={language}
>
{data?.title[language]}
</div>
</div>
<p className="break-words hypens-auto"lang={language}>{data?.description[language]}</p>
<p className="break-words hypens-auto" lang={language}>
{data?.description[language]}
</p>
<div className="group-hover:bg-gradient-to-t from-black/70 from-10% via-black/0 via-50% to-black/0 w-full h-full group-hover:block absolute top-0 left-0 hidden rounded-xl">
<Button
variant="default"

View File

@ -0,0 +1,173 @@
// src/components/ModelProviderCard.tsx
import { LlmIcon } from '@/components/svg-icon';
import { Button } from '@/components/ui/button';
import { useSetModalState, useTranslate } from '@/hooks/common-hooks';
import { LlmItem } from '@/hooks/llm-hooks';
import { getRealModelName } from '@/utils/llm-util';
import { EditOutlined, SettingOutlined } from '@ant-design/icons';
import { ChevronsDown, ChevronsUp, Trash2 } from 'lucide-react';
import { FC } from 'react';
import { isLocalLlmFactory } from '../../utils';
import { useHandleDeleteFactory, useHandleDeleteLlm } from '../hooks';
interface IModelCardProps {
item: LlmItem;
clickApiKey: (llmFactory: string) => void;
handleEditModel: (model: any, factory: LlmItem) => void;
}
type TagType =
| 'LLM'
| 'TEXT EMBEDDING'
| 'TEXT RE-RANK'
| 'TTS'
| 'SPEECH2TEXT'
| 'IMAGE2TEXT'
| 'MODERATION';
const sortTags = (tags: string) => {
const orderMap: Record<TagType, number> = {
LLM: 1,
'TEXT EMBEDDING': 2,
'TEXT RE-RANK': 3,
TTS: 4,
SPEECH2TEXT: 5,
IMAGE2TEXT: 6,
MODERATION: 7,
};
return tags
.split(',')
.map((tag) => tag.trim())
.sort(
(a, b) =>
(orderMap[a as TagType] || 999) - (orderMap[b as TagType] || 999),
);
};
export const ModelProviderCard: FC<IModelCardProps> = ({
item,
clickApiKey,
handleEditModel,
}) => {
const { visible, switchVisible } = useSetModalState();
const { t } = useTranslate('setting');
const { handleDeleteLlm } = useHandleDeleteLlm(item.name);
const { handleDeleteFactory } = useHandleDeleteFactory(item.name);
const handleApiKeyClick = () => {
clickApiKey(item.name);
};
const handleShowMoreClick = () => {
switchVisible();
};
return (
<div className={`w-full rounded-lg border border-border-default`}>
{/* Header */}
<div className="flex items-center justify-between p-4 cursor-pointer transition-colors">
<div className="flex items-center space-x-3">
<LlmIcon name={item.name} />
<div>
<h3 className="font-medium">{item.name}</h3>
</div>
</div>
<div className="flex items-center space-x-2">
<Button
onClick={(e) => {
e.stopPropagation();
handleApiKeyClick();
}}
className="px-3 py-1 text-sm bg-bg-input hover:bg-bg-input text-text-primary rounded-md transition-colors flex items-center space-x-1"
>
<SettingOutlined />
<span>
{isLocalLlmFactory(item.name) ? t('addTheModel') : 'API-Key'}
</span>
</Button>
<Button
onClick={(e) => {
e.stopPropagation();
handleShowMoreClick();
}}
className="px-3 py-1 text-sm bg-bg-input hover:bg-bg-input text-text-primary rounded-md transition-colors flex items-center space-x-1"
>
<span>{visible ? t('hideModels') : t('showMoreModels')}</span>
{visible ? <ChevronsDown /> : <ChevronsUp />}
</Button>
<Button
variant={'secondary'}
onClick={(e) => {
e.stopPropagation();
handleDeleteFactory();
}}
className="p-1 text-text-primary hover:text-state-error transition-colors"
>
<Trash2 />
</Button>
</div>
</div>
{/* Content */}
{visible && (
<div className="">
<div className="px-4 flex flex-wrap gap-1 mt-1">
{sortTags(item.tags).map((tag, index) => (
<span
key={index}
className="px-2 py-1 text-xs bg-bg-card text-text-secondary rounded-md"
>
{tag}
</span>
))}
</div>
<div className="m-4 bg-bg-card rounded-lg max-h-96 overflow-auto scrollbar-auto">
<div className="">
{item.llm.map((model) => (
<div
key={model.name}
className="flex items-center border-b border-border-default justify-between p-3 hover:bg-bg-card transition-colors"
>
<div className="flex items-center space-x-3">
<span className="font-medium">
{getRealModelName(model.name)}
</span>
<span className="px-2 py-1 text-xs bg-bg-card text-text-secondary rounded-md">
{model.type}
</span>
</div>
<div className="flex items-center space-x-2">
{isLocalLlmFactory(item.name) && (
<Button
variant={'secondary'}
onClick={() => handleEditModel(model, item)}
className="p-1 text-text-primary transition-colors"
>
<EditOutlined />
</Button>
)}
<Button
variant={'secondary'}
onClick={() => {
handleDeleteLlm(model.name);
console.log(handleDeleteLlm, model.name);
}}
className="p-1 hover:text-state-error transition-colors"
>
<Trash2 />
</Button>
</div>
</div>
))}
</div>
</div>
</div>
)}
</div>
);
};

View File

@ -0,0 +1,194 @@
import {
SelectWithSearch,
SelectWithSearchFlagOptionType,
} from '@/components/originui/select-with-search';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { LlmModelType } from '@/constants/knowledge';
import { useTranslate } from '@/hooks/common-hooks';
import {
ISystemModelSettingSavingParams,
useComposeLlmOptionsByModelTypes,
} from '@/hooks/llm-hooks';
import { CircleQuestionMark } from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useFetchSystemModelSettingOnMount } from '../hooks';
interface IProps {
loading: boolean;
onOk: (
payload: Omit<ISystemModelSettingSavingParams, 'tenant_id' | 'name'>,
) => void;
}
const SystemSetting = ({ onOk, loading }: IProps) => {
const { systemSetting: initialValues, allOptions } =
useFetchSystemModelSettingOnMount();
const { t } = useTranslate('setting');
const [formData, setFormData] = useState({
llm_id: '',
embd_id: '',
img2txt_id: '',
asr_id: '',
rerank_id: '',
tts_id: '',
});
const handleFieldChange = useCallback(
(field: string, value: string) => {
const updatedData = { ...formData, [field]: value || '' };
setFormData(updatedData);
console.log('updatedData', updatedData);
onOk(updatedData);
},
[formData, onOk],
);
useEffect(() => {
setFormData({
llm_id: initialValues.llm_id ?? '',
embd_id: initialValues.embd_id ?? '',
img2txt_id: initialValues.img2txt_id ?? '',
asr_id: initialValues.asr_id ?? '',
rerank_id: initialValues.rerank_id ?? '',
tts_id: initialValues.tts_id ?? '',
});
}, [initialValues]);
const modelOptions = useComposeLlmOptionsByModelTypes([
LlmModelType.Chat,
LlmModelType.Image2text,
]);
const llmList = useMemo(() => {
return [
{
id: 'llm_id',
label: t('chatModel'),
isRequired: true,
value: formData.llm_id,
options: modelOptions as SelectWithSearchFlagOptionType[],
tooltip: t('chatModelTip'),
},
{
id: 'embd_id',
label: t('embeddingModel'),
value: formData.embd_id,
options: allOptions[
LlmModelType.Embedding
] as SelectWithSearchFlagOptionType[],
tooltip: t('embeddingModelTip'),
},
{
id: 'img2txt_id',
label: t('img2txtModel'),
value: formData.img2txt_id,
options: allOptions[
LlmModelType.Image2text
] as SelectWithSearchFlagOptionType[],
tooltip: t('img2txtModelTip'),
},
{
id: 'asr_id',
label: t('sequence2txtModel'),
value: formData.asr_id,
options: allOptions[
LlmModelType.Speech2text
] as SelectWithSearchFlagOptionType[],
tooltip: t('sequence2txtModelTip'),
},
{
id: 'rerank_id',
label: t('rerankModel'),
value: formData.rerank_id,
options: allOptions[
LlmModelType.Rerank
] as SelectWithSearchFlagOptionType[],
tooltip: t('rerankModelTip'),
},
{
id: 'tts_id',
label: t('ttsModel'),
value: formData.tts_id,
options: allOptions[
LlmModelType.TTS
] as SelectWithSearchFlagOptionType[],
tooltip: t('ttsModelTip'),
},
];
}, [formData, modelOptions, t, allOptions]);
const Items = ({
label,
value,
options,
tooltip,
id,
isRequired,
}: {
id: string;
label: string;
value: string;
options: SelectWithSearchFlagOptionType[];
tooltip?: string;
isRequired?: boolean;
}) => {
return (
<div className="flex gap-3">
<label className="block text-sm font-medium text-text-primary mb-1 w-1/4">
{isRequired && <span className="text-red-500">*</span>}
{label}
{tooltip && (
<Tooltip>
<TooltipContent>{tooltip}</TooltipContent>
<TooltipTrigger>
<CircleQuestionMark
size={12}
className="ml-1 text-text-disabled text-xs"
/>
</TooltipTrigger>
</Tooltip>
)}
</label>
<SelectWithSearch
triggerClassName="w-3/4"
value={value}
options={options}
onChange={(value) => handleFieldChange(id, value)}
placeholder={t('common:selectPlaceholder')}
/>
</div>
);
};
return (
<div className="rounded-lg w-full">
<div className="flex flex-col py-4">
<div className="text-2xl font-semibold">{t('systemModelSettings')}</div>
<div className="text-sm text-text-secondary">
{t('systemModelDescription')}
</div>
</div>
<div className="px-7 py-6 space-y-6 max-h-[70vh] overflow-y-auto border rounded-lg">
{llmList.map((item) => (
<Items key={item.id} {...item} />
))}
</div>
{/* <div className="border-t px-6 py-4 flex justify-end">
<Button
onClick={hideModal}
disabled={loading}
className="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
>
{t('common:cancel')}
</Button>
</div> */}
</div>
);
};
export default SystemSetting;

View File

@ -0,0 +1,157 @@
// src/components/AvailableModels.tsx
import { LlmIcon } from '@/components/svg-icon';
import { Button } from '@/components/ui/button';
import { useTranslate } from '@/hooks/common-hooks';
import { useSelectLlmList } from '@/hooks/llm-hooks';
import { Plus, Search } from 'lucide-react';
import { FC, useMemo, useState } from 'react';
type TagType =
| 'LLM'
| 'TEXT EMBEDDING'
| 'TEXT RE-RANK'
| 'TTS'
| 'SPEECH2TEXT'
| 'IMAGE2TEXT'
| 'MODERATION';
const sortTags = (tags: string) => {
const orderMap: Record<TagType, number> = {
LLM: 1,
'TEXT EMBEDDING': 2,
'TEXT RE-RANK': 3,
TTS: 4,
SPEECH2TEXT: 5,
IMAGE2TEXT: 6,
MODERATION: 7,
};
return tags
.split(',')
.map((tag) => tag.trim())
.sort(
(a, b) =>
(orderMap[a as TagType] || 999) - (orderMap[b as TagType] || 999),
);
};
export const AvailableModels: FC<{
handleAddModel: (factory: string) => void;
}> = ({ handleAddModel }) => {
const { t } = useTranslate('setting');
const { factoryList } = useSelectLlmList();
const [searchTerm, setSearchTerm] = useState('');
const [selectedTag, setSelectedTag] = useState<string | null>(null);
// 过滤模型列表
const filteredModels = useMemo(() => {
return factoryList.filter((model) => {
const matchesSearch = model.name
.toLowerCase()
.includes(searchTerm.toLowerCase());
const matchesTag =
selectedTag === null ||
model.tags.split(',').some((tag) => tag.trim() === selectedTag);
return matchesSearch && matchesTag;
});
}, [factoryList, searchTerm, selectedTag]);
// 获取所有唯一的标签
const allTags = useMemo(() => {
const tagsSet = new Set<string>();
factoryList.forEach((model) => {
model.tags.split(',').forEach((tag) => tagsSet.add(tag.trim()));
});
return Array.from(tagsSet).sort();
}, [factoryList]);
const handleTagClick = (tag: string) => {
setSelectedTag(selectedTag === tag ? null : tag);
};
return (
<div className=" text-text-primary h-full p-4">
<div className="text-text-primary text-base mb-4">
{t('availableModels')}
</div>
{/* Search Bar */}
<div className="mb-6">
<div className="relative">
<input
type="text"
placeholder={t('search')}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full px-4 py-3 pl-10 bg-bg-input border border-border-default rounded-lg focus:outline-none focus:ring-1 focus:ring-border-button transition-colors"
/>
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-text-secondary" />
</div>
</div>
{/* Tags Filter */}
<div className="flex flex-wrap gap-2 mb-6">
<Button
variant={'secondary'}
onClick={() => setSelectedTag(null)}
className={`px-1 py-1 text-xs rounded-md bg-bg-card bg-bg-card h-5 transition-colors ${
selectedTag === null
? ' text-text-primary border border-text-primary'
: 'text-text-secondary bg-bg-input border-none'
}`}
>
All
</Button>
{allTags.map((tag) => (
<Button
variant={'secondary'}
key={tag}
onClick={() => handleTagClick(tag)}
className={`px-1 py-1 text-xs rounded-md bg-bg-card h-5 transition-colors ${
selectedTag === tag
? ' text-text-primary border border-text-primary'
: 'text-text-secondary border-none'
}`}
>
{tag}
</Button>
))}
</div>
{/* Models List */}
<div className="flex flex-col gap-4 overflow-auto h-[calc(100vh-300px)] scrollbar-auto">
{filteredModels.map((model) => (
<div
key={model.name}
className=" border border-border-default rounded-lg p-3 hover:bg-bg-input transition-colors"
>
<div className="flex items-center space-x-3 mb-3">
<LlmIcon name={model.name} imgClass="h-8 w-auto" />
<div className="flex-1">
<h3 className="font-medium truncate">{model.name}</h3>
</div>
<Button
className=" px-2 flex items-center gap-0 text-xs h-6 rounded-md transition-colors"
onClick={() => handleAddModel(model.name)}
>
<Plus size={12} />
{t('addTheModel')}
</Button>
</div>
<div className="flex flex-wrap gap-1 mb-3">
{sortTags(model.tags).map((tag, index) => (
<span
key={index}
className="px-1 flex items-center h-5 text-xs bg-bg-card text-text-secondary rounded-md"
>
{tag}
</span>
))}
</div>
</div>
))}
</div>
</div>
);
};

View File

@ -0,0 +1,27 @@
import { LlmItem, useSelectLlmList } from '@/hooks/llm-hooks';
import { ModelProviderCard } from './modal-card';
export const UsedModel = ({
handleAddModel,
handleEditModel,
}: {
handleAddModel: (factory: string) => void;
handleEditModel: (model: any, factory: LlmItem) => void;
}) => {
const { factoryList, myLlmList: llmList, loading } = useSelectLlmList();
return (
<div className="flex flex-col w-full">
<div className="text-text-primary text-2xl mb-4 mt-4">Added models</div>
{llmList.map((llm) => {
return (
<ModelProviderCard
key={llm.name}
item={llm}
clickApiKey={handleAddModel}
handleEditModel={handleEditModel}
/>
);
})}
</div>
);
};

View File

@ -3,8 +3,6 @@
.factoryOperationWrapper {
text-align: right;
}
.modelItem {
}
.llmList {
padding-top: 10px;
}

View File

@ -1,47 +1,16 @@
import { ReactComponent as MoreModelIcon } from '@/assets/svg/more-model.svg';
import { LlmIcon } from '@/components/svg-icon';
import { useTheme } from '@/components/theme-provider';
import { LLMFactory } from '@/constants/llm';
import { useSetModalState, useTranslate } from '@/hooks/common-hooks';
import {
LlmItem,
useFetchMyLlmListDetailed,
useSelectLlmList,
} from '@/hooks/llm-hooks';
import { getRealModelName } from '@/utils/llm-util';
import {
CloseCircleOutlined,
EditOutlined,
SettingOutlined,
} from '@ant-design/icons';
import {
Button,
Card,
Col,
Collapse,
CollapseProps,
Divider,
Flex,
List,
Row,
Space,
Spin,
Tag,
Tooltip,
Typography,
} from 'antd';
import { CircleHelp } from 'lucide-react';
import { LlmItem, useFetchMyLlmListDetailed } from '@/hooks/llm-hooks';
import { useCallback, useMemo } from 'react';
import SettingTitle from '../components/setting-title';
import { isLocalLlmFactory } from '../utils';
import ApiKeyModal from './api-key-modal';
import AzureOpenAIModal from './azure-openai-modal';
import BedrockModal from './bedrock-modal';
import SystemSetting from './components/system-setting';
import { AvailableModels } from './components/un-add-model';
import { UsedModel } from './components/used-model';
import FishAudioModal from './fish-audio-modal';
import GoogleModal from './google-modal';
import {
useHandleDeleteFactory,
useHandleDeleteLlm,
useSubmitApiKey,
useSubmitAzure,
useSubmitBedrock,
@ -56,164 +25,15 @@ import {
useSubmityiyan,
} from './hooks';
import HunyuanModal from './hunyuan-modal';
import styles from './index.less';
import TencentCloudModal from './next-tencent-modal';
import OllamaModal from './ollama-modal';
import SparkModal from './spark-modal';
import SystemModelSettingModal from './system-model-setting-modal';
import VolcEngineModal from './volcengine-modal';
import YiyanModal from './yiyan-modal';
const { Text } = Typography;
interface IModelCardProps {
item: LlmItem;
clickApiKey: (llmFactory: string) => void;
handleEditModel: (model: any, factory: LlmItem) => void;
}
type TagType =
| 'LLM'
| 'TEXT EMBEDDING'
| 'TEXT RE-RANK'
| 'TTS'
| 'SPEECH2TEXT'
| 'IMAGE2TEXT'
| 'MODERATION';
const sortTags = (tags: string) => {
const orderMap: Record<TagType, number> = {
LLM: 1,
'TEXT EMBEDDING': 2,
'TEXT RE-RANK': 3,
TTS: 4,
SPEECH2TEXT: 5,
IMAGE2TEXT: 6,
MODERATION: 7,
};
return tags
.split(',')
.map((tag) => tag.trim())
.sort(
(a, b) =>
(orderMap[a as TagType] || 999) - (orderMap[b as TagType] || 999),
);
};
const ModelCard = ({ item, clickApiKey, handleEditModel }: IModelCardProps) => {
const { visible, switchVisible } = useSetModalState();
const { t } = useTranslate('setting');
const { theme } = useTheme();
const { handleDeleteLlm } = useHandleDeleteLlm(item.name);
const { handleDeleteFactory } = useHandleDeleteFactory(item.name);
const handleApiKeyClick = () => {
clickApiKey(item.name);
};
const handleShowMoreClick = () => {
switchVisible();
};
return (
<List.Item>
<Card
className={theme === 'dark' ? styles.addedCardDark : styles.addedCard}
>
<Row align={'middle'}>
<Col span={12}>
<Flex gap={'middle'} align="center">
<LlmIcon name={item.name} />
<Flex vertical gap={'small'}>
<b>{item.name}</b>
<Flex wrap="wrap">
{sortTags(item.tags).map((tag, index) => (
<Tag
key={index}
style={{
fontSize: '12px',
margin: '1px',
paddingInline: '4px',
}}
>
{tag}
</Tag>
))}
</Flex>
</Flex>
</Flex>
</Col>
<Col span={12} className={styles.factoryOperationWrapper}>
<Space size={'middle'}>
<Button onClick={handleApiKeyClick}>
<Flex align="center" gap={4}>
{isLocalLlmFactory(item.name) ||
item.name === LLMFactory.VolcEngine ||
item.name === LLMFactory.TencentHunYuan ||
item.name === LLMFactory.XunFeiSpark ||
item.name === LLMFactory.BaiduYiYan ||
item.name === LLMFactory.FishAudio ||
item.name === LLMFactory.TencentCloud ||
item.name === LLMFactory.GoogleCloud ||
item.name === LLMFactory.AzureOpenAI
? t('addTheModel')
: 'API-Key'}
<SettingOutlined />
</Flex>
</Button>
<Button onClick={handleShowMoreClick}>
<Flex align="center" gap={4}>
{visible ? t('hideModels') : t('showMoreModels')}
<MoreModelIcon />
</Flex>
</Button>
<Button type={'text'} onClick={handleDeleteFactory}>
<Flex align="center">
<CloseCircleOutlined style={{ color: '#D92D20' }} />
</Flex>
</Button>
</Space>
</Col>
</Row>
{visible && (
<List
size="small"
dataSource={item.llm}
className={styles.llmList}
renderItem={(model) => (
<List.Item>
<Space>
{getRealModelName(model.name)}
<Tag color="#b8b8b8">{model.type}</Tag>
{isLocalLlmFactory(item.name) && (
<Tooltip title={t('edit', { keyPrefix: 'common' })}>
<Button
type={'text'}
onClick={() => handleEditModel(model, item)}
>
<EditOutlined style={{ color: '#1890ff' }} />
</Button>
</Tooltip>
)}
<Tooltip title={t('delete', { keyPrefix: 'common' })}>
<Button type={'text'} onClick={handleDeleteLlm(model.name)}>
<CloseCircleOutlined style={{ color: '#D92D20' }} />
</Button>
</Tooltip>
</Space>
</List.Item>
)}
/>
)}
</Card>
</List.Item>
);
};
const UserSettingModel = () => {
const { factoryList, myLlmList: llmList, loading } = useSelectLlmList();
const ModelProviders = () => {
const { saveSystemModelSettingLoading, onSystemSettingSavingOk } =
useSubmitSystemModelSetting();
const { data: detailedLlmList } = useFetchMyLlmListDetailed();
const { theme } = useTheme();
const {
saveApiKeyLoading,
initialApiKey,
@ -224,14 +44,6 @@ const UserSettingModel = () => {
hideApiKeyModal,
showApiKeyModal,
} = useSubmitApiKey();
const {
saveSystemModelSettingLoading,
onSystemSettingSavingOk,
systemSettingVisible,
hideSystemSettingModal,
showSystemSettingModal,
} = useSubmitSystemModelSetting();
const { t } = useTranslate('setting');
const {
llmAddingVisible,
hideLlmAddingModal,
@ -342,6 +154,7 @@ const UserSettingModel = () => {
const handleAddModel = useCallback(
(llmFactory: string) => {
console.log('handleAddModel', llmFactory);
if (isLocalLlmFactory(llmFactory)) {
showLlmAddingModal(llmFactory);
} else if (llmFactory in ModalMap) {
@ -378,116 +191,21 @@ const UserSettingModel = () => {
},
[showApiKeyModal, showLlmAddingModal, ModalMap, detailedLlmList],
);
const items: CollapseProps['items'] = [
{
key: '1',
label: t('addedModels'),
children: (
<List
grid={{ gutter: 16, column: 1 }}
dataSource={llmList}
renderItem={(item) => (
<ModelCard
item={item}
clickApiKey={handleAddModel}
handleEditModel={handleEditModel}
></ModelCard>
)}
/>
),
},
{
key: '2',
label: (
<div className="flex items-center gap-2">
{t('modelsToBeAdded')}
<Tooltip title={t('modelsToBeAddedTooltip')}>
<CircleHelp className="size-4" />
</Tooltip>
</div>
),
children: (
<List
grid={{
gutter: {
xs: 8,
sm: 10,
md: 12,
lg: 16,
xl: 20,
xxl: 24,
},
xs: 1,
sm: 1,
md: 2,
lg: 3,
xl: 4,
xxl: 8,
}}
dataSource={factoryList}
renderItem={(item) => (
<List.Item>
<Card
className={
theme === 'dark'
? styles.toBeAddedCardDark
: styles.toBeAddedCard
}
>
<Flex vertical gap={'middle'}>
<LlmIcon name={item.name} imgClass="h-12 w-auto" />
<Flex vertical gap={'middle'}>
<b>
<Text ellipsis={{ tooltip: item.name }}>{item.name}</Text>
</b>
<Flex wrap="wrap" style={{ minHeight: '50px' }}>
{sortTags(item.tags).map((tag, index) => (
<Tag
key={index}
style={{
fontSize: '8px',
margin: '1px',
paddingInline: '4px',
height: '22px',
}}
>
{tag}
</Tag>
))}
</Flex>
</Flex>
</Flex>
<Divider className={styles.modelDivider}></Divider>
<Button
type="link"
onClick={() => handleAddModel(item.name)}
className={styles.addButton}
>
{t('addTheModel')}
</Button>
</Card>
</List.Item>
)}
/>
),
},
];
return (
<section id="xx" className="w-full space-y-6">
<Spin spinning={loading}>
<section className={styles.modelContainer}>
<SettingTitle
title={t('model')}
description={t('modelDescription')}
showRightButton
clickButton={showSystemSettingModal}
></SettingTitle>
<Divider></Divider>
<Collapse defaultActiveKey={['1', '2']} ghost items={items} />
</section>
</Spin>
<div className="flex w-full">
<section className="flex flex-col gap-4 w-3/5 px-5 border-r border-border-button overflow-auto scrollbar-auto">
<SystemSetting
onOk={onSystemSettingSavingOk}
loading={saveSystemModelSettingLoading}
/>
<UsedModel
handleAddModel={handleAddModel}
handleEditModel={handleEditModel}
/>
</section>
<section className="flex flex-col w-2/5 overflow-auto scrollbar-auto">
<AvailableModels handleAddModel={handleAddModel} />
</section>
<ApiKeyModal
visible={apiKeyVisible}
hideModal={hideApiKeyModal}
@ -497,14 +215,6 @@ const UserSettingModel = () => {
onOk={onApiKeySavingOk}
llmFactory={llmFactory}
></ApiKeyModal>
{systemSettingVisible && (
<SystemModelSettingModal
visible={systemSettingVisible}
onOk={onSystemSettingSavingOk}
hideModal={hideSystemSettingModal}
loading={saveSystemModelSettingLoading}
></SystemModelSettingModal>
)}
<OllamaModal
visible={llmAddingVisible}
hideModal={hideLlmAddingModal}
@ -577,8 +287,7 @@ const UserSettingModel = () => {
loading={AzureAddingLoading}
llmFactory={LLMFactory.AzureOpenAI}
></AzureOpenAIModal>
</section>
</div>
);
};
export default UserSettingModel;
export default ModelProviders;

View File

@ -0,0 +1,584 @@
import { ReactComponent as MoreModelIcon } from '@/assets/svg/more-model.svg';
import { LlmIcon } from '@/components/svg-icon';
import { useTheme } from '@/components/theme-provider';
import { LLMFactory } from '@/constants/llm';
import { useSetModalState, useTranslate } from '@/hooks/common-hooks';
import {
LlmItem,
useFetchMyLlmListDetailed,
useSelectLlmList,
} from '@/hooks/llm-hooks';
import { getRealModelName } from '@/utils/llm-util';
import {
CloseCircleOutlined,
EditOutlined,
SettingOutlined,
} from '@ant-design/icons';
import {
Button,
Card,
Col,
Collapse,
CollapseProps,
Divider,
Flex,
List,
Row,
Space,
Spin,
Tag,
Tooltip,
Typography,
} from 'antd';
import { CircleHelp } from 'lucide-react';
import { useCallback, useMemo } from 'react';
import SettingTitle from '../components/setting-title';
import { isLocalLlmFactory } from '../utils';
import ApiKeyModal from './api-key-modal';
import AzureOpenAIModal from './azure-openai-modal';
import BedrockModal from './bedrock-modal';
import FishAudioModal from './fish-audio-modal';
import GoogleModal from './google-modal';
import {
useHandleDeleteFactory,
useHandleDeleteLlm,
useSubmitApiKey,
useSubmitAzure,
useSubmitBedrock,
useSubmitFishAudio,
useSubmitGoogle,
useSubmitHunyuan,
useSubmitOllama,
useSubmitSpark,
useSubmitSystemModelSetting,
useSubmitTencentCloud,
useSubmitVolcEngine,
useSubmityiyan,
} from './hooks';
import HunyuanModal from './hunyuan-modal';
import styles from './index.less';
import TencentCloudModal from './next-tencent-modal';
import OllamaModal from './ollama-modal';
import SparkModal from './spark-modal';
import SystemModelSettingModal from './system-model-setting-modal';
import VolcEngineModal from './volcengine-modal';
import YiyanModal from './yiyan-modal';
const { Text } = Typography;
interface IModelCardProps {
item: LlmItem;
clickApiKey: (llmFactory: string) => void;
handleEditModel: (model: any, factory: LlmItem) => void;
}
type TagType =
| 'LLM'
| 'TEXT EMBEDDING'
| 'TEXT RE-RANK'
| 'TTS'
| 'SPEECH2TEXT'
| 'IMAGE2TEXT'
| 'MODERATION';
const sortTags = (tags: string) => {
const orderMap: Record<TagType, number> = {
LLM: 1,
'TEXT EMBEDDING': 2,
'TEXT RE-RANK': 3,
TTS: 4,
SPEECH2TEXT: 5,
IMAGE2TEXT: 6,
MODERATION: 7,
};
return tags
.split(',')
.map((tag) => tag.trim())
.sort(
(a, b) =>
(orderMap[a as TagType] || 999) - (orderMap[b as TagType] || 999),
);
};
const ModelCard = ({ item, clickApiKey, handleEditModel }: IModelCardProps) => {
const { visible, switchVisible } = useSetModalState();
const { t } = useTranslate('setting');
const { theme } = useTheme();
const { handleDeleteLlm } = useHandleDeleteLlm(item.name);
const { handleDeleteFactory } = useHandleDeleteFactory(item.name);
const handleApiKeyClick = () => {
clickApiKey(item.name);
};
const handleShowMoreClick = () => {
switchVisible();
};
return (
<List.Item>
<Card
className={theme === 'dark' ? styles.addedCardDark : styles.addedCard}
>
<Row align={'middle'}>
<Col span={12}>
<Flex gap={'middle'} align="center">
<LlmIcon name={item.name} />
<Flex vertical gap={'small'}>
<b>{item.name}</b>
<Flex wrap="wrap">
{sortTags(item.tags).map((tag, index) => (
<Tag
key={index}
style={{
fontSize: '12px',
margin: '1px',
paddingInline: '4px',
}}
>
{tag}
</Tag>
))}
</Flex>
</Flex>
</Flex>
</Col>
<Col span={12} className={styles.factoryOperationWrapper}>
<Space size={'middle'}>
<Button onClick={handleApiKeyClick}>
<Flex align="center" gap={4}>
{isLocalLlmFactory(item.name) ||
item.name === LLMFactory.VolcEngine ||
item.name === LLMFactory.TencentHunYuan ||
item.name === LLMFactory.XunFeiSpark ||
item.name === LLMFactory.BaiduYiYan ||
item.name === LLMFactory.FishAudio ||
item.name === LLMFactory.TencentCloud ||
item.name === LLMFactory.GoogleCloud ||
item.name === LLMFactory.AzureOpenAI
? t('addTheModel')
: 'API-Key'}
<SettingOutlined />
</Flex>
</Button>
<Button onClick={handleShowMoreClick}>
<Flex align="center" gap={4}>
{visible ? t('hideModels') : t('showMoreModels')}
<MoreModelIcon />
</Flex>
</Button>
<Button type={'text'} onClick={handleDeleteFactory}>
<Flex align="center">
<CloseCircleOutlined style={{ color: '#D92D20' }} />
</Flex>
</Button>
</Space>
</Col>
</Row>
{visible && (
<List
size="small"
dataSource={item.llm}
className={styles.llmList}
renderItem={(model) => (
<List.Item>
<Space>
{getRealModelName(model.name)}
<Tag color="#b8b8b8">{model.type}</Tag>
{isLocalLlmFactory(item.name) && (
<Tooltip title={t('edit', { keyPrefix: 'common' })}>
<Button
type={'text'}
onClick={() => handleEditModel(model, item)}
>
<EditOutlined style={{ color: '#1890ff' }} />
</Button>
</Tooltip>
)}
<Tooltip title={t('delete', { keyPrefix: 'common' })}>
<Button type={'text'} onClick={handleDeleteLlm(model.name)}>
<CloseCircleOutlined style={{ color: '#D92D20' }} />
</Button>
</Tooltip>
</Space>
</List.Item>
)}
/>
)}
</Card>
</List.Item>
);
};
const UserSettingModel = () => {
const { factoryList, myLlmList: llmList, loading } = useSelectLlmList();
const { data: detailedLlmList } = useFetchMyLlmListDetailed();
const { theme } = useTheme();
const {
saveApiKeyLoading,
initialApiKey,
llmFactory,
editMode,
onApiKeySavingOk,
apiKeyVisible,
hideApiKeyModal,
showApiKeyModal,
} = useSubmitApiKey();
const {
saveSystemModelSettingLoading,
onSystemSettingSavingOk,
systemSettingVisible,
hideSystemSettingModal,
showSystemSettingModal,
} = useSubmitSystemModelSetting();
const { t } = useTranslate('setting');
const {
llmAddingVisible,
hideLlmAddingModal,
showLlmAddingModal,
onLlmAddingOk,
llmAddingLoading,
editMode: llmEditMode,
initialValues: llmInitialValues,
selectedLlmFactory,
} = useSubmitOllama();
const {
volcAddingVisible,
hideVolcAddingModal,
showVolcAddingModal,
onVolcAddingOk,
volcAddingLoading,
} = useSubmitVolcEngine();
const {
HunyuanAddingVisible,
hideHunyuanAddingModal,
showHunyuanAddingModal,
onHunyuanAddingOk,
HunyuanAddingLoading,
} = useSubmitHunyuan();
const {
GoogleAddingVisible,
hideGoogleAddingModal,
showGoogleAddingModal,
onGoogleAddingOk,
GoogleAddingLoading,
} = useSubmitGoogle();
const {
TencentCloudAddingVisible,
hideTencentCloudAddingModal,
showTencentCloudAddingModal,
onTencentCloudAddingOk,
TencentCloudAddingLoading,
} = useSubmitTencentCloud();
const {
SparkAddingVisible,
hideSparkAddingModal,
showSparkAddingModal,
onSparkAddingOk,
SparkAddingLoading,
} = useSubmitSpark();
const {
yiyanAddingVisible,
hideyiyanAddingModal,
showyiyanAddingModal,
onyiyanAddingOk,
yiyanAddingLoading,
} = useSubmityiyan();
const {
FishAudioAddingVisible,
hideFishAudioAddingModal,
showFishAudioAddingModal,
onFishAudioAddingOk,
FishAudioAddingLoading,
} = useSubmitFishAudio();
const {
bedrockAddingLoading,
onBedrockAddingOk,
bedrockAddingVisible,
hideBedrockAddingModal,
showBedrockAddingModal,
} = useSubmitBedrock();
const {
AzureAddingVisible,
hideAzureAddingModal,
showAzureAddingModal,
onAzureAddingOk,
AzureAddingLoading,
} = useSubmitAzure();
const ModalMap = useMemo(
() => ({
[LLMFactory.Bedrock]: showBedrockAddingModal,
[LLMFactory.VolcEngine]: showVolcAddingModal,
[LLMFactory.TencentHunYuan]: showHunyuanAddingModal,
[LLMFactory.XunFeiSpark]: showSparkAddingModal,
[LLMFactory.BaiduYiYan]: showyiyanAddingModal,
[LLMFactory.FishAudio]: showFishAudioAddingModal,
[LLMFactory.TencentCloud]: showTencentCloudAddingModal,
[LLMFactory.GoogleCloud]: showGoogleAddingModal,
[LLMFactory.AzureOpenAI]: showAzureAddingModal,
}),
[
showBedrockAddingModal,
showVolcAddingModal,
showHunyuanAddingModal,
showTencentCloudAddingModal,
showSparkAddingModal,
showyiyanAddingModal,
showFishAudioAddingModal,
showGoogleAddingModal,
showAzureAddingModal,
],
);
const handleAddModel = useCallback(
(llmFactory: string) => {
if (isLocalLlmFactory(llmFactory)) {
showLlmAddingModal(llmFactory);
} else if (llmFactory in ModalMap) {
ModalMap[llmFactory as keyof typeof ModalMap]();
} else {
showApiKeyModal({ llm_factory: llmFactory });
}
},
[showApiKeyModal, showLlmAddingModal, ModalMap],
);
const handleEditModel = useCallback(
(model: any, factory: LlmItem) => {
if (factory) {
const detailedFactory = detailedLlmList[factory.name];
const detailedModel = detailedFactory?.llm?.find(
(m: any) => m.name === model.name,
);
const editData = {
llm_factory: factory.name,
llm_name: model.name,
model_type: model.type,
};
if (isLocalLlmFactory(factory.name)) {
showLlmAddingModal(factory.name, true, editData, detailedModel);
} else if (factory.name in ModalMap) {
ModalMap[factory.name as keyof typeof ModalMap]();
} else {
showApiKeyModal(editData, true);
}
}
},
[showApiKeyModal, showLlmAddingModal, ModalMap, detailedLlmList],
);
const items: CollapseProps['items'] = [
{
key: '1',
label: t('addedModels'),
children: (
<List
grid={{ gutter: 16, column: 1 }}
dataSource={llmList}
renderItem={(item) => (
<ModelCard
item={item}
clickApiKey={handleAddModel}
handleEditModel={handleEditModel}
></ModelCard>
)}
/>
),
},
{
key: '2',
label: (
<div className="flex items-center gap-2">
{t('modelsToBeAdded')}
<Tooltip title={t('modelsToBeAddedTooltip')}>
<CircleHelp className="size-4" />
</Tooltip>
</div>
),
children: (
<List
grid={{
gutter: {
xs: 8,
sm: 10,
md: 12,
lg: 16,
xl: 20,
xxl: 24,
},
xs: 1,
sm: 1,
md: 2,
lg: 3,
xl: 4,
xxl: 8,
}}
dataSource={factoryList}
renderItem={(item) => (
<List.Item>
<Card
className={
theme === 'dark'
? styles.toBeAddedCardDark
: styles.toBeAddedCard
}
>
<Flex vertical gap={'middle'}>
<LlmIcon name={item.name} imgClass="h-12 w-auto" />
<Flex vertical gap={'middle'}>
<b>
<Text ellipsis={{ tooltip: item.name }}>{item.name}</Text>
</b>
<Flex wrap="wrap" style={{ minHeight: '50px' }}>
{sortTags(item.tags).map((tag, index) => (
<Tag
key={index}
style={{
fontSize: '8px',
margin: '1px',
paddingInline: '4px',
height: '22px',
}}
>
{tag}
</Tag>
))}
</Flex>
</Flex>
</Flex>
<Divider className={styles.modelDivider}></Divider>
<Button
type="link"
onClick={() => handleAddModel(item.name)}
className={styles.addButton}
>
{t('addTheModel')}
</Button>
</Card>
</List.Item>
)}
/>
),
},
];
return (
<section id="xx" className="w-full space-y-6">
<Spin spinning={loading}>
<section className={styles.modelContainer}>
<SettingTitle
title={t('model')}
description={t('modelDescription')}
showRightButton
clickButton={showSystemSettingModal}
></SettingTitle>
<Divider></Divider>
<Collapse defaultActiveKey={['1', '2']} ghost items={items} />
</section>
</Spin>
<ApiKeyModal
visible={apiKeyVisible}
hideModal={hideApiKeyModal}
loading={saveApiKeyLoading}
initialValue={initialApiKey}
editMode={editMode}
onOk={onApiKeySavingOk}
llmFactory={llmFactory}
></ApiKeyModal>
{systemSettingVisible && (
<SystemModelSettingModal
visible={systemSettingVisible}
onOk={onSystemSettingSavingOk}
hideModal={hideSystemSettingModal}
loading={saveSystemModelSettingLoading}
></SystemModelSettingModal>
)}
<OllamaModal
visible={llmAddingVisible}
hideModal={hideLlmAddingModal}
onOk={onLlmAddingOk}
loading={llmAddingLoading}
editMode={llmEditMode}
initialValues={llmInitialValues}
llmFactory={selectedLlmFactory}
></OllamaModal>
<VolcEngineModal
visible={volcAddingVisible}
hideModal={hideVolcAddingModal}
onOk={onVolcAddingOk}
loading={volcAddingLoading}
llmFactory={LLMFactory.VolcEngine}
></VolcEngineModal>
<HunyuanModal
visible={HunyuanAddingVisible}
hideModal={hideHunyuanAddingModal}
onOk={onHunyuanAddingOk}
loading={HunyuanAddingLoading}
llmFactory={LLMFactory.TencentHunYuan}
></HunyuanModal>
<GoogleModal
visible={GoogleAddingVisible}
hideModal={hideGoogleAddingModal}
onOk={onGoogleAddingOk}
loading={GoogleAddingLoading}
llmFactory={LLMFactory.GoogleCloud}
></GoogleModal>
<TencentCloudModal
visible={TencentCloudAddingVisible}
hideModal={hideTencentCloudAddingModal}
onOk={onTencentCloudAddingOk}
loading={TencentCloudAddingLoading}
llmFactory={LLMFactory.TencentCloud}
></TencentCloudModal>
<SparkModal
visible={SparkAddingVisible}
hideModal={hideSparkAddingModal}
onOk={onSparkAddingOk}
loading={SparkAddingLoading}
llmFactory={LLMFactory.XunFeiSpark}
></SparkModal>
<YiyanModal
visible={yiyanAddingVisible}
hideModal={hideyiyanAddingModal}
onOk={onyiyanAddingOk}
loading={yiyanAddingLoading}
llmFactory={LLMFactory.BaiduYiYan}
></YiyanModal>
<FishAudioModal
visible={FishAudioAddingVisible}
hideModal={hideFishAudioAddingModal}
onOk={onFishAudioAddingOk}
loading={FishAudioAddingLoading}
llmFactory={LLMFactory.FishAudio}
></FishAudioModal>
<BedrockModal
visible={bedrockAddingVisible}
hideModal={hideBedrockAddingModal}
onOk={onBedrockAddingOk}
loading={bedrockAddingLoading}
llmFactory={LLMFactory.Bedrock}
></BedrockModal>
<AzureOpenAIModal
visible={AzureAddingVisible}
hideModal={hideAzureAddingModal}
onOk={onAzureAddingOk}
loading={AzureAddingLoading}
llmFactory={LLMFactory.AzureOpenAI}
></AzureOpenAIModal>
</section>
);
};
export default UserSettingModel;