first-update
This commit is contained in:
346
easy-dataset-main/components/ModelSelect.js
Normal file
346
easy-dataset-main/components/ModelSelect.js
Normal file
@@ -0,0 +1,346 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useState, useMemo } from 'react';
|
||||
import { FormControl, Select, MenuItem, useTheme, ListSubheader, Box, IconButton, Tooltip } from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAtom, useAtomValue } from 'jotai/index';
|
||||
import { modelConfigListAtom, selectedModelInfoAtom } from '@/lib/store';
|
||||
import axios from 'axios';
|
||||
import SmartToyIcon from '@mui/icons-material/SmartToy';
|
||||
import { getModelIcon } from '@/lib/util/modelIcon';
|
||||
|
||||
export default function ModelSelect({
|
||||
size = 'small',
|
||||
minWidth = 50,
|
||||
projectId,
|
||||
minHeight = 36,
|
||||
required = false,
|
||||
onError
|
||||
}) {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const models = useAtomValue(modelConfigListAtom);
|
||||
const [selectedModelInfo, setSelectedModelInfo] = useAtom(selectedModelInfoAtom);
|
||||
const [selectedModel, setSelectedModel] = useState(() => {
|
||||
if (selectedModelInfo && selectedModelInfo.id) {
|
||||
return selectedModelInfo.id;
|
||||
}
|
||||
return '';
|
||||
});
|
||||
const [error, setError] = useState(false);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const handleModelChange = event => {
|
||||
if (!event || !event.target) return;
|
||||
const newModelId = event.target.value;
|
||||
|
||||
if (error) {
|
||||
setError(false);
|
||||
if (onError) onError(false);
|
||||
}
|
||||
|
||||
if (!newModelId) {
|
||||
setSelectedModel('');
|
||||
setSelectedModelInfo(null);
|
||||
updateDefaultModel(null);
|
||||
} else {
|
||||
const selectedModelObj = models.find(model => model.id === newModelId);
|
||||
if (selectedModelObj) {
|
||||
setSelectedModel(newModelId);
|
||||
setSelectedModelInfo(selectedModelObj);
|
||||
updateDefaultModel(newModelId);
|
||||
} else {
|
||||
setSelectedModel(newModelId);
|
||||
setSelectedModelInfo({ id: newModelId });
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
setIsHovered(false);
|
||||
setIsOpen(false);
|
||||
}, 200);
|
||||
};
|
||||
|
||||
const updateDefaultModel = async id => {
|
||||
const res = await axios.put(`/api/projects/${projectId}`, { projectId, defaultModelConfigId: id });
|
||||
if (res.status === 200) {
|
||||
console.log('更新成功');
|
||||
}
|
||||
};
|
||||
|
||||
const validateModel = () => {
|
||||
if (required && (!selectedModel || selectedModel === '')) {
|
||||
setError(true);
|
||||
if (onError) onError(true);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedModelInfo && selectedModelInfo.id) {
|
||||
setSelectedModel(selectedModelInfo.id);
|
||||
} else {
|
||||
setSelectedModel('');
|
||||
}
|
||||
}, [selectedModelInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
if (required) {
|
||||
validateModel();
|
||||
}
|
||||
}, [required]);
|
||||
|
||||
const renderSelectedValue = value => {
|
||||
if (!value) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<SmartToyIcon fontSize="small" />
|
||||
{t('models.unselectedModel', t('playground.selectModelFirst'))}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const selectedModelObj = models.find(model => model.id === value);
|
||||
if (!selectedModelObj) return null;
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Box
|
||||
component="img"
|
||||
src={getModelIcon(selectedModelObj.modelName || selectedModelObj.modelId)}
|
||||
alt={selectedModelObj.modelName}
|
||||
sx={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
objectFit: 'contain',
|
||||
flexShrink: 0,
|
||||
background: '#ffffffc9',
|
||||
borderRadius: '50%',
|
||||
marginBottom: '-2px'
|
||||
}}
|
||||
onError={e => {
|
||||
e.target.src = '/imgs/models/default.svg';
|
||||
}}
|
||||
/>
|
||||
{selectedModelObj.modelName}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const currentModelIcon = useMemo(() => {
|
||||
const selectedModelObj = models.find(model => model.id === selectedModel);
|
||||
return selectedModelObj ? getModelIcon(selectedModelObj.modelName, selectedModelObj.modelId) : null;
|
||||
}, [selectedModel, models]);
|
||||
|
||||
const shouldShowFullSelect = isHovered || isOpen;
|
||||
|
||||
return (
|
||||
<Box
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => {
|
||||
setIsHovered(false);
|
||||
if (!isOpen) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
{!shouldShowFullSelect && (
|
||||
<Tooltip
|
||||
title={
|
||||
selectedModel
|
||||
? models.find(m => m.id === selectedModel)?.modelName
|
||||
: t('playground.selectModelFirst', '请先选择模型')
|
||||
}
|
||||
placement="bottom"
|
||||
>
|
||||
<IconButton
|
||||
size="medium"
|
||||
sx={{
|
||||
bgcolor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.08)' : 'rgba(255, 255, 255, 0.69)',
|
||||
color: theme.palette.mode === 'dark' ? 'inherit' : 'white',
|
||||
borderRadius: 1.5,
|
||||
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
'&:hover': {
|
||||
bgcolor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.15)' : 'rgba(255, 255, 255, 0.35)'
|
||||
},
|
||||
...(error && {
|
||||
animation: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
||||
'@keyframes pulse': {
|
||||
'0%, 100%': {
|
||||
opacity: 1
|
||||
},
|
||||
'50%': {
|
||||
opacity: 0.5
|
||||
}
|
||||
}
|
||||
})
|
||||
}}
|
||||
>
|
||||
{currentModelIcon ? (
|
||||
<Box
|
||||
component="img"
|
||||
src={currentModelIcon}
|
||||
alt="model icon"
|
||||
sx={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
objectFit: 'contain'
|
||||
}}
|
||||
onError={e => {
|
||||
e.target.src = '/imgs/models/default.svg';
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<SmartToyIcon
|
||||
fontSize="small"
|
||||
color="red"
|
||||
sx={{
|
||||
color: error ? 'red' : 'red'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<FormControl
|
||||
size={size}
|
||||
sx={{
|
||||
minWidth: shouldShowFullSelect ? 200 : 0,
|
||||
minHeight,
|
||||
opacity: shouldShowFullSelect ? 1 : 0,
|
||||
width: shouldShowFullSelect ? 'auto' : 0,
|
||||
overflow: 'hidden',
|
||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
position: shouldShowFullSelect ? 'relative' : 'absolute',
|
||||
pointerEvents: shouldShowFullSelect ? 'auto' : 'none'
|
||||
}}
|
||||
error={error}
|
||||
>
|
||||
<Select
|
||||
value={selectedModel}
|
||||
onChange={handleModelChange}
|
||||
displayEmpty
|
||||
variant="outlined"
|
||||
onBlur={validateModel}
|
||||
renderValue={renderSelectedValue}
|
||||
onOpen={() => setIsOpen(true)}
|
||||
onClose={() => setIsOpen(false)}
|
||||
sx={{
|
||||
bgcolor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.08)' : 'rgba(255, 255, 255, 0.2)',
|
||||
color: theme.palette.mode === 'dark' ? 'inherit' : 'white',
|
||||
borderRadius: 1.5,
|
||||
'& .MuiSelect-select': {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '6px 32px 6px 12px'
|
||||
},
|
||||
'& .MuiSelect-icon': {
|
||||
color: theme.palette.mode === 'dark' ? 'inherit' : 'white',
|
||||
right: '8px'
|
||||
},
|
||||
'& .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: 'transparent'
|
||||
},
|
||||
'&:hover .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: 'transparent'
|
||||
},
|
||||
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: theme.palette.mode === 'dark' ? 'primary.main' : 'rgba(255, 255, 255, 0.5)'
|
||||
},
|
||||
minHeight: '36px'
|
||||
}}
|
||||
MenuProps={{
|
||||
PaperProps: {
|
||||
elevation: 2,
|
||||
sx: {
|
||||
mt: 1,
|
||||
borderRadius: 2,
|
||||
'& .MuiMenuItem-root': {
|
||||
minHeight: '30px'
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MenuItem value="">
|
||||
{error ? t('models.pleaseSelectModel') : t('models.unselectedModel', t('playground.selectModelFirst'))}
|
||||
</MenuItem>
|
||||
{(() => {
|
||||
const filteredModels = models.filter(m => {
|
||||
if (m.providerId?.toLowerCase() === 'ollama') {
|
||||
return m.modelName && m.endpoint;
|
||||
} else {
|
||||
return m.modelName && m.endpoint && m.apiKey;
|
||||
}
|
||||
});
|
||||
|
||||
const providers = [...new Set(filteredModels.map(m => m.providerName || 'Other'))];
|
||||
|
||||
return providers.map(provider => {
|
||||
const providerModels = filteredModels.filter(m => (m.providerName || 'Other') === provider);
|
||||
return [
|
||||
<ListSubheader
|
||||
key={`header-${provider}`}
|
||||
sx={{
|
||||
pl: 2,
|
||||
color: theme.palette.text.secondary,
|
||||
fontWeight: 500,
|
||||
mt: 1,
|
||||
mb: 0.5
|
||||
}}
|
||||
>
|
||||
{provider || 'Other'}
|
||||
</ListSubheader>,
|
||||
...providerModels.map(model => (
|
||||
<MenuItem
|
||||
key={model.id}
|
||||
value={model.id}
|
||||
sx={{
|
||||
pl: 3,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 2,
|
||||
minHeight: '30px',
|
||||
'&.Mui-selected': {
|
||||
bgcolor: theme.palette.action.selected,
|
||||
'&:hover': {
|
||||
bgcolor: theme.palette.action.selected
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="img"
|
||||
src={getModelIcon(model.modelName || model.modelId)}
|
||||
alt={model.modelName}
|
||||
sx={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
objectFit: 'contain',
|
||||
flexShrink: 0
|
||||
}}
|
||||
onError={e => {
|
||||
e.target.src = '/imgs/models/default.svg';
|
||||
}}
|
||||
/>
|
||||
<Box component="span" sx={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{model.modelName}
|
||||
</Box>
|
||||
</MenuItem>
|
||||
))
|
||||
];
|
||||
});
|
||||
})()}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user