c2053a7f43
- Add SSEViewer component for interactive SSE message inspection * Display SSE data stream with collapsible panels * Show parsed JSON with syntax highlighting * Display key information badges (content, tokens, finish reason) * Support copy individual or all SSE messages * Show error messages with detailed information - Support Ctrl+V to paste images in chat input * Enable image paste in CustomInputRender component * Auto-detect and add pasted images to image list * Show toast notifications for paste results - Add complete i18n support for 6 languages * Chinese (zh): Complete translations * English (en): Complete translations * Japanese (ja): Add 28 new translations * French (fr): Add 28 new translations * Russian (ru): Add 28 new translations * Vietnamese (vi): Add 32 new translations - Update .gitignore to exclude data directory
214 lines
6.5 KiB
React
214 lines
6.5 KiB
React
/*
|
|
Copyright (C) 2025 QuantumNous
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as
|
|
published by the Free Software Foundation, either version 3 of the
|
|
License, or (at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
For commercial licensing, please contact support@quantumnous.com
|
|
*/
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
TextArea,
|
|
Typography,
|
|
Button,
|
|
Switch,
|
|
Banner,
|
|
} 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(`${t('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 (
|
|
<div className='space-y-4'>
|
|
{/* 自定义模式开关 */}
|
|
<div className='flex items-center justify-between'>
|
|
<div className='flex items-center gap-2'>
|
|
<Code size={16} className='text-gray-500' />
|
|
<Typography.Text strong className='text-sm'>
|
|
{t('自定义请求体模式')}
|
|
</Typography.Text>
|
|
</div>
|
|
<Switch
|
|
checked={customRequestMode}
|
|
onChange={handleModeToggle}
|
|
checkedText={t('开')}
|
|
uncheckedText={t('关')}
|
|
size='small'
|
|
/>
|
|
</div>
|
|
|
|
{customRequestMode && (
|
|
<>
|
|
{/* 提示信息 */}
|
|
<Banner
|
|
type='warning'
|
|
description={t('启用此模式后,将使用您自定义的请求体发送API请求,模型配置面板的参数设置将被忽略。')}
|
|
icon={<AlertTriangle size={16} />}
|
|
className='!rounded-lg'
|
|
closeIcon={null}
|
|
/>
|
|
|
|
{/* JSON编辑器 */}
|
|
<div>
|
|
<div className='flex items-center justify-between mb-2'>
|
|
<Typography.Text strong className='text-sm'>
|
|
{t('请求体 JSON')}
|
|
</Typography.Text>
|
|
<div className='flex items-center gap-2'>
|
|
{isValid ? (
|
|
<div className='flex items-center gap-1 text-green-600'>
|
|
<Check size={14} />
|
|
<Typography.Text className='text-xs'>
|
|
{t('格式正确')}
|
|
</Typography.Text>
|
|
</div>
|
|
) : (
|
|
<div className='flex items-center gap-1 text-red-600'>
|
|
<X size={14} />
|
|
<Typography.Text className='text-xs'>
|
|
{t('格式错误')}
|
|
</Typography.Text>
|
|
</div>
|
|
)}
|
|
<Button
|
|
theme='borderless'
|
|
type='tertiary'
|
|
size='small'
|
|
icon={<Edit size={14} />}
|
|
onClick={formatJson}
|
|
disabled={!isValid}
|
|
className='!rounded-lg'
|
|
>
|
|
{t('格式化')}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<TextArea
|
|
value={localValue}
|
|
onChange={handleValueChange}
|
|
placeholder='{"model": "gpt-4o", "messages": [...], ...}'
|
|
autosize={{ minRows: 8, maxRows: 20 }}
|
|
className={`custom-request-textarea !rounded-lg font-mono text-sm ${!isValid ? '!border-red-500' : ''}`}
|
|
style={{
|
|
fontFamily: 'Consolas, Monaco, "Courier New", monospace',
|
|
lineHeight: '1.5',
|
|
}}
|
|
/>
|
|
|
|
{!isValid && errorMessage && (
|
|
<Typography.Text type='danger' className='text-xs mt-1 block'>
|
|
{errorMessage}
|
|
</Typography.Text>
|
|
)}
|
|
|
|
<Typography.Text className='text-xs text-gray-500 mt-2 block'>
|
|
{t('请输入有效的JSON格式的请求体。您可以参考预览面板中的默认请求体格式。')}
|
|
</Typography.Text>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default CustomRequestEditor;
|