Files
cc-switch/src/components/providers/forms/ProviderForm.tsx

785 lines
26 KiB
TypeScript
Raw Normal View History

import { useEffect, useMemo, useState } from "react";
2025-10-16 10:49:56 +08:00
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";
feat: complete stage 4 cleanup and code formatting This commit completes stage 4 of the refactoring plan, focusing on cleanup and optimization of the modernized codebase. ## Key Changes ### Code Cleanup - Remove legacy `src/lib/styles.ts` (no longer needed) - Remove old modal components (`ImportProgressModal.tsx`, `ProviderList.tsx`) - Streamline `src/lib/tauri-api.ts` from 712 lines to 17 lines (-97.6%) - Remove global `window.api` pollution - Keep only event listeners (`tauriEvents.onProviderSwitched`) - All API calls now use modular `@/lib/api/*` layer ### Type System - Clean up `src/vite-env.d.ts` (remove 156 lines of outdated types) - Remove obsolete global type declarations - All TypeScript checks pass with zero errors ### Code Formatting - Format all source files with Prettier (82 files) - Fix formatting issues in 15 files: - App.tsx and core components - MCP management components - Settings module components - Provider management components - UI components ### Documentation Updates - Update `REFACTORING_CHECKLIST.md` with stage 4 progress - Mark completed tasks in `REFACTORING_MASTER_PLAN.md` ## Impact **Code Reduction:** - Total: -1,753 lines, +384 lines (net -1,369 lines) - tauri-api.ts: 712 → 17 lines (-97.6%) - Removed styles.ts: -82 lines - Removed vite-env.d.ts declarations: -156 lines **Quality Improvements:** - ✅ Zero TypeScript errors - ✅ Zero TODO/FIXME comments - ✅ 100% Prettier compliant - ✅ Zero `window.api` references - ✅ Fully modular API layer ## Testing - [x] TypeScript compilation passes - [x] Code formatting validated - [x] No linting errors Stage 4 completion: 100% Ready for stage 5 (testing and bug fixes)
2025-10-16 12:13:51 +08:00
import { providerSchema, type ProviderFormData } from "@/lib/schemas/provider";
import type { AppType } from "@/lib/api";
import type { ProviderCategory, CustomEndpoint } from "@/types";
style: restore original color scheme to shadcn/ui components Restore the vibrant color palette from the pre-refactoring version while maintaining shadcn/ui component architecture and modern design patterns. ## Color Scheme Restoration ### Button Component - **default variant**: Blue primary (`bg-blue-500`) - matches old `primary` - **destructive variant**: Red (`bg-red-500`) - matches old `danger` - **secondary variant**: Gray text (`text-gray-500`) - matches old `secondary` - **ghost variant**: Transparent hover (`hover:bg-gray-100`) - matches old `ghost` - **mcp variant**: Emerald green (`bg-emerald-500`) - matches old `mcp` - Updated border-radius to `rounded-lg` for consistency ### CSS Variables - Set `--primary` to blue (`hsl(217 91% 60%)` ≈ `bg-blue-500`) - Added complete shadcn/ui theme variables for light/dark modes - Maintained semantic color tokens for maintainability ### Component-Specific Colors - **"Currently Using" badge**: Green (`bg-green-500/10 text-green-500`) - **Delete button hover**: Red (`hover:text-red-500 hover:bg-red-100`) - **MCP button**: Emerald green with minimum width (`min-w-[80px]`) - **Links/URLs**: Blue (`text-blue-500`) ## Benefits - ✅ Restored original vibrant UI (blue, green, red accents) - ✅ Maintained shadcn/ui component system (accessibility, animations) - ✅ Easy global theming via CSS variables - ✅ Consistent design language across all components - ✅ Code formatted with Prettier (shadcn/ui standards) ## Files Changed - `src/index.css`: Added shadcn/ui theme variables with blue primary - `src/components/ui/button.tsx`: Restored all original button color variants - `src/components/providers/ProviderCard.tsx`: Green badge for current provider - `src/components/providers/ProviderActions.tsx`: Red hover for delete button - `src/components/mcp/McpPanel.tsx`: Use `mcp` variant for consistency - `src/App.tsx`: MCP button with emerald color and wider width The UI now matches the original colorful design while leveraging modern shadcn/ui components for better maintainability and user experience.
2025-10-16 15:32:26 +08:00
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 CodexConfigEditor from "@/components/ProviderForm/CodexConfigEditor";
import KimiModelSelector from "@/components/ProviderForm/KimiModelSelector";
import { Zap } from "lucide-react";
import {
useProviderCategory,
useApiKeyState,
useBaseUrlState,
useModelState,
useCodexConfigState,
useApiKeyLink,
useCustomEndpoints,
useKimiModelSelector,
} from "./hooks";
const CLAUDE_DEFAULT_CONFIG = JSON.stringify({ env: {}, config: {} }, null, 2);
style: restore original color scheme to shadcn/ui components Restore the vibrant color palette from the pre-refactoring version while maintaining shadcn/ui component architecture and modern design patterns. ## Color Scheme Restoration ### Button Component - **default variant**: Blue primary (`bg-blue-500`) - matches old `primary` - **destructive variant**: Red (`bg-red-500`) - matches old `danger` - **secondary variant**: Gray text (`text-gray-500`) - matches old `secondary` - **ghost variant**: Transparent hover (`hover:bg-gray-100`) - matches old `ghost` - **mcp variant**: Emerald green (`bg-emerald-500`) - matches old `mcp` - Updated border-radius to `rounded-lg` for consistency ### CSS Variables - Set `--primary` to blue (`hsl(217 91% 60%)` ≈ `bg-blue-500`) - Added complete shadcn/ui theme variables for light/dark modes - Maintained semantic color tokens for maintainability ### Component-Specific Colors - **"Currently Using" badge**: Green (`bg-green-500/10 text-green-500`) - **Delete button hover**: Red (`hover:text-red-500 hover:bg-red-100`) - **MCP button**: Emerald green with minimum width (`min-w-[80px]`) - **Links/URLs**: Blue (`text-blue-500`) ## Benefits - ✅ Restored original vibrant UI (blue, green, red accents) - ✅ Maintained shadcn/ui component system (accessibility, animations) - ✅ Easy global theming via CSS variables - ✅ Consistent design language across all components - ✅ Code formatted with Prettier (shadcn/ui standards) ## Files Changed - `src/index.css`: Added shadcn/ui theme variables with blue primary - `src/components/ui/button.tsx`: Restored all original button color variants - `src/components/providers/ProviderCard.tsx`: Green badge for current provider - `src/components/providers/ProviderActions.tsx`: Red hover for delete button - `src/components/mcp/McpPanel.tsx`: Use `mcp` variant for consistency - `src/App.tsx`: MCP button with emerald color and wider width The UI now matches the original colorful design while leveraging modern shadcn/ui components for better maintainability and user experience.
2025-10-16 15:32:26 +08:00
const CODEX_DEFAULT_CONFIG = JSON.stringify({ auth: {}, config: "" }, null, 2);
type PresetEntry = {
id: string;
preset: ProviderPreset | CodexProviderPreset;
};
2025-10-16 10:49:56 +08:00
interface ProviderFormProps {
appType: AppType;
2025-10-16 10:49:56 +08:00
submitLabel: string;
onSubmit: (values: ProviderFormValues) => void;
2025-10-16 10:49:56 +08:00
onCancel: () => void;
initialData?: {
name?: string;
websiteUrl?: string;
settingsConfig?: Record<string, unknown>;
};
}
export function ProviderForm({
appType,
2025-10-16 10:49:56 +08:00
submitLabel,
onSubmit,
onCancel,
initialData,
}: ProviderFormProps) {
const { t } = useTranslation();
const { theme } = useTheme();
const isEditMode = Boolean(initialData);
const [selectedPresetId, setSelectedPresetId] = useState<string | null>(
initialData ? null : "custom"
);
const [activePreset, setActivePreset] = useState<{
id: string;
category?: ProviderCategory;
} | null>(null);
const [isEndpointModalOpen, setIsEndpointModalOpen] = useState(false);
// 新建供应商:收集端点测速弹窗中的"自定义端点",提交时一次性落盘到 meta.custom_endpoints
const [draftCustomEndpoints, setDraftCustomEndpoints] = useState<string[]>(
[]
);
// 使用 category hook
const { category } = useProviderCategory({
appType,
selectedPresetId,
isEditMode,
});
useEffect(() => {
setSelectedPresetId(initialData ? null : "custom");
setActivePreset(null);
}, [appType, initialData]);
2025-10-16 10:49:56 +08:00
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,
2025-10-16 10:49:56 +08:00
}),
[initialData, appType]
2025-10-16 10:49:56 +08:00
);
const form = useForm<ProviderFormData>({
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
},
});
// 使用 Model hook
const { claudeModel, claudeSmallFastModel, handleModelChange } =
useModelState({
settingsConfig: form.watch("settingsConfig"),
onConfigChange: (config) => form.setValue("settingsConfig", config),
});
// 使用 Codex 配置 hook (仅 Codex 模式)
const {
codexAuth,
codexConfig,
codexApiKey,
codexBaseUrl,
codexAuthError,
setCodexAuth,
handleCodexApiKeyChange,
handleCodexBaseUrlChange,
handleCodexConfigChange,
resetCodexConfig,
} = useCodexConfigState({ initialData });
const [isCodexEndpointModalOpen, setIsCodexEndpointModalOpen] =
useState(false);
2025-10-16 10:49:56 +08:00
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 presetCategoryLabels: Record<string, string> = 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<PresetEntry>((preset, index) => ({
id: `codex-${index}`,
preset,
}));
}
return providerPresets.map<PresetEntry>((preset, index) => ({
id: `claude-${index}`,
preset,
}));
}, [appType]);
// 使用 Kimi 模型选择器 hook
const {
shouldShow: shouldShowKimiSelector,
kimiAnthropicModel,
kimiAnthropicSmallFastModel,
handleKimiModelChange,
} = useKimiModelSelector({
initialData,
settingsConfig: form.watch("settingsConfig"),
onConfigChange: (config) => form.setValue("settingsConfig", config),
selectedPresetId,
presetName:
selectedPresetId && selectedPresetId !== "custom"
? presetEntries.find((item) => item.id === selectedPresetId)?.preset
.name || ""
: "",
});
2025-10-16 10:49:56 +08:00
const handleSubmit = (values: ProviderFormData) => {
let settingsConfig: string;
// Codex: 组合 auth 和 config
if (appType === "codex") {
try {
const authJson = JSON.parse(codexAuth);
const configObj = {
auth: authJson,
config: codexConfig ?? "",
};
settingsConfig = JSON.stringify(configObj);
} catch (err) {
// 如果解析失败,使用表单中的配置
settingsConfig = values.settingsConfig.trim();
}
} else {
// Claude: 使用表单配置
settingsConfig = values.settingsConfig.trim();
}
const payload: ProviderFormValues = {
2025-10-16 10:49:56 +08:00
...values,
name: values.name.trim(),
2025-10-16 10:49:56 +08:00
websiteUrl: values.websiteUrl?.trim() ?? "",
settingsConfig,
};
if (activePreset) {
payload.presetId = activePreset.id;
if (activePreset.category) {
payload.presetCategory = activePreset.category;
}
}
// 新建供应商时:添加自定义端点
if (!initialData && customEndpointsMap) {
payload.meta = { custom_endpoints: customEndpointsMap };
}
onSubmit(payload);
};
const groupedPresets = useMemo(() => {
return presetEntries.reduce<Record<string, PresetEntry[]>>((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";
// 使用 API Key 链接 hook (Claude)
const {
shouldShowApiKeyLink: shouldShowClaudeApiKeyLink,
websiteUrl: claudeWebsiteUrl,
} = useApiKeyLink({
appType: "claude",
category,
selectedPresetId,
presetEntries,
formWebsiteUrl: form.watch("websiteUrl") || "",
});
// 使用 API Key 链接 hook (Codex)
const {
shouldShowApiKeyLink: shouldShowCodexApiKeyLink,
websiteUrl: codexWebsiteUrl,
} = useApiKeyLink({
appType: "codex",
category,
selectedPresetId,
presetEntries,
formWebsiteUrl: form.watch("websiteUrl") || "",
});
// 使用自定义端点 hook
const customEndpointsMap = useCustomEndpoints({
appType,
selectedPresetId,
presetEntries,
draftCustomEndpoints,
baseUrl,
codexBaseUrl,
});
const handlePresetChange = (value: string) => {
setSelectedPresetId(value);
if (value === "custom") {
setActivePreset(null);
form.reset(defaultValues);
// Codex 自定义模式:重置为空配置
if (appType === "codex") {
resetCodexConfig({}, "");
}
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 auth = preset.auth ?? {};
const config = preset.config ?? "";
// 重置 Codex 配置
resetCodexConfig(auth, config);
// 更新表单其他字段
form.reset({
name: preset.name,
websiteUrl: preset.websiteUrl ?? "",
settingsConfig: JSON.stringify({ auth, 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),
2025-10-16 10:49:56 +08:00
});
};
return (
<Form {...form}>
feat: complete stage 4 cleanup and code formatting This commit completes stage 4 of the refactoring plan, focusing on cleanup and optimization of the modernized codebase. ## Key Changes ### Code Cleanup - Remove legacy `src/lib/styles.ts` (no longer needed) - Remove old modal components (`ImportProgressModal.tsx`, `ProviderList.tsx`) - Streamline `src/lib/tauri-api.ts` from 712 lines to 17 lines (-97.6%) - Remove global `window.api` pollution - Keep only event listeners (`tauriEvents.onProviderSwitched`) - All API calls now use modular `@/lib/api/*` layer ### Type System - Clean up `src/vite-env.d.ts` (remove 156 lines of outdated types) - Remove obsolete global type declarations - All TypeScript checks pass with zero errors ### Code Formatting - Format all source files with Prettier (82 files) - Fix formatting issues in 15 files: - App.tsx and core components - MCP management components - Settings module components - Provider management components - UI components ### Documentation Updates - Update `REFACTORING_CHECKLIST.md` with stage 4 progress - Mark completed tasks in `REFACTORING_MASTER_PLAN.md` ## Impact **Code Reduction:** - Total: -1,753 lines, +384 lines (net -1,369 lines) - tauri-api.ts: 712 → 17 lines (-97.6%) - Removed styles.ts: -82 lines - Removed vite-env.d.ts declarations: -156 lines **Quality Improvements:** - ✅ Zero TypeScript errors - ✅ Zero TODO/FIXME comments - ✅ 100% Prettier compliant - ✅ Zero `window.api` references - ✅ Fully modular API layer ## Testing - [x] TypeScript compilation passes - [x] Code formatting validated - [x] No linting errors Stage 4 completion: 100% Ready for stage 5 (testing and bug fixes)
2025-10-16 12:13:51 +08:00
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
{/* 预设供应商选择(仅新增模式显示) */}
{!initialData && (
<div className="space-y-3">
<FormLabel>
{t("providerPreset.label", { defaultValue: "预设供应商" })}
</FormLabel>
<div className="flex flex-wrap gap-2">
{/* 自定义按钮 */}
<button
type="button"
onClick={() => handlePresetChange("custom")}
className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
selectedPresetId === "custom"
? "bg-emerald-500 text-white dark:bg-emerald-600"
: "bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700"
}`}
>
{t("providerPreset.custom", { defaultValue: "自定义配置" })}
</button>
{/* 预设按钮 */}
{categoryKeys.map((category) => {
const entries = groupedPresets[category];
if (!entries || entries.length === 0) return null;
return entries.map((entry) => (
<button
key={entry.id}
type="button"
onClick={() => handlePresetChange(entry.id)}
className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
selectedPresetId === entry.id
? "bg-emerald-500 text-white dark:bg-emerald-600"
: "bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700"
}`}
title={
presetCategoryLabels[category] ??
t("providerPreset.categoryOther", {
defaultValue: "其他",
})
}
>
{entry.preset.name}
</button>
));
})}
</div>
<p className="text-xs text-muted-foreground">
{t("providerPreset.helper", {
defaultValue: "选择预设后可继续调整下方字段。",
})}
</p>
</div>
)}
2025-10-16 10:49:56 +08:00
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("provider.name", { defaultValue: "供应商名称" })}
</FormLabel>
<FormControl>
<Input
{...field}
placeholder={t("provider.namePlaceholder", {
defaultValue: "例如Claude 官方",
})}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="websiteUrl"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("provider.websiteUrl", { defaultValue: "官网链接" })}
</FormLabel>
<FormControl>
feat: complete stage 4 cleanup and code formatting This commit completes stage 4 of the refactoring plan, focusing on cleanup and optimization of the modernized codebase. ## Key Changes ### Code Cleanup - Remove legacy `src/lib/styles.ts` (no longer needed) - Remove old modal components (`ImportProgressModal.tsx`, `ProviderList.tsx`) - Streamline `src/lib/tauri-api.ts` from 712 lines to 17 lines (-97.6%) - Remove global `window.api` pollution - Keep only event listeners (`tauriEvents.onProviderSwitched`) - All API calls now use modular `@/lib/api/*` layer ### Type System - Clean up `src/vite-env.d.ts` (remove 156 lines of outdated types) - Remove obsolete global type declarations - All TypeScript checks pass with zero errors ### Code Formatting - Format all source files with Prettier (82 files) - Fix formatting issues in 15 files: - App.tsx and core components - MCP management components - Settings module components - Provider management components - UI components ### Documentation Updates - Update `REFACTORING_CHECKLIST.md` with stage 4 progress - Mark completed tasks in `REFACTORING_MASTER_PLAN.md` ## Impact **Code Reduction:** - Total: -1,753 lines, +384 lines (net -1,369 lines) - tauri-api.ts: 712 → 17 lines (-97.6%) - Removed styles.ts: -82 lines - Removed vite-env.d.ts declarations: -156 lines **Quality Improvements:** - ✅ Zero TypeScript errors - ✅ Zero TODO/FIXME comments - ✅ 100% Prettier compliant - ✅ Zero `window.api` references - ✅ Fully modular API layer ## Testing - [x] TypeScript compilation passes - [x] Code formatting validated - [x] No linting errors Stage 4 completion: 100% Ready for stage 5 (testing and bug fixes)
2025-10-16 12:13:51 +08:00
<Input {...field} placeholder="https://" />
2025-10-16 10:49:56 +08:00
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* API Key 输入框(仅 Claude 且非编辑模式显示) */}
{appType === "claude" &&
shouldShowApiKey(form.watch("settingsConfig"), isEditMode) && (
<div className="space-y-1">
<ApiKeyInput
value={apiKey}
onChange={handleApiKeyChange}
required={category !== "official"}
placeholder={
category === "official"
? t("providerForm.officialNoApiKey", {
defaultValue: "官方供应商无需 API Key",
})
: t("providerForm.apiKeyAutoFill", {
defaultValue: "输入 API Key将自动填充到配置",
})
}
disabled={category === "official"}
/>
{/* API Key 获取链接 */}
{shouldShowClaudeApiKeyLink && claudeWebsiteUrl && (
<div className="-mt-1 pl-1">
<a
href={claudeWebsiteUrl}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-blue-400 dark:text-blue-500 hover:text-blue-500 dark:hover:text-blue-400 transition-colors"
>
{t("providerForm.getApiKey", {
defaultValue: "获取 API Key",
})}
</a>
</div>
)}
</div>
)}
{/* Base URL 输入框(仅 Claude 第三方/自定义显示) */}
{appType === "claude" && shouldShowSpeedTest && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<FormLabel htmlFor="baseUrl">
{t("providerForm.apiEndpoint", { defaultValue: "API 端点" })}
</FormLabel>
<button
type="button"
onClick={() => setIsEndpointModalOpen(true)}
className="flex items-center gap-1 text-xs text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 transition-colors"
>
<Zap className="h-3.5 w-3.5" />
{t("providerForm.manageAndTest", {
defaultValue: "管理和测速",
})}
</button>
</div>
<Input
id="baseUrl"
type="url"
value={baseUrl}
onChange={(e) => handleClaudeBaseUrlChange(e.target.value)}
placeholder={t("providerForm.apiEndpointPlaceholder", {
defaultValue: "https://api.example.com",
})}
autoComplete="off"
/>
<div className="p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700 rounded-lg">
<p className="text-xs text-amber-600 dark:text-amber-400">
{t("providerForm.apiHint", {
defaultValue: "API 端点地址用于连接服务器",
})}
</p>
</div>
</div>
)}
{/* 端点测速弹窗 - Claude */}
{appType === "claude" && shouldShowSpeedTest && isEndpointModalOpen && (
<EndpointSpeedTest
appType={appType}
value={baseUrl}
onChange={handleClaudeBaseUrlChange}
initialEndpoints={[{ url: baseUrl }]}
visible={isEndpointModalOpen}
onClose={() => setIsEndpointModalOpen(false)}
onCustomEndpointsChange={setDraftCustomEndpoints}
/>
)}
{/* 模型选择器(仅 Claude 非官方且非 Kimi 供应商显示) */}
{appType === "claude" &&
category !== "official" &&
!shouldShowKimiSelector && (
<div className="space-y-3">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* ANTHROPIC_MODEL */}
<div className="space-y-2">
<FormLabel htmlFor="claudeModel">
{t("providerForm.anthropicModel", {
defaultValue: "主模型",
})}
</FormLabel>
<Input
id="claudeModel"
type="text"
value={claudeModel}
onChange={(e) =>
handleModelChange("ANTHROPIC_MODEL", e.target.value)
}
placeholder={t("providerForm.modelPlaceholder", {
defaultValue: "claude-3-7-sonnet-20250219",
})}
autoComplete="off"
/>
</div>
{/* ANTHROPIC_SMALL_FAST_MODEL */}
<div className="space-y-2">
<FormLabel htmlFor="claudeSmallFastModel">
{t("providerForm.anthropicSmallFastModel", {
defaultValue: "快速模型",
})}
</FormLabel>
<Input
id="claudeSmallFastModel"
type="text"
value={claudeSmallFastModel}
onChange={(e) =>
handleModelChange(
"ANTHROPIC_SMALL_FAST_MODEL",
e.target.value
)
}
placeholder={t("providerForm.smallModelPlaceholder", {
defaultValue: "claude-3-5-haiku-20241022",
})}
autoComplete="off"
/>
</div>
</div>
<p className="text-xs text-muted-foreground">
{t("providerForm.modelHelper", {
defaultValue:
"可选:指定默认使用的 Claude 模型,留空则使用系统默认。",
})}
</p>
</div>
)}
{/* Kimi 模型选择器(仅 Claude 且是 Kimi 供应商时显示) */}
{appType === "claude" && shouldShowKimiSelector && (
<KimiModelSelector
apiKey={apiKey}
anthropicModel={kimiAnthropicModel}
anthropicSmallFastModel={kimiAnthropicSmallFastModel}
onModelChange={handleKimiModelChange}
disabled={category === "official"}
/>
)}
{/* Codex API Key 输入框 */}
{appType === "codex" && !isEditMode && (
<div className="space-y-1">
<ApiKeyInput
id="codexApiKey"
label="API Key"
value={codexApiKey}
onChange={handleCodexApiKeyChange}
required={category !== "official"}
placeholder={
category === "official"
? t("providerForm.codexOfficialNoApiKey", {
defaultValue: "官方供应商无需 API Key",
})
: t("providerForm.codexApiKeyAutoFill", {
defaultValue: "输入 API Key将自动填充到配置",
})
}
disabled={category === "official"}
/>
{/* Codex API Key 获取链接 */}
{shouldShowCodexApiKeyLink && codexWebsiteUrl && (
<div className="-mt-1 pl-1">
<a
href={codexWebsiteUrl}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-blue-400 dark:text-blue-500 hover:text-blue-500 dark:hover:text-blue-400 transition-colors"
>
{t("providerForm.getApiKey", {
defaultValue: "获取 API Key",
})}
</a>
</div>
)}
</div>
)}
{/* Codex Base URL 输入框 */}
{appType === "codex" && shouldShowSpeedTest && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<FormLabel htmlFor="codexBaseUrl">
{t("codexConfig.apiUrlLabel", { defaultValue: "API 端点" })}
2025-10-16 10:49:56 +08:00
</FormLabel>
<button
type="button"
onClick={() => setIsCodexEndpointModalOpen(true)}
className="flex items-center gap-1 text-xs text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 transition-colors"
>
<Zap className="h-3.5 w-3.5" />
{t("providerForm.manageAndTest", {
defaultValue: "管理和测速",
})}
</button>
</div>
<Input
id="codexBaseUrl"
type="url"
value={codexBaseUrl}
onChange={(e) => handleCodexBaseUrlChange(e.target.value)}
placeholder={t("providerForm.codexApiEndpointPlaceholder", {
defaultValue: "https://api.example.com/v1",
})}
autoComplete="off"
/>
<div className="p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700 rounded-lg">
<p className="text-xs text-amber-600 dark:text-amber-400">
{t("providerForm.codexApiHint", {
defaultValue: "Codex API 端点地址",
})}
</p>
</div>
</div>
)}
{/* 端点测速弹窗 - Codex */}
{appType === "codex" &&
shouldShowSpeedTest &&
isCodexEndpointModalOpen && (
<EndpointSpeedTest
appType={appType}
value={codexBaseUrl}
onChange={handleCodexBaseUrlChange}
initialEndpoints={[{ url: codexBaseUrl }]}
visible={isCodexEndpointModalOpen}
onClose={() => setIsCodexEndpointModalOpen(false)}
onCustomEndpointsChange={setDraftCustomEndpoints}
/>
)}
{/* 配置编辑器Claude 使用 JSON 编辑器Codex 使用专用编辑器 */}
{appType === "codex" ? (
<CodexConfigEditor
authValue={codexAuth}
configValue={codexConfig}
onAuthChange={setCodexAuth}
onConfigChange={handleCodexConfigChange}
useCommonConfig={false}
onCommonConfigToggle={() => {}}
commonConfigSnippet=""
onCommonConfigSnippetChange={() => {}}
commonConfigError=""
authError={codexAuthError}
/>
) : (
<FormField
control={form.control}
name="settingsConfig"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("provider.configJson", { defaultValue: "配置 JSON" })}
</FormLabel>
<FormControl>
<div className="rounded-md border">
<JsonEditor
value={field.value}
onChange={field.onChange}
placeholder={CLAUDE_DEFAULT_CONFIG}
darkMode={isDarkMode}
rows={14}
showValidation
/>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
2025-10-16 10:49:56 +08:00
<div className="flex justify-end gap-2">
<Button variant="outline" type="button" onClick={onCancel}>
{t("common.cancel", { defaultValue: "取消" })}
</Button>
<Button type="submit">{submitLabel}</Button>
</div>
</form>
</Form>
);
}
export type ProviderFormValues = ProviderFormData & {
presetId?: string;
presetCategory?: ProviderCategory;
meta?: {
custom_endpoints?: Record<string, CustomEndpoint>;
};
};