| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import React, { useEffect, useMemo, useState } from 'react'; |
| import { useTranslation } from 'react-i18next'; |
| import { useIsMobile } from '../../../../hooks/common/useIsMobile'; |
| import { |
| Collapse, |
| Empty, |
| Input, |
| Modal, |
| Radio, |
| Typography, |
| } from '@douyinfe/semi-ui'; |
| import { |
| IllustrationNoResult, |
| IllustrationNoResultDark, |
| } from '@douyinfe/semi-illustrations'; |
| import { IconSearch } from '@douyinfe/semi-icons'; |
| import { getModelCategories } from '../../../../helpers/render'; |
|
|
| const SingleModelSelectModal = ({ |
| visible, |
| models = [], |
| selected = '', |
| onConfirm, |
| onCancel, |
| }) => { |
| const { t } = useTranslation(); |
| const isMobile = useIsMobile(); |
|
|
| const normalizeModelName = (model) => String(model ?? '').trim(); |
| const normalizedModels = useMemo(() => { |
| const list = Array.isArray(models) ? models : []; |
| return Array.from(new Set(list.map(normalizeModelName).filter(Boolean))); |
| }, [models]); |
|
|
| const [keyword, setKeyword] = useState(''); |
| const [selectedModel, setSelectedModel] = useState(''); |
|
|
| useEffect(() => { |
| if (visible) { |
| setKeyword(''); |
| setSelectedModel(normalizeModelName(selected)); |
| } |
| }, [visible, selected]); |
|
|
| const filteredModels = useMemo(() => { |
| const lower = keyword.trim().toLowerCase(); |
| if (!lower) return normalizedModels; |
| return normalizedModels.filter((m) => m.toLowerCase().includes(lower)); |
| }, [normalizedModels, keyword]); |
|
|
| const modelsByCategory = useMemo(() => { |
| const categories = getModelCategories(t); |
| const categorized = {}; |
| const uncategorized = []; |
|
|
| filteredModels.forEach((model) => { |
| let foundCategory = false; |
| for (const [key, category] of Object.entries(categories)) { |
| if (key !== 'all' && category.filter({ model_name: model })) { |
| if (!categorized[key]) { |
| categorized[key] = { |
| label: category.label, |
| icon: category.icon, |
| models: [], |
| }; |
| } |
| categorized[key].models.push(model); |
| foundCategory = true; |
| break; |
| } |
| } |
| if (!foundCategory) { |
| uncategorized.push(model); |
| } |
| }); |
|
|
| if (uncategorized.length > 0) { |
| categorized.other = { |
| label: t('其他'), |
| icon: null, |
| models: uncategorized, |
| }; |
| } |
|
|
| return categorized; |
| }, [filteredModels, t]); |
|
|
| const categoryEntries = useMemo( |
| () => Object.entries(modelsByCategory), |
| [modelsByCategory], |
| ); |
|
|
| return ( |
| <Modal |
| header={ |
| <div className='flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 sm:gap-4 py-4'> |
| <Typography.Title heading={5} className='m-0'> |
| {t('选择模型')} |
| </Typography.Title> |
| </div> |
| } |
| visible={visible} |
| onOk={() => onConfirm?.(selectedModel)} |
| onCancel={onCancel} |
| okText={t('确定')} |
| cancelText={t('取消')} |
| okButtonProps={{ disabled: !selectedModel }} |
| size={isMobile ? 'full-width' : 'large'} |
| closeOnEsc |
| maskClosable |
| centered |
| > |
| <Input |
| prefix={<IconSearch size={14} />} |
| placeholder={t('搜索模型')} |
| value={keyword} |
| onChange={(v) => setKeyword(v)} |
| showClear |
| /> |
| |
| <div style={{ maxHeight: 400, overflowY: 'auto', paddingRight: 8 }}> |
| {filteredModels.length === 0 ? ( |
| <Empty |
| image={<IllustrationNoResult style={{ width: 150, height: 150 }} />} |
| darkModeImage={ |
| <IllustrationNoResultDark style={{ width: 150, height: 150 }} /> |
| } |
| description={t('暂无匹配模型')} |
| style={{ padding: 30 }} |
| /> |
| ) : ( |
| <Radio.Group |
| className='w-full' |
| style={{ width: '100%' }} |
| value={selectedModel} |
| onChange={(val) => { |
| const next = val && val.target ? val.target.value : val; |
| setSelectedModel(next); |
| }} |
| > |
| <Collapse |
| className='w-full' |
| style={{ width: '100%' }} |
| defaultActiveKey={[]} |
| > |
| {categoryEntries.map(([key, categoryData], index) => ( |
| <Collapse.Panel |
| key={`${key}_${index}`} |
| itemKey={`${key}_${index}`} |
| header={ |
| <span className='flex items-center gap-2'> |
| {categoryData.icon} |
| <span> |
| {categoryData.label} ({categoryData.models.length}) |
| </span> |
| </span> |
| } |
| > |
| <div className='grid grid-cols-2 gap-x-4'> |
| {categoryData.models.map((model) => ( |
| <Radio key={model} value={model} className='my-1'> |
| {model} |
| </Radio> |
| ))} |
| </div> |
| </Collapse.Panel> |
| ))} |
| </Collapse> |
| </Radio.Group> |
| )} |
| </div> |
| </Modal> |
| ); |
| }; |
|
|
| export default SingleModelSelectModal; |
|
|