From 5107f1b84a385ab4e804e1c38fece6463f1adbdf Mon Sep 17 00:00:00 2001 From: "Apple\\Apple" Date: Sun, 1 Jun 2025 17:07:36 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20Add=20custom=20request=20bo?= =?UTF-8?q?dy=20editor=20with=20persistent=20message=20storage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add CustomRequestEditor component with JSON validation and real-time formatting - Implement bidirectional sync between chat messages and custom request body - Add persistent local storage for chat messages (separate from config) - Remove redundant System Prompt field in custom mode - Refactor configuration storage to separate messages and settings New Features: • Custom request body mode with JSON editor and syntax highlighting • Real-time bidirectional synchronization between chat UI and custom request body • Persistent message storage that survives page refresh • Enhanced configuration export/import including message data • Improved parameter organization with collapsible sections Technical Changes: • Add loadMessages/saveMessages functions in configStorage • Update usePlaygroundState hook to handle message persistence • Refactor SettingsPanel to remove System Prompt in custom mode • Add STORAGE_KEYS constants for better storage key management • Implement debounced auto-save for both config and messages • Add hash-based change detection to prevent unnecessary updates UI/UX Improvements: • Disabled state styling for parameters in custom mode • Warning banners and visual feedback for mode switching • Mobile-responsive design for custom request editor • Consistent styling with existing design system --- .../components/playground/ConfigManager.js | 40 ++- .../playground/CustomRequestEditor.js | 197 +++++++++++ web/src/components/playground/DebugPanel.js | 6 + .../components/playground/ImageUrlInput.js | 26 +- .../playground/OptimizedComponents.js | 56 +++ .../components/playground/ParameterControl.js | 31 +- .../components/playground/SettingsPanel.js | 101 +++--- .../components/playground/ThinkingContent.js | 98 ++++++ .../components/playground/configStorage.js | 106 ++++-- web/src/hooks/useDataLoader.js | 70 ++++ web/src/hooks/useMessageEdit.js | 93 +++++ web/src/hooks/usePlaygroundState.js | 84 ++++- web/src/hooks/useSyncMessageAndCustomBody.js | 111 ++++++ web/src/pages/Playground/index.js | 324 +++++++----------- web/src/utils/apiUtils.js | 53 +-- web/src/utils/constants.js | 55 ++- web/src/utils/messageUtils.js | 128 +++++-- 17 files changed, 1199 insertions(+), 380 deletions(-) create mode 100644 web/src/components/playground/CustomRequestEditor.js create mode 100644 web/src/components/playground/OptimizedComponents.js create mode 100644 web/src/components/playground/ThinkingContent.js create mode 100644 web/src/hooks/useDataLoader.js create mode 100644 web/src/hooks/useMessageEdit.js create mode 100644 web/src/hooks/useSyncMessageAndCustomBody.js diff --git a/web/src/components/playground/ConfigManager.js b/web/src/components/playground/ConfigManager.js index c5b9eea4..12be1f4d 100644 --- a/web/src/components/playground/ConfigManager.js +++ b/web/src/components/playground/ConfigManager.js @@ -20,13 +20,21 @@ const ConfigManager = ({ onConfigImport, onConfigReset, styleState, + messages, }) => { const { t } = useTranslation(); const fileInputRef = useRef(null); const handleExport = () => { try { - exportConfig(currentConfig); + // 在导出前先保存当前配置,确保导出的是最新内容 + const configWithTimestamp = { + ...currentConfig, + timestamp: new Date().toISOString(), + }; + localStorage.setItem('playground_config', JSON.stringify(configWithTimestamp)); + + exportConfig(currentConfig, messages); Toast.success({ content: t('配置已导出到下载文件夹'), duration: 3, @@ -84,11 +92,31 @@ const ConfigManager = ({ type: 'danger', }, onOk: () => { - clearConfig(); - onConfigReset(); - Toast.success({ - content: t('配置已重置为默认值'), - duration: 3, + // 询问是否同时重置消息 + Modal.confirm({ + title: t('重置选项'), + content: t('是否同时重置对话消息?选择"是"将清空所有对话记录并恢复默认示例;选择"否"将保留当前对话记录。'), + okText: t('同时重置消息'), + cancelText: t('仅重置配置'), + okButtonProps: { + type: 'danger', + }, + onOk: () => { + clearConfig(); + onConfigReset({ resetMessages: true }); + Toast.success({ + content: t('配置和消息已全部重置'), + duration: 3, + }); + }, + onCancel: () => { + clearConfig(); + onConfigReset({ resetMessages: false }); + Toast.success({ + content: t('配置已重置,对话消息已保留'), + duration: 3, + }); + }, }); }, }); diff --git a/web/src/components/playground/CustomRequestEditor.js b/web/src/components/playground/CustomRequestEditor.js new file mode 100644 index 00000000..504ba261 --- /dev/null +++ b/web/src/components/playground/CustomRequestEditor.js @@ -0,0 +1,197 @@ +import React, { useState, useEffect } from 'react'; +import { + Card, + TextArea, + Typography, + Button, + Switch, + Banner, + Tag, +} from '@douyinfe/semi-ui'; +import { + Code, + Edit, + Check, + X, + AlertTriangle, +} from 'lucide-react'; +import { useTranslation } from 'react-i18next'; + +const CustomRequestEditor = ({ + customRequestMode, + customRequestBody, + onCustomRequestModeChange, + onCustomRequestBodyChange, + defaultPayload, +}) => { + const { t } = useTranslation(); + const [isValid, setIsValid] = useState(true); + const [errorMessage, setErrorMessage] = useState(''); + const [localValue, setLocalValue] = useState(customRequestBody || ''); + + // 当切换到自定义模式时,用默认payload初始化 + useEffect(() => { + if (customRequestMode && (!customRequestBody || customRequestBody.trim() === '')) { + const defaultJson = defaultPayload ? JSON.stringify(defaultPayload, null, 2) : ''; + setLocalValue(defaultJson); + onCustomRequestBodyChange(defaultJson); + } + }, [customRequestMode, defaultPayload, customRequestBody, onCustomRequestBodyChange]); + + // 同步外部传入的customRequestBody到本地状态 + useEffect(() => { + if (customRequestBody !== localValue) { + setLocalValue(customRequestBody || ''); + validateJson(customRequestBody || ''); + } + }, [customRequestBody]); + + // 验证JSON格式 + const validateJson = (value) => { + if (!value.trim()) { + setIsValid(true); + setErrorMessage(''); + return true; + } + + try { + JSON.parse(value); + setIsValid(true); + setErrorMessage(''); + return true; + } catch (error) { + setIsValid(false); + setErrorMessage(`JSON格式错误: ${error.message}`); + return false; + } + }; + + const handleValueChange = (value) => { + setLocalValue(value); + validateJson(value); + // 始终保存用户输入,让预览逻辑处理JSON解析错误 + onCustomRequestBodyChange(value); + }; + + const handleModeToggle = (enabled) => { + onCustomRequestModeChange(enabled); + if (enabled && defaultPayload) { + const defaultJson = JSON.stringify(defaultPayload, null, 2); + setLocalValue(defaultJson); + onCustomRequestBodyChange(defaultJson); + } + }; + + const formatJson = () => { + try { + const parsed = JSON.parse(localValue); + const formatted = JSON.stringify(parsed, null, 2); + setLocalValue(formatted); + onCustomRequestBodyChange(formatted); + setIsValid(true); + setErrorMessage(''); + } catch (error) { + // 如果格式化失败,保持原样 + } + }; + + return ( +
+ {/* 自定义模式开关 */} +
+
+ + + 自定义请求体模式 + + {customRequestMode && ( + + 已启用 + + )} +
+ +
+ + {customRequestMode && ( + <> + {/* 提示信息 */} + } + className="!rounded-lg" + closable={false} + /> + + {/* JSON编辑器 */} +
+
+ + 请求体 JSON + +
+ {isValid ? ( +
+ + + 格式正确 + +
+ ) : ( +
+ + + 格式错误 + +
+ )} + +
+
+ +