fix: unify dialog layout and fix content padding issues

- Fix negative margin overflow in all dialog content areas
- Standardize dialog structure with flex-col layout
- Add consistent py-4 spacing to all content areas
- Ensure proper spacing between header, content, and footer

Affected components:
- AddProviderDialog, EditProviderDialog
- McpFormModal, McpPanel
- UsageScriptModal
- SettingsDialog

All dialogs now follow unified layout pattern:
- DialogContent: flex flex-col max-h-[90vh]
- Content area: flex-1 overflow-y-auto px-6 py-4
- No negative margins that cause content overflow
This commit is contained in:
Jason
2025-10-18 16:52:02 +08:00
parent 404297cd30
commit 57552b3159
31 changed files with 306 additions and 208 deletions

View File

@@ -4,7 +4,12 @@ import { toast } from "sonner";
import { Plus, Settings } from "lucide-react"; import { Plus, Settings } from "lucide-react";
import type { Provider } from "@/types"; import type { Provider } from "@/types";
import { useProvidersQuery } from "@/lib/query"; import { useProvidersQuery } from "@/lib/query";
import { providersApi, settingsApi, type AppType, type ProviderSwitchEvent } from "@/lib/api"; import {
providersApi,
settingsApi,
type AppType,
type ProviderSwitchEvent,
} from "@/lib/api";
import { useProviderActions } from "@/hooks/useProviderActions"; import { useProviderActions } from "@/hooks/useProviderActions";
import { extractErrorMessage } from "@/utils/errorUtils"; import { extractErrorMessage } from "@/utils/errorUtils";
import { AppSwitcher } from "@/components/AppSwitcher"; import { AppSwitcher } from "@/components/AppSwitcher";

View File

