feat(models): add models page and refactor profile form

This commit is contained in:
2025-11-14 16:48:42 +08:00
parent 97402674cd
commit ef8076d87f
10 changed files with 267 additions and 94 deletions

398
src/pages/models/models.tsx Normal file
View File

@@ -0,0 +1,398 @@
import React, { useState, useCallback, useMemo } from 'react';
import {
Box,
Typography,
Card,
CardContent,
Button,
Chip,
IconButton,
Collapse,
Grid,
Alert,
CircularProgress,
Tooltip,
Accordion,
AccordionSummary,
AccordionDetails,
Divider,
} from '@mui/material';
import {
ExpandMore as ExpandMoreIcon,
ExpandLess as ExpandLessIcon,
Edit as EditIcon,
Delete as DeleteIcon,
} from '@mui/icons-material';
import { useTranslation } from 'react-i18next';
import { useLlmModelSetting } from '@/hooks/setting-hooks';
import { useModelDialogs } from '../setting/hooks/useModelDialogs';
import type { IFactory, IMyLlmModel, ILlmItem } from '@/interfaces/database/llm';
import LLMFactoryCard, { MODEL_TYPE_COLORS } from '../setting/components/LLMFactoryCard';
import { ModelDialogs } from '../setting/components/ModelDialogs';
import { useDialog } from '@/hooks/useDialog';
import logger from '@/utils/logger';
import { LLM_FACTORY_LIST, LocalLlmFactories, type LLMFactory } from '@/constants/llm';
import { LlmSvgIcon } from '@/components/AppSvgIcon';
import { getFactoryIconName } from '@/utils/common';
interface MyLlmGridItemProps {
model: ILlmItem,
onDelete: (model: ILlmItem) => void,
onEditLlm?: (model: ILlmItem) => void,
}
function MyLlmGridItem({ model, onDelete, onEditLlm }: MyLlmGridItemProps) {
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 sx={{
display: 'flex',
gap: 1,
}}>
{
onEditLlm && (
<IconButton
size="small"
color="primary"
onClick={() => onEditLlm(model)}
>
<EditIcon fontSize="small" />
</IconButton>
)
}
<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() {
const { t } = useTranslation();
const { llmFactory, myLlm, refreshLlmModel } = useLlmModelSetting();
const modelDialogs = useModelDialogs(refreshLlmModel);
// 折叠状态管理 - 使用 Map 来管理每个工厂的折叠状态,默认所有工厂都是折叠的
const [collapsedFactories, setCollapsedFactories] = useState<Record<string, boolean>>({});
// 切换工厂折叠状态
const toggleFactoryCollapse = useCallback((factoryName: string) => {
setCollapsedFactories(prev => ({
...prev,
[factoryName]: !prev[factoryName]
}));
}, []);
const showLlmFactory = useMemo(() => {
const modelFactoryNames = Object.keys(myLlm || {});
const filterFactory = llmFactory?.filter(factory => !modelFactoryNames.includes(factory.name));
return filterFactory || [];
}, [llmFactory, myLlm]);
const showAddModel = (factoryName: string) => {
const configurationFactories: LLMFactory[] = [
LLM_FACTORY_LIST.AzureOpenAI,
LLM_FACTORY_LIST.Bedrock,
LLM_FACTORY_LIST.BaiduYiYan,
LLM_FACTORY_LIST.FishAudio,
LLM_FACTORY_LIST.GoogleCloud,
LLM_FACTORY_LIST.TencentCloud,
LLM_FACTORY_LIST.TencentHunYuan,
LLM_FACTORY_LIST.XunFeiSpark,
LLM_FACTORY_LIST.VolcEngine,
]
const fN = factoryName as LLMFactory;
if (!fN) {
return false;
}
return LocalLlmFactories.includes(fN) ||
configurationFactories.includes(fN);
}
// 处理配置模型工厂
const handleConfigureFactory = useCallback((factory: IFactory) => {
if (factory == null) {
return;
}
// llm 的配置很多,有很多种类型 首先是local llm 然后是配置项不一样的
// 然后有很多自定义的配置项,需要单独用 dialog 来配置
const factoryName = factory.name as LLMFactory;
const configurationFactories: LLMFactory[] = [
LLM_FACTORY_LIST.AzureOpenAI,
LLM_FACTORY_LIST.Bedrock,
LLM_FACTORY_LIST.BaiduYiYan,
LLM_FACTORY_LIST.FishAudio,
LLM_FACTORY_LIST.GoogleCloud,
LLM_FACTORY_LIST.TencentCloud,
LLM_FACTORY_LIST.TencentHunYuan,
LLM_FACTORY_LIST.XunFeiSpark,
LLM_FACTORY_LIST.VolcEngine,
]
if (LocalLlmFactories.includes(factoryName)) {
// local llm
modelDialogs.ollamaDialog.openDialog({
llm_factory: factory.name,
});
} else if (configurationFactories.includes(factoryName)) {
// custom configuration llm
modelDialogs.configurationDialog.openConfigurationDialog(factory.name);
} else {
// llm set api
modelDialogs.apiKeyDialog.openApiKeyDialog(factoryName);
}
logger.debug('handleConfigureFactory', factory);
}, [modelDialogs]);
const handleEditLlmFactory = useCallback((factoryName: string, llmmodel?: ILlmItem) => {
if (factoryName == null) {
return;
}
const factoryN = factoryName as LLMFactory;
const configurationFactories: LLMFactory[] = [
LLM_FACTORY_LIST.AzureOpenAI,
LLM_FACTORY_LIST.Bedrock,
LLM_FACTORY_LIST.BaiduYiYan,
LLM_FACTORY_LIST.FishAudio,
LLM_FACTORY_LIST.GoogleCloud,
LLM_FACTORY_LIST.TencentCloud,
LLM_FACTORY_LIST.TencentHunYuan,
LLM_FACTORY_LIST.XunFeiSpark,
LLM_FACTORY_LIST.VolcEngine,
]
if (LocalLlmFactories.includes(factoryN)) {
// local llm
modelDialogs.ollamaDialog.openDialog({
llm_factory: factoryN,
...llmmodel,
llm_name: llmmodel?.name || '',
model_type: llmmodel?.type || 'chat',
}, true);
} else if (configurationFactories.includes(factoryN)) {
// custom configuration llm
modelDialogs.configurationDialog.openConfigurationDialog(factoryN, {
...llmmodel,
llm_name: llmmodel?.name || '',
model_type: llmmodel?.type || 'chat',
});
} else {
// llm set api
modelDialogs.apiKeyDialog.openApiKeyDialog(factoryN, {}, true);
}
logger.debug('handleEditLlmFactory', factoryN);
}, []);
const dialog = useDialog();
// 处理删除单个模型
const handleDeleteModel = useCallback(async (factoryName: string, modelName: string) => {
dialog.confirm({
title: t('setting.confirmDelete'),
content: t('setting.confirmDeleteModel', { modelName }),
showCancel: true,
onConfirm: async () => {
await modelDialogs.deleteOps.deleteLlm(factoryName, modelName);
},
});
}, [dialog, modelDialogs.deleteOps, t]);
// 处理删除模型工厂
const handleDeleteFactory = useCallback(async (factoryName: string) => {
dialog.confirm({
title: t('setting.confirmDelete'),
content: t('setting.confirmDeleteFactory', { factoryName }),
showCancel: true,
onConfirm: async () => {
await modelDialogs.deleteOps.deleteFactory(factoryName);
},
});
}, [dialog, modelDialogs.deleteOps, t]);
if (!llmFactory || !myLlm) {
return (
<Box display="flex" justifyContent="center" alignItems="center" minHeight="400px">
<CircularProgress />
</Box>
);
}
return (
<Box sx={{ p: 3 }}>
<Box mb={4} sx={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
}}>
<Box>
<Typography variant="h4" gutterBottom>
{t('setting.modelSettings')}
</Typography>
<Typography variant="body1" color="text.secondary" gutterBottom>
{t('setting.modelSettingsDescription')}
</Typography>
</Box>
{/* 设置默认模型 */}
<Button variant="contained" color="primary" onClick={() => modelDialogs.systemDialog.openDialog()}>
{t('setting.setDefaultModel')}
</Button>
</Box>
{/* My LLM 部分 */}
<Box mb={4} mt={2}>
{!myLlm || Object.keys(myLlm).length === 0 ? (
<Alert severity="info">
{t('setting.noModelsConfigured')}
</Alert>
) : (
<Accordion defaultExpanded>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography variant="h5" gutterBottom>
{t('setting.myLlmModels')}
</Typography>
</AccordionSummary>
<AccordionDetails>
<Grid container spacing={2}>
{Object.entries(myLlm).map(([factoryName, group]) => (
<Grid size={12} key={factoryName} >
<Card variant="outlined">
<CardContent sx={{ padding: 2 }}>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
cursor: 'pointer',
'&:hover': {
backgroundColor: 'rgba(0, 0, 0, 0.04)'
}
}}
onClick={() => toggleFactoryCollapse(factoryName)}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{/* 折叠/展开图标 */}
<IconButton size="small">
{collapsedFactories[factoryName] ? <ExpandLessIcon /> : <ExpandMoreIcon />}
</IconButton>
<Box>
<Box sx={{ display: 'flex', flexDirection: 'row', gap: 2, alignItems: 'center' }}>
{/* svg icon */}
<LlmSvgIcon name={getFactoryIconName(factoryName as LLMFactory)} sx={{ fontSize: 36 }} />
{/* 模型工厂名称 */}
<Typography variant="h6" gutterBottom>
{factoryName}
</Typography></Box>
{/* 模型标签 */}
<Box display="flex" gap={1} mt={2}>
{group.tags.split(',').map((tag) => (
<Chip
key={tag}
label={tag.trim()}
size="small"
sx={{
backgroundColor: MODEL_TYPE_COLORS[tag.trim()] || '#757575',
color: 'white',
}}
/>
))}
</Box>
</Box>
</Box>
{/* edit and delete factory button */}
<Box sx={{ display: 'flex', gap: 1 }} onClick={(e) => e.stopPropagation()}>
<Button
variant='contained' color='primary' startIcon={<EditIcon />}
onClick={() => handleEditLlmFactory(factoryName)}
>
{showAddModel(factoryName) ? t('setting.addModel') : t('setting.edit')}
</Button>
<Button
variant='outlined' color='primary' startIcon={<DeleteIcon />}
onClick={() => handleDeleteFactory(factoryName)}
>
{t('setting.delete')}
</Button>
</Box>
</Box>
{/* 模型列表 - 使用 Collapse 组件包装 */}
<Collapse in={collapsedFactories[factoryName]} timeout="auto" unmountOnExit>
<Box sx={{ mt: 2 }}>
<Grid container spacing={2}>
{group.llm.map((model) => (
<MyLlmGridItem
key={model.name}
model={model}
onEditLlm={showAddModel(factoryName) ? (llm) => {
handleEditLlmFactory(factoryName, llm);
} : undefined}
onDelete={() => handleDeleteModel(factoryName, model.name)}
/>
))}
</Grid>
</Box>
</Collapse>
</CardContent>
</Card>
</Grid>
))}
</Grid>
</AccordionDetails>
</Accordion>
)}
</Box>
{/* LLM Factory 部分 */}
<Box>
<Accordion defaultExpanded>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography variant="h5" gutterBottom>
{t('setting.llmModelFactories')}
</Typography>
</AccordionSummary>
<AccordionDetails>
<Grid container spacing={2}>
{
showLlmFactory.map((factory) => (
<Grid size={{ xs: 6, sm: 4, md: 3 }} key={factory.name}>
<LLMFactoryCard
key={factory.name}
factory={factory}
onConfigure={handleConfigureFactory}
/>
</Grid>
))
}
</Grid>
</AccordionDetails>
</Accordion>
</Box>
{/* 模型配置对话框 */}
{/* @ts-ignore */}
<ModelDialogs {...modelDialogs} />
</Box>
);
};
export default ModelsPage;