refactor(codex): remove configuration wizard and unify provider setup experience

- Remove CodexQuickWizardModal component (~300 lines)
- Add "Custom (Blank Template)" preset with annotated TOML template
- Unify configuration experience across Claude/Codex/Gemini
- Remove wizard-related i18n keys, keep apiUrlLabel for CodexFormFields
- Simplify component integration by removing wizard state management

This change reduces code complexity by ~250 lines while providing better
user education through commented configuration templates in Chinese.

Users can now:
1. Select "Custom (Blank Template)" preset
2. See annotated TOML template with inline documentation
3. Follow step-by-step comments to configure custom providers

BREAKING CHANGE: Configuration wizard UI removed, replaced with template-based approach
This commit is contained in:
Jason
2025-11-16 12:29:18 +08:00
parent 031ea3a58f
commit 6a6980c82c
7 changed files with 49 additions and 419 deletions

View File

@@ -1,6 +1,5 @@
import React, { useState, useEffect } from "react";
import { CodexAuthSection, CodexConfigSection } from "./CodexConfigSections";
import { CodexQuickWizardModal } from "./CodexQuickWizardModal";
import { CodexCommonConfigModal } from "./CodexCommonConfigModal";
interface CodexConfigEditorProps {
@@ -27,14 +26,6 @@ interface CodexConfigEditorProps {
authError: string;
configError: string; // config.toml 错误提示
onWebsiteUrlChange?: (url: string) => void; // 更新网址回调
isTemplateModalOpen?: boolean; // 模态框状态
setIsTemplateModalOpen?: (open: boolean) => void; // 设置模态框状态
onNameChange?: (name: string) => void; // 更新供应商名称回调
}
const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
@@ -50,21 +41,9 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
commonConfigError,
authError,
configError,
onWebsiteUrlChange,
onNameChange,
isTemplateModalOpen: externalTemplateModalOpen,
setIsTemplateModalOpen: externalSetTemplateModalOpen,
}) => {
const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false);
// Use internal state or external state
const [internalTemplateModalOpen, setInternalTemplateModalOpen] =
useState(false);
const isTemplateModalOpen =
externalTemplateModalOpen ?? internalTemplateModalOpen;
const setIsTemplateModalOpen =
externalSetTemplateModalOpen ?? setInternalTemplateModalOpen;
// Auto-open common config modal if there's an error
useEffect(() => {
if (commonConfigError && !isCommonConfigModalOpen) {
@@ -72,23 +51,6 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
}
}, [commonConfigError, isCommonConfigModalOpen]);
const handleQuickWizardApply = (
auth: string,
config: string,
extras: { websiteUrl?: string; displayName?: string },
) => {
onAuthChange(auth);
onConfigChange(config);
if (onWebsiteUrlChange && extras.websiteUrl) {
onWebsiteUrlChange(extras.websiteUrl);
}
if (onNameChange && extras.displayName) {
onNameChange(extras.displayName);
}
};
return (
<div className="space-y-6">
{/* Auth JSON Section */}
@@ -110,13 +72,6 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
configError={configError}
/>
{/* Quick Wizard Modal */}
<CodexQuickWizardModal
isOpen={isTemplateModalOpen}
onClose={() => setIsTemplateModalOpen(false)}
onApply={handleQuickWizardApply}
/>
{/* Common Config Modal */}
<CodexCommonConfigModal
isOpen={isCommonConfigModalOpen}

View File

@@ -1,298 +0,0 @@
import React, { useState, useRef } from "react";
import { Save } from "lucide-react";
import { useTranslation } from "react-i18next";
import {
generateThirdPartyAuth,
generateThirdPartyConfig,
} from "@/config/codexProviderPresets";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
interface CodexQuickWizardModalProps {
isOpen: boolean;
onClose: () => void;
onApply: (
auth: string,
config: string,
extras: {
websiteUrl?: string;
displayName?: string;
},
) => void;
}
/**
* CodexQuickWizardModal - Codex quick configuration wizard
* Helps users quickly generate auth.json and config.toml
*/
export const CodexQuickWizardModal: React.FC<CodexQuickWizardModalProps> = ({
isOpen,
onClose,
onApply,
}) => {
const { t } = useTranslation();
const [templateApiKey, setTemplateApiKey] = useState("");
const [templateProviderName, setTemplateProviderName] = useState("");
const [templateBaseUrl, setTemplateBaseUrl] = useState("");
const [templateWebsiteUrl, setTemplateWebsiteUrl] = useState("");
const [templateModelName, setTemplateModelName] = useState("gpt-5-codex");
const [templateDisplayName, setTemplateDisplayName] = useState("");
const apiKeyInputRef = useRef<HTMLInputElement>(null);
const baseUrlInputRef = useRef<HTMLInputElement>(null);
const modelNameInputRef = useRef<HTMLInputElement>(null);
const displayNameInputRef = useRef<HTMLInputElement>(null);
const resetForm = () => {
setTemplateApiKey("");
setTemplateProviderName("");
setTemplateBaseUrl("");
setTemplateWebsiteUrl("");
setTemplateModelName("gpt-5-codex");
setTemplateDisplayName("");
};
const handleClose = () => {
resetForm();
onClose();
};
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,
);
onApply(JSON.stringify(auth, null, 2), config, {
websiteUrl: templateWebsiteUrl.trim(),
displayName: templateDisplayName.trim(),
});
resetForm();
onClose();
};
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
e.preventDefault();
e.stopPropagation();
applyTemplate();
}
};
return (
<Dialog open={isOpen} onOpenChange={(open) => !open && handleClose()}>
<DialogContent
zIndex="nested"
className="max-w-2xl max-h-[90vh] flex flex-col p-0"
>
<DialogHeader className="px-6 pt-6 pb-0">
<DialogTitle>{t("codexConfig.quickWizard")}</DialogTitle>
</DialogHeader>
<div className="flex-1 min-h-0 space-y-4 overflow-auto px-6 py-4">
<div className="rounded-lg border border-blue-200 bg-blue-50 p-3 dark:border-blue-800 dark:bg-blue-900/20">
<p className="text-sm text-blue-800 dark:text-blue-200">
{t("codexConfig.wizardHint")}
</p>
</div>
<div className="space-y-4">
{/* API Key */}
<div>
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
{t("codexConfig.apiKeyLabel")}
</label>
<Input
type="text"
value={templateApiKey}
ref={apiKeyInputRef}
onChange={(e) => setTemplateApiKey(e.target.value)}
onKeyDown={handleInputKeyDown}
pattern=".*\S.*"
title={t("common.enterValidValue")}
placeholder={t("codexConfig.apiKeyPlaceholder")}
required
className="font-mono"
/>
</div>
{/* Display Name */}
<div>
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
{t("codexConfig.supplierNameLabel")}
</label>
<Input
type="text"
value={templateDisplayName}
ref={displayNameInputRef}
onChange={(e) => setTemplateDisplayName(e.target.value)}
onKeyDown={handleInputKeyDown}
placeholder={t("codexConfig.supplierNamePlaceholder")}
required
pattern=".*\S.*"
title={t("common.enterValidValue")}
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{t("codexConfig.supplierNameHint")}
</p>
</div>
{/* Provider Name */}
<div>
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
{t("codexConfig.supplierCodeLabel")}
</label>
<Input
type="text"
value={templateProviderName}
onChange={(e) => setTemplateProviderName(e.target.value)}
onKeyDown={handleInputKeyDown}
placeholder={t("codexConfig.supplierCodePlaceholder")}
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{t("codexConfig.supplierCodeHint")}
</p>
</div>
{/* Base URL */}
<div>
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
{t("codexConfig.apiUrlLabel")}
</label>
<Input
type="url"
value={templateBaseUrl}
ref={baseUrlInputRef}
onChange={(e) => setTemplateBaseUrl(e.target.value)}
onKeyDown={handleInputKeyDown}
placeholder={t("codexConfig.apiUrlPlaceholder")}
required
className="font-mono"
/>
</div>
{/* Website URL */}
<div>
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
{t("codexConfig.websiteLabel")}
</label>
<Input
type="url"
value={templateWebsiteUrl}
onChange={(e) => setTemplateWebsiteUrl(e.target.value)}
onKeyDown={handleInputKeyDown}
placeholder={t("codexConfig.websitePlaceholder")}
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{t("codexConfig.websiteHint")}
</p>
</div>
{/* Model Name */}
<div>
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
{t("codexConfig.modelNameLabel")}
</label>
<Input
type="text"
value={templateModelName}
ref={modelNameInputRef}
onChange={(e) => setTemplateModelName(e.target.value)}
onKeyDown={handleInputKeyDown}
pattern=".*\S.*"
title={t("common.enterValidValue")}
placeholder={t("codexConfig.modelNamePlaceholder")}
required
/>
</div>
</div>
{/* Preview */}
{(templateApiKey || templateProviderName || templateBaseUrl) && (
<div className="space-y-2 border-t border-border-default pt-4 ">
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">
{t("codexConfig.configPreview")}
</h3>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
<div>
<label className="mb-1 block text-xs font-medium text-gray-500 dark:text-gray-400">
auth.json
</label>
<pre className="overflow-x-auto rounded-lg bg-gray-50 p-3 text-xs font-mono text-gray-700 dark:bg-gray-800 dark:text-gray-300">
{JSON.stringify(
generateThirdPartyAuth(templateApiKey),
null,
2,
)}
</pre>
</div>
<div>
<label className="mb-1 block text-xs font-medium text-gray-500 dark:text-gray-400">
config.toml
</label>
<pre className="whitespace-pre-wrap rounded-lg bg-gray-50 p-3 text-xs font-mono text-gray-700 dark:bg-gray-800 dark:text-gray-300">
{templateProviderName && templateBaseUrl
? generateThirdPartyConfig(
templateProviderName,
templateBaseUrl,
templateModelName,
)
: ""}
</pre>
</div>
</div>
</div>
)}
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={handleClose}>
{t("common.cancel")}
</Button>
<Button
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
applyTemplate();
}}
className="gap-2"
>
<Save className="h-4 w-4" />
{t("codexConfig.applyConfig")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@@ -220,9 +220,6 @@ export function ProviderForm({
[originalHandleCodexConfigChange, debouncedValidate],
);
const [isCodexTemplateModalOpen, setIsCodexTemplateModalOpen] =
useState(false);
useEffect(() => {
form.reset(defaultValues);
}, [defaultValues, form]);
@@ -615,11 +612,6 @@ export function ProviderForm({
onPresetChange={handlePresetChange}
category={category}
appId={appId}
onOpenWizard={
appId === "codex"
? () => setIsCodexTemplateModalOpen(true)
: undefined
}
/>
)}
@@ -739,10 +731,6 @@ export function ProviderForm({
commonConfigError={codexCommonConfigError}
authError={codexAuthError}
configError={codexConfigError}
onWebsiteUrlChange={(url) => form.setValue("websiteUrl", url)}
onNameChange={(name) => form.setValue("name", name)}
isTemplateModalOpen={isCodexTemplateModalOpen}
setIsTemplateModalOpen={setIsCodexTemplateModalOpen}
/>
{/* 配置验证错误显示 */}
<FormField

View File

@@ -19,9 +19,8 @@ interface ProviderPresetSelectorProps {
categoryKeys: string[];
presetCategoryLabels: Record<string, string>;
onPresetChange: (value: string) => void;
category?: ProviderCategory; // 新增:当前选中的分类
category?: ProviderCategory; // 当前选中的分类
appId?: AppId;
onOpenWizard?: () => void; // Codex 专用:打开配置向导
}
export function ProviderPresetSelector({
@@ -32,7 +31,6 @@ export function ProviderPresetSelector({
onPresetChange,
category,
appId,
onOpenWizard,
}: ProviderPresetSelectorProps) {
const { t } = useTranslation();
@@ -56,23 +54,6 @@ export function ProviderPresetSelector({
defaultValue: "💡 第三方供应商需要填写 API Key 和请求地址",
});
case "custom":
// Codex 自定义:在此位置显示"手动配置…或者 使用配置向导"
if (appId === "codex" && onOpenWizard) {
return (
<>
{t("providerForm.manualConfig")}
<button
type="button"
onClick={onOpenWizard}
className="ml-1 text-blue-500 dark:text-blue-400 hover:text-blue-600 dark:hover:text-blue-300 underline-offset-2 hover:underline"
aria-label={t("providerForm.openConfigWizard")}
>
{t("providerForm.useConfigWizard")}
</button>
</>
);
}
// 其他情况沿用原提示
return t("providerForm.customApiKeyHint", {
defaultValue: "💡 自定义配置需手动填写所有必要字段",
});