feat(settings): refactor LLM model management UI and components

This commit is contained in:
2025-10-21 18:21:48 +08:00
parent bcfcc4b40a
commit 4784fcb23f
7 changed files with 225 additions and 360 deletions

View File

@@ -1 +1 @@
<svg id="_层_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 274.37 172.76"><defs><style>.cls-2{fill:#36cfd1}.cls-3{fill:#624aff}</style></defs><g id="_层_1-2"><path class="cls-3" d="M24.78 73.55h25.65V99.2H24.78zm99.14 25.66h25.65v25.65h-25.65zm76.95 25.65h-25.65v22.19h47.84V99.21h-22.19v25.65z"/><path class="cls-2" d="M149.57 73.55h25.65V99.2h-25.65zM24.78 47.9h25.65v25.65H24.78z"/><path class="cls-3" d="M223.06 73.55h25.65V99.2h-25.65z"/><path class="cls-2" d="M223.06 47.9h25.65v25.65h-25.65z"/><path class="cls-3" d="M175.22 25.71V47.9h25.65v25.65h22.19V25.71h-47.84z"/><path class="cls-2" d="M98.27 73.55h25.65V99.2H98.27z"/><path class="cls-3" d="M72.62 47.9h25.65V25.71H50.43v47.84h22.19V47.9zm0 51.31H50.43v47.84h47.84v-22.19H72.62V99.21z"/><path style="fill:none" d="M0 0h274.37v172.76H0z"/></g></svg>
<svg id="layer_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 274.37 172.76"><defs><style>.cls-2{fill:#36cfd1}.cls-3{fill:#624aff}</style></defs><g id="layer_1-2"><path class="cls-3" d="M24.78 73.55h25.65V99.2H24.78zm99.14 25.66h25.65v25.65h-25.65zm76.95 25.65h-25.65v22.19h47.84V99.21h-22.19v25.65z"/><path class="cls-2" d="M149.57 73.55h25.65V99.2h-25.65zM24.78 47.9h25.65v25.65H24.78z"/><path class="cls-3" d="M223.06 73.55h25.65V99.2h-25.65z"/><path class="cls-2" d="M223.06 47.9h25.65v25.65h-25.65z"/><path class="cls-3" d="M175.22 25.71V47.9h25.65v25.65h22.19V25.71h-47.84z"/><path class="cls-2" d="M98.27 73.55h25.65V99.2H98.27z"/><path class="cls-3" d="M72.62 47.9h25.65V25.71H50.43v47.84h22.19V47.9zm0 51.31H50.43v47.84h47.84v-22.19H72.62V99.21z"/><path style="fill:none" d="M0 0h274.37v172.76H0z"/></g></svg>

Before

Width:  |  Height:  |  Size: 821 B

After

Width:  |  Height:  |  Size: 822 B

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_图层_2" data-name="图层 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 111.08 53.12">
<svg id="layer_2" data-name="layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 111.08 53.12">
<defs>
<style>
.cls-1 {
@@ -7,7 +7,7 @@
}
</style>
</defs>
<g id="_图层_1-2" data-name="图层 1">
<g id="layer_1-2" data-name="layer 1">
<path class="cls-1" d="M106.25,0h-48.3c-2.67,0-4.83,2.16-4.83,4.83v14.49c0,2.67-2.16,4.83-4.83,4.83H4.83c-2.67,0-4.83,2.16-4.83,4.83v19.32c0,2.67,2.16,4.83,4.83,4.83h48.3c2.67,0,4.83-2.16,4.83-4.83v-14.49c0-2.67,2.16-4.83,4.83-4.83h43.47c2.67,0,4.83-2.16,4.83-4.83V4.83c0-2.67-2.16-4.83-4.83-4.83Z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 595 B

After

Width:  |  Height:  |  Size: 589 B

View File

@@ -1,28 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_图层_1" data-name="图层 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
<svg id="layer_1" data-name="layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 283.46 283.46">
<defs>
<style>
.cls-1 {
fill: url(#_未命名的渐变_5-2);
fill: url(#gradient_5-2);
}
.cls-2 {
fill: url(#_未命名的渐变_9);
fill: url(#gradient_9);
}
.cls-3 {
fill: url(#_未命名的渐变_5);
fill: url(#gradient_5);
}
</style>
<linearGradient id="_未命名的渐变_5" data-name="未命名的渐变 5" x1="27.03" y1="287.05" x2="253.15" y2="1.14"
<linearGradient id="gradient_5" data-name="gradient 5" x1="27.03" y1="287.05" x2="253.15" y2="1.14"
gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#e9a85e" />
<stop offset="1" stop-color="#f52b76" />
</linearGradient>
<linearGradient id="_未命名的渐变_5-2" data-name="未命名的渐变 5" x1="25.96" y1="286.21" x2="252.09" y2=".3"
xlink:href="#_未命名的渐变_5" />
<linearGradient id="_未命名的渐变_9" data-name="未命名的渐变 9" x1="-474.33" y1="476.58" x2="-160.37" y2="476.58"
<linearGradient id="gradient_5-2" data-name="gradient 5" x1="25.96" y1="286.21" x2="252.09" y2=".3"
xlink:href="#gradient_5" />
<linearGradient id="gradient_9" data-name="gradient 9" x1="-474.33" y1="476.58" x2="-160.37" y2="476.58"
gradientTransform="translate(669.07 -75.9) rotate(33.75)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#6a0cf5" />
<stop offset="1" stop-color="#ab66f3" />

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -6,7 +6,7 @@ const svgPath = '/src/assets/svg'
interface AppSvgIconProps extends SvgIconProps {
name: string;
point: 'llm' | 'chunk' | 'file' | 'default';
point?: 'llm' | 'chunk' | 'file' | 'default';
}
const getPointPath = (point: AppSvgIconProps['point']) => {

View File

@@ -1,12 +1,100 @@
import { Box, Typography } from "@mui/material";
import {
Settings as SettingsIcon,
} from '@mui/icons-material'
import { LlmSvgIcon } from "@/components/AppSvgIcon";
import { IconMap, type LLMFactory } from "@/constants/llm";
import type { IFactory } from "@/interfaces/database/llm";
import { Box, Button, Card, CardContent, Chip, Typography } from "@mui/material";
import { useState } from "react";
function LLMFactoryCard() {
return (
<Box>
<Typography variant="h6" gutterBottom>
</Typography>
</Box>
)
// 模型类型标签颜色映射
export const MODEL_TYPE_COLORS: Record<string, string> = {
'LLM': '#1976d2',
'TEXT EMBEDDING': '#388e3c',
'TEXT RE-RANK': '#f57c00',
'TTS': '#7b1fa2',
'SPEECH2TEXT': '#c2185b',
'IMAGE2TEXT': '#5d4037',
'MODERATION': '#455a64',
};
// 模型工厂卡片组件
interface ModelFactoryCardProps {
factory: IFactory;
onConfigure: (factory: IFactory) => void;
}
export default LLMFactoryCard;
const LLMFactoryCard: React.FC<ModelFactoryCardProps> = ({
factory,
onConfigure,
}) => {
// 获取工厂图标名称
const getFactoryIconName = (factoryName: LLMFactory) => {
return IconMap[factoryName] || 'default';
};
return (
<Card sx={{
mb: 2,
border: '1px solid #e0e0e0',
height: '100%',
display: 'flex',
flexDirection: 'column'
}}>
<CardContent sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
textAlign: 'left',
gap: 2,
flex: 1,
}}>
{/* 图标 */}
<LlmSvgIcon
name={getFactoryIconName(factory.name as LLMFactory)}
sx={{ width: 48, height: 48, color: 'primary.main' }}
/>
{/* 标题 */}
<Typography variant="h6" component="div" sx={{ fontWeight: 'bold' }}>
{factory.name}
</Typography>
{/* 标签 */}
<Box display="flex" flexWrap="wrap" gap={0.5} justifyContent="left">
{factory.model_types.map((type) => (
<Chip
key={type}
label={type.toUpperCase()}
size="small"
sx={{
backgroundColor: MODEL_TYPE_COLORS[type.toUpperCase()] || '#757575',
color: 'white',
fontSize: '0.65rem',
height: '20px',
}}
/>
))}
</Box>
{/* 配置按钮 */}
<Button
variant="text"
size="small"
onClick={() => onConfigure(factory)}
sx={{
mt: 'auto',
color: 'primary.main',
alignSelf: 'center',
width: 'fit-content'
}}
>
</Button>
</CardContent>
</Card>
);
};
export default LLMFactoryCard

View File

@@ -9,266 +9,27 @@ import {
IconButton,
Collapse,
Grid,
Divider,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
FormControl,
InputLabel,
Select,
MenuItem,
Alert,
CircularProgress,
Tooltip,
Accordion,
AccordionSummary,
AccordionDetails,
Divider,
} from '@mui/material';
import {
ExpandMore as ExpandMoreIcon,
ExpandLess as ExpandLessIcon,
Settings as SettingsIcon,
Delete as DeleteIcon,
Edit as EditIcon,
Add as AddIcon,
Visibility as VisibilityIcon,
VisibilityOff as VisibilityOffIcon,
Delete as DeleteIcon,
} from '@mui/icons-material';
import { useLlmModelSetting } from '@/hooks/setting-hooks';
import { useModelDialogs } from './hooks/useModelDialogs';
import { LlmSvgIcon } from '@/components/AppSvgIcon';
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 LLMFactoryCard, { MODEL_TYPE_COLORS } from './components/LLMFactoryCard';
// 模型类型标签颜色映射
const MODEL_TYPE_COLORS: Record<string, string> = {
'LLM': '#1976d2',
'TEXT EMBEDDING': '#388e3c',
'TEXT RE-RANK': '#f57c00',
'TTS': '#7b1fa2',
'SPEECH2TEXT': '#c2185b',
'IMAGE2TEXT': '#5d4037',
'MODERATION': '#455a64',
};
// 模型工厂卡片组件
interface ModelFactoryCardProps {
factory: IFactory;
myModels: ILlmItem[];
onConfigure: (factory: IFactory) => void;
onDeleteFactory: (factoryName: string) => void;
onDeleteModel: (factoryName: string, modelName: string) => void;
onEditModel: (factory: IFactory, model: ILlmItem) => void;
}
const ModelFactoryCard: React.FC<ModelFactoryCardProps> = ({
factory,
myModels,
onConfigure,
onDeleteFactory,
onDeleteModel,
onEditModel,
}) => {
const [expanded, setExpanded] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const handleExpandClick = () => {
setExpanded(!expanded);
};
const handleDeleteFactory = () => {
onDeleteFactory(factory.name);
setShowDeleteConfirm(false);
};
// 获取工厂图标名称
const getFactoryIconName = (factoryName: LLMFactory) => {
return IconMap[factoryName] || 'default';
};
return (
<>
<Card sx={{ mb: 2, border: '1px solid #e0e0e0' }}>
<CardContent>
<Box display="flex" alignItems="center" justifyContent="space-between">
<Box display="flex" alignItems="center" gap={2}>
<LlmSvgIcon
name={getFactoryIconName(factory.name as LLMFactory)}
sx={{ width: 40, height: 40, color: 'primary.main' }}/>
<Box>
<Typography variant="h6" component="div">
{factory.name}
</Typography>
<Box display="flex" gap={1} mt={1}>
{factory.model_types.map((type) => (
<Chip
key={type}
label={type.toUpperCase()}
size="small"
sx={{
backgroundColor: MODEL_TYPE_COLORS[type.toUpperCase()] || '#757575',
color: 'white',
fontSize: '0.7rem',
}}
/>
))}
</Box>
</Box>
</Box>
<Box display="flex" alignItems="center" gap={1}>
<Button
variant="contained"
size="small"
startIcon={<SettingsIcon />}
onClick={() => onConfigure(factory)}
>
</Button>
{myModels.length > 0 && (
<IconButton onClick={handleExpandClick}>
{expanded ? <ExpandLessIcon /> : <ExpandMoreIcon />}
</IconButton>
)}
<IconButton
color="error"
onClick={() => setShowDeleteConfirm(true)}
>
<DeleteIcon />
</IconButton>
</Box>
</Box>
<Collapse in={expanded} timeout="auto" unmountOnExit>
<Divider sx={{ my: 2 }} />
<Typography variant="subtitle2" gutterBottom>
({myModels.length})
</Typography>
<Grid container spacing={2}>
{myModels.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="start">
<Box>
<Typography variant="body2" fontWeight="bold">
{model.name}
</Typography>
<Typography variant="caption" color="text.secondary">
{model.type}
</Typography>
<Typography variant="caption" display="block" color="text.secondary">
Max Tokens: {model.max_tokens}
</Typography>
<Typography variant="caption" display="block" color="text.secondary">
Used: {model.used_token}
</Typography>
</Box>
<Box>
<IconButton
size="small"
onClick={() => onEditModel(factory, model)}
>
<EditIcon fontSize="small" />
</IconButton>
<IconButton
size="small"
color="error"
onClick={() => onDeleteModel(factory.name, model.name)}
>
<DeleteIcon fontSize="small" />
</IconButton>
</Box>
</Box>
</Card>
</Grid>
))}
</Grid>
</Collapse>
</CardContent>
</Card>
{/* 删除确认对话框 */}
<Dialog open={showDeleteConfirm} onClose={() => setShowDeleteConfirm(false)}>
<DialogTitle></DialogTitle>
<DialogContent>
<Typography>
"{factory.name}"
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setShowDeleteConfirm(false)}></Button>
<Button onClick={handleDeleteFactory} color="error" variant="contained">
</Button>
</DialogActions>
</Dialog>
</>
);
};
// 系统默认模型设置组件
interface SystemModelSettingProps {
myLlm: Record<string, IMyLlmModel> | undefined;
}
const SystemModelSetting: React.FC<SystemModelSettingProps> = ({ myLlm }) => {
const [defaultModel, setDefaultModel] = useState('');
const [loading, setLoading] = useState(false);
// 获取所有可用的聊天模型
const chatModels = myLlm ? Object.values(myLlm).flatMap(group =>
group.llm.filter(model => model.type.toLowerCase().includes('chat') || model.type.toLowerCase().includes('llm'))
) : [];
const handleSaveDefaultModel = async () => {
if (!defaultModel) return;
setLoading(true);
try {
// TODO: 调用设置系统默认模型的 API
console.log('Setting default model:', defaultModel);
// await userService.setSystemDefaultModel({ model: defaultModel });
} catch (error) {
console.error('设置默认模型失败:', error);
} finally {
setLoading(false);
}
};
return (
<Card sx={{ mb: 3 }}>
<CardContent>
<Typography variant="h6" gutterBottom>
LLM
</Typography>
<Typography variant="body2" color="text.secondary" gutterBottom>
使
</Typography>
<Box display="flex" gap={2} alignItems="center" mt={2}>
<FormControl sx={{ minWidth: 300 }}>
<InputLabel></InputLabel>
<Select
value={defaultModel}
label="选择默认模型"
onChange={(e) => setDefaultModel(e.target.value)}
>
{chatModels.map((model) => (
<MenuItem key={`${model.name}-${model.type}`} value={model.name}>
{model.name} ({model.type})
</MenuItem>
))}
</Select>
</FormControl>
<Button
variant="contained"
onClick={handleSaveDefaultModel}
disabled={!defaultModel || loading}
startIcon={loading ? <CircularProgress size={16} /> : undefined}
>
</Button>
</Box>
</CardContent>
</Card>
);
};
// 主页面组件
function ModelsPage() {
@@ -336,116 +97,124 @@ function ModelsPage() {
LLM
</Typography>
{/* 系统默认模型设置 */}
<SystemModelSetting myLlm={myLlm} />
{/* My LLM 部分 */}
{/* <Box mb={4}>
<Typography variant="h5" gutterBottom>
我的 LLM 模型
</Typography>
<Box mb={4} mt={2}>
{!myLlm || Object.keys(myLlm).length === 0 ? (
<Alert severity="info">
LLM
</Alert>
) : (
<Grid container spacing={2}>
{Object.entries(myLlm).map(([factoryName, group]) => (
<Grid size={12} key={factoryName}>
<Card variant="outlined">
<CardContent>
<Typography variant="h6" gutterBottom>
{factoryName}
</Typography>
<Box display="flex" gap={1} mb={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>
<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>
<Accordion defaultExpanded>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography variant="h5" gutterBottom>
LLM
</Typography>
</AccordionSummary>
<AccordionDetails>
<Grid container spacing={2}>
{Object.entries(myLlm).map(([factoryName, group]) => (
<Grid size={12} key={factoryName}>
<Card variant="outlined">
<CardContent>
<Typography variant="h6" gutterBottom>
{factoryName}
</Typography>
<Box display="flex" gap={1} mb={2}>
{group.tags.split(',').map((tag) => (
<Chip
label={model.type}
key={tag}
label={tag.trim()}
size="small"
sx={{
backgroundColor: MODEL_TYPE_COLORS[model.type.toUpperCase()] || '#757575',
backgroundColor: MODEL_TYPE_COLORS[tag.trim()] || '#757575',
color: 'white',
mb: 1,
}}
/>
<Typography variant="caption" display="block" color="text.secondary">
Max Tokens: {model.max_tokens}
</Typography>
<Typography variant="caption" display="block" color="text.secondary">
Used: {model.used_token}
</Typography>
{model.api_base && (
<Typography variant="caption" display="block" color="text.secondary">
Base URL: {model.api_base}
</Typography>
)}
</Card>
))}
</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
label={model.type}
size="small"
sx={{
backgroundColor: MODEL_TYPE_COLORS[model.type.toUpperCase()] || '#757575',
color: 'white',
mb: 1,
}}
/>
<Typography variant="caption" display="block" color="text.secondary">
Max Tokens: {model.max_tokens}
</Typography>
<Typography variant="caption" display="block" color="text.secondary">
Used: {model.used_token}
</Typography>
{model.api_base && (
<Typography variant="caption" display="block" color="text.secondary">
Base URL: {model.api_base}
</Typography>
)}
</Card>
</Grid>
))}
</Grid>
))}
</Grid>
</CardContent>
</Card>
</CardContent>
</Card>
</Grid>
))}
</Grid>
))}
</Grid>
</AccordionDetails>
</Accordion>
)}
</Box> */}
</Box>
{/* LLM Factory 部分 */}
<Box>
<Typography variant="h5" gutterBottom>
LLM
</Typography>
<Typography variant="body2" color="text.secondary" gutterBottom>
AI
</Typography>
{llmFactory.map((factory) => (
<ModelFactoryCard
key={factory.name}
factory={factory}
myModels={getModelsForFactory(factory.name as LLMFactory)}
onConfigure={handleConfigureFactory}
onDeleteFactory={handleDeleteFactory}
onDeleteModel={handleDeleteModel}
onEditModel={handleEditModel}
/>
))}
<Accordion defaultExpanded>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography variant="h5" gutterBottom>
LLM
</Typography>
<AppSvgIcon name='arxiv' />
</AccordionSummary>
<AccordionDetails>
<Grid container spacing={2}>
{
llmFactory.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>
{/* 模型配置对话框 */}

View File

@@ -5,7 +5,15 @@ import svgr from "vite-plugin-svgr";
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), svgr()],
plugins: [
react(),
svgr({
svgrOptions: {
icon: true,
prettier: true,
}
})
],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),