From 33462df1ff65d8b1f9f5f1882f4a40bd91f3c03b Mon Sep 17 00:00:00 2001 From: Supra4E8C Date: Fri, 27 Mar 2026 19:11:35 +0800 Subject: [PATCH] feat(AiProviders): add model selection and toolbar for visible models --- src/components/ui/SelectionCheckbox.tsx | 2 +- src/i18n/locales/en.json | 3 + src/i18n/locales/ru.json | 3 + src/i18n/locales/zh-CN.json | 3 + src/pages/AiProvidersClaudeModelsPage.tsx | 109 +++++++++++--- src/pages/AiProvidersCodexEditPage.tsx | 160 ++++++++++++++++---- src/pages/AiProvidersGeminiEditPage.tsx | 173 ++++++++++++++++++---- src/pages/AiProvidersOpenAIModelsPage.tsx | 109 +++++++++++--- src/pages/AiProvidersPage.module.scss | 37 ++++- 9 files changed, 496 insertions(+), 103 deletions(-) diff --git a/src/components/ui/SelectionCheckbox.tsx b/src/components/ui/SelectionCheckbox.tsx index f55fac7..b537d2d 100644 --- a/src/components/ui/SelectionCheckbox.tsx +++ b/src/components/ui/SelectionCheckbox.tsx @@ -44,7 +44,7 @@ export function SelectionCheckbox({ disabled={disabled} /> {checked ? : null} - {label ? {label} : null} + {label ?
{label}
: null} ); } diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index efa130f..d5abdd3 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -218,6 +218,9 @@ "gemini_models_fetch_empty": "No models returned. Please check Base URL, API key, or headers.", "gemini_models_fetch_error": "Failed to fetch Gemini models", "gemini_models_fetch_apply": "Add selected models", + "model_discovery_select_visible": "Select current list", + "model_discovery_clear_selection": "Clear selection", + "model_discovery_selected_count": "{{count}} selected", "gemini_models_search_label": "Search models", "gemini_models_search_placeholder": "Filter by name, alias, or description", "gemini_models_search_empty": "No models match your search. Try a different keyword.", diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index 892ff87..5e3ed62 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -218,6 +218,9 @@ "gemini_models_fetch_empty": "Модели не получены. Проверьте Base URL, API-ключ или заголовки.", "gemini_models_fetch_error": "Не удалось получить модели Gemini", "gemini_models_fetch_apply": "Добавить выбранные модели", + "model_discovery_select_visible": "Выбрать текущий список", + "model_discovery_clear_selection": "Очистить выбор", + "model_discovery_selected_count": "Выбрано: {{count}}", "gemini_models_search_label": "Поиск моделей", "gemini_models_search_placeholder": "Фильтр по имени, псевдониму или описанию", "gemini_models_search_empty": "Нет подходящих моделей. Попробуйте другой запрос.", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index ca5f272..c18895b 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -218,6 +218,9 @@ "gemini_models_fetch_empty": "未获取到模型,请检查 Base URL、API Key 或请求头。", "gemini_models_fetch_error": "获取 Gemini 模型失败", "gemini_models_fetch_apply": "添加所选模型", + "model_discovery_select_visible": "全选当前列表", + "model_discovery_clear_selection": "清空已选", + "model_discovery_selected_count": "已选 {{count}} 项", "gemini_models_search_label": "搜索模型", "gemini_models_search_placeholder": "按名称、别名或描述筛选", "gemini_models_search_empty": "没有匹配的模型,请更换关键字试试。", diff --git a/src/pages/AiProvidersClaudeModelsPage.tsx b/src/pages/AiProvidersClaudeModelsPage.tsx index b26e9da..47a4363 100644 --- a/src/pages/AiProvidersClaudeModelsPage.tsx +++ b/src/pages/AiProvidersClaudeModelsPage.tsx @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'; import { Button } from '@/components/ui/Button'; import { Card } from '@/components/ui/Card'; import { Input } from '@/components/ui/Input'; +import { SelectionCheckbox } from '@/components/ui/SelectionCheckbox'; import { SecondaryScreenShell } from '@/components/common/SecondaryScreenShell'; import { useEdgeSwipeBack } from '@/hooks/useEdgeSwipeBack'; import { modelsApi } from '@/services/api'; @@ -48,6 +49,14 @@ export function AiProvidersClaudeModelsPage() { return name.includes(filter) || alias.includes(filter) || desc.includes(filter); }); }, [models, search]); + const visibleModelNames = useMemo( + () => filteredModels.map((model) => model.name), + [filteredModels] + ); + const allVisibleSelected = useMemo( + () => visibleModelNames.length > 0 && visibleModelNames.every((name) => selected.has(name)), + [selected, visibleModelNames] + ); const fetchClaudeModelDiscovery = useCallback(async () => { setFetching(true); @@ -117,6 +126,22 @@ export function AiProvidersClaudeModelsPage() { void fetchClaudeModelDiscovery(); }, [fetchClaudeModelDiscovery, form.apiKey, form.baseUrl, form.headers, initialLoading]); + useEffect(() => { + const availableNames = new Set(models.map((model) => model.name)); + setSelected((prev) => { + let changed = false; + const next = new Set(); + prev.forEach((name) => { + if (availableNames.has(name)) { + next.add(name); + } else { + changed = true; + } + }); + return changed ? next : prev; + }); + }, [models]); + const handleBack = useCallback(() => { navigate(-1); }, [navigate]); @@ -145,6 +170,18 @@ export function AiProvidersClaudeModelsPage() { }); }; + const handleSelectVisible = useCallback(() => { + setSelected((prev) => { + const next = new Set(prev); + visibleModelNames.forEach((name) => next.add(name)); + return next; + }); + }, [visibleModelNames]); + + const handleClearSelection = useCallback(() => { + setSelected(new Set()); + }, []); + const handleApply = () => { const selectedModels = models.filter((model) => selected.has(model.name)); if (selectedModels.length) { @@ -153,7 +190,7 @@ export function AiProvidersClaudeModelsPage() { handleBack(); }; - const canApply = !disableControls && !saving && !fetching; + const canApply = !disableControls && !saving && !fetching && selected.size > 0; return ( setSearch(e.target.value)} disabled={fetching} /> + {models.length > 0 && ( +
+
+ + +
+
+ {t('ai_providers.model_discovery_selected_count', { count: selected.size })} +
+
+ )} {error &&
{error}
} {fetching ? ( -
{t('ai_providers.claude_models_fetch_loading')}
+
+ {t('ai_providers.claude_models_fetch_loading')} +
) : models.length === 0 ? (
{t('ai_providers.claude_models_fetch_empty')}
) : filteredModels.length === 0 ? ( @@ -231,29 +301,30 @@ export function AiProvidersClaudeModelsPage() { {filteredModels.map((model) => { const checked = selected.has(model.name); return ( -