import { useCallback, useState } from "react"; import { useTranslation } from "react-i18next"; import { Plus } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { FullScreenPanel } from "@/components/common/FullScreenPanel"; import type { Provider, CustomEndpoint, UniversalProvider } from "@/types"; import type { AppId } from "@/lib/api"; import { universalProvidersApi } from "@/lib/api"; import { ProviderForm, type ProviderFormValues, } from "@/components/providers/forms/ProviderForm"; import { UniversalProviderFormModal } from "@/components/universal/UniversalProviderFormModal"; import { UniversalProviderPanel } from "@/components/universal"; import { providerPresets } from "@/config/claudeProviderPresets"; import { codexProviderPresets } from "@/config/codexProviderPresets"; import { geminiProviderPresets } from "@/config/geminiProviderPresets"; // Note: opencodeProviderPresets is loaded via ProviderForm, not needed here import type { UniversalProviderPreset } from "@/config/universalProviderPresets"; interface AddProviderDialogProps { open: boolean; onOpenChange: (open: boolean) => void; appId: AppId; onSubmit: (provider: Omit & { providerKey?: string }) => Promise | void; } export function AddProviderDialog({ open, onOpenChange, appId, onSubmit, }: AddProviderDialogProps) { const { t } = useTranslation(); // OpenCode doesn't support universal providers const showUniversalTab = appId !== "opencode"; const [activeTab, setActiveTab] = useState<"app-specific" | "universal">( "app-specific", ); const [universalFormOpen, setUniversalFormOpen] = useState(false); const [selectedUniversalPreset, setSelectedUniversalPreset] = useState(null); // Handle universal provider save const handleUniversalProviderSave = useCallback( async (provider: UniversalProvider) => { try { await universalProvidersApi.upsert(provider); toast.success( t("universalProvider.addSuccess", { defaultValue: "统一供应商添加成功", }), ); setUniversalFormOpen(false); setSelectedUniversalPreset(null); onOpenChange(false); } catch (error) { console.error( "[AddProviderDialog] Failed to save universal provider", error, ); toast.error( t("universalProvider.addFailed", { defaultValue: "统一供应商添加失败", }), ); } }, [t, onOpenChange], ); // Close universal form and return to main dialog const handleUniversalFormClose = useCallback(() => { setUniversalFormOpen(false); setSelectedUniversalPreset(null); }, []); const handleSubmit = useCallback( async (values: ProviderFormValues) => { const parsedConfig = JSON.parse(values.settingsConfig) as Record< string, unknown >; // 构造基础提交数据 const providerData: Omit & { providerKey?: string } = { name: values.name.trim(), notes: values.notes?.trim() || undefined, websiteUrl: values.websiteUrl?.trim() || undefined, settingsConfig: parsedConfig, icon: values.icon?.trim() || undefined, iconColor: values.iconColor?.trim() || undefined, ...(values.presetCategory ? { category: values.presetCategory } : {}), ...(values.meta ? { meta: values.meta } : {}), }; // OpenCode: pass providerKey for ID generation if (appId === "opencode" && values.providerKey) { providerData.providerKey = values.providerKey; } const hasCustomEndpoints = providerData.meta?.custom_endpoints && Object.keys(providerData.meta.custom_endpoints).length > 0; if (!hasCustomEndpoints) { // 收集端点候选(仅在缺少自定义端点时兜底) const urlSet = new Set(); const addUrl = (rawUrl?: string) => { const url = (rawUrl || "").trim().replace(/\/+$/, ""); if (url && url.startsWith("http")) { urlSet.add(url); } }; if (values.presetId) { if (appId === "claude") { const presets = providerPresets; const presetIndex = parseInt( values.presetId.replace("claude-", ""), ); if ( !isNaN(presetIndex) && presetIndex >= 0 && presetIndex < presets.length ) { const preset = presets[presetIndex]; if (preset?.endpointCandidates) { preset.endpointCandidates.forEach(addUrl); } } } else if (appId === "codex") { const presets = codexProviderPresets; const presetIndex = parseInt(values.presetId.replace("codex-", "")); if ( !isNaN(presetIndex) && presetIndex >= 0 && presetIndex < presets.length ) { const preset = presets[presetIndex]; if (Array.isArray(preset.endpointCandidates)) { preset.endpointCandidates.forEach(addUrl); } } } else if (appId === "gemini") { const presets = geminiProviderPresets; const presetIndex = parseInt( values.presetId.replace("gemini-", ""), ); if ( !isNaN(presetIndex) && presetIndex >= 0 && presetIndex < presets.length ) { const preset = presets[presetIndex]; if (Array.isArray(preset.endpointCandidates)) { preset.endpointCandidates.forEach(addUrl); } } } // Note: OpenCode doesn't use endpointCandidates - it handles endpoints internally } if (appId === "claude") { const env = parsedConfig.env as Record | undefined; if (env?.ANTHROPIC_BASE_URL) { addUrl(env.ANTHROPIC_BASE_URL); } } else if (appId === "codex") { const config = parsedConfig.config as string | undefined; if (config) { const baseUrlMatch = config.match( /base_url\s*=\s*["']([^"']+)["']/, ); if (baseUrlMatch?.[1]) { addUrl(baseUrlMatch[1]); } } } else if (appId === "gemini") { const env = parsedConfig.env as Record | undefined; if (env?.GOOGLE_GEMINI_BASE_URL) { addUrl(env.GOOGLE_GEMINI_BASE_URL); } } else if (appId === "opencode") { // OpenCode uses options.baseURL const options = parsedConfig.options as Record | undefined; if (options?.baseURL) { addUrl(options.baseURL); } } const urls = Array.from(urlSet); if (urls.length > 0) { const now = Date.now(); const customEndpoints: Record = {}; urls.forEach((url) => { customEndpoints[url] = { url, addedAt: now, lastUsed: undefined, }; }); providerData.meta = { ...(providerData.meta ?? {}), custom_endpoints: customEndpoints, }; } } await onSubmit(providerData); onOpenChange(false); }, [appId, onSubmit, onOpenChange], ); // 动态 footer:根据当前 Tab 显示不同按钮 const footer = !showUniversalTab || activeTab === "app-specific" ? ( <> ) : ( <> ); return ( onOpenChange(false)} footer={footer} > {showUniversalTab ? ( setActiveTab(v as "app-specific" | "universal")} > {t(`apps.${appId}`)} {t("provider.tabProvider")} {t("provider.tabUniversal")} onOpenChange(false)} showButtons={false} /> ) : ( // OpenCode: directly show form without tabs onOpenChange(false)} showButtons={false} /> )} {/* Universal Provider Form Modal */} {showUniversalTab && ( )} ); }