/* Copyright (C) 2025 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import React, { useState, useEffect, useMemo } from 'react'; import { useIsMobile } from '../../../../hooks/common/useIsMobile'; import { Modal, Checkbox, Spin, Input, Typography, Empty, Tabs, Collapse, Tooltip, } from '@douyinfe/semi-ui'; import { IllustrationNoResult, IllustrationNoResultDark, } from '@douyinfe/semi-illustrations'; import { IconSearch, IconInfoCircle } from '@douyinfe/semi-icons'; import { useTranslation } from 'react-i18next'; import { getModelCategories } from '../../../../helpers/render'; const ModelSelectModal = ({ visible, models = [], selected = [], redirectModels = [], onConfirm, onCancel, }) => { const { t } = useTranslation(); const [checkedList, setCheckedList] = useState(selected); const [keyword, setKeyword] = useState(''); const [activeTab, setActiveTab] = useState('new'); const isMobile = useIsMobile(); const normalizeModelName = (model) => typeof model === 'string' ? model.trim() : ''; const normalizedRedirectModels = useMemo( () => Array.from( new Set( (redirectModels || []) .map((model) => normalizeModelName(model)) .filter(Boolean), ), ), [redirectModels], ); const normalizedSelectedSet = useMemo(() => { const set = new Set(); (selected || []).forEach((model) => { const normalized = normalizeModelName(model); if (normalized) { set.add(normalized); } }); return set; }, [selected]); const classificationSet = useMemo(() => { const set = new Set(normalizedSelectedSet); normalizedRedirectModels.forEach((model) => set.add(model)); return set; }, [normalizedSelectedSet, normalizedRedirectModels]); const redirectOnlySet = useMemo(() => { const set = new Set(); normalizedRedirectModels.forEach((model) => { if (!normalizedSelectedSet.has(model)) { set.add(model); } }); return set; }, [normalizedRedirectModels, normalizedSelectedSet]); const filteredModels = models.filter((m) => String(m || '').toLowerCase().includes(keyword.toLowerCase()), ); // 分类模型:新获取的模型和已有模型 const isExistingModel = (model) => classificationSet.has(normalizeModelName(model)); const newModels = filteredModels.filter((model) => !isExistingModel(model)); const existingModels = filteredModels.filter((model) => isExistingModel(model), ); // 同步外部选中值 useEffect(() => { if (visible) { setCheckedList(selected); } }, [visible, selected]); // 当模型列表变化时,设置默认tab useEffect(() => { if (visible) { // 默认显示新获取模型tab,如果没有新模型则显示已有模型 const hasNewModels = newModels.length > 0; setActiveTab(hasNewModels ? 'new' : 'existing'); } }, [visible, newModels.length, selected]); const handleOk = () => { onConfirm && onConfirm(checkedList); }; // 按厂商分类模型 const categorizeModels = (models) => { const categories = getModelCategories(t); const categorizedModels = {}; const uncategorizedModels = []; models.forEach((model) => { let foundCategory = false; for (const [key, category] of Object.entries(categories)) { if (key !== 'all' && category.filter({ model_name: model })) { if (!categorizedModels[key]) { categorizedModels[key] = { label: category.label, icon: category.icon, models: [], }; } categorizedModels[key].models.push(model); foundCategory = true; break; } } if (!foundCategory) { uncategorizedModels.push(model); } }); // 如果有未分类模型,添加到"其他"分类 if (uncategorizedModels.length > 0) { categorizedModels['other'] = { label: t('其他'), icon: null, models: uncategorizedModels, }; } return categorizedModels; }; const newModelsByCategory = categorizeModels(newModels); const existingModelsByCategory = categorizeModels(existingModels); // Tab列表配置 const tabList = [ ...(newModels.length > 0 ? [ { tab: `${t('新获取的模型')} (${newModels.length})`, itemKey: 'new', }, ] : []), ...(existingModels.length > 0 ? [ { tab: `${t('已有的模型')} (${existingModels.length})`, itemKey: 'existing', }, ] : []), ]; // 处理分类全选/取消全选 const handleCategorySelectAll = (categoryModels, isChecked) => { let newCheckedList = [...checkedList]; if (isChecked) { // 全选:添加该分类下所有未选中的模型 categoryModels.forEach((model) => { if (!newCheckedList.includes(model)) { newCheckedList.push(model); } }); } else { // 取消全选:移除该分类下所有已选中的模型 newCheckedList = newCheckedList.filter( (model) => !categoryModels.includes(model), ); } setCheckedList(newCheckedList); }; // 检查分类是否全选 const isCategoryAllSelected = (categoryModels) => { return ( categoryModels.length > 0 && categoryModels.every((model) => checkedList.includes(model)) ); }; // 检查分类是否部分选中 const isCategoryIndeterminate = (categoryModels) => { const selectedCount = categoryModels.filter((model) => checkedList.includes(model), ).length; return selectedCount > 0 && selectedCount < categoryModels.length; }; const renderModelsByCategory = (modelsByCategory, categoryKeyPrefix) => { const categoryEntries = Object.entries(modelsByCategory); if (categoryEntries.length === 0) return null; // 生成所有面板的key,确保都展开 const allActiveKeys = categoryEntries.map( (_, index) => `${categoryKeyPrefix}_${index}`, ); return ( {categoryEntries.map(([key, categoryData], index) => ( { e.stopPropagation(); // 防止触发面板折叠 handleCategorySelectAll( categoryData.models, e.target.checked, ); }} onClick={(e) => e.stopPropagation()} // 防止点击checkbox时折叠面板 /> } >
{categoryData.icon} {t('已选择 {{selected}} / {{total}}', { selected: categoryData.models.filter((model) => checkedList.includes(model), ).length, total: categoryData.models.length, })}
{categoryData.models.map((model) => ( {model} {redirectOnlySet.has(normalizeModelName(model)) && ( )} ))}
))}
); }; return ( {t('选择模型')}
setActiveTab(key)} />
} visible={visible} onOk={handleOk} onCancel={onCancel} okText={t('确定')} cancelText={t('取消')} size={isMobile ? 'full-width' : 'large'} closeOnEsc maskClosable centered > } placeholder={t('搜索模型')} value={keyword} onChange={(v) => setKeyword(v)} showClear />
{filteredModels.length === 0 ? ( } darkModeImage={ } description={t('暂无匹配模型')} style={{ padding: 30 }} /> ) : ( setCheckedList(vals)} > {activeTab === 'new' && newModels.length > 0 && (
{renderModelsByCategory(newModelsByCategory, 'new')}
)} {activeTab === 'existing' && existingModels.length > 0 && (
{renderModelsByCategory(existingModelsByCategory, 'existing')}
)}
)}
{(() => { const currentModels = activeTab === 'new' ? newModels : existingModels; const currentSelected = currentModels.filter((model) => checkedList.includes(model), ).length; const isAllSelected = currentModels.length > 0 && currentSelected === currentModels.length; const isIndeterminate = currentSelected > 0 && currentSelected < currentModels.length; return ( <> {t('已选择 {{selected}} / {{total}}', { selected: currentSelected, total: currentModels.length, })} { handleCategorySelectAll(currentModels, e.target.checked); }} /> ); })()}
); }; export default ModelSelectModal;