diff --git a/src/components/ProviderForm/ApiKeyInput.tsx b/src/components/ProviderForm/ApiKeyInput.tsx new file mode 100644 index 0000000..d78fd0d --- /dev/null +++ b/src/components/ProviderForm/ApiKeyInput.tsx @@ -0,0 +1,72 @@ +import React, { useState } from "react"; +import { Eye, EyeOff } from "lucide-react"; +import { useTranslation } from "react-i18next"; + +interface ApiKeyInputProps { + value: string; + onChange: (value: string) => void; + placeholder?: string; + disabled?: boolean; + required?: boolean; + label?: string; + id?: string; +} + +const ApiKeyInput: React.FC = ({ + value, + onChange, + placeholder, + disabled = false, + required = false, + label = "API Key", + id = "apiKey", +}) => { + const { t } = useTranslation(); + const [showKey, setShowKey] = useState(false); + + const toggleShowKey = () => { + setShowKey(!showKey); + }; + + const inputClass = `w-full px-3 py-2 pr-10 border rounded-lg text-sm transition-colors ${ + disabled + ? "bg-gray-100 dark:bg-gray-800 border-gray-200 dark:border-gray-700 text-gray-400 dark:text-gray-500 cursor-not-allowed" + : "border-gray-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:focus:ring-blue-400/20 focus:border-blue-500 dark:focus:border-blue-400" + }`; + + return ( +
+ +
+ onChange(e.target.value)} + placeholder={placeholder ?? t("apiKeyInput.placeholder")} + disabled={disabled} + required={required} + autoComplete="off" + className={inputClass} + /> + {!disabled && value && ( + + )} +
+
+ ); +}; + +export default ApiKeyInput; diff --git a/src/components/ProviderForm/ClaudeConfigEditor.tsx b/src/components/ProviderForm/ClaudeConfigEditor.tsx new file mode 100644 index 0000000..1def4e6 --- /dev/null +++ b/src/components/ProviderForm/ClaudeConfigEditor.tsx @@ -0,0 +1,205 @@ +import React, { useEffect, useState } from "react"; +import JsonEditor from "../JsonEditor"; +import { X, Save } from "lucide-react"; +import { isLinux } from "../../lib/platform"; +import { useTranslation } from "react-i18next"; + +interface ClaudeConfigEditorProps { + value: string; + onChange: (value: string) => void; + useCommonConfig: boolean; + onCommonConfigToggle: (checked: boolean) => void; + commonConfigSnippet: string; + onCommonConfigSnippetChange: (value: string) => void; + commonConfigError: string; + configError: string; +} + +const ClaudeConfigEditor: React.FC = ({ + value, + onChange, + useCommonConfig, + onCommonConfigToggle, + commonConfigSnippet, + onCommonConfigSnippetChange, + commonConfigError, + configError, +}) => { + const { t } = useTranslation(); + const [isDarkMode, setIsDarkMode] = useState(false); + const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false); + + useEffect(() => { + // 检测暗色模式 + const checkDarkMode = () => { + setIsDarkMode(document.documentElement.classList.contains("dark")); + }; + + checkDarkMode(); + + // 监听暗色模式变化 + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.attributeName === "class") { + checkDarkMode(); + } + }); + }); + + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ["class"], + }); + + return () => observer.disconnect(); + }, []); + + useEffect(() => { + if (commonConfigError && !isCommonConfigModalOpen) { + setIsCommonConfigModalOpen(true); + } + }, [commonConfigError, isCommonConfigModalOpen]); + + // 支持按下 ESC 关闭弹窗 + useEffect(() => { + if (!isCommonConfigModalOpen) return; + + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") { + e.preventDefault(); + closeModal(); + } + }; + window.addEventListener("keydown", onKeyDown); + return () => window.removeEventListener("keydown", onKeyDown); + }, [isCommonConfigModalOpen]); + + const closeModal = () => { + setIsCommonConfigModalOpen(false); + }; + return ( +
+
+ + +
+
+ +
+ {commonConfigError && !isCommonConfigModalOpen && ( +

+ {commonConfigError} +

+ )} + + {configError && ( +

{configError}

+ )} +

+ {t("claudeConfig.fullSettingsHint")} +

+ {isCommonConfigModalOpen && ( +
{ + if (e.target === e.currentTarget) closeModal(); + }} + > + {/* Backdrop - 统一背景样式 */} +
+ + {/* Modal - 统一窗口样式 */} +
+ {/* Header - 统一标题栏样式 */} +
+

+ {t("claudeConfig.editCommonConfigTitle")} +

+ +
+ + {/* Content - 统一内容区域样式 */} +
+

+ {t("claudeConfig.commonConfigHint")} +

+ + {commonConfigError && ( +

+ {commonConfigError} +

+ )} +
+ + {/* Footer - 统一底部按钮样式 */} +
+ + +
+
+
+ )} +
+ ); +}; + +export default ClaudeConfigEditor; diff --git a/src/components/ProviderForm/CodexConfigEditor.tsx b/src/components/ProviderForm/CodexConfigEditor.tsx new file mode 100644 index 0000000..17f6201 --- /dev/null +++ b/src/components/ProviderForm/CodexConfigEditor.tsx @@ -0,0 +1,667 @@ +import React, { useState, useEffect, useRef } from "react"; + +import { X, Save } from "lucide-react"; + +import { isLinux } from "../../lib/platform"; +import { useTranslation } from "react-i18next"; + +import { + generateThirdPartyAuth, + generateThirdPartyConfig, +} from "../../config/codexProviderPresets"; + +interface CodexConfigEditorProps { + authValue: string; + + configValue: string; + + onAuthChange: (value: string) => void; + + onConfigChange: (value: string) => void; + + onAuthBlur?: () => void; + + useCommonConfig: boolean; + + onCommonConfigToggle: (checked: boolean) => void; + + commonConfigSnippet: string; + + onCommonConfigSnippetChange: (value: string) => void; + + commonConfigError: string; + + authError: string; + + isCustomMode?: boolean; // 新增:是否为自定义模式 + + onWebsiteUrlChange?: (url: string) => void; // 新增:更新网址回调 + + isTemplateModalOpen?: boolean; // 新增:模态框状态 + + setIsTemplateModalOpen?: (open: boolean) => void; // 新增:设置模态框状态 + + onNameChange?: (name: string) => void; // 新增:更新供应商名称回调 +} + +const CodexConfigEditor: React.FC = ({ + authValue, + + configValue, + + onAuthChange, + + onConfigChange, + + onAuthBlur, + + useCommonConfig, + + onCommonConfigToggle, + + commonConfigSnippet, + + onCommonConfigSnippetChange, + + commonConfigError, + + authError, + + onWebsiteUrlChange, + + onNameChange, + + isTemplateModalOpen: externalTemplateModalOpen, + + setIsTemplateModalOpen: externalSetTemplateModalOpen, +}) => { + const { t } = useTranslation(); + const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false); + + // 使用内部状态或外部状态 + + const [internalTemplateModalOpen, setInternalTemplateModalOpen] = + useState(false); + + const isTemplateModalOpen = + externalTemplateModalOpen ?? internalTemplateModalOpen; + + const setIsTemplateModalOpen = + externalSetTemplateModalOpen ?? setInternalTemplateModalOpen; + + const [templateApiKey, setTemplateApiKey] = useState(""); + + const [templateProviderName, setTemplateProviderName] = useState(""); + + const [templateBaseUrl, setTemplateBaseUrl] = useState(""); + + const [templateWebsiteUrl, setTemplateWebsiteUrl] = useState(""); + + const [templateModelName, setTemplateModelName] = useState("gpt-5-codex"); + const apiKeyInputRef = useRef(null); + + const baseUrlInputRef = useRef(null); + + const modelNameInputRef = useRef(null); + const displayNameInputRef = useRef(null); + + // 移除自动填充逻辑,因为现在在点击自定义按钮时就已经填充 + + const [templateDisplayName, setTemplateDisplayName] = useState(""); + + useEffect(() => { + if (commonConfigError && !isCommonConfigModalOpen) { + setIsCommonConfigModalOpen(true); + } + }, [commonConfigError, isCommonConfigModalOpen]); + + // 支持按下 ESC 关闭弹窗 + + useEffect(() => { + if (!isCommonConfigModalOpen) return; + + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") { + e.preventDefault(); + + closeModal(); + } + }; + + window.addEventListener("keydown", onKeyDown); + + return () => window.removeEventListener("keydown", onKeyDown); + }, [isCommonConfigModalOpen]); + + const closeModal = () => { + setIsCommonConfigModalOpen(false); + }; + + const closeTemplateModal = () => { + setIsTemplateModalOpen(false); + }; + + const applyTemplate = () => { + const requiredInputs = [ + displayNameInputRef.current, + apiKeyInputRef.current, + baseUrlInputRef.current, + modelNameInputRef.current, + ]; + + for (const input of requiredInputs) { + if (input && !input.checkValidity()) { + input.reportValidity(); + input.focus(); + return; + } + } + + const trimmedKey = templateApiKey.trim(); + + const trimmedBaseUrl = templateBaseUrl.trim(); + + const trimmedModel = templateModelName.trim(); + + const auth = generateThirdPartyAuth(trimmedKey); + + const config = generateThirdPartyConfig( + templateProviderName || "custom", + + trimmedBaseUrl, + + trimmedModel, + ); + + onAuthChange(JSON.stringify(auth, null, 2)); + + onConfigChange(config); + + if (onWebsiteUrlChange) { + const trimmedWebsite = templateWebsiteUrl.trim(); + + if (trimmedWebsite) { + onWebsiteUrlChange(trimmedWebsite); + } + } + + if (onNameChange) { + const trimmedName = templateDisplayName.trim(); + if (trimmedName) { + onNameChange(trimmedName); + } + } + + setTemplateApiKey(""); + + setTemplateProviderName(""); + + setTemplateBaseUrl(""); + + setTemplateWebsiteUrl(""); + + setTemplateModelName("gpt-5-codex"); + + setTemplateDisplayName(""); + + closeTemplateModal(); + }; + + const handleTemplateInputKeyDown = ( + e: React.KeyboardEvent, + ) => { + if (e.key === "Enter") { + e.preventDefault(); + + e.stopPropagation(); + + applyTemplate(); + } + }; + + const handleAuthChange = (value: string) => { + onAuthChange(value); + }; + + const handleConfigChange = (value: string) => { + onConfigChange(value); + }; + + const handleCommonConfigSnippetChange = (value: string) => { + onCommonConfigSnippetChange(value); + }; + + return ( +
+
+ + +