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 type { Provider } from "@/types";
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 { extractErrorMessage } from "@/utils/errorUtils";
import { AppSwitcher } from "@/components/AppSwitcher";

View File

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

View File

@@ -179,7 +179,7 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
</DialogHeader>
{/* 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">
<input

View File

@@ -22,8 +22,15 @@ import { mcpApi, type AppType } from "@/lib/api";
import { McpServer, McpServerSpec } from "@/types";
import { mcpPresets, getMcpPresetWithDescription } from "@/config/mcpPresets";
import McpWizardModal from "./McpWizardModal";
import { extractErrorMessage, translateMcpBackendError } from "@/utils/errorUtils";
import { tomlToMcpServer, extractIdFromToml, mcpServerToToml } from "@/utils/tomlUtils";
import {
extractErrorMessage,
translateMcpBackendError,
} from "@/utils/errorUtils";
import {
tomlToMcpServer,
extractIdFromToml,
mcpServerToToml,
} from "@/utils/tomlUtils";
import { useMcpValidation } from "./useMcpValidation";
interface McpFormModalProps {
@@ -426,7 +433,7 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
</DialogHeader>
{/* 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 && (
<div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -33,7 +33,10 @@ export const CodexCommonConfigModal: React.FC<CodexCommonConfigModalProps> = ({
return (
<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">
<DialogTitle>{t("codexConfig.editCommonConfigTitle")}</DialogTitle>
</DialogHeader>

View File

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

View File

@@ -18,10 +18,14 @@ import { Input } from "@/components/ui/input";
interface CodexQuickWizardModalProps {
isOpen: boolean;
onClose: () => void;
onApply: (auth: string, config: string, extras: {
onApply: (
auth: string,
config: string,
extras: {
websiteUrl?: string;
displayName?: string;
}) => void;
},
) => void;
}
/**
@@ -88,14 +92,10 @@ export const CodexQuickWizardModal: React.FC<CodexQuickWizardModalProps> = ({
trimmedModel,
);
onApply(
JSON.stringify(auth, null, 2),
config,
{
onApply(JSON.stringify(auth, null, 2), config, {
websiteUrl: templateWebsiteUrl.trim(),
displayName: templateDisplayName.trim(),
}
);
});
resetForm();
onClose();
@@ -111,7 +111,10 @@ export const CodexQuickWizardModal: React.FC<CodexQuickWizardModalProps> = ({
return (
<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">
<DialogTitle>{t("codexConfig.quickWizard")}</DialogTitle>
</DialogHeader>

View File

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

View File

@@ -13,7 +13,6 @@ import {
DialogFooter,
} from "@/components/ui/dialog";
// 临时类型定义,待后端 API 实现后替换
interface CustomEndpoint {
url: string;
@@ -118,12 +117,17 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
try {
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(
(ep: CustomEndpoint) => ({
url: ep.url,
isCustom: true,
}));
}),
);
setEntries((prev) => {
const map = new Map<string, EndpointEntry>();
@@ -391,7 +395,9 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
if (autoSelect) {
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));
const best = successful[0];
if (best && best.url && best.url !== normalizedSelected) {
@@ -430,7 +436,10 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
return (
<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">
<DialogTitle>{t("endpointTest.title")}</DialogTitle>
</DialogHeader>

View File

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

View File

@@ -53,7 +53,8 @@ export function useApiKeyLink({
}, [selectedPresetId, presetEntries, formWebsiteUrl]);
return {
shouldShowApiKeyLink: appType === "claude"
shouldShowApiKeyLink:
appType === "claude"
? shouldShowApiKeyLink
: appType === "codex"
? shouldShowApiKeyLink

View File

@@ -1,5 +1,8 @@
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";
interface UseBaseUrlStateProps {
@@ -93,7 +96,10 @@ export function useBaseUrlState({
}
isUpdatingRef.current = true;
const updatedConfig = setCodexBaseUrlInConfig(codexConfig || "", sanitized);
const updatedConfig = setCodexBaseUrlInConfig(
codexConfig || "",
sanitized,
);
onCodexConfigChange(updatedConfig);
setTimeout(() => {

View File

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

View File

@@ -1,5 +1,8 @@
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 {
initialData?: {
@@ -31,7 +34,10 @@ export function useCodexConfigState({ initialData }: UseCodexConfigStateProps) {
setCodexAuthState(JSON.stringify(auth, null, 2));
// 设置 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);
// 提取 Base URL
@@ -77,22 +83,29 @@ export function useCodexConfigState({ initialData }: UseCodexConfigStateProps) {
}, []);
// 设置 auth 并验证
const setCodexAuth = useCallback((value: string) => {
const setCodexAuth = useCallback(
(value: string) => {
setCodexAuthState(value);
setCodexAuthError(validateCodexAuth(value));
}, [validateCodexAuth]);
},
[validateCodexAuth],
);
// 设置 config (支持函数更新)
const setCodexConfig = useCallback((value: string | ((prev: string) => string)) => {
const setCodexConfig = useCallback(
(value: string | ((prev: string) => string)) => {
setCodexConfigState((prev) =>
typeof value === "function"
? (value as (input: string) => string)(prev)
: value,
);
}, []);
},
[],
);
// 处理 Codex API Key 输入并写回 auth.json
const handleCodexApiKeyChange = useCallback((key: string) => {
const handleCodexApiKeyChange = useCallback(
(key: string) => {
setCodexApiKey(key);
try {
const auth = JSON.parse(codexAuth || "{}");
@@ -101,10 +114,13 @@ export function useCodexConfigState({ initialData }: UseCodexConfigStateProps) {
} catch {
// ignore
}
}, [codexAuth, setCodexAuth]);
},
[codexAuth, setCodexAuth],
);
// 处理 Codex Base URL 变化
const handleCodexBaseUrlChange = useCallback((url: string) => {
const handleCodexBaseUrlChange = useCallback(
(url: string) => {
const sanitized = url.trim().replace(/\/+$/, "");
setCodexBaseUrl(sanitized);
@@ -117,10 +133,13 @@ export function useCodexConfigState({ initialData }: UseCodexConfigStateProps) {
setTimeout(() => {
isUpdatingCodexBaseUrlRef.current = false;
}, 0);
}, [setCodexConfig]);
},
[setCodexConfig],
);
// 处理 config 变化(同步 Base URL
const handleCodexConfigChange = useCallback((value: string) => {
const handleCodexConfigChange = useCallback(
(value: string) => {
setCodexConfig(value);
if (!isUpdatingCodexBaseUrlRef.current) {
@@ -129,10 +148,13 @@ export function useCodexConfigState({ initialData }: UseCodexConfigStateProps) {
setCodexBaseUrl(extracted);
}
}
}, [setCodexConfig, codexBaseUrl]);
},
[setCodexConfig, codexBaseUrl],
);
// 重置配置(用于预设切换)
const resetCodexConfig = useCallback((auth: Record<string, unknown>, config: string) => {
const resetCodexConfig = useCallback(
(auth: Record<string, unknown>, config: string) => {
const authString = JSON.stringify(auth, null, 2);
setCodexAuth(authString);
setCodexConfig(config);
@@ -152,7 +174,9 @@ export function useCodexConfigState({ initialData }: UseCodexConfigStateProps) {
} catch {
setCodexApiKey("");
}
}, [setCodexAuth, setCodexConfig]);
},
[setCodexAuth, setCodexConfig],
);
// 获取 API Key从 auth JSON
const getCodexAuthApiKey = useCallback((authString: string): string => {

View File

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

View File

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

View File

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

View File

@@ -17,7 +17,10 @@ export function useModelState({
const [claudeSmallFastModel, setClaudeSmallFastModel] = useState("");
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") {
setClaudeModel(value);
} else {

View File

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

View File

@@ -1,5 +1,8 @@
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 { applyTemplateValues } from "@/utils/providerConfigUtils";

View File

@@ -169,13 +169,13 @@ export function SettingsDialog({
return (
<Dialog open={open} onOpenChange={handleDialogChange}>
<DialogContent className="max-w-3xl">
<DialogContent className="max-w-3xl max-h-[90vh] flex flex-col">
<DialogHeader>
<DialogTitle>{t("settings.title")}</DialogTitle>
</DialogHeader>
{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" />
</div>
) : (
@@ -195,7 +195,10 @@ export function SettingsDialog({
<TabsTrigger value="about">{t("common.about")}</TabsTrigger>
</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 ? (
<>
<LanguageSettings
@@ -211,7 +214,10 @@ export function SettingsDialog({
) : null}
</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 ? (
<>
<DirectorySettings
@@ -268,7 +274,10 @@ export function SettingsDialog({
</DialogFooter>
</DialogContent>
<Dialog open={showRestartPrompt} onOpenChange={(open) => !open && handleRestartLater()}>
<Dialog
open={showRestartPrompt}
onOpenChange={(open) => !open && handleRestartLater()}
>
<DialogContent zIndex="alert" className="max-w-md">
<DialogHeader>
<DialogTitle>{t("settings.restartRequired")}</DialogTitle>

View File

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

View File

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

View File

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

View File

@@ -33,10 +33,7 @@ export function useSettingsMetadata(): UseSettingsMetadataResult {
setIsPortable(portable);
} catch (error) {
console.error(
"[useSettingsMetadata] Failed to load metadata",
error,
);
console.error("[useSettingsMetadata] Failed to load metadata", error);
} finally {
if (active) {
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 type { Provider, Settings, UsageResult } from "@/types";