Files
CC-Switch/src/components/providers/forms/CodexFormFields.tsx
T
Jason 84998aa217 feat: differentiate fetch models error messages by failure type
Distinguish between missing API key, missing endpoint, auth failure,
unsupported provider (404/405), and timeout errors instead of showing
a generic failure toast for all cases.
2026-04-04 22:53:20 +08:00

212 lines
6.3 KiB
TypeScript

import { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { toast } from "sonner";
import { Download, Loader2 } from "lucide-react";
import EndpointSpeedTest from "./EndpointSpeedTest";
import { ApiKeySection, EndpointField, ModelInputWithFetch } from "./shared";
import {
fetchModelsForConfig,
showFetchModelsError,
type FetchedModel,
} from "@/lib/api/model-fetch";
import type { ProviderCategory } from "@/types";
interface EndpointCandidate {
url: string;
}
interface CodexFormFieldsProps {
providerId?: string;
// API Key
codexApiKey: string;
onApiKeyChange: (key: string) => void;
category?: ProviderCategory;
shouldShowApiKeyLink: boolean;
websiteUrl: string;
isPartner?: boolean;
partnerPromotionKey?: string;
// Base URL
shouldShowSpeedTest: boolean;
codexBaseUrl: string;
onBaseUrlChange: (url: string) => void;
isFullUrl: boolean;
onFullUrlChange: (value: boolean) => void;
isEndpointModalOpen: boolean;
onEndpointModalToggle: (open: boolean) => void;
onCustomEndpointsChange?: (endpoints: string[]) => void;
autoSelect: boolean;
onAutoSelectChange: (checked: boolean) => void;
// Model Name
shouldShowModelField?: boolean;
modelName?: string;
onModelNameChange?: (model: string) => void;
// Speed Test Endpoints
speedTestEndpoints: EndpointCandidate[];
}
export function CodexFormFields({
providerId,
codexApiKey,
onApiKeyChange,
category,
shouldShowApiKeyLink,
websiteUrl,
isPartner,
partnerPromotionKey,
shouldShowSpeedTest,
codexBaseUrl,
onBaseUrlChange,
isFullUrl,
onFullUrlChange,
isEndpointModalOpen,
onEndpointModalToggle,
onCustomEndpointsChange,
autoSelect,
onAutoSelectChange,
shouldShowModelField = true,
modelName = "",
onModelNameChange,
speedTestEndpoints,
}: CodexFormFieldsProps) {
const { t } = useTranslation();
const [fetchedModels, setFetchedModels] = useState<FetchedModel[]>([]);
const [isFetchingModels, setIsFetchingModels] = useState(false);
const handleFetchModels = useCallback(() => {
if (!codexBaseUrl || !codexApiKey) {
showFetchModelsError(null, t, {
hasApiKey: !!codexApiKey,
hasBaseUrl: !!codexBaseUrl,
});
return;
}
setIsFetchingModels(true);
fetchModelsForConfig(codexBaseUrl, codexApiKey, isFullUrl)
.then((models) => {
setFetchedModels(models);
if (models.length === 0) {
toast.info(t("providerForm.fetchModelsEmpty"));
} else {
toast.success(
t("providerForm.fetchModelsSuccess", { count: models.length }),
);
}
})
.catch((err) => {
console.warn("[ModelFetch] Failed:", err);
showFetchModelsError(err, t);
})
.finally(() => setIsFetchingModels(false));
}, [codexBaseUrl, codexApiKey, isFullUrl, t]);
return (
<>
{/* Codex API Key 输入框 */}
<ApiKeySection
id="codexApiKey"
label="API Key"
value={codexApiKey}
onChange={onApiKeyChange}
category={category}
shouldShowLink={shouldShowApiKeyLink}
websiteUrl={websiteUrl}
isPartner={isPartner}
partnerPromotionKey={partnerPromotionKey}
placeholder={{
official: t("providerForm.codexOfficialNoApiKey", {
defaultValue: "官方供应商无需 API Key",
}),
thirdParty: t("providerForm.codexApiKeyAutoFill", {
defaultValue: "输入 API Key,将自动填充到配置",
}),
}}
/>
{/* Codex Base URL 输入框 */}
{shouldShowSpeedTest && (
<EndpointField
id="codexBaseUrl"
label={t("codexConfig.apiUrlLabel")}
value={codexBaseUrl}
onChange={onBaseUrlChange}
placeholder={t("providerForm.codexApiEndpointPlaceholder")}
hint={t("providerForm.codexApiHint")}
showFullUrlToggle
isFullUrl={isFullUrl}
onFullUrlChange={onFullUrlChange}
onManageClick={() => onEndpointModalToggle(true)}
/>
)}
{/* Codex Model Name 输入框 */}
{shouldShowModelField && onModelNameChange && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<label
htmlFor="codexModelName"
className="block text-sm font-medium text-foreground"
>
{t("codexConfig.modelName", { defaultValue: "模型名称" })}
</label>
<Button
type="button"
variant="outline"
size="sm"
onClick={handleFetchModels}
disabled={isFetchingModels}
className="h-7 gap-1"
>
{isFetchingModels ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<Download className="h-3.5 w-3.5" />
)}
{t("providerForm.fetchModels")}
</Button>
</div>
<ModelInputWithFetch
id="codexModelName"
value={modelName}
onChange={(v) => onModelNameChange!(v)}
placeholder={t("codexConfig.modelNamePlaceholder", {
defaultValue: "例如: gpt-5.4",
})}
fetchedModels={fetchedModels}
isLoading={isFetchingModels}
/>
<p className="text-xs text-muted-foreground">
{modelName.trim()
? t("codexConfig.modelNameHint", {
defaultValue: "指定使用的模型,将自动更新到 config.toml 中",
})
: t("providerForm.modelHint", {
defaultValue: "💡 留空将使用供应商的默认模型",
})}
</p>
</div>
)}
{/* 端点测速弹窗 - Codex */}
{shouldShowSpeedTest && isEndpointModalOpen && (
<EndpointSpeedTest
appId="codex"
providerId={providerId}
value={codexBaseUrl}
onChange={onBaseUrlChange}
initialEndpoints={speedTestEndpoints}
visible={isEndpointModalOpen}
onClose={() => onEndpointModalToggle(false)}
autoSelect={autoSelect}
onAutoSelectChange={onAutoSelectChange}
onCustomEndpointsChange={onCustomEndpointsChange}
/>
)}
</>
);
}