import { useEffect, useMemo, useState } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { useTranslation } from "react-i18next"; import { Button } from "@/components/ui/button"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { useTheme } from "@/components/theme-provider"; import JsonEditor from "@/components/JsonEditor"; import { providerSchema, type ProviderFormData } from "@/lib/schemas/provider"; import type { AppType } from "@/lib/api"; import type { ProviderCategory } from "@/types"; import { providerPresets, type ProviderPreset } from "@/config/providerPresets"; import { codexProviderPresets, type CodexProviderPreset, } from "@/config/codexProviderPresets"; import { applyTemplateValues } from "@/utils/providerConfigUtils"; import ApiKeyInput from "@/components/ProviderForm/ApiKeyInput"; import EndpointSpeedTest from "@/components/ProviderForm/EndpointSpeedTest"; import { Zap } from "lucide-react"; import { useProviderCategory, useApiKeyState, useBaseUrlState } from "./hooks"; const CLAUDE_DEFAULT_CONFIG = JSON.stringify({ env: {}, config: {} }, null, 2); const CODEX_DEFAULT_CONFIG = JSON.stringify({ auth: {}, config: "" }, null, 2); type PresetEntry = { id: string; preset: ProviderPreset | CodexProviderPreset; }; interface ProviderFormProps { appType: AppType; submitLabel: string; onSubmit: (values: ProviderFormValues) => void; onCancel: () => void; initialData?: { name?: string; websiteUrl?: string; settingsConfig?: Record; }; } export function ProviderForm({ appType, submitLabel, onSubmit, onCancel, initialData, }: ProviderFormProps) { const { t } = useTranslation(); const { theme } = useTheme(); const isEditMode = Boolean(initialData); const [selectedPresetId, setSelectedPresetId] = useState( initialData ? null : "custom", ); const [activePreset, setActivePreset] = useState<{ id: string; category?: ProviderCategory; } | null>(null); const [isEndpointModalOpen, setIsEndpointModalOpen] = useState(false); // 使用 category hook const { category } = useProviderCategory({ appType, selectedPresetId, isEditMode, }); useEffect(() => { setSelectedPresetId(initialData ? null : "custom"); setActivePreset(null); }, [appType, initialData]); const defaultValues: ProviderFormData = useMemo( () => ({ name: initialData?.name ?? "", websiteUrl: initialData?.websiteUrl ?? "", settingsConfig: initialData?.settingsConfig ? JSON.stringify(initialData.settingsConfig, null, 2) : appType === "codex" ? CODEX_DEFAULT_CONFIG : CLAUDE_DEFAULT_CONFIG, }), [initialData, appType], ); const form = useForm({ resolver: zodResolver(providerSchema), defaultValues, mode: "onSubmit", }); // 使用 API Key hook const { apiKey, handleApiKeyChange, showApiKey: shouldShowApiKey } = useApiKeyState({ initialConfig: form.watch("settingsConfig"), onConfigChange: (config) => form.setValue("settingsConfig", config), selectedPresetId, }); // 使用 Base URL hook const { baseUrl, // codexBaseUrl, // TODO: 等 Codex 支持时使用 handleClaudeBaseUrlChange, // handleCodexBaseUrlChange, // TODO: 等 Codex 支持时使用 } = useBaseUrlState({ appType, category, settingsConfig: form.watch("settingsConfig"), codexConfig: "", // TODO: 从 settingsConfig 中提取 codex config onSettingsConfigChange: (config) => form.setValue("settingsConfig", config), onCodexConfigChange: () => { // TODO: 更新 codex config }, }); useEffect(() => { form.reset(defaultValues); }, [defaultValues, form]); const isDarkMode = useMemo(() => { if (theme === "dark") return true; if (theme === "light") return false; return typeof window !== "undefined" ? window.document.documentElement.classList.contains("dark") : false; }, [theme]); const handleSubmit = (values: ProviderFormData) => { const payload: ProviderFormValues = { ...values, name: values.name.trim(), websiteUrl: values.websiteUrl?.trim() ?? "", settingsConfig: values.settingsConfig.trim(), }; if (activePreset) { payload.presetId = activePreset.id; if (activePreset.category) { payload.presetCategory = activePreset.category; } } onSubmit(payload); }; const presetCategoryLabels: Record = useMemo( () => ({ official: t("providerPreset.categoryOfficial", { defaultValue: "官方推荐", }), cn_official: t("providerPreset.categoryCnOfficial", { defaultValue: "国内官方", }), aggregator: t("providerPreset.categoryAggregator", { defaultValue: "聚合服务", }), third_party: t("providerPreset.categoryThirdParty", { defaultValue: "第三方", }), }), [t], ); const presetEntries = useMemo(() => { if (appType === "codex") { return codexProviderPresets.map((preset, index) => ({ id: `codex-${index}`, preset, })); } return providerPresets.map((preset, index) => ({ id: `claude-${index}`, preset, })); }, [appType]); const groupedPresets = useMemo(() => { return presetEntries.reduce>((acc, entry) => { const category = entry.preset.category ?? "others"; if (!acc[category]) { acc[category] = []; } acc[category].push(entry); return acc; }, {}); }, [presetEntries]); const categoryKeys = useMemo(() => { return Object.keys(groupedPresets).filter( (key) => key !== "custom" && groupedPresets[key]?.length, ); }, [groupedPresets]); // 判断是否显示端点测速(仅第三方和自定义类别) const shouldShowSpeedTest = category === "third_party" || category === "custom"; const handlePresetChange = (value: string) => { setSelectedPresetId(value); if (value === "custom") { setActivePreset(null); form.reset(defaultValues); return; } const entry = presetEntries.find((item) => item.id === value); if (!entry) { return; } setActivePreset({ id: value, category: entry.preset.category, }); if (appType === "codex") { const preset = entry.preset as CodexProviderPreset; const config = { auth: preset.auth ?? {}, config: preset.config ?? "", }; form.reset({ name: preset.name, websiteUrl: preset.websiteUrl ?? "", settingsConfig: JSON.stringify(config, null, 2), }); return; } const preset = entry.preset as ProviderPreset; const config = applyTemplateValues( preset.settingsConfig, preset.templateValues, ); form.reset({ name: preset.name, websiteUrl: preset.websiteUrl ?? "", settingsConfig: JSON.stringify(config, null, 2), }); }; return (
{/* 预设供应商选择(仅新增模式显示) */} {!initialData && (
{t("providerPreset.label", { defaultValue: "预设供应商" })}
{/* 自定义按钮 */} {/* 预设按钮 */} {categoryKeys.map((category) => { const entries = groupedPresets[category]; if (!entries || entries.length === 0) return null; return entries.map((entry) => ( )); })}

{t("providerPreset.helper", { defaultValue: "选择预设后可继续调整下方字段。", })}

)} ( {t("provider.name", { defaultValue: "供应商名称" })} )} /> ( {t("provider.websiteUrl", { defaultValue: "官网链接" })} )} /> {/* API Key 输入框(仅 Claude 且非编辑模式显示) */} {appType === "claude" && shouldShowApiKey(form.watch("settingsConfig"), isEditMode) && (
)} {/* Base URL 输入框(仅 Claude 第三方/自定义显示) */} {appType === "claude" && shouldShowSpeedTest && (
{t("providerForm.apiEndpoint", { defaultValue: "API 端点" })}
handleClaudeBaseUrlChange(e.target.value)} placeholder={t("providerForm.apiEndpointPlaceholder", { defaultValue: "https://api.example.com" })} autoComplete="off" />

{t("providerForm.apiHint", { defaultValue: "API 端点地址用于连接服务器" })}

)} {/* 端点测速弹窗 - Claude */} {appType === "claude" && shouldShowSpeedTest && isEndpointModalOpen && ( setIsEndpointModalOpen(false)} /> )} ( {t("provider.configJson", { defaultValue: "配置 JSON" })}
)} />
); } export type ProviderFormValues = ProviderFormData & { presetId?: string; presetCategory?: ProviderCategory; };