| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
|
|
| 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;
|
|
|