import { App, Button, Checkbox, Input, Modal, Tabs } from "antd"; import { RefreshCw, Search } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { fetchChannelModels } from "@/services/api/image"; import type { ModelChannel } from "@/stores/use-config-store"; // 选择渠道模型弹窗:拉取上游模型列表或手动增加,勾选后才会进入渠道模型列表。 export function ModelSelectModal({ open, channel, selectedNames, onConfirm, onClose }: { open: boolean; channel: ModelChannel | null; selectedNames: string[]; onConfirm: (names: string[]) => void; onClose: () => void }) { const { message } = App.useApp(); const { t } = useTranslation(); const [existing, setExisting] = useState([]); const [fetched, setFetched] = useState([]); const [selected, setSelected] = useState>(new Set()); const [activeTab, setActiveTab] = useState("new"); const [search, setSearch] = useState(""); const [manual, setManual] = useState(""); const [loading, setLoading] = useState(false); useEffect(() => { if (!open) return; setExisting(selectedNames); setFetched([]); setSelected(new Set(selectedNames)); setActiveTab(selectedNames.length ? "existing" : "new"); setSearch(""); setManual(""); }, [open, selectedNames]); const currentList = activeTab === "new" ? fetched : existing; const visibleList = useMemo(() => { const keyword = search.trim().toLowerCase(); return keyword ? currentList.filter((name) => name.toLowerCase().includes(keyword)) : currentList; }, [currentList, search]); const visibleSelectedCount = visibleList.filter((name) => selected.has(name)).length; const toggle = (name: string, checked: boolean) => setSelected((current) => { const next = new Set(current); if (checked) next.add(name); else next.delete(name); return next; }); const selectVisible = (checked: boolean) => setSelected((current) => { const next = new Set(current); visibleList.forEach((name) => (checked ? next.add(name) : next.delete(name))); return next; }); const addManual = () => { const name = manual.trim(); if (!name) return; if (!fetched.includes(name) && !existing.includes(name)) setFetched((current) => [name, ...current]); setSelected((current) => new Set(current).add(name)); setManual(""); setActiveTab("new"); }; const fetchModels = async () => { if (!channel) return; if (!channel.baseUrl.trim() || !channel.apiKey.trim()) { message.error(t("config.modelSelect.missingConfig")); return; } setLoading(true); try { const models = await fetchChannelModels(channel); setFetched(models); setActiveTab("new"); message.success(t("config.modelSelect.fetched", { count: models.length })); } catch (error) { message.error(error instanceof Error ? error.message : t("config.modelSelect.fetchFailed")); } finally { setLoading(false); } }; const confirm = () => { const ordered = [...existing, ...fetched].filter((name, index, list) => list.indexOf(name) === index).filter((name) => selected.has(name)); onConfirm(ordered); onClose(); }; return ( {t("config.modelSelect.title")} {t("config.modelSelect.selected", { selected: selected.size, total: new Set([...existing, ...fetched]).size })} } styles={{ body: { maxHeight: "62vh", overflowY: "auto" } }} footer={[ , , ]} >
setSearch(event.target.value)} placeholder={t("config.modelSelect.search")} prefix={} allowClear /> setManual(event.target.value)} onPressEnter={addManual} placeholder={t("config.modelSelect.modelName")} />
{t("config.modelSelect.description")}
{t("config.modelSelect.visibleSelected", { selected: visibleSelectedCount, total: visibleList.length })}
{visibleList.length ? (
{visibleList.map((name) => ( toggle(name, event.target.checked)}> {name} ))}
) : (
{t(activeTab === "new" ? "config.modelSelect.fetchedEmpty" : "config.modelSelect.existingEmpty")}
)}
); }