@@ -15,11 +15,11 @@ const UsageFooter: React.FC<UsageFooterProps> = ({
appType, appType,
usageEnabled, usageEnabled,
}) => { }) => {
const { data: usage, isLoading: loading, refetch } = useUsageQuery( const {
providerId, data: usage,
appType, isLoading: loading,
usageEnabled, refetch,
); } = useUsageQuery(providerId, appType, usageEnabled);
// 只在启用用量查询且有数据时显示 // 只在启用用量查询且有数据时显示
if (!usageEnabled || !usage) return null; if (!usageEnabled || !usage) return null;

View File

@@ -179,7 +179,7 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
</DialogHeader> </DialogHeader>
{/* Content - Scrollable */} {/* Content - Scrollable */}
<div className="flex-1 overflow-y-auto -mx-6 px-6 space-y-4"> <div className="flex-1 overflow-y-auto px-6 py-4 space-y-4">
{/* 启用开关 */} {/* 启用开关 */}
<label className="flex items-center gap-2 cursor-pointer"> <label className="flex items-center gap-2 cursor-pointer">
<input <input

View File

@@ -22,8 +22,15 @@ import { mcpApi, type AppType } from "@/lib/api";
import { McpServer, McpServerSpec } from "@/types"; import { McpServer, McpServerSpec } from "@/types";
import { mcpPresets, getMcpPresetWithDescription } from "@/config/mcpPresets"; import { mcpPresets, getMcpPresetWithDescription } from "@/config/mcpPresets";
import McpWizardModal from "./McpWizardModal"; import McpWizardModal from "./McpWizardModal";
import { extractErrorMessage, translateMcpBackendError } from "@/utils/errorUtils"; import {
import { tomlToMcpServer, extractIdFromToml, mcpServerToToml } from "@/utils/tomlUtils"; extractErrorMessage,
translateMcpBackendError,
} from "@/utils/errorUtils";
import {
tomlToMcpServer,
extractIdFromToml,
mcpServerToToml,
} from "@/utils/tomlUtils";
import { useMcpValidation } from "./useMcpValidation"; import { useMcpValidation } from "./useMcpValidation";
interface McpFormModalProps { interface McpFormModalProps {
@@ -426,7 +433,7 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
</DialogHeader> </DialogHeader>
{/* Content - Scrollable */} {/* Content - Scrollable */}
<div className="flex-1 overflow-y-auto -mx-6 px-6 space-y-4"> <div className="flex-1 overflow-y-auto px-6 py-4 space-y-4">
{/* 预设选择(仅新增时展示) */} {/* 预设选择(仅新增时展示) */}
{!isEditing && ( {!isEditing && (
<div> <div>

View File

@@ -25,11 +25,7 @@ interface McpPanelProps {
* MCP 管理面板 * MCP 管理面板
* 采用与主界面一致的设计风格,右上角添加按钮,每个 MCP 占一行 * 采用与主界面一致的设计风格,右上角添加按钮,每个 MCP 占一行
*/ */
const McpPanel: React.FC<McpPanelProps> = ({ const McpPanel: React.FC<McpPanelProps> = ({ open, onOpenChange, appType }) => {
open,
onOpenChange,
appType,
}) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [isFormOpen, setIsFormOpen] = useState(false); const [isFormOpen, setIsFormOpen] = useState(false);
const [editingId, setEditingId] = useState<string | null>(null); const [editingId, setEditingId] = useState<string | null>(null);
@@ -142,7 +138,7 @@ const McpPanel: React.FC<McpPanelProps> = ({
</div> </div>
{/* Content - Scrollable */} {/* Content - Scrollable */}
<div className="flex-1 overflow-y-auto -mx-6 px-6"> <div className="flex-1 overflow-y-auto px-6 py-4">
{loading ? ( {loading ? (
<div className="text-center py-12 text-gray-500 dark:text-gray-400"> <div className="text-center py-12 text-gray-500 dark:text-gray-400">
{t("mcp.loading")} {t("mcp.loading")}

View File

@@ -63,7 +63,11 @@ export function AddProviderDialog({
if (appType === "claude") { if (appType === "claude") {
const presets = providerPresets; const presets = providerPresets;
const presetIndex = parseInt(values.presetId.replace("claude-", "")); const presetIndex = parseInt(values.presetId.replace("claude-", ""));
if (!isNaN(presetIndex) && presetIndex >= 0 && presetIndex < presets.length) { if (
!isNaN(presetIndex) &&
presetIndex >= 0 &&
presetIndex < presets.length
) {
const preset = presets[presetIndex]; const preset = presets[presetIndex];
if (preset?.endpointCandidates) { if (preset?.endpointCandidates) {
preset.endpointCandidates.forEach(addUrl); preset.endpointCandidates.forEach(addUrl);
@@ -72,7 +76,11 @@ export function AddProviderDialog({
} else if (appType === "codex") { } else if (appType === "codex") {
const presets = codexProviderPresets; const presets = codexProviderPresets;
const presetIndex = parseInt(values.presetId.replace("codex-", "")); const presetIndex = parseInt(values.presetId.replace("codex-", ""));
if (!isNaN(presetIndex) && presetIndex >= 0 && presetIndex < presets.length) { if (
!isNaN(presetIndex) &&
presetIndex >= 0 &&
presetIndex < presets.length
) {
const preset = presets[presetIndex]; const preset = presets[presetIndex];
if ((preset as any).endpointCandidates) { if ((preset as any).endpointCandidates) {
(preset as any).endpointCandidates.forEach(addUrl); (preset as any).endpointCandidates.forEach(addUrl);
@@ -139,7 +147,7 @@ export function AddProviderDialog({
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="flex-1 overflow-y-auto -mx-6 px-6"> <div className="flex-1 overflow-y-auto px-6 py-4">
<ProviderForm <ProviderForm
appType={appType} appType={appType}
submitLabel={t("common.add", { defaultValue: "添加" })} submitLabel={t("common.add", { defaultValue: "添加" })}

View File

@@ -72,7 +72,7 @@ export function EditProviderDialog({
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="flex-1 overflow-y-auto -mx-6 px-6"> <div className="flex-1 overflow-y-auto px-6 py-4">
<ProviderForm <ProviderForm
appType={appType} appType={appType}
submitLabel={t("common.save", { defaultValue: "保存" })} submitLabel={t("common.save", { defaultValue: "保存" })}

View File

@@ -122,8 +122,14 @@ const ClaudeConfigEditor: React.FC<ClaudeConfigEditorProps> = ({
{t("claudeConfig.fullSettingsHint")} {t("claudeConfig.fullSettingsHint")}
</p> </p>
<Dialog open={isCommonConfigModalOpen} onOpenChange={(open) => !open && closeModal()}> <Dialog
<DialogContent zIndex="nested" className="max-w-2xl max-h-[90vh] flex flex-col p-0"> open={isCommonConfigModalOpen}
onOpenChange={(open) => !open && closeModal()}
>
<DialogContent
zIndex="nested"
className="max-w-2xl max-h-[90vh] flex flex-col p-0"
>
<DialogHeader className="px-6 pt-6 pb-0"> <DialogHeader className="px-6 pt-6 pb-0">
<DialogTitle>{t("claudeConfig.editCommonConfigTitle")}</DialogTitle> <DialogTitle>{t("claudeConfig.editCommonConfigTitle")}</DialogTitle>
</DialogHeader> </DialogHeader>

View File

@@ -41,7 +41,7 @@ interface ClaudeFormFieldsProps {
claudeSmallFastModel: string; claudeSmallFastModel: string;
onModelChange: ( onModelChange: (
field: "ANTHROPIC_MODEL" | "ANTHROPIC_SMALL_FAST_MODEL", field: "ANTHROPIC_MODEL" | "ANTHROPIC_SMALL_FAST_MODEL",
value: string value: string,
) => void; ) => void;
// Kimi Model Selector // Kimi Model Selector
@@ -49,7 +49,7 @@ interface ClaudeFormFieldsProps {
kimiAnthropicSmallFastModel: string; kimiAnthropicSmallFastModel: string;
onKimiModelChange: ( onKimiModelChange: (
field: "ANTHROPIC_MODEL" | "ANTHROPIC_SMALL_FAST_MODEL", field: "ANTHROPIC_MODEL" | "ANTHROPIC_SMALL_FAST_MODEL",
value: string value: string,
) => void; ) => void;
// Speed Test Endpoints // Speed Test Endpoints

View File

@@ -33,7 +33,10 @@ export const CodexCommonConfigModal: React.FC<CodexCommonConfigModalProps> = ({
return ( return (
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}> <Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<DialogContent zIndex="nested" className="max-w-2xl max-h-[90vh] flex flex-col p-0"> <DialogContent
zIndex="nested"
className="max-w-2xl max-h-[90vh] flex flex-col p-0"
>
<DialogHeader className="px-6 pt-6 pb-0"> <DialogHeader className="px-6 pt-6 pb-0">
<DialogTitle>{t("codexConfig.editCommonConfigTitle")}</DialogTitle> <DialogTitle>{t("codexConfig.editCommonConfigTitle")}</DialogTitle>
</DialogHeader> </DialogHeader>

View File

@@ -60,9 +60,12 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false); const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false);
// Use internal state or external state // Use internal state or external state
const [internalTemplateModalOpen, setInternalTemplateModalOpen] = useState(false); const [internalTemplateModalOpen, setInternalTemplateModalOpen] =
const isTemplateModalOpen = externalTemplateModalOpen ?? internalTemplateModalOpen; useState(false);
const setIsTemplateModalOpen = externalSetTemplateModalOpen ?? setInternalTemplateModalOpen; const isTemplateModalOpen =
externalTemplateModalOpen ?? internalTemplateModalOpen;
const setIsTemplateModalOpen =
externalSetTemplateModalOpen ?? setInternalTemplateModalOpen;
// Auto-open common config modal if there's an error // Auto-open common config modal if there's an error
useEffect(() => { useEffect(() => {
@@ -74,7 +77,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
const handleQuickWizardApply = ( const handleQuickWizardApply = (
auth: string, auth: string,
config: string, config: string,
extras: { websiteUrl?: string; displayName?: string } extras: { websiteUrl?: string; displayName?: string },
) => { ) => {
onAuthChange(auth); onAuthChange(auth);
onConfigChange(config); onConfigChange(config);

View File

@@ -18,10 +18,14 @@ import { Input } from "@/components/ui/input";
interface CodexQuickWizardModalProps { interface CodexQuickWizardModalProps {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
onApply: (auth: string, config: string, extras: { onApply: (
websiteUrl?: string; auth: string,
displayName?: string; config: string,
}) => void; extras: {
websiteUrl?: string;
displayName?: string;
},
) => void;
} }
/** /**
@@ -88,14 +92,10 @@ export const CodexQuickWizardModal: React.FC<CodexQuickWizardModalProps> = ({
trimmedModel, trimmedModel,
); );
onApply( onApply(JSON.stringify(auth, null, 2), config, {
JSON.stringify(auth, null, 2), websiteUrl: templateWebsiteUrl.trim(),
config, displayName: templateDisplayName.trim(),
{ });
websiteUrl: templateWebsiteUrl.trim(),
displayName: templateDisplayName.trim(),
}
);
resetForm(); resetForm();
onClose(); onClose();
@@ -111,7 +111,10 @@ export const CodexQuickWizardModal: React.FC<CodexQuickWizardModalProps> = ({
return ( return (
<Dialog open={isOpen} onOpenChange={(open) => !open && handleClose()}> <Dialog open={isOpen} onOpenChange={(open) => !open && handleClose()}>
<DialogContent zIndex="nested" className="max-w-2xl max-h-[90vh] flex flex-col p-0"> <DialogContent
zIndex="nested"
className="max-w-2xl max-h-[90vh] flex flex-col p-0"
>
<DialogHeader className="px-6 pt-6 pb-0"> <DialogHeader className="px-6 pt-6 pb-0">
<DialogTitle>{t("codexConfig.quickWizard")}</DialogTitle> <DialogTitle>{t("codexConfig.quickWizard")}</DialogTitle>
</DialogHeader> </DialogHeader>

View File

@@ -108,7 +108,10 @@ export function CommonConfigEditor({
</p> </p>
</div> </div>
<Dialog open={isModalOpen} onOpenChange={(open) => !open && onModalClose()}> <Dialog
open={isModalOpen}
onOpenChange={(open) => !open && onModalClose()}
>
<DialogContent className="sm:max-w-[600px]"> <DialogContent className="sm:max-w-[600px]">
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
@@ -120,8 +123,7 @@ export function CommonConfigEditor({
<div className="space-y-4 py-4"> <div className="space-y-4 py-4">
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{t("claudeConfig.commonConfigHint", { {t("claudeConfig.commonConfigHint", {
defaultValue: defaultValue: "通用配置片段将合并到所有启用它的供应商配置中",
"通用配置片段将合并到所有启用它的供应商配置中",
})} })}
</p> </p>
<div className="rounded-md border"> <div className="rounded-md border">

View File

@@ -13,7 +13,6 @@ import {
DialogFooter, DialogFooter,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
// 临时类型定义,待后端 API 实现后替换 // 临时类型定义,待后端 API 实现后替换
interface CustomEndpoint { interface CustomEndpoint {
url: string; url: string;
@@ -118,12 +117,17 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
try { try {
if (!providerId) return; if (!providerId) return;
const customEndpoints = await vscodeApi.getCustomEndpoints(appType, providerId); const customEndpoints = await vscodeApi.getCustomEndpoints(
appType,
providerId,
);
const candidates: EndpointCandidate[] = customEndpoints.map((ep: CustomEndpoint) => ({ const candidates: EndpointCandidate[] = customEndpoints.map(
url: ep.url, (ep: CustomEndpoint) => ({
isCustom: true, url: ep.url,
})); isCustom: true,
}),
);
setEntries((prev) => { setEntries((prev) => {
const map = new Map<string, EndpointEntry>(); const map = new Map<string, EndpointEntry>();
@@ -391,7 +395,9 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
if (autoSelect) { if (autoSelect) {
const successful = results const successful = results
.filter((item) => typeof item.latency === "number" && item.latency !== null) .filter(
(item) => typeof item.latency === "number" && item.latency !== null,
)
.sort((a, b) => (a.latency! || 0) - (b.latency! || 0)); .sort((a, b) => (a.latency! || 0) - (b.latency! || 0));
const best = successful[0]; const best = successful[0];
if (best && best.url && best.url !== normalizedSelected) { if (best && best.url && best.url !== normalizedSelected) {
@@ -430,7 +436,10 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
return ( return (
<Dialog open={visible} onOpenChange={(open) => !open && onClose()}> <Dialog open={visible} onOpenChange={(open) => !open && onClose()}>
<DialogContent zIndex="nested" className="max-w-2xl max-h-[80vh] flex flex-col p-0"> <DialogContent
zIndex="nested"
className="max-w-2xl max-h-[80vh] flex flex-col p-0"
>
<DialogHeader className="px-6 pt-6 pb-0"> <DialogHeader className="px-6 pt-6 pb-0">
<DialogTitle>{t("endpointTest.title")}</DialogTitle> <DialogTitle>{t("endpointTest.title")}</DialogTitle>
</DialogHeader> </DialogHeader>

View File

@@ -66,7 +66,7 @@ export function ProviderForm({
const isEditMode = Boolean(initialData); const isEditMode = Boolean(initialData);
const [selectedPresetId, setSelectedPresetId] = useState<string | null>( const [selectedPresetId, setSelectedPresetId] = useState<string | null>(
initialData ? null : "custom" initialData ? null : "custom",
); );
const [activePreset, setActivePreset] = useState<{ const [activePreset, setActivePreset] = useState<{
id: string; id: string;
@@ -76,7 +76,7 @@ export function ProviderForm({
// 新建供应商:收集端点测速弹窗中的"自定义端点",提交时一次性落盘到 meta.custom_endpoints // 新建供应商:收集端点测速弹窗中的"自定义端点",提交时一次性落盘到 meta.custom_endpoints
const [draftCustomEndpoints, setDraftCustomEndpoints] = useState<string[]>( const [draftCustomEndpoints, setDraftCustomEndpoints] = useState<string[]>(
[] [],
); );
// 使用 category hook // 使用 category hook
@@ -101,7 +101,7 @@ export function ProviderForm({
? CODEX_DEFAULT_CONFIG ? CODEX_DEFAULT_CONFIG
: CLAUDE_DEFAULT_CONFIG, : CLAUDE_DEFAULT_CONFIG,
}), }),
[initialData, appType] [initialData, appType],
); );
const form = useForm<ProviderFormData>({ const form = useForm<ProviderFormData>({
@@ -155,13 +155,17 @@ export function ProviderForm({
} = useCodexConfigState({ initialData }); } = useCodexConfigState({ initialData });
// 使用 Codex TOML 校验 hook (仅 Codex 模式) // 使用 Codex TOML 校验 hook (仅 Codex 模式)
const { configError: codexConfigError, debouncedValidate } = useCodexTomlValidation(); const { configError: codexConfigError, debouncedValidate } =
useCodexTomlValidation();
// 包装 handleCodexConfigChange添加实时校验 // 包装 handleCodexConfigChange添加实时校验
const handleCodexConfigChange = useCallback((value: string) => { const handleCodexConfigChange = useCallback(
originalHandleCodexConfigChange(value); (value: string) => {
debouncedValidate(value); originalHandleCodexConfigChange(value);
}, [originalHandleCodexConfigChange, debouncedValidate]); debouncedValidate(value);
},
[originalHandleCodexConfigChange, debouncedValidate],
);
const [isCodexEndpointModalOpen, setIsCodexEndpointModalOpen] = const [isCodexEndpointModalOpen, setIsCodexEndpointModalOpen] =
useState(false); useState(false);
@@ -187,7 +191,7 @@ export function ProviderForm({
defaultValue: "第三方", defaultValue: "第三方",
}), }),
}), }),
[t] [t],
); );
const presetEntries = useMemo(() => { const presetEntries = useMemo(() => {
@@ -334,7 +338,7 @@ export function ProviderForm({
const categoryKeys = useMemo(() => { const categoryKeys = useMemo(() => {
return Object.keys(groupedPresets).filter( return Object.keys(groupedPresets).filter(
(key) => key !== "custom" && groupedPresets[key]?.length (key) => key !== "custom" && groupedPresets[key]?.length,
); );
}, [groupedPresets]); }, [groupedPresets]);
@@ -429,7 +433,7 @@ export function ProviderForm({
const preset = entry.preset as ProviderPreset; const preset = entry.preset as ProviderPreset;
const config = applyTemplateValues( const config = applyTemplateValues(
preset.settingsConfig, preset.settingsConfig,
preset.templateValues preset.templateValues,
); );
form.reset({ form.reset({
@@ -461,7 +465,7 @@ export function ProviderForm({
<ClaudeFormFields <ClaudeFormFields
shouldShowApiKey={shouldShowApiKey( shouldShowApiKey={shouldShowApiKey(
form.watch("settingsConfig"), form.watch("settingsConfig"),
isEditMode isEditMode,
)} )}
apiKey={apiKey} apiKey={apiKey}
onApiKeyChange={handleApiKeyChange} onApiKeyChange={handleApiKeyChange}
@@ -479,7 +483,9 @@ export function ProviderForm({
onEndpointModalToggle={setIsEndpointModalOpen} onEndpointModalToggle={setIsEndpointModalOpen}
onCustomEndpointsChange={setDraftCustomEndpoints} onCustomEndpointsChange={setDraftCustomEndpoints}
shouldShowKimiSelector={shouldShowKimiSelector} shouldShowKimiSelector={shouldShowKimiSelector}
shouldShowModelSelector={category !== "official" && !shouldShowKimiSelector} shouldShowModelSelector={
category !== "official" && !shouldShowKimiSelector
}
claudeModel={claudeModel} claudeModel={claudeModel}
claudeSmallFastModel={claudeSmallFastModel} claudeSmallFastModel={claudeSmallFastModel}
onModelChange={handleModelChange} onModelChange={handleModelChange}

View File

@@ -53,11 +53,12 @@ export function useApiKeyLink({
}, [selectedPresetId, presetEntries, formWebsiteUrl]); }, [selectedPresetId, presetEntries, formWebsiteUrl]);
return { return {
shouldShowApiKeyLink: appType === "claude" shouldShowApiKeyLink:
? shouldShowApiKeyLink appType === "claude"
: appType === "codex"
? shouldShowApiKeyLink ? shouldShowApiKeyLink
: false, : appType === "codex"
? shouldShowApiKeyLink
: false,
websiteUrl: getWebsiteUrl, websiteUrl: getWebsiteUrl,
}; };
} }

View File

@@ -1,5 +1,8 @@
import { useState, useCallback, useRef, useEffect } from "react"; import { useState, useCallback, useRef, useEffect } from "react";
import { extractCodexBaseUrl, setCodexBaseUrl as setCodexBaseUrlInConfig } from "@/utils/providerConfigUtils"; import {
extractCodexBaseUrl,
setCodexBaseUrl as setCodexBaseUrlInConfig,
} from "@/utils/providerConfigUtils";
import type { ProviderCategory } from "@/types"; import type { ProviderCategory } from "@/types";
interface UseBaseUrlStateProps { interface UseBaseUrlStateProps {
@@ -93,7 +96,10 @@ export function useBaseUrlState({
} }
isUpdatingRef.current = true; isUpdatingRef.current = true;
const updatedConfig = setCodexBaseUrlInConfig(codexConfig || "", sanitized); const updatedConfig = setCodexBaseUrlInConfig(
codexConfig || "",
sanitized,
);
onCodexConfigChange(updatedConfig); onCodexConfigChange(updatedConfig);
setTimeout(() => { setTimeout(() => {

View File

@@ -31,7 +31,9 @@ export function useCodexCommonConfig({
return DEFAULT_CODEX_COMMON_CONFIG_SNIPPET; return DEFAULT_CODEX_COMMON_CONFIG_SNIPPET;
} }
try { try {
const stored = window.localStorage.getItem(CODEX_COMMON_CONFIG_STORAGE_KEY); const stored = window.localStorage.getItem(
CODEX_COMMON_CONFIG_STORAGE_KEY,
);
if (stored && stored.trim()) { if (stored && stored.trim()) {
return stored; return stored;
} }
@@ -78,11 +80,12 @@ export function useCodexCommonConfig({
// 处理通用配置开关 // 处理通用配置开关
const handleCommonConfigToggle = useCallback( const handleCommonConfigToggle = useCallback(
(checked: boolean) => { (checked: boolean) => {
const { updatedConfig, error: snippetError } = updateTomlCommonConfigSnippet( const { updatedConfig, error: snippetError } =
codexConfig, updateTomlCommonConfigSnippet(
commonConfigSnippet, codexConfig,
checked, commonConfigSnippet,
); checked,
);
if (snippetError) { if (snippetError) {
setCommonConfigError(snippetError); setCommonConfigError(snippetError);
@@ -157,12 +160,7 @@ export function useCodexCommonConfig({
}, 0); }, 0);
} }
}, },
[ [commonConfigSnippet, codexConfig, useCommonConfig, onConfigChange],
commonConfigSnippet,
codexConfig,
useCommonConfig,
onConfigChange,
],
); );
// 当配置变化时检查是否包含通用配置(但避免在通过通用配置更新时检查) // 当配置变化时检查是否包含通用配置(但避免在通过通用配置更新时检查)

View File

@@ -1,5 +1,8 @@
import { useState, useCallback, useEffect, useRef } from "react"; import { useState, useCallback, useEffect, useRef } from "react";
import { extractCodexBaseUrl, setCodexBaseUrl as setCodexBaseUrlInConfig } from "@/utils/providerConfigUtils"; import {
extractCodexBaseUrl,
setCodexBaseUrl as setCodexBaseUrlInConfig,
} from "@/utils/providerConfigUtils";
interface UseCodexConfigStateProps { interface UseCodexConfigStateProps {
initialData?: { initialData?: {
@@ -31,7 +34,10 @@ export function useCodexConfigState({ initialData }: UseCodexConfigStateProps) {
setCodexAuthState(JSON.stringify(auth, null, 2)); setCodexAuthState(JSON.stringify(auth, null, 2));
// 设置 config.toml // 设置 config.toml
const configStr = typeof (config as any).config === "string" ? (config as any).config : ""; const configStr =
typeof (config as any).config === "string"
? (config as any).config
: "";
setCodexConfigState(configStr); setCodexConfigState(configStr);
// 提取 Base URL // 提取 Base URL
@@ -77,82 +83,100 @@ export function useCodexConfigState({ initialData }: UseCodexConfigStateProps) {
}, []); }, []);
// 设置 auth 并验证 // 设置 auth 并验证
const setCodexAuth = useCallback((value: string) => { const setCodexAuth = useCallback(
setCodexAuthState(value); (value: string) => {
setCodexAuthError(validateCodexAuth(value)); setCodexAuthState(value);
}, [validateCodexAuth]); setCodexAuthError(validateCodexAuth(value));
},
[validateCodexAuth],
);
// 设置 config (支持函数更新) // 设置 config (支持函数更新)
const setCodexConfig = useCallback((value: string | ((prev: string) => string)) => { const setCodexConfig = useCallback(
setCodexConfigState((prev) => (value: string | ((prev: string) => string)) => {
typeof value === "function" setCodexConfigState((prev) =>
? (value as (input: string) => string)(prev) typeof value === "function"
: value, ? (value as (input: string) => string)(prev)
); : value,
}, []); );
},
[],
);
// 处理 Codex API Key 输入并写回 auth.json // 处理 Codex API Key 输入并写回 auth.json
const handleCodexApiKeyChange = useCallback((key: string) => { const handleCodexApiKeyChange = useCallback(
setCodexApiKey(key); (key: string) => {
try { setCodexApiKey(key);
const auth = JSON.parse(codexAuth || "{}"); try {
auth.OPENAI_API_KEY = key.trim(); const auth = JSON.parse(codexAuth || "{}");
setCodexAuth(JSON.stringify(auth, null, 2)); auth.OPENAI_API_KEY = key.trim();
} catch { setCodexAuth(JSON.stringify(auth, null, 2));
// ignore } catch {
} // ignore
}, [codexAuth, setCodexAuth]); }
},
[codexAuth, setCodexAuth],
);
// 处理 Codex Base URL 变化 // 处理 Codex Base URL 变化
const handleCodexBaseUrlChange = useCallback((url: string) => { const handleCodexBaseUrlChange = useCallback(
const sanitized = url.trim().replace(/\/+$/, ""); (url: string) => {
setCodexBaseUrl(sanitized); const sanitized = url.trim().replace(/\/+$/, "");
setCodexBaseUrl(sanitized);
if (!sanitized) { if (!sanitized) {
return; return;
} }
isUpdatingCodexBaseUrlRef.current = true; isUpdatingCodexBaseUrlRef.current = true;
setCodexConfig((prev) => setCodexBaseUrlInConfig(prev, sanitized)); setCodexConfig((prev) => setCodexBaseUrlInConfig(prev, sanitized));
setTimeout(() => { setTimeout(() => {
isUpdatingCodexBaseUrlRef.current = false; isUpdatingCodexBaseUrlRef.current = false;
}, 0); }, 0);
}, [setCodexConfig]); },
[setCodexConfig],
);
// 处理 config 变化(同步 Base URL // 处理 config 变化(同步 Base URL
const handleCodexConfigChange = useCallback((value: string) => { const handleCodexConfigChange = useCallback(
setCodexConfig(value); (value: string) => {
setCodexConfig(value);
if (!isUpdatingCodexBaseUrlRef.current) { if (!isUpdatingCodexBaseUrlRef.current) {
const extracted = extractCodexBaseUrl(value) || ""; const extracted = extractCodexBaseUrl(value) || "";
if (extracted !== codexBaseUrl) { if (extracted !== codexBaseUrl) {
setCodexBaseUrl(extracted); setCodexBaseUrl(extracted);
}
} }
} },
}, [setCodexConfig, codexBaseUrl]); [setCodexConfig, codexBaseUrl],
);
// 重置配置(用于预设切换) // 重置配置(用于预设切换)
const resetCodexConfig = useCallback((auth: Record<string, unknown>, config: string) => { const resetCodexConfig = useCallback(
const authString = JSON.stringify(auth, null, 2); (auth: Record<string, unknown>, config: string) => {
setCodexAuth(authString); const authString = JSON.stringify(auth, null, 2);
setCodexConfig(config); setCodexAuth(authString);
setCodexConfig(config);
const baseUrl = extractCodexBaseUrl(config); const baseUrl = extractCodexBaseUrl(config);
if (baseUrl) { if (baseUrl) {
setCodexBaseUrl(baseUrl); setCodexBaseUrl(baseUrl);
} }
// 提取 API Key // 提取 API Key
try { try {
if (auth && typeof auth.OPENAI_API_KEY === "string") { if (auth && typeof auth.OPENAI_API_KEY === "string") {
setCodexApiKey(auth.OPENAI_API_KEY); setCodexApiKey(auth.OPENAI_API_KEY);
} else { } else {
setCodexApiKey("");
}
} catch {
setCodexApiKey(""); setCodexApiKey("");
} }
} catch { },
setCodexApiKey(""); [setCodexAuth, setCodexConfig],
} );
}, [setCodexAuth, setCodexConfig]);
// 获取 API Key从 auth JSON // 获取 API Key从 auth JSON
const getCodexAuthApiKey = useCallback((authString: string): string => { const getCodexAuthApiKey = useCallback((authString: string): string => {

View File

@@ -1,12 +1,12 @@
import { useState, useCallback, useEffect, useRef } from 'react'; import { useState, useCallback, useEffect, useRef } from "react";
import TOML from 'smol-toml'; import TOML from "smol-toml";
/** /**
* Codex config.toml 格式校验 Hook * Codex config.toml 格式校验 Hook
* 使用 smol-toml 进行实时 TOML 语法校验(带 debounce * 使用 smol-toml 进行实时 TOML 语法校验(带 debounce
*/ */
export function useCodexTomlValidation() { export function useCodexTomlValidation() {
const [configError, setConfigError] = useState(''); const [configError, setConfigError] = useState("");
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null); const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
/** /**
@@ -17,18 +17,17 @@ export function useCodexTomlValidation() {
const validateToml = useCallback((tomlText: string): boolean => { const validateToml = useCallback((tomlText: string): boolean => {
// 空字符串视为合法(允许为空) // 空字符串视为合法(允许为空)
if (!tomlText.trim()) { if (!tomlText.trim()) {
setConfigError(''); setConfigError("");
return true; return true;
} }
try { try {
TOML.parse(tomlText); TOML.parse(tomlText);
setConfigError(''); setConfigError("");
return true; return true;
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error const errorMessage =
? error.message error instanceof Error ? error.message : "TOML 格式错误";
: 'TOML 格式错误';
setConfigError(errorMessage); setConfigError(errorMessage);
return false; return false;
} }
@@ -38,23 +37,26 @@ export function useCodexTomlValidation() {
* 带 debounce 的校验函数500ms 延迟) * 带 debounce 的校验函数500ms 延迟)
* @param tomlText - 待校验的 TOML 文本 * @param tomlText - 待校验的 TOML 文本
*/ */
const debouncedValidate = useCallback((tomlText: string) => { const debouncedValidate = useCallback(
// 清除之前的定时器 (tomlText: string) => {
if (debounceTimerRef.current) { // 清除之前的定时器
clearTimeout(debounceTimerRef.current); if (debounceTimerRef.current) {
} clearTimeout(debounceTimerRef.current);
}
// 设置新的定时器 // 设置新的定时器
debounceTimerRef.current = setTimeout(() => { debounceTimerRef.current = setTimeout(() => {
validateToml(tomlText); validateToml(tomlText);
}, 500); }, 500);
}, [validateToml]); },
[validateToml],
);
/** /**
* 清空错误信息 * 清空错误信息
*/ */
const clearError = useCallback(() => { const clearError = useCallback(() => {
setConfigError(''); setConfigError("");
}, []); }, []);
// 清理定时器 // 清理定时器

View File

@@ -51,11 +51,7 @@ export function useCommonConfigSnippet({
// 初始化时检查通用配置片段(编辑模式) // 初始化时检查通用配置片段(编辑模式)
useEffect(() => { useEffect(() => {
if (initialData) { if (initialData) {
const configString = JSON.stringify( const configString = JSON.stringify(initialData.settingsConfig, null, 2);
initialData.settingsConfig,
null,
2,
);
const hasCommon = hasCommonConfigSnippet( const hasCommon = hasCommonConfigSnippet(
configString, configString,
commonConfigSnippet, commonConfigSnippet,
@@ -168,12 +164,7 @@ export function useCommonConfigSnippet({
}, 0); }, 0);
} }
}, },
[ [commonConfigSnippet, settingsConfig, useCommonConfig, onConfigChange],
commonConfigSnippet,
settingsConfig,
useCommonConfig,
onConfigChange,
],
); );
// 当配置变化时检查是否包含通用配置(但避免在通过通用配置更新时检查) // 当配置变化时检查是否包含通用配置(但避免在通过通用配置更新时检查)

View File

@@ -21,7 +21,8 @@ export function useKimiModelSelector({
presetName = "", presetName = "",
}: UseKimiModelSelectorProps) { }: UseKimiModelSelectorProps) {
const [kimiAnthropicModel, setKimiAnthropicModel] = useState(""); const [kimiAnthropicModel, setKimiAnthropicModel] = useState("");
const [kimiAnthropicSmallFastModel, setKimiAnthropicSmallFastModel] = useState(""); const [kimiAnthropicSmallFastModel, setKimiAnthropicSmallFastModel] =
useState("");
// 判断是否显示 Kimi 模型选择器 // 判断是否显示 Kimi 模型选择器
const shouldShowKimiSelector = const shouldShowKimiSelector =
@@ -32,23 +33,30 @@ export function useKimiModelSelector({
// 判断是否正在编辑 Kimi 供应商 // 判断是否正在编辑 Kimi 供应商
const isEditingKimi = Boolean( const isEditingKimi = Boolean(
initialData && initialData &&
(settingsConfig.includes("api.moonshot.cn") && settingsConfig.includes("api.moonshot.cn") &&
settingsConfig.includes("ANTHROPIC_MODEL")) settingsConfig.includes("ANTHROPIC_MODEL"),
); );
const shouldShow = shouldShowKimiSelector || isEditingKimi; const shouldShow = shouldShowKimiSelector || isEditingKimi;
// 初始化 Kimi 模型选择(编辑模式) // 初始化 Kimi 模型选择(编辑模式)
useEffect(() => { useEffect(() => {
if (initialData?.settingsConfig && typeof initialData.settingsConfig === "object") { if (
const config = initialData.settingsConfig as { env?: Record<string, unknown> }; initialData?.settingsConfig &&
typeof initialData.settingsConfig === "object"
) {
const config = initialData.settingsConfig as {
env?: Record<string, unknown>;
};
if (config.env) { if (config.env) {
const model = typeof config.env.ANTHROPIC_MODEL === "string" const model =
? config.env.ANTHROPIC_MODEL typeof config.env.ANTHROPIC_MODEL === "string"
: ""; ? config.env.ANTHROPIC_MODEL
const smallFastModel = typeof config.env.ANTHROPIC_SMALL_FAST_MODEL === "string" : "";
? config.env.ANTHROPIC_SMALL_FAST_MODEL const smallFastModel =
: ""; typeof config.env.ANTHROPIC_SMALL_FAST_MODEL === "string"
? config.env.ANTHROPIC_SMALL_FAST_MODEL
: "";
setKimiAnthropicModel(model); setKimiAnthropicModel(model);
setKimiAnthropicSmallFastModel(smallFastModel); setKimiAnthropicSmallFastModel(smallFastModel);
} }
@@ -57,7 +65,10 @@ export function useKimiModelSelector({
// 处理 Kimi 模型变化 // 处理 Kimi 模型变化
const handleKimiModelChange = useCallback( const handleKimiModelChange = useCallback(
(field: "ANTHROPIC_MODEL" | "ANTHROPIC_SMALL_FAST_MODEL", value: string) => { (
field: "ANTHROPIC_MODEL" | "ANTHROPIC_SMALL_FAST_MODEL",
value: string,
) => {
if (field === "ANTHROPIC_MODEL") { if (field === "ANTHROPIC_MODEL") {
setKimiAnthropicModel(value); setKimiAnthropicModel(value);
} else { } else {

View File

@@ -17,7 +17,10 @@ export function useModelState({
const [claudeSmallFastModel, setClaudeSmallFastModel] = useState(""); const [claudeSmallFastModel, setClaudeSmallFastModel] = useState("");
const handleModelChange = useCallback( const handleModelChange = useCallback(
(field: "ANTHROPIC_MODEL" | "ANTHROPIC_SMALL_FAST_MODEL", value: string) => { (
field: "ANTHROPIC_MODEL" | "ANTHROPIC_SMALL_FAST_MODEL",
value: string,
) => {
if (field === "ANTHROPIC_MODEL") { if (field === "ANTHROPIC_MODEL") {
setClaudeModel(value); setClaudeModel(value);
} else { } else {

View File

@@ -122,7 +122,7 @@ export function useSpeedTestEndpoints({
// 添加预设自己的 baseUrl // 添加预设自己的 baseUrl
const presetConfig = preset.config || ""; const presetConfig = preset.config || "";
const presetMatch = /base_url\s*=\s*["']([^"']+)["']/i.exec( const presetMatch = /base_url\s*=\s*["']([^"']+)["']/i.exec(
presetConfig presetConfig,
); );
if (presetMatch?.[1]) { if (presetMatch?.[1]) {
add(presetMatch[1]); add(presetMatch[1]);

View File

@@ -1,5 +1,8 @@
import { useState, useEffect, useCallback, useMemo } from "react"; import { useState, useEffect, useCallback, useMemo } from "react";
import type { ProviderPreset, TemplateValueConfig } from "@/config/providerPresets"; import type {
ProviderPreset,
TemplateValueConfig,
} from "@/config/providerPresets";
import type { CodexProviderPreset } from "@/config/codexProviderPresets"; import type { CodexProviderPreset } from "@/config/codexProviderPresets";
import { applyTemplateValues } from "@/utils/providerConfigUtils"; import { applyTemplateValues } from "@/utils/providerConfigUtils";

View File

@@ -169,13 +169,13 @@ export function SettingsDialog({
return ( return (
<Dialog open={open} onOpenChange={handleDialogChange}> <Dialog open={open} onOpenChange={handleDialogChange}>
<DialogContent className="max-w-3xl"> <DialogContent className="max-w-3xl max-h-[90vh] flex flex-col">
<DialogHeader> <DialogHeader>
<DialogTitle>{t("settings.title")}</DialogTitle> <DialogTitle>{t("settings.title")}</DialogTitle>
</DialogHeader> </DialogHeader>
{isBusy ? ( {isBusy ? (
<div className="flex min-h-[320px] items-center justify-center px-6"> <div className="flex min-h-[320px] items-center justify-center">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" /> <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div> </div>
) : ( ) : (
@@ -195,7 +195,10 @@ export function SettingsDialog({
<TabsTrigger value="about">{t("common.about")}</TabsTrigger> <TabsTrigger value="about">{t("common.about")}</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="general" className="space-y-6 mt-6 min-h-[400px]"> <TabsContent
value="general"
className="space-y-6 mt-6 min-h-[400px]"
>
{settings ? ( {settings ? (
<> <>
<LanguageSettings <LanguageSettings
@@ -211,7 +214,10 @@ export function SettingsDialog({
) : null} ) : null}
</TabsContent> </TabsContent>
<TabsContent value="advanced" className="space-y-6 mt-6 min-h-[400px]"> <TabsContent
value="advanced"
className="space-y-6 mt-6 min-h-[400px]"
>
{settings ? ( {settings ? (
<> <>
<DirectorySettings <DirectorySettings
@@ -268,7 +274,10 @@ export function SettingsDialog({
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
<Dialog open={showRestartPrompt} onOpenChange={(open) => !open && handleRestartLater()}> <Dialog
open={showRestartPrompt}
onOpenChange={(open) => !open && handleRestartLater()}
>
<DialogContent zIndex="alert" className="max-w-md"> <DialogContent zIndex="alert" className="max-w-md">
<DialogHeader> <DialogHeader>
<DialogTitle>{t("settings.restartRequired")}</DialogTitle> <DialogTitle>{t("settings.restartRequired")}</DialogTitle>

View File

@@ -52,7 +52,12 @@ interface ThemeButtonProps {
children: React.ReactNode; children: React.ReactNode;
} }
function ThemeButton({ active, onClick, icon: Icon, children }: ThemeButtonProps) { function ThemeButton({
active,
onClick,
icon: Icon,
children,
}: ThemeButtonProps) {
return ( return (
<Button <Button
type="button" type="button"

View File

@@ -66,10 +66,9 @@ export function useMcpActions(appType: AppType): UseMcpActionsResult {
try { try {
await mcpApi.setEnabled(appType, id, enabled); await mcpApi.setEnabled(appType, id, enabled);
toast.success( toast.success(enabled ? t("mcp.msg.enabled") : t("mcp.msg.disabled"), {
enabled ? t("mcp.msg.enabled") : t("mcp.msg.disabled"), duration: 1500,
{ duration: 1500 }, });
);
} catch (error) { } catch (error) {
// Rollback on failure // Rollback on failure
setServers(previousServers); setServers(previousServers);

View File

@@ -4,10 +4,7 @@ import { toast } from "sonner";
import { settingsApi, type AppType } from "@/lib/api"; import { settingsApi, type AppType } from "@/lib/api";
import { useSettingsQuery, useSaveSettingsMutation } from "@/lib/query"; import { useSettingsQuery, useSaveSettingsMutation } from "@/lib/query";
import type { Settings } from "@/types"; import type { Settings } from "@/types";
import { import { useSettingsForm, type SettingsFormState } from "./useSettingsForm";
useSettingsForm,
type SettingsFormState,
} from "./useSettingsForm";
import { import {
useDirectorySettings, useDirectorySettings,
type ResolvedDirectories, type ResolvedDirectories,

View File

@@ -33,10 +33,7 @@ export function useSettingsMetadata(): UseSettingsMetadataResult {
setIsPortable(portable); setIsPortable(portable);
} catch (error) { } catch (error) {
console.error( console.error("[useSettingsMetadata] Failed to load metadata", error);
"[useSettingsMetadata] Failed to load metadata",
error,
);
} finally { } finally {
if (active) { if (active) {
setIsLoading(false); setIsLoading(false);

View File

@@ -1,4 +1,8 @@
import { useQuery, type UseQueryResult, keepPreviousData } from "@tanstack/react-query"; import {
useQuery,
type UseQueryResult,
keepPreviousData,
} from "@tanstack/react-query";
import { providersApi, settingsApi, usageApi, type AppType } from "@/lib/api"; import { providersApi, settingsApi, usageApi, type AppType } from "@/lib/api";
import type { Provider, Settings, UsageResult } from "@/types"; import type { Provider, Settings, UsageResult } from "@/types";