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:
@@ -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>
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user