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:
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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: "添加" })}
|
||||
|
||||
@@ -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: "保存" })}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -53,7 +53,8 @@ export function useApiKeyLink({
|
||||
}, [selectedPresetId, presetEntries, formWebsiteUrl]);
|
||||
|
||||
return {
|
||||
shouldShowApiKeyLink: appType === "claude"
|
||||
shouldShowApiKeyLink:
|
||||
appType === "claude"
|
||||
? shouldShowApiKeyLink
|
||||
: appType === "codex"
|
||||
? shouldShowApiKeyLink
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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],
|
||||
);
|
||||
|
||||
// 当配置变化时检查是否包含通用配置(但避免在通过通用配置更新时检查)
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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("");
|
||||
}, []);
|
||||
|
||||
// 清理定时器
|
||||
|
||||
@@ -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],
|
||||
);
|
||||
|
||||
// 当配置变化时检查是否包含通用配置(但避免在通过通用配置更新时检查)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user