feat(models): enhance model management with improved dialogs and state handling

This commit is contained in:
2025-10-22 15:27:31 +08:00
parent 497ebfba9f
commit 9137ae3063
6 changed files with 197 additions and 155 deletions

View File

@@ -11,7 +11,7 @@ import type { LLMFactory } from "@/constants/llm";
* 个人中心设置 * 个人中心设置
*/ */
export function useProfileSetting() { export function useProfileSetting() {
const {fetchUserInfo, userInfo} = useUserData(); const { fetchUserInfo, userInfo } = useUserData();
useEffect(() => { useEffect(() => {
fetchUserInfo(); fetchUserInfo();
@@ -53,36 +53,42 @@ export function useLlmModelSetting() {
const [llmFactory, setLlmFactory] = useState<IFactory[]>([]); const [llmFactory, setLlmFactory] = useState<IFactory[]>([]);
const [myLlm, setMyLlm] = useState<Record<LLMFactory, IMyLlmModel>>(); const [myLlm, setMyLlm] = useState<Record<LLMFactory, IMyLlmModel>>();
const fetchLlmFactory = async () => {
try {
const res = await userService.llm_factories_list();
const arr = res.data.data || [];
setLlmFactory(arr);
} catch (error) {
logger.error('获取模型工厂失败:', error);
throw error;
}
}
const fetchMyLlm = async () => {
try {
const res = await userService.my_llm();
const llm_dic = res.data.data || {};
setMyLlm(llm_dic);
} catch (error) {
logger.error('获取我的模型失败:', error);
throw error;
}
}
useEffect(() => { useEffect(() => {
const fetchLlmFactory = async () => {
try {
const res = await userService.llm_factories_list();
const arr = res.data.data || [];
setLlmFactory(arr);
} catch (error) {
logger.error('获取模型工厂失败:', error);
throw error;
}
}
const fetchMyLlm = async () => {
try {
const res = await userService.my_llm();
const llm_dic = res.data.data || {};
setMyLlm(llm_dic);
} catch (error) {
logger.error('获取我的模型失败:', error);
throw error;
}
}
fetchLlmFactory(); fetchLlmFactory();
fetchMyLlm(); fetchMyLlm();
}, []); // 空依赖数组,只在组件挂载时执行一次 }, []);
const refreshLlmModel = async () => {
await fetchMyLlm();
// await fetchLlmFactory();
logger.info('刷新我的模型成功');
}
return { return {
llmFactory, llmFactory,
myLlm, myLlm,
refreshLlmModel,
} }
} }

View File

@@ -1,3 +1,11 @@
export interface ISetApiKeyRequestBody {
llm_factory: string;
api_key: string;
llm_name?: string;
model_type?: string;
base_url?: string;
}
export interface IAddLlmRequestBody { export interface IAddLlmRequestBody {
llm_factory: string; // Ollama llm_factory: string; // Ollama
llm_name: string; llm_name: string;

View File

@@ -25,6 +25,7 @@ import { IconMap, type LLMFactory } from '@/constants/llm';
import type { ITenantInfo } from '@/interfaces/database/knowledge'; import type { ITenantInfo } from '@/interfaces/database/knowledge';
import type { LlmModelType } from '@/constants/knowledge'; import type { LlmModelType } from '@/constants/knowledge';
import type { IMyLlmModel, IThirdOAIModel } from '@/interfaces/database/llm'; import type { IMyLlmModel, IThirdOAIModel } from '@/interfaces/database/llm';
import logger from '@/utils/logger';
// 基础对话框状态 // 基础对话框状态
interface BaseDialogState { interface BaseDialogState {
@@ -164,6 +165,8 @@ export const ApiKeyDialog: React.FC<ApiKeyDialogProps> = ({
}); });
const [showApiKey, setShowApiKey] = React.useState(false); const [showApiKey, setShowApiKey] = React.useState(false);
logger.info('ApiKeyDialog 初始化:', { open, editMode, factoryName, initialData });
useEffect(() => { useEffect(() => {
if (open && initialData) { if (open && initialData) {
reset(initialData); reset(initialData);
@@ -664,6 +667,7 @@ export const SystemModelDialog: React.FC<SystemModelDialogProps> = ({
onSubmit, onSubmit,
loading, loading,
initialData, initialData,
editMode = false,
allModelOptions allModelOptions
}) => { }) => {
const { control, handleSubmit, reset, formState: { errors } } = useForm<ITenantInfo>({ const { control, handleSubmit, reset, formState: { errors } } = useForm<ITenantInfo>({
@@ -676,7 +680,6 @@ export const SystemModelDialog: React.FC<SystemModelDialogProps> = ({
}; };
// all model options 包含了全部的 options // all model options 包含了全部的 options
const llmOptions = useMemo(() => allModelOptions?.llmOptions || [], [allModelOptions]); const llmOptions = useMemo(() => allModelOptions?.llmOptions || [], [allModelOptions]);
const embdOptions = useMemo(() => allModelOptions?.embeddingOptions || [], [allModelOptions]); const embdOptions = useMemo(() => allModelOptions?.embeddingOptions || [], [allModelOptions]);
const img2txtOptions = useMemo(() => allModelOptions?.image2textOptions || [], [allModelOptions]); const img2txtOptions = useMemo(() => allModelOptions?.image2textOptions || [], [allModelOptions]);

View File

@@ -1,4 +1,4 @@
import { useState, useCallback, useMemo } from 'react'; import { useState, useCallback, useMemo, useEffect } from 'react';
import { useMessage } from '@/hooks/useSnackbar'; import { useMessage } from '@/hooks/useSnackbar';
import userService from '@/services/user_service'; import userService from '@/services/user_service';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
@@ -11,6 +11,8 @@ import type {
import type { ITenantInfo } from '@/interfaces/database/knowledge'; import type { ITenantInfo } from '@/interfaces/database/knowledge';
import { useLlmList } from '@/hooks/llm-hooks'; import { useLlmList } from '@/hooks/llm-hooks';
import type { LlmModelType } from '@/constants/knowledge'; import type { LlmModelType } from '@/constants/knowledge';
import { useUserData } from '@/hooks/useUserData';
import type { ISetApiKeyRequestBody } from '@/interfaces/request/llm';
// 对话框状态管理 hook // 对话框状态管理 hook
export const useDialogState = () => { export const useDialogState = () => {
@@ -20,7 +22,9 @@ export const useDialogState = () => {
const [initialData, setInitialData] = useState<any>(null); const [initialData, setInitialData] = useState<any>(null);
const openDialog = useCallback((data?: any, isEdit = false) => { const openDialog = useCallback((data?: any, isEdit = false) => {
setInitialData(data); if (data != null) {
setInitialData(data);
}
setEditMode(isEdit); setEditMode(isEdit);
setOpen(true); setOpen(true);
}, []); }, []);
@@ -55,19 +59,26 @@ export const useApiKeyDialog = () => {
const submitApiKey = useCallback(async (data: ApiKeyFormData) => { const submitApiKey = useCallback(async (data: ApiKeyFormData) => {
dialogState.setLoading(true); dialogState.setLoading(true);
logger.info('提交 API Key:', data);
try { try {
await userService.set_api_key({ const params: ISetApiKeyRequestBody = {
factory_name: factoryName, llm_factory: factoryName,
model_name: '', // 根据实际需求调整 api_key: data.api_key,
// api_key: data.api_key, };
...data
}); if (data.base_url && data.base_url.trim() !== '') {
params.base_url = data.base_url;
}
if (data.group_id && data.group_id.trim() !== '') {
// params.group_id = data.group_id;
}
await userService.set_api_key(params);
showMessage.success('API Key 配置成功'); showMessage.success('API Key 配置成功');
dialogState.closeDialog(); dialogState.closeDialog();
} catch (error) { } catch (error) {
logger.error('API Key 配置失败:', error); logger.error('API Key 配置失败:', error);
showMessage.error('API Key 配置失败');
throw error;
} finally { } finally {
dialogState.setLoading(false); dialogState.setLoading(false);
} }
@@ -91,8 +102,8 @@ export const useAzureOpenAIDialog = () => {
try { try {
// 调用 Azure OpenAI 特定的 API // 调用 Azure OpenAI 特定的 API
await userService.set_api_key({ await userService.set_api_key({
factory_name: 'AzureOpenAI', llm_factory: 'AzureOpenAI',
model_name: data.deployment_name, llm_name: data.deployment_name,
api_key: data.api_key, api_key: data.api_key,
// azure_endpoint: data.azure_endpoint, // azure_endpoint: data.azure_endpoint,
// api_version: data.api_version, // api_version: data.api_version,
@@ -124,8 +135,8 @@ export const useBedrockDialog = () => {
try { try {
// 调用 Bedrock 特定的 API // 调用 Bedrock 特定的 API
await userService.set_api_key({ await userService.set_api_key({
factory_name: 'Bedrock', llm_factory: 'Bedrock',
model_name: '', llm_name: '',
api_key: '', // Bedrock 使用 access key api_key: '', // Bedrock 使用 access key
// access_key_id: data.access_key_id, // access_key_id: data.access_key_id,
// secret_access_key: data.secret_access_key, // secret_access_key: data.secret_access_key,
@@ -158,8 +169,8 @@ export const useOllamaDialog = () => {
try { try {
// 调用添加 LLM 的 API // 调用添加 LLM 的 API
await userService.add_llm({ await userService.add_llm({
factory_name: 'Ollama', llm_factory: 'Ollama',
model_name: data.model_name, llm_name: data.model_name,
// base_url: data.base_url, // base_url: data.base_url,
}); });
showMessage.success('Ollama 模型添加成功'); showMessage.success('Ollama 模型添加成功');
@@ -188,14 +199,12 @@ export const useDeleteOperations = () => {
setLoading(true); setLoading(true);
try { try {
await userService.delete_llm({ await userService.delete_llm({
factory_name: factoryName, llm_factory: factoryName,
model_name: modelName, llm_name: modelName,
}); });
showMessage.success('模型删除成功'); showMessage.success('模型删除成功');
} catch (error) { } catch (error) {
logger.error('模型删除失败:', error); logger.error('模型删除失败:', error);
showMessage.error('模型删除失败');
throw error;
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -205,13 +214,11 @@ export const useDeleteOperations = () => {
setLoading(true); setLoading(true);
try { try {
await userService.deleteFactory({ await userService.deleteFactory({
factory_name: factoryName, llm_factory: factoryName,
}); });
showMessage.success('模型工厂删除成功'); showMessage.success('模型工厂删除成功');
} catch (error) { } catch (error) {
logger.error('模型工厂删除失败:', error); logger.error('模型工厂删除失败:', error);
showMessage.error('模型工厂删除失败');
throw error;
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -231,6 +238,12 @@ export const useSystemModelSetting = () => {
const { data: llmList } = useLlmList(); const { data: llmList } = useLlmList();
const { tenantInfo, fetchTenantInfo } = useUserData();
useEffect(() => {
fetchTenantInfo();
}, []);
const getOptionsByModelType = useCallback((modelType: LlmModelType) => { const getOptionsByModelType = useCallback((modelType: LlmModelType) => {
return Object.entries(llmList) return Object.entries(llmList)
.filter(([, value]) => .filter(([, value]) =>
@@ -278,11 +291,16 @@ export const useSystemModelSetting = () => {
const submitSystemModelSetting = useCallback(async (data: Partial<ITenantInfo>) => { const submitSystemModelSetting = useCallback(async (data: Partial<ITenantInfo>) => {
dialogState.setLoading(true); dialogState.setLoading(true);
logger.debug('submitSystemModelSetting data:', data);
try { try {
delete data.role;
// 这里需要根据实际的 API 接口调整 // 这里需要根据实际的 API 接口调整
// await userService.setSystemDefaultModel(data); await userService.setTenantInfo({
...data,
});
showMessage.success('系统默认模型设置成功'); showMessage.success('系统默认模型设置成功');
dialogState.closeDialog(); dialogState.closeDialog();
fetchTenantInfo();
} catch (error) { } catch (error) {
logger.error('系统默认模型设置失败:', error); logger.error('系统默认模型设置失败:', error);
showMessage.error('系统默认模型设置失败'); showMessage.error('系统默认模型设置失败');
@@ -296,6 +314,7 @@ export const useSystemModelSetting = () => {
...dialogState, ...dialogState,
submitSystemModelSetting, submitSystemModelSetting,
allModelOptions, allModelOptions,
initialData: tenantInfo,
}; };
}; };

View File

@@ -25,61 +25,82 @@ import {
} from '@mui/icons-material'; } from '@mui/icons-material';
import { useLlmModelSetting } from '@/hooks/setting-hooks'; import { useLlmModelSetting } from '@/hooks/setting-hooks';
import { useModelDialogs } from './hooks/useModelDialogs'; import { useModelDialogs } from './hooks/useModelDialogs';
import AppSvgIcon, { LlmSvgIcon } from '@/components/AppSvgIcon';
import { LLM_FACTORY_LIST, IconMap, type LLMFactory } from '@/constants/llm';
import type { IFactory, IMyLlmModel, ILlmItem } from '@/interfaces/database/llm'; import type { IFactory, IMyLlmModel, ILlmItem } from '@/interfaces/database/llm';
import LLMFactoryCard, { MODEL_TYPE_COLORS } from './components/LLMFactoryCard'; import LLMFactoryCard, { MODEL_TYPE_COLORS } from './components/LLMFactoryCard';
import { ModelDialogs } from './components/ModelDialogs'; import { ModelDialogs } from './components/ModelDialogs';
import { useDialog } from '@/hooks/useDialog';
import logger from '@/utils/logger';
import { useMessage } from '@/hooks/useSnackbar';
function MyLlmGridItem({ model, onDelete }: { model: ILlmItem, onDelete: (model: ILlmItem) => void }) {
return (
<Grid size={{ xs: 6, sm: 4, md: 3 }} key={model.name}>
<Card variant="outlined" sx={{ p: 2 }}>
<Box display="flex" justifyContent="space-between" alignItems="flex-start" mb={1}>
<Typography variant="body2" fontWeight="bold">
{model.name}
</Typography>
<Box>
<IconButton
size="small"
color="error"
onClick={() => onDelete(model)}
>
<DeleteIcon fontSize="small" />
</IconButton>
</Box>
</Box>
<Chip
label={model.type}
size="small"
sx={{
backgroundColor: MODEL_TYPE_COLORS[model.type.toUpperCase()] || '#757575',
color: 'white',
mb: 1,
}}
/>
</Card>
</Grid>
);
}
// 主页面组件 // 主页面组件
function ModelsPage() { function ModelsPage() {
const { llmFactory, myLlm } = useLlmModelSetting(); const { llmFactory, myLlm, refreshLlmModel } = useLlmModelSetting();
const modelDialogs = useModelDialogs(); const modelDialogs = useModelDialogs();
// 处理配置模型工厂 // 处理配置模型工厂
const handleConfigureFactory = useCallback((factory: IFactory) => { const handleConfigureFactory = useCallback((factory: IFactory) => {
// modelDialogs.openDialog(factory.name); modelDialogs.apiKeyDialog.openApiKeyDialog(factory.name);
}, [modelDialogs]); }, [modelDialogs, refreshLlmModel]);
// 处理删除模型工厂 const dialog = useDialog();
const handleDeleteFactory = useCallback(async (factoryName: string) => {
try {
// await modelDialogs.deleteOperations.deleteFactory(factoryName);
// 刷新数据
window.location.reload();
} catch (error) {
console.error('删除工厂失败:', error);
}
}, []);
// 处理删除单个模型 // 处理删除单个模型
const handleDeleteModel = useCallback(async (factoryName: string, modelName: string) => { const handleDeleteModel = useCallback(async (factoryName: string, modelName: string) => {
try { dialog.confirm({
// await modelDialogs.deleteOperations.deleteLlm(factoryName, modelName); title: '确认删除',
// 刷新数据 content: `是否确认删除模型 ${modelName}`,
window.location.reload(); showCancel: true,
} catch (error) { onConfirm: async () => {
console.error('删除模型失败:', error); await modelDialogs.deleteOps.deleteLlm(factoryName, modelName);
} await refreshLlmModel();
}, []); },
});
}, [dialog, refreshLlmModel]);
// 处理编辑模型 // 处理删除模型工厂
const handleEditModel = useCallback((factory: IFactory, model: ILlmItem) => { const handleDeleteFactory = useCallback(async (factoryName: string) => {
// 设置编辑模式并打开对话框 dialog.confirm({
// modelDialogs.openDialog(factory.name, { title: '确认删除',
// model_name: model.name, content: `是否确认删除模型工厂 ${factoryName}`,
// api_base: model.api_base, showCancel: true,
// max_tokens: model.max_tokens, onConfirm: async () => {
// }); await modelDialogs.deleteOps.deleteFactory(factoryName);
}, [modelDialogs]); await refreshLlmModel();
},
// 根据工厂名称获取对应的模型列表 });
const getModelsForFactory = (factoryName: LLMFactory): ILlmItem[] => { }, [dialog, refreshLlmModel]);
if (!myLlm) return [];
const factoryGroup = myLlm[factoryName];
return factoryGroup?.llm || [];
};
if (!llmFactory || !myLlm) { if (!llmFactory || !myLlm) {
return ( return (
@@ -98,12 +119,12 @@ function ModelsPage() {
justifyContent: 'space-between', justifyContent: 'space-between',
}}> }}>
<Box> <Box>
<Typography variant="h4" gutterBottom> <Typography variant="h4" gutterBottom>
</Typography> </Typography>
<Typography variant="body1" color="text.secondary" gutterBottom> <Typography variant="body1" color="text.secondary" gutterBottom>
LLM LLM
</Typography> </Typography>
</Box> </Box>
{/* 设置默认模型 */} {/* 设置默认模型 */}
<Button variant="contained" color="primary" onClick={() => modelDialogs.systemDialog.openDialog()}> <Button variant="contained" color="primary" onClick={() => modelDialogs.systemDialog.openDialog()}>
@@ -129,68 +150,51 @@ function ModelsPage() {
<Grid size={12} key={factoryName}> <Grid size={12} key={factoryName}>
<Card variant="outlined"> <Card variant="outlined">
<CardContent> <CardContent>
<Typography variant="h6" gutterBottom> <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
{factoryName} <Box>
</Typography> {/* 模型工厂名称 */}
<Box display="flex" gap={1} mb={2}> <Typography variant="h6" gutterBottom>
{group.tags.split(',').map((tag) => ( {factoryName}
<Chip </Typography>
key={tag} {/* 模型标签 */}
label={tag.trim()} <Box display="flex" gap={1} mb={2}>
size="small" {group.tags.split(',').map((tag) => (
sx={{
backgroundColor: MODEL_TYPE_COLORS[tag.trim()] || '#757575',
color: 'white',
}}
/>
))}
</Box>
<Grid container spacing={2}>
{group.llm.map((model) => (
<Grid size={{ xs: 12, sm: 6, md: 4 }} key={model.name}>
<Card variant="outlined" sx={{ p: 2 }}>
<Box display="flex" justifyContent="space-between" alignItems="flex-start" mb={1}>
<Typography variant="body2" fontWeight="bold">
{model.name}
</Typography>
<Box>
<IconButton
size="small"
onClick={() => handleEditModel({ name: factoryName } as IFactory, model)}
>
<EditIcon fontSize="small" />
</IconButton>
<IconButton
size="small"
color="error"
onClick={() => handleDeleteModel(factoryName, model.name)}
>
<DeleteIcon fontSize="small" />
</IconButton>
</Box>
</Box>
<Chip <Chip
label={model.type} key={tag}
label={tag.trim()}
size="small" size="small"
sx={{ sx={{
backgroundColor: MODEL_TYPE_COLORS[model.type.toUpperCase()] || '#757575', backgroundColor: MODEL_TYPE_COLORS[tag.trim()] || '#757575',
color: 'white', color: 'white',
mb: 1,
}} }}
/> />
<Typography variant="caption" display="block" color="text.secondary"> ))}
Max Tokens: {model.max_tokens} </Box>
</Typography> </Box>
<Typography variant="caption" display="block" color="text.secondary"> {/* edit and delete factory button */}
Used: {model.used_token} <Box sx={{ display: 'flex', gap: 1 }}>
</Typography> <Button
{model.api_base && ( variant='contained' color='primary' startIcon={<EditIcon />}
<Typography variant="caption" display="block" color="text.secondary"> onClick={() => modelDialogs.apiKeyDialog.openApiKeyDialog(factoryName)}
Base URL: {model.api_base} >
</Typography>
)} </Button>
</Card> <Button
</Grid> variant='outlined' color='primary' startIcon={<DeleteIcon />}
onClick={() => handleDeleteFactory(factoryName)}
>
</Button>
</Box>
</Box>
{/* 模型列表 */}
<Grid container spacing={2}>
{group.llm.map((model) => (
<MyLlmGridItem
key={model.name}
model={model}
onDelete={() => handleDeleteModel(factoryName, model.name)}
/>
))} ))}
</Grid> </Grid>
</CardContent> </CardContent>
@@ -230,6 +234,7 @@ function ModelsPage() {
</Box> </Box>
{/* 模型配置对话框 */} {/* 模型配置对话框 */}
{/* @ts-ignore */}
<ModelDialogs {...modelDialogs} /> <ModelDialogs {...modelDialogs} />
</Box> </Box>
); );

View File

@@ -3,6 +3,7 @@ import request, { post } from '@/utils/request';
import type { ITenantInfo } from '@/interfaces/database/knowledge'; import type { ITenantInfo } from '@/interfaces/database/knowledge';
import type { IUserInfo, ITenant } from '@/interfaces/database/user-setting'; import type { IUserInfo, ITenant } from '@/interfaces/database/user-setting';
import type { LlmModelType } from '@/constants/knowledge'; import type { LlmModelType } from '@/constants/knowledge';
import type { IAddLlmRequestBody, IDeleteLlmRequestBody, ISetApiKeyRequestBody } from '@/interfaces/request/llm';
// 用户相关API服务 // 用户相关API服务
const userService = { const userService = {
@@ -53,7 +54,7 @@ const userService = {
}, },
// 设置租户信息 // 设置租户信息
setTenantInfo: (data: ITenantInfo) => { setTenantInfo: (data: Partial<Omit<ITenantInfo, 'role'>>) => {
return post(api.set_tenant_info, data); return post(api.set_tenant_info, data);
}, },
// 租户用户管理 // 租户用户管理
@@ -95,22 +96,22 @@ const userService = {
}, },
// add llm // add llm
add_llm: (data: { factory_name: string; model_name: string }) => { add_llm: (data: Partial<IAddLlmRequestBody>) => {
return request.post(api.add_llm, data); return request.post(api.add_llm, data);
}, },
// delete llm // delete llm
delete_llm: (data: { factory_name: string; model_name: string }) => { delete_llm: (data: IDeleteLlmRequestBody) => {
return request.post(api.delete_llm, data); return request.post(api.delete_llm, data);
}, },
// delete factory // delete factory
deleteFactory: (data: { factory_name: string }) => { deleteFactory: (data: IDeleteLlmRequestBody) => {
return request.post(api.deleteFactory, data); return request.post(api.deleteFactory, data);
}, },
// set api key // set api key
set_api_key: (data: { factory_name: string; model_name: string; api_key: string }) => { set_api_key: (data: ISetApiKeyRequestBody) => {
return request.post(api.set_api_key, data); return request.post(api.set_api_key, data);
}, },
}; };