feat: add real-time TOML validation for Codex config

- Add smol-toml dependency for client-side TOML parsing
- Create useCodexTomlValidation hook with 500ms debounce
- Display validation errors below config.toml textarea
- Trigger validation on onChange for immediate user feedback
- Backend validation remains as fallback for data integrity
This commit is contained in:
Jason
2025-10-16 23:56:30 +08:00
parent 51c68ef192
commit 54b0b3b139
4 changed files with 102 additions and 7 deletions

View File

@@ -33,15 +33,17 @@ interface CodexConfigEditorProps {
authError: string; authError: string;
isCustomMode?: boolean; // 新增:是否为自定义模式 configError: string; // config.toml 错误提示
onWebsiteUrlChange?: (url: string) => void; // 新增:更新网址回调 isCustomMode?: boolean; // 是否为自定义模式
isTemplateModalOpen?: boolean; // 新增:模态框状态 onWebsiteUrlChange?: (url: string) => void; // 更新网址回调
setIsTemplateModalOpen?: (open: boolean) => void; // 新增:设置模态框状态 isTemplateModalOpen?: boolean; // 模态框状态
onNameChange?: (name: string) => void; // 新增:更新供应商名称回调 setIsTemplateModalOpen?: (open: boolean) => void; // 设置模态框状态
onNameChange?: (name: string) => void; // 更新供应商名称回调
} }
const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
@@ -67,6 +69,8 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
authError, authError,
configError,
onWebsiteUrlChange, onWebsiteUrlChange,
onNameChange, onNameChange,
@@ -324,6 +328,10 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
data-enable-grammarly="false" data-enable-grammarly="false"
/> />
{configError && (
<p className="text-xs text-red-500 dark:text-red-400">{configError}</p>
)}
<p className="text-xs text-gray-500 dark:text-gray-400"> <p className="text-xs text-gray-500 dark:text-gray-400">
{t("codexConfig.configTomlHint")} {t("codexConfig.configTomlHint")}
</p> </p>

View File

@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState, useCallback } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -32,6 +32,7 @@ import {
useCommonConfigSnippet, useCommonConfigSnippet,
useCodexCommonConfig, useCodexCommonConfig,
useSpeedTestEndpoints, useSpeedTestEndpoints,
useCodexTomlValidation,
} from "./hooks"; } from "./hooks";
const CLAUDE_DEFAULT_CONFIG = JSON.stringify({ env: {}, config: {} }, null, 2); const CLAUDE_DEFAULT_CONFIG = JSON.stringify({ env: {}, config: {} }, null, 2);
@@ -149,10 +150,19 @@ export function ProviderForm({
setCodexAuth, setCodexAuth,
handleCodexApiKeyChange, handleCodexApiKeyChange,
handleCodexBaseUrlChange, handleCodexBaseUrlChange,
handleCodexConfigChange, handleCodexConfigChange: originalHandleCodexConfigChange,
resetCodexConfig, resetCodexConfig,
} = useCodexConfigState({ initialData }); } = useCodexConfigState({ initialData });
// 使用 Codex TOML 校验 hook (仅 Codex 模式)
const { configError: codexConfigError, debouncedValidate } = useCodexTomlValidation();
// 包装 handleCodexConfigChange添加实时校验
const handleCodexConfigChange = useCallback((value: string) => {
originalHandleCodexConfigChange(value);
debouncedValidate(value);
}, [originalHandleCodexConfigChange, debouncedValidate]);
const [isCodexEndpointModalOpen, setIsCodexEndpointModalOpen] = const [isCodexEndpointModalOpen, setIsCodexEndpointModalOpen] =
useState(false); useState(false);
const [isCodexTemplateModalOpen, setIsCodexTemplateModalOpen] = const [isCodexTemplateModalOpen, setIsCodexTemplateModalOpen] =
@@ -511,6 +521,7 @@ export function ProviderForm({
onCommonConfigSnippetChange={handleCodexCommonConfigSnippetChange} onCommonConfigSnippetChange={handleCodexCommonConfigSnippetChange}
commonConfigError={codexCommonConfigError} commonConfigError={codexCommonConfigError}
authError={codexAuthError} authError={codexAuthError}
configError={codexConfigError}
isCustomMode={selectedPresetId === "custom"} isCustomMode={selectedPresetId === "custom"}
onWebsiteUrlChange={(url) => form.setValue("websiteUrl", url)} onWebsiteUrlChange={(url) => form.setValue("websiteUrl", url)}
onNameChange={(name) => form.setValue("name", name)} onNameChange={(name) => form.setValue("name", name)}

View File

@@ -10,3 +10,4 @@ export { useTemplateValues } from "./useTemplateValues";
export { useCommonConfigSnippet } from "./useCommonConfigSnippet"; export { useCommonConfigSnippet } from "./useCommonConfigSnippet";
export { useCodexCommonConfig } from "./useCodexCommonConfig"; export { useCodexCommonConfig } from "./useCodexCommonConfig";
export { useSpeedTestEndpoints } from "./useSpeedTestEndpoints"; export { useSpeedTestEndpoints } from "./useSpeedTestEndpoints";
export { useCodexTomlValidation } from "./useCodexTomlValidation";

View File

@@ -0,0 +1,75 @@
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 debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
/**
* 校验 TOML 格式
* @param tomlText - 待校验的 TOML 文本
* @returns 是否校验通过
*/
const validateToml = useCallback((tomlText: string): boolean => {
// 空字符串视为合法(允许为空)
if (!tomlText.trim()) {
setConfigError('');
return true;
}
try {
TOML.parse(tomlText);
setConfigError('');
return true;
} catch (error) {
const errorMessage = error instanceof Error
? error.message
: 'TOML 格式错误';
setConfigError(errorMessage);
return false;
}
}, []);
/**
* 带 debounce 的校验函数500ms 延迟)
* @param tomlText - 待校验的 TOML 文本
*/
const debouncedValidate = useCallback((tomlText: string) => {
// 清除之前的定时器
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
// 设置新的定时器
debounceTimerRef.current = setTimeout(() => {
validateToml(tomlText);
}, 500);
}, [validateToml]);
/**
* 清空错误信息
*/
const clearError = useCallback(() => {
setConfigError('');
}, []);
// 清理定时器
useEffect(() => {
return () => {
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
};
}, []);
return {
configError,
validateToml,
debouncedValidate,
clearError,
};
}