✨ feat: Add custom request body editor with persistent message storage
- 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
This commit is contained in:
@@ -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,
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
197
web/src/components/playground/CustomRequestEditor.js
Normal file
197
web/src/components/playground/CustomRequestEditor.js
Normal file
@@ -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 (
|
||||
<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">
|
||||
自定义请求体模式
|
||||
</Typography.Text>
|
||||
{customRequestMode && (
|
||||
<Tag color="green" size="small" shape='circle'>
|
||||
已启用
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
<Switch
|
||||
checked={customRequestMode}
|
||||
onChange={handleModeToggle}
|
||||
checkedText="开"
|
||||
uncheckedText="关"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{customRequestMode && (
|
||||
<>
|
||||
{/* 提示信息 */}
|
||||
<Banner
|
||||
type="warning"
|
||||
description="启用此模式后,将使用您自定义的请求体发送API请求,模型配置面板的参数设置将被忽略。"
|
||||
icon={<AlertTriangle size={16} />}
|
||||
className="!rounded-lg"
|
||||
closable={false}
|
||||
/>
|
||||
|
||||
{/* JSON编辑器 */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Typography.Text strong className="text-sm">
|
||||
请求体 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">
|
||||
格式正确
|
||||
</Typography.Text>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1 text-red-600">
|
||||
<X size={14} />
|
||||
<Typography.Text className="text-xs">
|
||||
格式错误
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
theme="borderless"
|
||||
type="tertiary"
|
||||
size="small"
|
||||
icon={<Edit size={14} />}
|
||||
onClick={formatJson}
|
||||
disabled={!isValid}
|
||||
className="!rounded-lg"
|
||||
>
|
||||
格式化
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TextArea
|
||||
value={localValue}
|
||||
onChange={handleValueChange}
|
||||
placeholder='{"model": "gpt-4o", "messages": [...], ...}'
|
||||
autosize={{ minRows: 8, maxRows: 20 }}
|
||||
className={`!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">
|
||||
请输入有效的JSON格式的请求体。您可以参考预览面板中的默认请求体格式。
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomRequestEditor;
|
||||
@@ -24,6 +24,7 @@ const DebugPanel = ({
|
||||
onActiveDebugTabChange,
|
||||
styleState,
|
||||
onCloseDebugPanel,
|
||||
customRequestMode,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -128,6 +129,11 @@ const DebugPanel = ({
|
||||
<div className="flex items-center gap-2">
|
||||
<Eye size={16} />
|
||||
{t('预览请求体')}
|
||||
{customRequestMode && (
|
||||
<span className="px-1.5 py-0.5 text-xs bg-orange-100 text-orange-600 rounded-full">
|
||||
自定义
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
} itemKey="preview">
|
||||
<CodeViewer
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
Image,
|
||||
} from 'lucide-react';
|
||||
|
||||
const ImageUrlInput = ({ imageUrls, imageEnabled, onImageUrlsChange, onImageEnabledChange }) => {
|
||||
const ImageUrlInput = ({ imageUrls, imageEnabled, onImageUrlsChange, onImageEnabledChange, disabled = false }) => {
|
||||
const handleAddImageUrl = () => {
|
||||
const newUrls = [...imageUrls, ''];
|
||||
onImageUrlsChange(newUrls);
|
||||
@@ -31,13 +31,18 @@ const ImageUrlInput = ({ imageUrls, imageEnabled, onImageUrlsChange, onImageEnab
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={disabled ? 'opacity-50' : ''}>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Image size={16} className={imageEnabled ? "text-blue-500" : "text-gray-400"} />
|
||||
<Image size={16} className={imageEnabled && !disabled ? "text-blue-500" : "text-gray-400"} />
|
||||
<Typography.Text strong className="text-sm">
|
||||
图片地址
|
||||
</Typography.Text>
|
||||
{disabled && (
|
||||
<Typography.Text className="text-xs text-orange-600">
|
||||
(已在自定义模式中忽略)
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
@@ -47,6 +52,7 @@ const ImageUrlInput = ({ imageUrls, imageEnabled, onImageUrlsChange, onImageEnab
|
||||
uncheckedText="停用"
|
||||
size="small"
|
||||
className="flex-shrink-0"
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Button
|
||||
icon={<Plus size={14} />}
|
||||
@@ -55,26 +61,26 @@ const ImageUrlInput = ({ imageUrls, imageEnabled, onImageUrlsChange, onImageEnab
|
||||
type="primary"
|
||||
onClick={handleAddImageUrl}
|
||||
className="!rounded-full !w-4 !h-4 !p-0 !min-w-0"
|
||||
disabled={!imageEnabled || imageUrls.length >= 5}
|
||||
disabled={!imageEnabled || imageUrls.length >= 5 || disabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!imageEnabled ? (
|
||||
<Typography.Text className="text-xs text-gray-500 mb-2 block">
|
||||
图片发送已停用,启用后可添加图片URL进行多模态对话
|
||||
{disabled ? '图片功能在自定义请求体模式下不可用' : '启用后可添加图片URL进行多模态对话'}
|
||||
</Typography.Text>
|
||||
) : imageUrls.length === 0 ? (
|
||||
<Typography.Text className="text-xs text-gray-500 mb-2 block">
|
||||
点击 + 按钮添加图片URL,支持最多5张图片
|
||||
{disabled ? '图片功能在自定义请求体模式下不可用' : '点击 + 按钮添加图片URL,支持最多5张图片'}
|
||||
</Typography.Text>
|
||||
) : (
|
||||
<Typography.Text className="text-xs text-gray-500 mb-2 block">
|
||||
已添加 {imageUrls.length}/5 张图片
|
||||
已添加 {imageUrls.length}/5 张图片{disabled ? ' (自定义模式下不可用)' : ''}
|
||||
</Typography.Text>
|
||||
)}
|
||||
|
||||
<div className={`space-y-2 max-h-32 overflow-y-auto ${!imageEnabled ? 'opacity-50' : ''}`}>
|
||||
<div className={`space-y-2 max-h-32 overflow-y-auto ${!imageEnabled || disabled ? 'opacity-50' : ''}`}>
|
||||
{imageUrls.map((url, index) => (
|
||||
<div key={index} className="flex items-center gap-2">
|
||||
<div className="flex-1">
|
||||
@@ -85,7 +91,7 @@ const ImageUrlInput = ({ imageUrls, imageEnabled, onImageUrlsChange, onImageEnab
|
||||
className="!rounded-lg"
|
||||
size="small"
|
||||
prefix={<IconFile size='small' />}
|
||||
disabled={!imageEnabled}
|
||||
disabled={!imageEnabled || disabled}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
@@ -95,7 +101,7 @@ const ImageUrlInput = ({ imageUrls, imageEnabled, onImageUrlsChange, onImageEnab
|
||||
type="danger"
|
||||
onClick={() => handleRemoveImageUrl(index)}
|
||||
className="!rounded-full !w-6 !h-6 !p-0 !min-w-0 !text-red-500 hover:!bg-red-50 flex-shrink-0"
|
||||
disabled={!imageEnabled}
|
||||
disabled={!imageEnabled || disabled}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
56
web/src/components/playground/OptimizedComponents.js
Normal file
56
web/src/components/playground/OptimizedComponents.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
import MessageContent from './MessageContent';
|
||||
import MessageActions from './MessageActions';
|
||||
import SettingsPanel from './SettingsPanel';
|
||||
import DebugPanel from './DebugPanel';
|
||||
|
||||
// 优化的消息内容组件
|
||||
export const OptimizedMessageContent = React.memo(MessageContent, (prevProps, nextProps) => {
|
||||
// 只有这些属性变化时才重新渲染
|
||||
return (
|
||||
prevProps.message.id === nextProps.message.id &&
|
||||
prevProps.message.content === nextProps.message.content &&
|
||||
prevProps.message.status === nextProps.message.status &&
|
||||
prevProps.message.isReasoningExpanded === nextProps.message.isReasoningExpanded &&
|
||||
prevProps.isEditing === nextProps.isEditing &&
|
||||
prevProps.editValue === nextProps.editValue &&
|
||||
prevProps.styleState.isMobile === nextProps.styleState.isMobile
|
||||
);
|
||||
});
|
||||
|
||||
// 优化的消息操作组件
|
||||
export const OptimizedMessageActions = React.memo(MessageActions, (prevProps, nextProps) => {
|
||||
return (
|
||||
prevProps.message.id === nextProps.message.id &&
|
||||
prevProps.isAnyMessageGenerating === nextProps.isAnyMessageGenerating &&
|
||||
prevProps.isEditing === nextProps.isEditing
|
||||
);
|
||||
});
|
||||
|
||||
// 优化的设置面板组件
|
||||
export const OptimizedSettingsPanel = React.memo(SettingsPanel, (prevProps, nextProps) => {
|
||||
return (
|
||||
JSON.stringify(prevProps.inputs) === JSON.stringify(nextProps.inputs) &&
|
||||
JSON.stringify(prevProps.parameterEnabled) === JSON.stringify(nextProps.parameterEnabled) &&
|
||||
JSON.stringify(prevProps.models) === JSON.stringify(nextProps.models) &&
|
||||
JSON.stringify(prevProps.groups) === JSON.stringify(nextProps.groups) &&
|
||||
prevProps.customRequestMode === nextProps.customRequestMode &&
|
||||
prevProps.customRequestBody === nextProps.customRequestBody &&
|
||||
prevProps.showDebugPanel === nextProps.showDebugPanel &&
|
||||
prevProps.showSettings === nextProps.showSettings &&
|
||||
JSON.stringify(prevProps.previewPayload) === JSON.stringify(nextProps.previewPayload) &&
|
||||
JSON.stringify(prevProps.messages) === JSON.stringify(nextProps.messages)
|
||||
);
|
||||
});
|
||||
|
||||
// 优化的调试面板组件
|
||||
export const OptimizedDebugPanel = React.memo(DebugPanel, (prevProps, nextProps) => {
|
||||
return (
|
||||
prevProps.show === nextProps.show &&
|
||||
prevProps.activeTab === nextProps.activeTab &&
|
||||
JSON.stringify(prevProps.debugData) === JSON.stringify(nextProps.debugData) &&
|
||||
JSON.stringify(prevProps.previewPayload) === JSON.stringify(nextProps.previewPayload) &&
|
||||
prevProps.customRequestMode === nextProps.customRequestMode &&
|
||||
prevProps.showDebugPanel === nextProps.showDebugPanel
|
||||
);
|
||||
});
|
||||
@@ -22,11 +22,12 @@ const ParameterControl = ({
|
||||
parameterEnabled,
|
||||
onInputChange,
|
||||
onParameterToggle,
|
||||
disabled = false,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{/* Temperature */}
|
||||
<div className={`transition-opacity duration-200 ${!parameterEnabled.temperature ? 'opacity-50' : ''}`}>
|
||||
<div className={`transition-opacity duration-200 ${!parameterEnabled.temperature || disabled ? 'opacity-50' : ''}`}>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Thermometer size={16} className="text-gray-500" />
|
||||
@@ -44,6 +45,7 @@ const ParameterControl = ({
|
||||
icon={parameterEnabled.temperature ? <Check size={10} /> : <X size={10} />}
|
||||
onClick={() => onParameterToggle('temperature')}
|
||||
className="!rounded-full !w-4 !h-4 !p-0 !min-w-0"
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
<Typography.Text className="text-xs text-gray-500 mb-2">
|
||||
@@ -56,12 +58,12 @@ const ParameterControl = ({
|
||||
value={inputs.temperature}
|
||||
onChange={(value) => onInputChange('temperature', value)}
|
||||
className="mt-2"
|
||||
disabled={!parameterEnabled.temperature}
|
||||
disabled={!parameterEnabled.temperature || disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Top P */}
|
||||
<div className={`transition-opacity duration-200 ${!parameterEnabled.top_p ? 'opacity-50' : ''}`}>
|
||||
<div className={`transition-opacity duration-200 ${!parameterEnabled.top_p || disabled ? 'opacity-50' : ''}`}>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Target size={16} className="text-gray-500" />
|
||||
@@ -79,6 +81,7 @@ const ParameterControl = ({
|
||||
icon={parameterEnabled.top_p ? <Check size={10} /> : <X size={10} />}
|
||||
onClick={() => onParameterToggle('top_p')}
|
||||
className="!rounded-full !w-4 !h-4 !p-0 !min-w-0"
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
<Typography.Text className="text-xs text-gray-500 mb-2">
|
||||
@@ -91,12 +94,12 @@ const ParameterControl = ({
|
||||
value={inputs.top_p}
|
||||
onChange={(value) => onInputChange('top_p', value)}
|
||||
className="mt-2"
|
||||
disabled={!parameterEnabled.top_p}
|
||||
disabled={!parameterEnabled.top_p || disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Frequency Penalty */}
|
||||
<div className={`transition-opacity duration-200 ${!parameterEnabled.frequency_penalty ? 'opacity-50' : ''}`}>
|
||||
<div className={`transition-opacity duration-200 ${!parameterEnabled.frequency_penalty || disabled ? 'opacity-50' : ''}`}>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Repeat size={16} className="text-gray-500" />
|
||||
@@ -114,6 +117,7 @@ const ParameterControl = ({
|
||||
icon={parameterEnabled.frequency_penalty ? <Check size={10} /> : <X size={10} />}
|
||||
onClick={() => onParameterToggle('frequency_penalty')}
|
||||
className="!rounded-full !w-4 !h-4 !p-0 !min-w-0"
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
<Typography.Text className="text-xs text-gray-500 mb-2">
|
||||
@@ -126,12 +130,12 @@ const ParameterControl = ({
|
||||
value={inputs.frequency_penalty}
|
||||
onChange={(value) => onInputChange('frequency_penalty', value)}
|
||||
className="mt-2"
|
||||
disabled={!parameterEnabled.frequency_penalty}
|
||||
disabled={!parameterEnabled.frequency_penalty || disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Presence Penalty */}
|
||||
<div className={`transition-opacity duration-200 ${!parameterEnabled.presence_penalty ? 'opacity-50' : ''}`}>
|
||||
<div className={`transition-opacity duration-200 ${!parameterEnabled.presence_penalty || disabled ? 'opacity-50' : ''}`}>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Ban size={16} className="text-gray-500" />
|
||||
@@ -149,6 +153,7 @@ const ParameterControl = ({
|
||||
icon={parameterEnabled.presence_penalty ? <Check size={10} /> : <X size={10} />}
|
||||
onClick={() => onParameterToggle('presence_penalty')}
|
||||
className="!rounded-full !w-4 !h-4 !p-0 !min-w-0"
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
<Typography.Text className="text-xs text-gray-500 mb-2">
|
||||
@@ -161,12 +166,12 @@ const ParameterControl = ({
|
||||
value={inputs.presence_penalty}
|
||||
onChange={(value) => onInputChange('presence_penalty', value)}
|
||||
className="mt-2"
|
||||
disabled={!parameterEnabled.presence_penalty}
|
||||
disabled={!parameterEnabled.presence_penalty || disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* MaxTokens */}
|
||||
<div className={`transition-opacity duration-200 ${!parameterEnabled.max_tokens ? 'opacity-50' : ''}`}>
|
||||
<div className={`transition-opacity duration-200 ${!parameterEnabled.max_tokens || disabled ? 'opacity-50' : ''}`}>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Hash size={16} className="text-gray-500" />
|
||||
@@ -181,6 +186,7 @@ const ParameterControl = ({
|
||||
icon={parameterEnabled.max_tokens ? <Check size={10} /> : <X size={10} />}
|
||||
onClick={() => onParameterToggle('max_tokens')}
|
||||
className="!rounded-full !w-4 !h-4 !p-0 !min-w-0"
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
@@ -192,12 +198,12 @@ const ParameterControl = ({
|
||||
value={inputs.max_tokens}
|
||||
onChange={(value) => onInputChange('max_tokens', value)}
|
||||
className="!rounded-lg"
|
||||
disabled={!parameterEnabled.max_tokens}
|
||||
disabled={!parameterEnabled.max_tokens || disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Seed */}
|
||||
<div className={`transition-opacity duration-200 ${!parameterEnabled.seed ? 'opacity-50' : ''}`}>
|
||||
<div className={`transition-opacity duration-200 ${!parameterEnabled.seed || disabled ? 'opacity-50' : ''}`}>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Shuffle size={16} className="text-gray-500" />
|
||||
@@ -215,6 +221,7 @@ const ParameterControl = ({
|
||||
icon={parameterEnabled.seed ? <Check size={10} /> : <X size={10} />}
|
||||
onClick={() => onParameterToggle('seed')}
|
||||
className="!rounded-full !w-4 !h-4 !p-0 !min-w-0"
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
@@ -224,7 +231,7 @@ const ParameterControl = ({
|
||||
value={inputs.seed || ''}
|
||||
onChange={(value) => onInputChange('seed', value === '' ? null : value)}
|
||||
className="!rounded-lg"
|
||||
disabled={!parameterEnabled.seed}
|
||||
disabled={!parameterEnabled.seed || disabled}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -7,42 +7,49 @@ import {
|
||||
Button,
|
||||
Switch,
|
||||
Divider,
|
||||
Banner,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
Sparkles,
|
||||
Users,
|
||||
Type,
|
||||
ToggleLeft,
|
||||
X,
|
||||
AlertTriangle,
|
||||
} from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { renderGroupOption } from '../../helpers/render.js';
|
||||
import ParameterControl from './ParameterControl';
|
||||
import ImageUrlInput from './ImageUrlInput';
|
||||
import ConfigManager from './ConfigManager';
|
||||
import CustomRequestEditor from './CustomRequestEditor';
|
||||
|
||||
const SettingsPanel = ({
|
||||
inputs,
|
||||
parameterEnabled,
|
||||
models,
|
||||
groups,
|
||||
systemPrompt,
|
||||
styleState,
|
||||
showDebugPanel,
|
||||
customRequestMode,
|
||||
customRequestBody,
|
||||
onInputChange,
|
||||
onParameterToggle,
|
||||
onSystemPromptChange,
|
||||
onCloseSettings,
|
||||
onConfigImport,
|
||||
onConfigReset,
|
||||
onCustomRequestModeChange,
|
||||
onCustomRequestBodyChange,
|
||||
previewPayload,
|
||||
messages,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const currentConfig = {
|
||||
inputs,
|
||||
parameterEnabled,
|
||||
systemPrompt,
|
||||
showDebugPanel,
|
||||
customRequestMode,
|
||||
customRequestBody,
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -63,6 +70,7 @@ const SettingsPanel = ({
|
||||
onConfigImport={onConfigImport}
|
||||
onConfigReset={onConfigReset}
|
||||
styleState={styleState}
|
||||
messages={messages}
|
||||
/>
|
||||
<Button
|
||||
icon={<X size={16} />}
|
||||
@@ -76,13 +84,27 @@ const SettingsPanel = ({
|
||||
)}
|
||||
|
||||
<div className="space-y-6 overflow-y-auto flex-1 pr-2 model-settings-scroll">
|
||||
{/* 自定义请求体编辑器 */}
|
||||
<CustomRequestEditor
|
||||
customRequestMode={customRequestMode}
|
||||
customRequestBody={customRequestBody}
|
||||
onCustomRequestModeChange={onCustomRequestModeChange}
|
||||
onCustomRequestBodyChange={onCustomRequestBodyChange}
|
||||
defaultPayload={previewPayload}
|
||||
/>
|
||||
|
||||
{/* 分组选择 */}
|
||||
<div>
|
||||
<div className={customRequestMode ? 'opacity-50' : ''}>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Users size={16} className="text-gray-500" />
|
||||
<Typography.Text strong className="text-sm">
|
||||
{t('分组')}
|
||||
</Typography.Text>
|
||||
{customRequestMode && (
|
||||
<Typography.Text className="text-xs text-orange-600">
|
||||
(已在自定义模式中忽略)
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
<Select
|
||||
placeholder={t('请选择分组')}
|
||||
@@ -96,16 +118,22 @@ const SettingsPanel = ({
|
||||
renderOptionItem={renderGroupOption}
|
||||
style={{ width: '100%' }}
|
||||
className="!rounded-lg"
|
||||
disabled={customRequestMode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 模型选择 */}
|
||||
<div>
|
||||
<div className={customRequestMode ? 'opacity-50' : ''}>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Sparkles size={16} className="text-gray-500" />
|
||||
<Typography.Text strong className="text-sm">
|
||||
{t('模型')}
|
||||
</Typography.Text>
|
||||
{customRequestMode && (
|
||||
<Typography.Text className="text-xs text-orange-600">
|
||||
(已在自定义模式中忽略)
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
<Select
|
||||
placeholder={t('请选择模型')}
|
||||
@@ -119,33 +147,45 @@ const SettingsPanel = ({
|
||||
autoComplete='new-password'
|
||||
optionList={models}
|
||||
className="!rounded-lg"
|
||||
disabled={customRequestMode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 图片URL输入 */}
|
||||
<ImageUrlInput
|
||||
imageUrls={inputs.imageUrls}
|
||||
imageEnabled={inputs.imageEnabled}
|
||||
onImageUrlsChange={(urls) => onInputChange('imageUrls', urls)}
|
||||
onImageEnabledChange={(enabled) => onInputChange('imageEnabled', enabled)}
|
||||
/>
|
||||
<div className={customRequestMode ? 'opacity-50' : ''}>
|
||||
<ImageUrlInput
|
||||
imageUrls={inputs.imageUrls}
|
||||
imageEnabled={inputs.imageEnabled}
|
||||
onImageUrlsChange={(urls) => onInputChange('imageUrls', urls)}
|
||||
onImageEnabledChange={(enabled) => onInputChange('imageEnabled', enabled)}
|
||||
disabled={customRequestMode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 参数控制组件 */}
|
||||
<ParameterControl
|
||||
inputs={inputs}
|
||||
parameterEnabled={parameterEnabled}
|
||||
onInputChange={onInputChange}
|
||||
onParameterToggle={onParameterToggle}
|
||||
/>
|
||||
<div className={customRequestMode ? 'opacity-50' : ''}>
|
||||
<ParameterControl
|
||||
inputs={inputs}
|
||||
parameterEnabled={parameterEnabled}
|
||||
onInputChange={onInputChange}
|
||||
onParameterToggle={onParameterToggle}
|
||||
disabled={customRequestMode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 流式输出开关 */}
|
||||
<div>
|
||||
<div className={customRequestMode ? 'opacity-50' : ''}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<ToggleLeft size={16} className="text-gray-500" />
|
||||
<Typography.Text strong className="text-sm">
|
||||
流式输出
|
||||
</Typography.Text>
|
||||
{customRequestMode && (
|
||||
<Typography.Text className="text-xs text-orange-600">
|
||||
(已在自定义模式中忽略)
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
<Switch
|
||||
checked={inputs.stream}
|
||||
@@ -153,30 +193,10 @@ const SettingsPanel = ({
|
||||
checkedText="开"
|
||||
uncheckedText="关"
|
||||
size="small"
|
||||
disabled={customRequestMode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* System Prompt */}
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Type size={16} className="text-gray-500" />
|
||||
<Typography.Text strong className="text-sm">
|
||||
System Prompt
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<TextArea
|
||||
placeholder='System Prompt'
|
||||
name='system'
|
||||
required
|
||||
autoComplete='new-password'
|
||||
autosize
|
||||
defaultValue={systemPrompt}
|
||||
onChange={onSystemPromptChange}
|
||||
className="!rounded-lg"
|
||||
maxHeight={200}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 桌面端的配置管理放在底部 */}
|
||||
@@ -187,6 +207,7 @@ const SettingsPanel = ({
|
||||
onConfigImport={onConfigImport}
|
||||
onConfigReset={onConfigReset}
|
||||
styleState={styleState}
|
||||
messages={messages}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
98
web/src/components/playground/ThinkingContent.js
Normal file
98
web/src/components/playground/ThinkingContent.js
Normal file
@@ -0,0 +1,98 @@
|
||||
import React from 'react';
|
||||
import { Typography } from '@douyinfe/semi-ui';
|
||||
import MarkdownRenderer from '../common/MarkdownRenderer';
|
||||
import { ChevronRight, ChevronUp, Brain, Loader2 } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const ThinkingContent = ({
|
||||
message,
|
||||
finalExtractedThinkingContent,
|
||||
thinkingSource,
|
||||
styleState,
|
||||
onToggleReasoningExpansion
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!finalExtractedThinkingContent) return null;
|
||||
|
||||
const isThinkingStatus = message.status === 'loading' || message.status === 'incomplete';
|
||||
const headerText = (isThinkingStatus && !message.isThinkingComplete) ? t('思考中...') : t('思考过程');
|
||||
|
||||
return (
|
||||
<div className="rounded-xl sm:rounded-2xl mb-2 sm:mb-4 overflow-hidden shadow-sm backdrop-blur-sm">
|
||||
<div
|
||||
className="flex items-center justify-between p-3 sm:p-5 cursor-pointer hover:bg-gradient-to-r hover:from-white/20 hover:to-purple-50/30 transition-all"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #4c1d95 0%, #6d28d9 50%, #7c3aed 100%)',
|
||||
position: 'relative'
|
||||
}}
|
||||
onClick={() => onToggleReasoningExpansion(message.id)}
|
||||
>
|
||||
<div className="absolute inset-0 overflow-hidden">
|
||||
<div className="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full"></div>
|
||||
<div className="absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full"></div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 sm:gap-4 relative">
|
||||
<div className="w-6 h-6 sm:w-8 sm:h-8 rounded-full bg-white/20 flex items-center justify-center shadow-lg">
|
||||
<Brain style={{ color: 'white' }} size={styleState.isMobile ? 12 : 16} />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<Typography.Text strong style={{ color: 'white' }} className="text-sm sm:text-base">
|
||||
{headerText}
|
||||
</Typography.Text>
|
||||
{thinkingSource && (
|
||||
<Typography.Text style={{ color: 'white' }} className="text-xs mt-0.5 opacity-80 hidden sm:block">
|
||||
来源: {thinkingSource}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 sm:gap-3 relative">
|
||||
{isThinkingStatus && !message.isThinkingComplete && (
|
||||
<div className="flex items-center gap-1 sm:gap-2">
|
||||
<Loader2 style={{ color: 'white' }} className="animate-spin" size={styleState.isMobile ? 14 : 18} />
|
||||
<Typography.Text style={{ color: 'white' }} className="text-xs sm:text-sm font-medium opacity-90">
|
||||
思考中
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
{(!isThinkingStatus || message.isThinkingComplete) && (
|
||||
<div className="w-5 h-5 sm:w-6 sm:h-6 rounded-full bg-white/20 flex items-center justify-center">
|
||||
{message.isReasoningExpanded ?
|
||||
<ChevronUp size={styleState.isMobile ? 12 : 16} style={{ color: 'white' }} /> :
|
||||
<ChevronRight size={styleState.isMobile ? 12 : 16} style={{ color: 'white' }} />
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`transition-all duration-500 ease-out ${message.isReasoningExpanded ? 'max-h-96 opacity-100' : 'max-h-0 opacity-0'
|
||||
} overflow-hidden bg-gradient-to-br from-purple-50 via-indigo-50 to-violet-50`}
|
||||
>
|
||||
{message.isReasoningExpanded && (
|
||||
<div className="p-3 sm:p-5 pt-2 sm:pt-4">
|
||||
<div
|
||||
className="bg-white/70 backdrop-blur-sm rounded-lg sm:rounded-xl p-2 shadow-inner overflow-x-auto overflow-y-auto thinking-content-scroll"
|
||||
style={{
|
||||
maxHeight: '200px',
|
||||
minHeight: '100px',
|
||||
scrollbarWidth: 'thin',
|
||||
scrollbarColor: 'rgba(0, 0, 0, 0.3) transparent'
|
||||
}}
|
||||
>
|
||||
<div className="prose prose-xs sm:prose-sm prose-purple max-w-none text-xs sm:text-sm">
|
||||
<MarkdownRenderer
|
||||
content={finalExtractedThinkingContent}
|
||||
className=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThinkingContent;
|
||||
@@ -1,30 +1,6 @@
|
||||
const STORAGE_KEY = 'playground_config';
|
||||
import { STORAGE_KEYS, DEFAULT_CONFIG } from '../../utils/constants';
|
||||
|
||||
const DEFAULT_CONFIG = {
|
||||
inputs: {
|
||||
model: 'deepseek-r1',
|
||||
group: '',
|
||||
max_tokens: 0,
|
||||
temperature: 0,
|
||||
top_p: 1,
|
||||
frequency_penalty: 0,
|
||||
presence_penalty: 0,
|
||||
seed: null,
|
||||
stream: true,
|
||||
imageUrls: [],
|
||||
imageEnabled: false,
|
||||
},
|
||||
parameterEnabled: {
|
||||
max_tokens: true,
|
||||
temperature: true,
|
||||
top_p: false,
|
||||
frequency_penalty: false,
|
||||
presence_penalty: false,
|
||||
seed: false,
|
||||
},
|
||||
systemPrompt: 'You are a helpful assistant. You can help me by answering my questions. You can also ask me questions.',
|
||||
showDebugPanel: false,
|
||||
};
|
||||
const MESSAGES_STORAGE_KEY = 'playground_messages';
|
||||
|
||||
/**
|
||||
* 保存配置到 localStorage
|
||||
@@ -36,20 +12,37 @@ export const saveConfig = (config) => {
|
||||
...config,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(configToSave));
|
||||
localStorage.setItem(STORAGE_KEYS.CONFIG, JSON.stringify(configToSave));
|
||||
console.log('配置已保存到本地存储');
|
||||
} catch (error) {
|
||||
console.error('保存配置失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 保存消息到 localStorage
|
||||
* @param {Array} messages - 要保存的消息数组
|
||||
*/
|
||||
export const saveMessages = (messages) => {
|
||||
try {
|
||||
const messagesToSave = {
|
||||
messages,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
localStorage.setItem(STORAGE_KEYS.MESSAGES, JSON.stringify(messagesToSave));
|
||||
console.log('消息已保存到本地存储');
|
||||
} catch (error) {
|
||||
console.error('保存消息失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 从 localStorage 加载配置
|
||||
* @returns {Object} 配置对象,如果不存在则返回默认配置
|
||||
*/
|
||||
export const loadConfig = () => {
|
||||
try {
|
||||
const savedConfig = localStorage.getItem(STORAGE_KEY);
|
||||
const savedConfig = localStorage.getItem(STORAGE_KEYS.CONFIG);
|
||||
if (savedConfig) {
|
||||
const parsedConfig = JSON.parse(savedConfig);
|
||||
|
||||
@@ -62,8 +55,9 @@ export const loadConfig = () => {
|
||||
...DEFAULT_CONFIG.parameterEnabled,
|
||||
...parsedConfig.parameterEnabled,
|
||||
},
|
||||
systemPrompt: parsedConfig.systemPrompt || DEFAULT_CONFIG.systemPrompt,
|
||||
showDebugPanel: parsedConfig.showDebugPanel || DEFAULT_CONFIG.showDebugPanel,
|
||||
customRequestMode: parsedConfig.customRequestMode || DEFAULT_CONFIG.customRequestMode,
|
||||
customRequestBody: parsedConfig.customRequestBody || DEFAULT_CONFIG.customRequestBody,
|
||||
};
|
||||
|
||||
console.log('配置已从本地存储加载');
|
||||
@@ -77,25 +71,58 @@ export const loadConfig = () => {
|
||||
return DEFAULT_CONFIG;
|
||||
};
|
||||
|
||||
/**
|
||||
* 从 localStorage 加载消息
|
||||
* @returns {Array} 消息数组,如果不存在则返回 null
|
||||
*/
|
||||
export const loadMessages = () => {
|
||||
try {
|
||||
const savedMessages = localStorage.getItem(STORAGE_KEYS.MESSAGES);
|
||||
if (savedMessages) {
|
||||
const parsedMessages = JSON.parse(savedMessages);
|
||||
console.log('消息已从本地存储加载');
|
||||
return parsedMessages.messages || null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载消息失败:', error);
|
||||
}
|
||||
|
||||
console.log('没有找到保存的消息');
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* 清除保存的配置
|
||||
*/
|
||||
export const clearConfig = () => {
|
||||
try {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
console.log('配置已清除');
|
||||
localStorage.removeItem(STORAGE_KEYS.CONFIG);
|
||||
localStorage.removeItem(STORAGE_KEYS.MESSAGES); // 同时清除消息
|
||||
console.log('配置和消息已清除');
|
||||
} catch (error) {
|
||||
console.error('清除配置失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 清除保存的消息
|
||||
*/
|
||||
export const clearMessages = () => {
|
||||
try {
|
||||
localStorage.removeItem(STORAGE_KEYS.MESSAGES);
|
||||
console.log('消息已清除');
|
||||
} catch (error) {
|
||||
console.error('清除消息失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查是否有保存的配置
|
||||
* @returns {boolean} 是否存在保存的配置
|
||||
*/
|
||||
export const hasStoredConfig = () => {
|
||||
try {
|
||||
return localStorage.getItem(STORAGE_KEY) !== null;
|
||||
return localStorage.getItem(STORAGE_KEYS.CONFIG) !== null;
|
||||
} catch (error) {
|
||||
console.error('检查配置失败:', error);
|
||||
return false;
|
||||
@@ -108,7 +135,7 @@ export const hasStoredConfig = () => {
|
||||
*/
|
||||
export const getConfigTimestamp = () => {
|
||||
try {
|
||||
const savedConfig = localStorage.getItem(STORAGE_KEY);
|
||||
const savedConfig = localStorage.getItem(STORAGE_KEYS.CONFIG);
|
||||
if (savedConfig) {
|
||||
const parsedConfig = JSON.parse(savedConfig);
|
||||
return parsedConfig.timestamp || null;
|
||||
@@ -120,13 +147,15 @@ export const getConfigTimestamp = () => {
|
||||
};
|
||||
|
||||
/**
|
||||
* 导出配置为 JSON 文件
|
||||
* 导出配置为 JSON 文件(包含消息)
|
||||
* @param {Object} config - 要导出的配置
|
||||
* @param {Array} messages - 要导出的消息
|
||||
*/
|
||||
export const exportConfig = (config) => {
|
||||
export const exportConfig = (config, messages = null) => {
|
||||
try {
|
||||
const configToExport = {
|
||||
...config,
|
||||
messages: messages || loadMessages(), // 包含消息数据
|
||||
exportTime: new Date().toISOString(),
|
||||
version: '1.0',
|
||||
};
|
||||
@@ -148,7 +177,7 @@ export const exportConfig = (config) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* 从文件导入配置
|
||||
* 从文件导入配置(包含消息)
|
||||
* @param {File} file - 包含配置的 JSON 文件
|
||||
* @returns {Promise<Object>} 导入的配置对象
|
||||
*/
|
||||
@@ -161,6 +190,11 @@ export const importConfig = (file) => {
|
||||
const importedConfig = JSON.parse(e.target.result);
|
||||
|
||||
if (importedConfig.inputs && importedConfig.parameterEnabled) {
|
||||
// 如果导入的配置包含消息,也一起导入
|
||||
if (importedConfig.messages && Array.isArray(importedConfig.messages)) {
|
||||
saveMessages(importedConfig.messages);
|
||||
}
|
||||
|
||||
console.log('配置已从文件导入');
|
||||
resolve(importedConfig);
|
||||
} else {
|
||||
|
||||
70
web/src/hooks/useDataLoader.js
Normal file
70
web/src/hooks/useDataLoader.js
Normal file
@@ -0,0 +1,70 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { API, showError } from '../helpers/index.js';
|
||||
import { API_ENDPOINTS } from '../utils/constants';
|
||||
import { processModelsData, processGroupsData } from '../utils/apiUtils';
|
||||
|
||||
export const useDataLoader = (
|
||||
userState,
|
||||
inputs,
|
||||
handleInputChange,
|
||||
setModels,
|
||||
setGroups
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const loadModels = useCallback(async () => {
|
||||
try {
|
||||
const res = await API.get(API_ENDPOINTS.USER_MODELS);
|
||||
const { success, message, data } = res.data;
|
||||
|
||||
if (success) {
|
||||
const { modelOptions, selectedModel } = processModelsData(data, inputs.model);
|
||||
setModels(modelOptions);
|
||||
|
||||
if (selectedModel !== inputs.model) {
|
||||
handleInputChange('model', selectedModel);
|
||||
}
|
||||
} else {
|
||||
showError(t(message));
|
||||
}
|
||||
} catch (error) {
|
||||
showError(t('加载模型失败'));
|
||||
}
|
||||
}, [inputs.model, handleInputChange, setModels, t]);
|
||||
|
||||
const loadGroups = useCallback(async () => {
|
||||
try {
|
||||
const res = await API.get(API_ENDPOINTS.USER_GROUPS);
|
||||
const { success, message, data } = res.data;
|
||||
|
||||
if (success) {
|
||||
const userGroup = userState?.user?.group || JSON.parse(localStorage.getItem('user'))?.group;
|
||||
const groupOptions = processGroupsData(data, userGroup);
|
||||
setGroups(groupOptions);
|
||||
|
||||
const hasCurrentGroup = groupOptions.some(option => option.value === inputs.group);
|
||||
if (!hasCurrentGroup) {
|
||||
handleInputChange('group', groupOptions[0]?.value || '');
|
||||
}
|
||||
} else {
|
||||
showError(t(message));
|
||||
}
|
||||
} catch (error) {
|
||||
showError(t('加载分组失败'));
|
||||
}
|
||||
}, [userState, inputs.group, handleInputChange, setGroups, t]);
|
||||
|
||||
// 自动加载数据
|
||||
useEffect(() => {
|
||||
if (userState?.user) {
|
||||
loadModels();
|
||||
loadGroups();
|
||||
}
|
||||
}, [userState?.user, loadModels, loadGroups]);
|
||||
|
||||
return {
|
||||
loadModels,
|
||||
loadGroups
|
||||
};
|
||||
};
|
||||
93
web/src/hooks/useMessageEdit.js
Normal file
93
web/src/hooks/useMessageEdit.js
Normal file
@@ -0,0 +1,93 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Toast, Modal } from '@douyinfe/semi-ui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getTextContent, buildApiPayload, createLoadingAssistantMessage } from '../utils/messageUtils';
|
||||
import { MESSAGE_ROLES } from '../utils/constants';
|
||||
|
||||
export const useMessageEdit = (
|
||||
setMessage,
|
||||
inputs,
|
||||
parameterEnabled,
|
||||
sendRequest
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
const [editingMessageId, setEditingMessageId] = useState(null);
|
||||
const [editValue, setEditValue] = useState('');
|
||||
|
||||
const handleMessageEdit = useCallback((targetMessage) => {
|
||||
const editableContent = getTextContent(targetMessage);
|
||||
setEditingMessageId(targetMessage.id);
|
||||
setEditValue(editableContent);
|
||||
}, []);
|
||||
|
||||
const handleEditSave = useCallback(() => {
|
||||
if (!editingMessageId || !editValue.trim()) return;
|
||||
|
||||
setMessage(prevMessages => {
|
||||
const messageIndex = prevMessages.findIndex(msg => msg.id === editingMessageId);
|
||||
if (messageIndex === -1) return prevMessages;
|
||||
|
||||
const targetMessage = prevMessages[messageIndex];
|
||||
let newContent;
|
||||
|
||||
if (Array.isArray(targetMessage.content)) {
|
||||
newContent = targetMessage.content.map(item =>
|
||||
item.type === 'text' ? { ...item, text: editValue.trim() } : item
|
||||
);
|
||||
} else {
|
||||
newContent = editValue.trim();
|
||||
}
|
||||
|
||||
const updatedMessages = prevMessages.map(msg =>
|
||||
msg.id === editingMessageId ? { ...msg, content: newContent } : msg
|
||||
);
|
||||
|
||||
// 处理用户消息编辑后的重新生成
|
||||
if (targetMessage.role === MESSAGE_ROLES.USER) {
|
||||
const hasSubsequentAssistantReply = messageIndex < prevMessages.length - 1 &&
|
||||
prevMessages[messageIndex + 1].role === MESSAGE_ROLES.ASSISTANT;
|
||||
|
||||
if (hasSubsequentAssistantReply) {
|
||||
Modal.confirm({
|
||||
title: t('消息已编辑'),
|
||||
content: t('检测到该消息后有AI回复,是否删除后续回复并重新生成?'),
|
||||
okText: t('重新生成'),
|
||||
cancelText: t('仅保存'),
|
||||
onOk: () => {
|
||||
const messagesUntilUser = updatedMessages.slice(0, messageIndex + 1);
|
||||
setMessage(messagesUntilUser);
|
||||
|
||||
setTimeout(() => {
|
||||
const payload = buildApiPayload(messagesUntilUser, null, inputs, parameterEnabled);
|
||||
setMessage(prevMsg => [...prevMsg, createLoadingAssistantMessage()]);
|
||||
sendRequest(payload, inputs.stream);
|
||||
}, 100);
|
||||
},
|
||||
onCancel: () => setMessage(updatedMessages)
|
||||
});
|
||||
return prevMessages;
|
||||
}
|
||||
}
|
||||
|
||||
return updatedMessages;
|
||||
});
|
||||
|
||||
setEditingMessageId(null);
|
||||
setEditValue('');
|
||||
Toast.success({ content: t('消息已更新'), duration: 2 });
|
||||
}, [editingMessageId, editValue, t, inputs, parameterEnabled, sendRequest, setMessage]);
|
||||
|
||||
const handleEditCancel = useCallback(() => {
|
||||
setEditingMessageId(null);
|
||||
setEditValue('');
|
||||
}, []);
|
||||
|
||||
return {
|
||||
editingMessageId,
|
||||
editValue,
|
||||
setEditValue,
|
||||
handleMessageEdit,
|
||||
handleEditSave,
|
||||
handleEditCancel
|
||||
};
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import { DEFAULT_MESSAGES, DEFAULT_CONFIG, DEBUG_TABS } from '../utils/constants';
|
||||
import { loadConfig, saveConfig } from '../components/playground/configStorage';
|
||||
import { loadConfig, saveConfig, loadMessages, saveMessages } from '../components/playground/configStorage';
|
||||
|
||||
export const usePlaygroundState = () => {
|
||||
// 使用 ref 缓存初始配置,只加载一次
|
||||
@@ -10,17 +10,23 @@ export const usePlaygroundState = () => {
|
||||
}
|
||||
const savedConfig = initialConfigRef.current;
|
||||
|
||||
// 加载保存的消息,如果没有则使用默认消息
|
||||
const initialMessages = loadMessages() || DEFAULT_MESSAGES;
|
||||
|
||||
// 基础配置状态
|
||||
const [inputs, setInputs] = useState(savedConfig.inputs || DEFAULT_CONFIG.inputs);
|
||||
const [parameterEnabled, setParameterEnabled] = useState(
|
||||
savedConfig.parameterEnabled || DEFAULT_CONFIG.parameterEnabled
|
||||
);
|
||||
const [systemPrompt, setSystemPrompt] = useState(
|
||||
savedConfig.systemPrompt || DEFAULT_CONFIG.systemPrompt
|
||||
);
|
||||
const [showDebugPanel, setShowDebugPanel] = useState(
|
||||
savedConfig.showDebugPanel || DEFAULT_CONFIG.showDebugPanel
|
||||
);
|
||||
const [customRequestMode, setCustomRequestMode] = useState(
|
||||
savedConfig.customRequestMode || DEFAULT_CONFIG.customRequestMode
|
||||
);
|
||||
const [customRequestBody, setCustomRequestBody] = useState(
|
||||
savedConfig.customRequestBody || DEFAULT_CONFIG.customRequestBody
|
||||
);
|
||||
|
||||
// UI状态
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
@@ -28,8 +34,8 @@ export const usePlaygroundState = () => {
|
||||
const [groups, setGroups] = useState([]);
|
||||
const [status, setStatus] = useState({});
|
||||
|
||||
// 消息相关状态
|
||||
const [message, setMessage] = useState(DEFAULT_MESSAGES);
|
||||
// 消息相关状态 - 使用加载的消息初始化
|
||||
const [message, setMessage] = useState(initialMessages);
|
||||
|
||||
// 调试状态
|
||||
const [debugData, setDebugData] = useState({
|
||||
@@ -50,6 +56,7 @@ export const usePlaygroundState = () => {
|
||||
const sseSourceRef = useRef(null);
|
||||
const chatRef = useRef(null);
|
||||
const saveConfigTimeoutRef = useRef(null);
|
||||
const saveMessagesTimeoutRef = useRef(null);
|
||||
|
||||
// 配置更新函数
|
||||
const handleInputChange = useCallback((name, value) => {
|
||||
@@ -63,6 +70,17 @@ export const usePlaygroundState = () => {
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// 消息保存函数
|
||||
const debouncedSaveMessages = useCallback(() => {
|
||||
if (saveMessagesTimeoutRef.current) {
|
||||
clearTimeout(saveMessagesTimeoutRef.current);
|
||||
}
|
||||
|
||||
saveMessagesTimeoutRef.current = setTimeout(() => {
|
||||
saveMessages(message);
|
||||
}, 1000);
|
||||
}, [message]);
|
||||
|
||||
// 配置保存
|
||||
const debouncedSaveConfig = useCallback(() => {
|
||||
if (saveConfigTimeoutRef.current) {
|
||||
@@ -73,12 +91,13 @@ export const usePlaygroundState = () => {
|
||||
const configToSave = {
|
||||
inputs,
|
||||
parameterEnabled,
|
||||
systemPrompt,
|
||||
showDebugPanel,
|
||||
customRequestMode,
|
||||
customRequestBody,
|
||||
};
|
||||
saveConfig(configToSave);
|
||||
}, 1000);
|
||||
}, [inputs, parameterEnabled, systemPrompt, showDebugPanel]);
|
||||
}, [inputs, parameterEnabled, showDebugPanel, customRequestMode, customRequestBody]);
|
||||
|
||||
// 配置导入/重置
|
||||
const handleConfigImport = useCallback((importedConfig) => {
|
||||
@@ -88,27 +107,60 @@ export const usePlaygroundState = () => {
|
||||
if (importedConfig.parameterEnabled) {
|
||||
setParameterEnabled(prev => ({ ...prev, ...importedConfig.parameterEnabled }));
|
||||
}
|
||||
if (importedConfig.systemPrompt) {
|
||||
setSystemPrompt(importedConfig.systemPrompt);
|
||||
}
|
||||
if (typeof importedConfig.showDebugPanel === 'boolean') {
|
||||
setShowDebugPanel(importedConfig.showDebugPanel);
|
||||
}
|
||||
if (importedConfig.customRequestMode) {
|
||||
setCustomRequestMode(importedConfig.customRequestMode);
|
||||
}
|
||||
if (importedConfig.customRequestBody) {
|
||||
setCustomRequestBody(importedConfig.customRequestBody);
|
||||
}
|
||||
// 如果导入的配置包含消息,也恢复消息
|
||||
if (importedConfig.messages && Array.isArray(importedConfig.messages)) {
|
||||
setMessage(importedConfig.messages);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleConfigReset = useCallback(() => {
|
||||
const handleConfigReset = useCallback((options = {}) => {
|
||||
const { resetMessages = false } = options;
|
||||
|
||||
setInputs(DEFAULT_CONFIG.inputs);
|
||||
setParameterEnabled(DEFAULT_CONFIG.parameterEnabled);
|
||||
setSystemPrompt(DEFAULT_CONFIG.systemPrompt);
|
||||
setShowDebugPanel(DEFAULT_CONFIG.showDebugPanel);
|
||||
setCustomRequestMode(DEFAULT_CONFIG.customRequestMode);
|
||||
setCustomRequestBody(DEFAULT_CONFIG.customRequestBody);
|
||||
|
||||
// 只有在明确指定时才重置消息
|
||||
if (resetMessages) {
|
||||
setMessage(DEFAULT_MESSAGES);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 监听消息变化并自动保存
|
||||
useEffect(() => {
|
||||
debouncedSaveMessages();
|
||||
}, [debouncedSaveMessages]);
|
||||
|
||||
// 清理定时器
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (saveConfigTimeoutRef.current) {
|
||||
clearTimeout(saveConfigTimeoutRef.current);
|
||||
}
|
||||
if (saveMessagesTimeoutRef.current) {
|
||||
clearTimeout(saveMessagesTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
// 配置状态
|
||||
inputs,
|
||||
parameterEnabled,
|
||||
systemPrompt,
|
||||
showDebugPanel,
|
||||
customRequestMode,
|
||||
customRequestBody,
|
||||
|
||||
// UI状态
|
||||
showSettings,
|
||||
@@ -136,8 +188,9 @@ export const usePlaygroundState = () => {
|
||||
// 更新函数
|
||||
setInputs,
|
||||
setParameterEnabled,
|
||||
setSystemPrompt,
|
||||
setShowDebugPanel,
|
||||
setCustomRequestMode,
|
||||
setCustomRequestBody,
|
||||
setShowSettings,
|
||||
setModels,
|
||||
setGroups,
|
||||
@@ -153,6 +206,7 @@ export const usePlaygroundState = () => {
|
||||
handleInputChange,
|
||||
handleParameterToggle,
|
||||
debouncedSaveConfig,
|
||||
debouncedSaveMessages,
|
||||
handleConfigImport,
|
||||
handleConfigReset,
|
||||
};
|
||||
|
||||
111
web/src/hooks/useSyncMessageAndCustomBody.js
Normal file
111
web/src/hooks/useSyncMessageAndCustomBody.js
Normal file
@@ -0,0 +1,111 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { MESSAGE_ROLES } from '../utils/constants';
|
||||
|
||||
export const useSyncMessageAndCustomBody = (
|
||||
customRequestMode,
|
||||
customRequestBody,
|
||||
message,
|
||||
inputs,
|
||||
setCustomRequestBody,
|
||||
setMessage,
|
||||
debouncedSaveConfig
|
||||
) => {
|
||||
const isUpdatingFromMessage = useRef(false);
|
||||
const isUpdatingFromCustomBody = useRef(false);
|
||||
const lastMessageHash = useRef('');
|
||||
const lastCustomBodyHash = useRef('');
|
||||
|
||||
const getMessageHash = useCallback((messages) => {
|
||||
return JSON.stringify(messages.map(msg => ({
|
||||
id: msg.id,
|
||||
role: msg.role,
|
||||
content: msg.content
|
||||
})));
|
||||
}, []);
|
||||
|
||||
const getCustomBodyHash = useCallback((customBody) => {
|
||||
try {
|
||||
const parsed = JSON.parse(customBody);
|
||||
return JSON.stringify(parsed.messages || []);
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}, []);
|
||||
|
||||
const syncMessageToCustomBody = useCallback(() => {
|
||||
if (!customRequestMode || isUpdatingFromCustomBody.current) return;
|
||||
|
||||
const currentMessageHash = getMessageHash(message);
|
||||
if (currentMessageHash === lastMessageHash.current) return;
|
||||
|
||||
try {
|
||||
isUpdatingFromMessage.current = true;
|
||||
let customPayload;
|
||||
|
||||
try {
|
||||
customPayload = JSON.parse(customRequestBody || '{}');
|
||||
} catch {
|
||||
customPayload = {
|
||||
model: inputs.model || 'gpt-4o',
|
||||
messages: [],
|
||||
temperature: inputs.temperature || 0.7,
|
||||
stream: inputs.stream !== false
|
||||
};
|
||||
}
|
||||
|
||||
customPayload.messages = message.map(msg => ({
|
||||
role: msg.role,
|
||||
content: msg.content
|
||||
}));
|
||||
|
||||
const newCustomBody = JSON.stringify(customPayload, null, 2);
|
||||
setCustomRequestBody(newCustomBody);
|
||||
lastMessageHash.current = currentMessageHash;
|
||||
lastCustomBodyHash.current = getCustomBodyHash(newCustomBody);
|
||||
|
||||
setTimeout(() => {
|
||||
debouncedSaveConfig();
|
||||
}, 0);
|
||||
} finally {
|
||||
isUpdatingFromMessage.current = false;
|
||||
}
|
||||
}, [customRequestMode, customRequestBody, message, inputs.model, inputs.temperature, inputs.stream, getMessageHash, getCustomBodyHash, setCustomRequestBody, debouncedSaveConfig]);
|
||||
|
||||
const syncCustomBodyToMessage = useCallback(() => {
|
||||
if (!customRequestMode || isUpdatingFromMessage.current) return;
|
||||
|
||||
const currentCustomBodyHash = getCustomBodyHash(customRequestBody);
|
||||
if (currentCustomBodyHash === lastCustomBodyHash.current) return;
|
||||
|
||||
try {
|
||||
isUpdatingFromCustomBody.current = true;
|
||||
const customPayload = JSON.parse(customRequestBody || '{}');
|
||||
|
||||
if (customPayload.messages && Array.isArray(customPayload.messages)) {
|
||||
const newMessages = customPayload.messages.map((msg, index) => ({
|
||||
id: msg.id || (index + 1).toString(),
|
||||
role: msg.role || MESSAGE_ROLES.USER,
|
||||
content: msg.content || '',
|
||||
createAt: Date.now(),
|
||||
...(msg.role === MESSAGE_ROLES.ASSISTANT && {
|
||||
reasoningContent: msg.reasoningContent || '',
|
||||
isReasoningExpanded: false
|
||||
})
|
||||
}));
|
||||
|
||||
setMessage(newMessages);
|
||||
lastCustomBodyHash.current = currentCustomBodyHash;
|
||||
lastMessageHash.current = getMessageHash(newMessages);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('同步自定义请求体到消息失败:', error);
|
||||
} finally {
|
||||
isUpdatingFromCustomBody.current = false;
|
||||
}
|
||||
}, [customRequestMode, customRequestBody, getCustomBodyHash, getMessageHash, setMessage]);
|
||||
|
||||
return {
|
||||
syncMessageToCustomBody,
|
||||
syncCustomBodyToMessage
|
||||
};
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useContext, useEffect, useCallback } from 'react';
|
||||
import React, { useContext, useEffect, useCallback, useRef } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Layout, Toast, Modal } from '@douyinfe/semi-ui';
|
||||
@@ -8,36 +8,37 @@ import { UserContext } from '../../context/User/index.js';
|
||||
import { StyleContext } from '../../context/Style/index.js';
|
||||
|
||||
// Utils and hooks
|
||||
import { API, showError, getLogo, isMobile } from '../../helpers/index.js';
|
||||
import { getLogo } from '../../helpers/index.js';
|
||||
import { stringToColor } from '../../helpers/render.js';
|
||||
import { usePlaygroundState } from '../../hooks/usePlaygroundState.js';
|
||||
import { useMessageActions } from '../../hooks/useMessageActions.js';
|
||||
import { useApiRequest } from '../../hooks/useApiRequest.js';
|
||||
import { useSyncMessageAndCustomBody } from '../../hooks/useSyncMessageAndCustomBody.js';
|
||||
import { useMessageEdit } from '../../hooks/useMessageEdit.js';
|
||||
import { useDataLoader } from '../../hooks/useDataLoader.js';
|
||||
|
||||
// Constants and utils
|
||||
import {
|
||||
DEFAULT_MESSAGES,
|
||||
MESSAGE_ROLES,
|
||||
API_ENDPOINTS
|
||||
ERROR_MESSAGES
|
||||
} from '../../utils/constants.js';
|
||||
import {
|
||||
buildMessageContent,
|
||||
createMessage,
|
||||
createLoadingAssistantMessage,
|
||||
getTextContent
|
||||
getTextContent,
|
||||
buildApiPayload
|
||||
} from '../../utils/messageUtils.js';
|
||||
import {
|
||||
buildApiPayload,
|
||||
processModelsData,
|
||||
processGroupsData
|
||||
} from '../../utils/apiUtils.js';
|
||||
|
||||
// Components
|
||||
import SettingsPanel from '../../components/playground/SettingsPanel.js';
|
||||
import {
|
||||
OptimizedSettingsPanel,
|
||||
OptimizedDebugPanel,
|
||||
OptimizedMessageContent,
|
||||
OptimizedMessageActions
|
||||
} from '../../components/playground/OptimizedComponents.js';
|
||||
import ChatArea from '../../components/playground/ChatArea.js';
|
||||
import DebugPanel from '../../components/playground/DebugPanel.js';
|
||||
import MessageContent from '../../components/playground/MessageContent.js';
|
||||
import MessageActions from '../../components/playground/MessageActions.js';
|
||||
import FloatingButtons from '../../components/playground/FloatingButtons.js';
|
||||
|
||||
// 生成头像
|
||||
@@ -67,8 +68,9 @@ const Playground = () => {
|
||||
const {
|
||||
inputs,
|
||||
parameterEnabled,
|
||||
systemPrompt,
|
||||
showDebugPanel,
|
||||
customRequestMode,
|
||||
customRequestBody,
|
||||
showSettings,
|
||||
models,
|
||||
groups,
|
||||
@@ -77,8 +79,6 @@ const Playground = () => {
|
||||
debugData,
|
||||
activeDebugTab,
|
||||
previewPayload,
|
||||
editingMessageId,
|
||||
editValue,
|
||||
sseSourceRef,
|
||||
chatRef,
|
||||
handleInputChange,
|
||||
@@ -94,10 +94,9 @@ const Playground = () => {
|
||||
setDebugData,
|
||||
setActiveDebugTab,
|
||||
setPreviewPayload,
|
||||
setEditingMessageId,
|
||||
setEditValue,
|
||||
setSystemPrompt,
|
||||
setShowDebugPanel,
|
||||
setCustomRequestMode,
|
||||
setCustomRequestBody,
|
||||
} = state;
|
||||
|
||||
// API 请求相关
|
||||
@@ -108,6 +107,30 @@ const Playground = () => {
|
||||
sseSourceRef
|
||||
);
|
||||
|
||||
// 数据加载
|
||||
useDataLoader(userState, inputs, handleInputChange, setModels, setGroups);
|
||||
|
||||
// 消息编辑
|
||||
const {
|
||||
editingMessageId,
|
||||
editValue,
|
||||
setEditValue,
|
||||
handleMessageEdit,
|
||||
handleEditSave,
|
||||
handleEditCancel
|
||||
} = useMessageEdit(setMessage, inputs, parameterEnabled, sendRequest);
|
||||
|
||||
// 消息和自定义请求体同步
|
||||
const { syncMessageToCustomBody, syncCustomBodyToMessage } = useSyncMessageAndCustomBody(
|
||||
customRequestMode,
|
||||
customRequestBody,
|
||||
message,
|
||||
inputs,
|
||||
setCustomRequestBody,
|
||||
setMessage,
|
||||
debouncedSaveConfig
|
||||
);
|
||||
|
||||
// 角色信息
|
||||
const roleInfo = {
|
||||
user: {
|
||||
@@ -130,12 +153,16 @@ const Playground = () => {
|
||||
// 构建预览请求体
|
||||
const constructPreviewPayload = useCallback(() => {
|
||||
try {
|
||||
const systemMessage = systemPrompt !== '' ? createMessage(
|
||||
MESSAGE_ROLES.SYSTEM,
|
||||
systemPrompt,
|
||||
{ id: '1', createAt: 1715676751919 }
|
||||
) : null;
|
||||
// 如果是自定义请求体模式且有自定义内容,直接返回解析后的自定义请求体
|
||||
if (customRequestMode && customRequestBody && customRequestBody.trim()) {
|
||||
try {
|
||||
return JSON.parse(customRequestBody);
|
||||
} catch (parseError) {
|
||||
console.warn('自定义请求体JSON解析失败,回退到默认预览:', parseError);
|
||||
}
|
||||
}
|
||||
|
||||
// 默认预览逻辑
|
||||
let messages = [...message];
|
||||
|
||||
// 如果没有用户消息,添加默认消息
|
||||
@@ -145,7 +172,6 @@ const Playground = () => {
|
||||
messages = [createMessage(MESSAGE_ROLES.USER, content)];
|
||||
} else {
|
||||
// 处理最后一个用户消息的图片
|
||||
const lastUserMessageIndex = messages.length - 1;
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
if (messages[i].role === MESSAGE_ROLES.USER) {
|
||||
if (inputs.imageEnabled && inputs.imageUrls) {
|
||||
@@ -161,33 +187,51 @@ const Playground = () => {
|
||||
}
|
||||
}
|
||||
|
||||
return buildApiPayload(messages, systemMessage, inputs, parameterEnabled);
|
||||
return buildApiPayload(messages, null, inputs, parameterEnabled);
|
||||
} catch (error) {
|
||||
console.error('构造预览请求体失败:', error);
|
||||
return null;
|
||||
}
|
||||
}, [inputs, parameterEnabled, systemPrompt, message]);
|
||||
}, [inputs, parameterEnabled, message, customRequestMode, customRequestBody]);
|
||||
|
||||
// 发送消息
|
||||
function onMessageSend(content, attachment) {
|
||||
console.log('attachment: ', attachment);
|
||||
|
||||
const validImageUrls = inputs.imageUrls.filter(url => url.trim() !== '');
|
||||
const messageContent = buildMessageContent(content, validImageUrls, inputs.imageEnabled);
|
||||
|
||||
const userMessage = createMessage(MESSAGE_ROLES.USER, messageContent);
|
||||
// 创建用户消息和加载消息
|
||||
const userMessage = createMessage(MESSAGE_ROLES.USER, content);
|
||||
const loadingMessage = createLoadingAssistantMessage();
|
||||
|
||||
// 如果是自定义请求体模式
|
||||
if (customRequestMode && customRequestBody) {
|
||||
try {
|
||||
const customPayload = JSON.parse(customRequestBody);
|
||||
|
||||
setMessage(prevMessage => {
|
||||
const newMessages = [...prevMessage, userMessage, loadingMessage];
|
||||
|
||||
// 发送自定义请求体
|
||||
sendRequest(customPayload, customPayload.stream !== false);
|
||||
|
||||
return newMessages;
|
||||
});
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error('自定义请求体JSON解析失败:', error);
|
||||
Toast.error(ERROR_MESSAGES.JSON_PARSE_ERROR);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 默认模式
|
||||
const validImageUrls = inputs.imageUrls.filter(url => url.trim() !== '');
|
||||
const messageContent = buildMessageContent(content, validImageUrls, inputs.imageEnabled);
|
||||
const userMessageWithImages = createMessage(MESSAGE_ROLES.USER, messageContent);
|
||||
|
||||
setMessage(prevMessage => {
|
||||
const newMessages = [...prevMessage, userMessage];
|
||||
const newMessages = [...prevMessage, userMessageWithImages];
|
||||
|
||||
const systemMessage = systemPrompt !== '' ? createMessage(
|
||||
MESSAGE_ROLES.SYSTEM,
|
||||
systemPrompt,
|
||||
{ id: '1', createAt: 1715676751919 }
|
||||
) : null;
|
||||
|
||||
const payload = buildApiPayload(newMessages, systemMessage, inputs, parameterEnabled);
|
||||
const payload = buildApiPayload(newMessages, null, inputs, parameterEnabled);
|
||||
sendRequest(payload, inputs.stream);
|
||||
|
||||
// 禁用图片模式
|
||||
@@ -201,127 +245,8 @@ const Playground = () => {
|
||||
});
|
||||
}
|
||||
|
||||
// 加载模型和分组
|
||||
const loadModels = async () => {
|
||||
try {
|
||||
const res = await API.get(API_ENDPOINTS.USER_MODELS);
|
||||
const { success, message, data } = res.data;
|
||||
|
||||
if (success) {
|
||||
const { modelOptions, selectedModel } = processModelsData(data, inputs.model);
|
||||
setModels(modelOptions);
|
||||
|
||||
if (selectedModel !== inputs.model) {
|
||||
handleInputChange('model', selectedModel);
|
||||
}
|
||||
} else {
|
||||
showError(t(message));
|
||||
}
|
||||
} catch (error) {
|
||||
showError(t('加载模型失败'));
|
||||
}
|
||||
};
|
||||
|
||||
const loadGroups = async () => {
|
||||
try {
|
||||
const res = await API.get(API_ENDPOINTS.USER_GROUPS);
|
||||
const { success, message, data } = res.data;
|
||||
|
||||
if (success) {
|
||||
const userGroup = userState?.user?.group || JSON.parse(localStorage.getItem('user'))?.group;
|
||||
const groupOptions = processGroupsData(data, userGroup);
|
||||
setGroups(groupOptions);
|
||||
|
||||
const hasCurrentGroup = groupOptions.some(option => option.value === inputs.group);
|
||||
if (!hasCurrentGroup) {
|
||||
handleInputChange('group', groupOptions[0]?.value || '');
|
||||
}
|
||||
} else {
|
||||
showError(t(message));
|
||||
}
|
||||
} catch (error) {
|
||||
showError(t('加载分组失败'));
|
||||
}
|
||||
};
|
||||
|
||||
// 编辑消息相关
|
||||
const handleMessageEdit = useCallback((targetMessage) => {
|
||||
const editableContent = getTextContent(targetMessage);
|
||||
setEditingMessageId(targetMessage.id);
|
||||
setEditValue(editableContent);
|
||||
}, [setEditingMessageId, setEditValue]);
|
||||
|
||||
const handleEditSave = useCallback(() => {
|
||||
if (!editingMessageId || !editValue.trim()) return;
|
||||
|
||||
setMessage(prevMessages => {
|
||||
const messageIndex = prevMessages.findIndex(msg => msg.id === editingMessageId);
|
||||
if (messageIndex === -1) return prevMessages;
|
||||
|
||||
const targetMessage = prevMessages[messageIndex];
|
||||
let newContent;
|
||||
|
||||
if (Array.isArray(targetMessage.content)) {
|
||||
newContent = targetMessage.content.map(item =>
|
||||
item.type === 'text' ? { ...item, text: editValue.trim() } : item
|
||||
);
|
||||
} else {
|
||||
newContent = editValue.trim();
|
||||
}
|
||||
|
||||
const updatedMessages = prevMessages.map(msg =>
|
||||
msg.id === editingMessageId ? { ...msg, content: newContent } : msg
|
||||
);
|
||||
|
||||
// 处理用户消息编辑后的重新生成
|
||||
if (targetMessage.role === MESSAGE_ROLES.USER) {
|
||||
const hasSubsequentAssistantReply = messageIndex < prevMessages.length - 1 &&
|
||||
prevMessages[messageIndex + 1].role === MESSAGE_ROLES.ASSISTANT;
|
||||
|
||||
if (hasSubsequentAssistantReply) {
|
||||
Modal.confirm({
|
||||
title: t('消息已编辑'),
|
||||
content: t('检测到该消息后有AI回复,是否删除后续回复并重新生成?'),
|
||||
okText: t('重新生成'),
|
||||
cancelText: t('仅保存'),
|
||||
onOk: () => {
|
||||
const messagesUntilUser = updatedMessages.slice(0, messageIndex + 1);
|
||||
setMessage(messagesUntilUser);
|
||||
|
||||
setTimeout(() => {
|
||||
const systemMessage = systemPrompt !== '' ? createMessage(
|
||||
MESSAGE_ROLES.SYSTEM,
|
||||
systemPrompt,
|
||||
{ id: '1', createAt: 1715676751919 }
|
||||
) : null;
|
||||
|
||||
const payload = buildApiPayload(messagesUntilUser, systemMessage, inputs, parameterEnabled);
|
||||
|
||||
setMessage(prevMsg => [...prevMsg, createLoadingAssistantMessage()]);
|
||||
sendRequest(payload, inputs.stream);
|
||||
}, 100);
|
||||
},
|
||||
onCancel: () => setMessage(updatedMessages)
|
||||
});
|
||||
return prevMessages;
|
||||
}
|
||||
}
|
||||
|
||||
return updatedMessages;
|
||||
});
|
||||
|
||||
setEditingMessageId(null);
|
||||
setEditValue('');
|
||||
Toast.success({ content: t('消息已更新'), duration: 2 });
|
||||
}, [editingMessageId, editValue, t, systemPrompt, inputs, parameterEnabled, sendRequest, setMessage, setEditingMessageId, setEditValue]);
|
||||
|
||||
const handleEditCancel = useCallback(() => {
|
||||
setEditingMessageId(null);
|
||||
setEditValue('');
|
||||
}, [setEditingMessageId, setEditValue]);
|
||||
|
||||
// 切换推理展开状态
|
||||
const toggleReasoningExpansion = (messageId) => {
|
||||
const toggleReasoningExpansion = useCallback((messageId) => {
|
||||
setMessage(prevMessages =>
|
||||
prevMessages.map(msg =>
|
||||
msg.id === messageId && msg.role === MESSAGE_ROLES.ASSISTANT
|
||||
@@ -329,7 +254,7 @@ const Playground = () => {
|
||||
: msg
|
||||
)
|
||||
);
|
||||
};
|
||||
}, [setMessage]);
|
||||
|
||||
// 渲染函数
|
||||
const renderCustomChatContent = useCallback(
|
||||
@@ -337,7 +262,7 @@ const Playground = () => {
|
||||
const isCurrentlyEditing = editingMessageId === message.id;
|
||||
|
||||
return (
|
||||
<MessageContent
|
||||
<OptimizedMessageContent
|
||||
message={message}
|
||||
className={className}
|
||||
styleState={styleState}
|
||||
@@ -350,7 +275,7 @@ const Playground = () => {
|
||||
/>
|
||||
);
|
||||
},
|
||||
[styleState, editingMessageId, editValue, handleEditSave, handleEditCancel, setEditValue],
|
||||
[styleState, editingMessageId, editValue, handleEditSave, handleEditCancel, setEditValue, toggleReasoningExpansion],
|
||||
);
|
||||
|
||||
const renderChatBoxAction = useCallback((props) => {
|
||||
@@ -361,7 +286,7 @@ const Playground = () => {
|
||||
const isCurrentlyEditing = editingMessageId === currentMessage.id;
|
||||
|
||||
return (
|
||||
<MessageActions
|
||||
<OptimizedMessageActions
|
||||
message={currentMessage}
|
||||
styleState={styleState}
|
||||
onMessageReset={messageActions.handleMessageReset}
|
||||
@@ -376,47 +301,56 @@ const Playground = () => {
|
||||
}, [messageActions, styleState, message, editingMessageId, handleMessageEdit]);
|
||||
|
||||
// Effects
|
||||
|
||||
// 同步消息和自定义请求体
|
||||
useEffect(() => {
|
||||
syncMessageToCustomBody();
|
||||
}, [message, syncMessageToCustomBody]);
|
||||
|
||||
useEffect(() => {
|
||||
syncCustomBodyToMessage();
|
||||
}, [customRequestBody, syncCustomBodyToMessage]);
|
||||
|
||||
// 处理URL参数
|
||||
useEffect(() => {
|
||||
if (searchParams.get('expired')) {
|
||||
showError(t('未登录或登录已过期,请重新登录!'));
|
||||
Toast.warning(t('登录过期,请重新登录!'));
|
||||
}
|
||||
|
||||
const savedStatus = localStorage.getItem('status');
|
||||
if (savedStatus) {
|
||||
setStatus(JSON.parse(savedStatus));
|
||||
}
|
||||
|
||||
loadModels();
|
||||
loadGroups();
|
||||
}, [searchParams, t]);
|
||||
|
||||
// 处理窗口大小变化
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
styleDispatch({
|
||||
type: 'set_is_mobile',
|
||||
payload: isMobile(),
|
||||
});
|
||||
const mobile = window.innerWidth < 768;
|
||||
if (styleState.isMobile !== mobile) {
|
||||
styleDispatch({ type: 'SET_IS_MOBILE', payload: mobile });
|
||||
}
|
||||
};
|
||||
|
||||
handleResize();
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, [styleDispatch]);
|
||||
}, [styleState.isMobile, styleDispatch]);
|
||||
|
||||
// 构建预览payload
|
||||
useEffect(() => {
|
||||
const newPreviewPayload = constructPreviewPayload();
|
||||
setPreviewPayload(newPreviewPayload);
|
||||
setDebugData(prev => ({
|
||||
...prev,
|
||||
previewRequest: newPreviewPayload,
|
||||
previewTimestamp: new Date().toISOString()
|
||||
}));
|
||||
}, [constructPreviewPayload, setPreviewPayload, setDebugData]);
|
||||
const timer = setTimeout(() => {
|
||||
const preview = constructPreviewPayload();
|
||||
setPreviewPayload(preview);
|
||||
setDebugData(prev => ({
|
||||
...prev,
|
||||
previewRequest: preview ? JSON.stringify(preview, null, 2) : null,
|
||||
previewTimestamp: preview ? new Date().toISOString() : null
|
||||
}));
|
||||
}, 300);
|
||||
|
||||
// 监听配置变化并自动保存
|
||||
return () => clearTimeout(timer);
|
||||
}, [message, inputs, parameterEnabled, customRequestMode, customRequestBody, constructPreviewPayload, setPreviewPayload, setDebugData]);
|
||||
|
||||
// 自动保存配置
|
||||
useEffect(() => {
|
||||
debouncedSaveConfig();
|
||||
}, [inputs, parameterEnabled, systemPrompt, showDebugPanel]);
|
||||
}, [inputs, parameterEnabled, showDebugPanel, customRequestMode, customRequestBody, debouncedSaveConfig]);
|
||||
|
||||
return (
|
||||
<div className="h-full bg-gray-50">
|
||||
@@ -442,21 +376,25 @@ const Playground = () => {
|
||||
width={styleState.isMobile ? '100%' : 320}
|
||||
className={styleState.isMobile ? 'bg-white shadow-lg' : ''}
|
||||
>
|
||||
<SettingsPanel
|
||||
<OptimizedSettingsPanel
|
||||
inputs={inputs}
|
||||
parameterEnabled={parameterEnabled}
|
||||
models={models}
|
||||
groups={groups}
|
||||
systemPrompt={systemPrompt}
|
||||
styleState={styleState}
|
||||
showSettings={showSettings}
|
||||
showDebugPanel={showDebugPanel}
|
||||
customRequestMode={customRequestMode}
|
||||
customRequestBody={customRequestBody}
|
||||
onInputChange={handleInputChange}
|
||||
onParameterToggle={handleParameterToggle}
|
||||
onSystemPromptChange={setSystemPrompt}
|
||||
onCloseSettings={() => setShowSettings(false)}
|
||||
onConfigImport={handleConfigImport}
|
||||
onConfigReset={handleConfigReset}
|
||||
onCustomRequestModeChange={setCustomRequestMode}
|
||||
onCustomRequestBodyChange={setCustomRequestBody}
|
||||
previewPayload={previewPayload}
|
||||
messages={message}
|
||||
/>
|
||||
</Layout.Sider>
|
||||
)}
|
||||
@@ -487,11 +425,12 @@ const Playground = () => {
|
||||
{/* 调试面板 - 桌面端 */}
|
||||
{showDebugPanel && !styleState.isMobile && (
|
||||
<div className="w-96 flex-shrink-0 h-full">
|
||||
<DebugPanel
|
||||
<OptimizedDebugPanel
|
||||
debugData={debugData}
|
||||
activeDebugTab={activeDebugTab}
|
||||
onActiveDebugTabChange={setActiveDebugTab}
|
||||
styleState={styleState}
|
||||
customRequestMode={customRequestMode}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -512,13 +451,14 @@ const Playground = () => {
|
||||
}}
|
||||
className="shadow-lg"
|
||||
>
|
||||
<DebugPanel
|
||||
<OptimizedDebugPanel
|
||||
debugData={debugData}
|
||||
activeDebugTab={activeDebugTab}
|
||||
onActiveDebugTabChange={setActiveDebugTab}
|
||||
styleState={styleState}
|
||||
showDebugPanel={showDebugPanel}
|
||||
onCloseDebugPanel={() => setShowDebugPanel(false)}
|
||||
customRequestMode={customRequestMode}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,37 +1,42 @@
|
||||
import { formatMessageForAPI } from './messageUtils';
|
||||
|
||||
// 构建API请求载荷
|
||||
export const buildApiPayload = (messages, systemMessage, inputs, parameterEnabled) => {
|
||||
const formattedMessages = messages.map(formatMessageForAPI);
|
||||
export const buildApiPayload = (messages, systemPrompt, inputs, parameterEnabled) => {
|
||||
const processedMessages = messages.map(formatMessageForAPI);
|
||||
|
||||
if (systemMessage) {
|
||||
formattedMessages.unshift(formatMessageForAPI(systemMessage));
|
||||
// 如果有系统提示,插入到消息开头
|
||||
if (systemPrompt && systemPrompt.trim()) {
|
||||
processedMessages.unshift({
|
||||
role: 'system',
|
||||
content: systemPrompt.trim()
|
||||
});
|
||||
}
|
||||
|
||||
const payload = {
|
||||
messages: formattedMessages,
|
||||
stream: inputs.stream,
|
||||
model: inputs.model,
|
||||
group: inputs.group,
|
||||
messages: processedMessages,
|
||||
stream: inputs.stream,
|
||||
};
|
||||
|
||||
// 添加可选参数
|
||||
const optionalParams = [
|
||||
'max_tokens', 'temperature', 'top_p',
|
||||
'frequency_penalty', 'presence_penalty', 'seed'
|
||||
];
|
||||
|
||||
optionalParams.forEach(param => {
|
||||
if (parameterEnabled[param]) {
|
||||
if (param === 'max_tokens' && inputs[param] > 0) {
|
||||
payload[param] = parseInt(inputs[param]);
|
||||
} else if (param === 'seed' && inputs[param] !== null && inputs[param] !== '') {
|
||||
payload[param] = parseInt(inputs[param]);
|
||||
} else if (param !== 'max_tokens' && param !== 'seed') {
|
||||
payload[param] = inputs[param];
|
||||
}
|
||||
}
|
||||
});
|
||||
// 添加启用的参数
|
||||
if (parameterEnabled.temperature && inputs.temperature !== undefined) {
|
||||
payload.temperature = inputs.temperature;
|
||||
}
|
||||
if (parameterEnabled.top_p && inputs.top_p !== undefined) {
|
||||
payload.top_p = inputs.top_p;
|
||||
}
|
||||
if (parameterEnabled.max_tokens && inputs.max_tokens !== undefined) {
|
||||
payload.max_tokens = inputs.max_tokens;
|
||||
}
|
||||
if (parameterEnabled.frequency_penalty && inputs.frequency_penalty !== undefined) {
|
||||
payload.frequency_penalty = inputs.frequency_penalty;
|
||||
}
|
||||
if (parameterEnabled.presence_penalty && inputs.presence_penalty !== undefined) {
|
||||
payload.presence_penalty = inputs.presence_penalty;
|
||||
}
|
||||
if (parameterEnabled.seed && inputs.seed !== undefined && inputs.seed !== null) {
|
||||
payload.seed = inputs.seed;
|
||||
}
|
||||
|
||||
return payload;
|
||||
};
|
||||
|
||||
@@ -1,21 +1,4 @@
|
||||
// Playground 相关常量
|
||||
export const DEFAULT_MESSAGES = [
|
||||
{
|
||||
role: 'user',
|
||||
id: '2',
|
||||
createAt: 1715676751919,
|
||||
content: '你好',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
id: '3',
|
||||
createAt: 1715676751919,
|
||||
content: '你好,请问有什么可以帮助您的吗?',
|
||||
reasoningContent: '',
|
||||
isReasoningExpanded: false,
|
||||
},
|
||||
];
|
||||
|
||||
// ========== 消息相关常量 ==========
|
||||
export const MESSAGE_STATUS = {
|
||||
LOADING: 'loading',
|
||||
INCOMPLETE: 'incomplete',
|
||||
@@ -29,21 +12,42 @@ export const MESSAGE_ROLES = {
|
||||
SYSTEM: 'system',
|
||||
};
|
||||
|
||||
// 默认消息示例
|
||||
export const DEFAULT_MESSAGES = [
|
||||
{
|
||||
role: MESSAGE_ROLES.USER,
|
||||
id: '2',
|
||||
createAt: 1715676751919,
|
||||
content: '你好',
|
||||
},
|
||||
{
|
||||
role: MESSAGE_ROLES.ASSISTANT,
|
||||
id: '3',
|
||||
createAt: 1715676751919,
|
||||
content: '你好,请问有什么可以帮助您的吗?',
|
||||
reasoningContent: '',
|
||||
isReasoningExpanded: false,
|
||||
},
|
||||
];
|
||||
|
||||
// ========== UI 相关常量 ==========
|
||||
export const DEBUG_TABS = {
|
||||
PREVIEW: 'preview',
|
||||
REQUEST: 'request',
|
||||
RESPONSE: 'response',
|
||||
};
|
||||
|
||||
// ========== API 相关常量 ==========
|
||||
export const API_ENDPOINTS = {
|
||||
CHAT_COMPLETIONS: '/pg/chat/completions',
|
||||
USER_MODELS: '/api/user/models',
|
||||
USER_GROUPS: '/api/user/self/groups',
|
||||
};
|
||||
|
||||
// ========== 配置默认值 ==========
|
||||
export const DEFAULT_CONFIG = {
|
||||
inputs: {
|
||||
model: 'gpt-4',
|
||||
model: 'gpt-4o',
|
||||
group: '',
|
||||
temperature: 0.7,
|
||||
top_p: 1,
|
||||
@@ -65,14 +69,27 @@ export const DEFAULT_CONFIG = {
|
||||
},
|
||||
systemPrompt: '',
|
||||
showDebugPanel: false,
|
||||
customRequestMode: false,
|
||||
customRequestBody: '',
|
||||
};
|
||||
|
||||
// ========== 正则表达式 ==========
|
||||
export const THINK_TAG_REGEX = /<think>([\s\S]*?)<\/think>/g;
|
||||
|
||||
// ========== 错误消息 ==========
|
||||
export const ERROR_MESSAGES = {
|
||||
NO_TEXT_CONTENT: '此消息没有可复制的文本内容',
|
||||
INVALID_MESSAGE_TYPE: '无法复制此类型的消息内容',
|
||||
COPY_FAILED: '复制失败,请手动选择文本复制',
|
||||
COPY_HTTPS_REQUIRED: '复制功能需要 HTTPS 环境,请手动复制',
|
||||
BROWSER_NOT_SUPPORTED: '浏览器不支持复制功能,请手动复制',
|
||||
JSON_PARSE_ERROR: '自定义请求体格式错误,请检查JSON格式',
|
||||
API_REQUEST_ERROR: '请求发生错误',
|
||||
NETWORK_ERROR: '网络连接失败或服务器无响应',
|
||||
};
|
||||
|
||||
// ========== 存储键名 ==========
|
||||
export const STORAGE_KEYS = {
|
||||
CONFIG: 'playground_config',
|
||||
MESSAGES: 'playground_messages',
|
||||
};
|
||||
@@ -6,6 +6,8 @@ export const generateMessageId = () => `${messageId++}`;
|
||||
|
||||
// 提取消息中的文本内容
|
||||
export const getTextContent = (message) => {
|
||||
if (!message || !message.content) return '';
|
||||
|
||||
if (Array.isArray(message.content)) {
|
||||
const textContent = message.content.find(item => item.type === 'text');
|
||||
return textContent?.text || '';
|
||||
@@ -15,12 +17,12 @@ export const getTextContent = (message) => {
|
||||
|
||||
// 处理 think 标签
|
||||
export const processThinkTags = (content, reasoningContent = '') => {
|
||||
if (!content.includes('<think>')) {
|
||||
if (!content || !content.includes('<think>')) {
|
||||
return { content, reasoningContent };
|
||||
}
|
||||
|
||||
let thoughts = [];
|
||||
let replyParts = [];
|
||||
const thoughts = [];
|
||||
const replyParts = [];
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
|
||||
@@ -33,14 +35,10 @@ export const processThinkTags = (content, reasoningContent = '') => {
|
||||
replyParts.push(content.substring(lastIndex));
|
||||
|
||||
const processedContent = replyParts.join('').replace(/<\/?think>/g, '').trim();
|
||||
|
||||
let processedReasoningContent = reasoningContent;
|
||||
if (thoughts.length > 0) {
|
||||
const thoughtsStr = thoughts.join('\n\n---\n\n');
|
||||
processedReasoningContent = reasoningContent
|
||||
? `${reasoningContent}\n\n---\n\n${thoughtsStr}`
|
||||
: thoughtsStr;
|
||||
}
|
||||
const thoughtsStr = thoughts.join('\n\n---\n\n');
|
||||
const processedReasoningContent = reasoningContent && thoughtsStr
|
||||
? `${reasoningContent}\n\n---\n\n${thoughtsStr}`
|
||||
: reasoningContent || thoughtsStr;
|
||||
|
||||
return {
|
||||
content: processedContent,
|
||||
@@ -50,6 +48,8 @@ export const processThinkTags = (content, reasoningContent = '') => {
|
||||
|
||||
// 处理未完成的 think 标签
|
||||
export const processIncompleteThinkTags = (content, reasoningContent = '') => {
|
||||
if (!content) return { content: '', reasoningContent };
|
||||
|
||||
const lastOpenThinkIndex = content.lastIndexOf('<think>');
|
||||
if (lastOpenThinkIndex === -1) {
|
||||
return processThinkTags(content, reasoningContent);
|
||||
@@ -59,13 +59,9 @@ export const processIncompleteThinkTags = (content, reasoningContent = '') => {
|
||||
if (!fragmentAfterLastOpen.includes('</think>')) {
|
||||
const unclosedThought = fragmentAfterLastOpen.substring('<think>'.length).trim();
|
||||
const cleanContent = content.substring(0, lastOpenThinkIndex);
|
||||
|
||||
let processedReasoningContent = reasoningContent;
|
||||
if (unclosedThought) {
|
||||
processedReasoningContent = reasoningContent
|
||||
? `${reasoningContent}\n\n---\n\n${unclosedThought}`
|
||||
: unclosedThought;
|
||||
}
|
||||
const processedReasoningContent = unclosedThought
|
||||
? reasoningContent ? `${reasoningContent}\n\n---\n\n${unclosedThought}` : unclosedThought
|
||||
: reasoningContent;
|
||||
|
||||
return processThinkTags(cleanContent, processedReasoningContent);
|
||||
}
|
||||
@@ -75,11 +71,15 @@ export const processIncompleteThinkTags = (content, reasoningContent = '') => {
|
||||
|
||||
// 构建消息内容(包含图片)
|
||||
export const buildMessageContent = (textContent, imageUrls = [], imageEnabled = false) => {
|
||||
const validImageUrls = imageUrls.filter(url => url.trim() !== '');
|
||||
if (!textContent && (!imageUrls || imageUrls.length === 0)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const validImageUrls = imageUrls.filter(url => url && url.trim() !== '');
|
||||
|
||||
if (imageEnabled && validImageUrls.length > 0) {
|
||||
return [
|
||||
{ type: 'text', text: textContent },
|
||||
{ type: 'text', text: textContent || '' },
|
||||
...validImageUrls.map(url => ({
|
||||
type: 'image_url',
|
||||
image_url: { url: url.trim() }
|
||||
@@ -87,7 +87,7 @@ export const buildMessageContent = (textContent, imageUrls = [], imageEnabled =
|
||||
];
|
||||
}
|
||||
|
||||
return textContent;
|
||||
return textContent || '';
|
||||
};
|
||||
|
||||
// 创建新消息
|
||||
@@ -114,12 +114,88 @@ export const createLoadingAssistantMessage = () => createMessage(
|
||||
|
||||
// 检查消息是否包含图片
|
||||
export const hasImageContent = (message) => {
|
||||
return Array.isArray(message.content) &&
|
||||
return message &&
|
||||
Array.isArray(message.content) &&
|
||||
message.content.some(item => item.type === 'image_url');
|
||||
};
|
||||
|
||||
// 格式化消息用于API请求
|
||||
export const formatMessageForAPI = (message) => ({
|
||||
role: message.role,
|
||||
content: message.content
|
||||
});
|
||||
export const formatMessageForAPI = (message) => {
|
||||
if (!message) return null;
|
||||
|
||||
return {
|
||||
role: message.role,
|
||||
content: message.content
|
||||
};
|
||||
};
|
||||
|
||||
// 验证消息是否有效
|
||||
export const isValidMessage = (message) => {
|
||||
return message &&
|
||||
message.role &&
|
||||
(message.content || message.content === '');
|
||||
};
|
||||
|
||||
// 获取最后一条用户消息
|
||||
export const getLastUserMessage = (messages) => {
|
||||
if (!Array.isArray(messages)) return null;
|
||||
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
if (messages[i].role === MESSAGE_ROLES.USER) {
|
||||
return messages[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// 获取最后一条助手消息
|
||||
export const getLastAssistantMessage = (messages) => {
|
||||
if (!Array.isArray(messages)) return null;
|
||||
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
if (messages[i].role === MESSAGE_ROLES.ASSISTANT) {
|
||||
return messages[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// 构建API请求负载(从apiUtils移动过来)
|
||||
export const buildApiPayload = (messages, systemPrompt, inputs, parameterEnabled) => {
|
||||
const processedMessages = messages
|
||||
.filter(isValidMessage)
|
||||
.map(formatMessageForAPI)
|
||||
.filter(Boolean);
|
||||
|
||||
// 如果有系统提示,插入到消息开头
|
||||
if (systemPrompt && systemPrompt.trim()) {
|
||||
processedMessages.unshift({
|
||||
role: MESSAGE_ROLES.SYSTEM,
|
||||
content: systemPrompt.trim()
|
||||
});
|
||||
}
|
||||
|
||||
const payload = {
|
||||
model: inputs.model,
|
||||
messages: processedMessages,
|
||||
stream: inputs.stream,
|
||||
};
|
||||
|
||||
// 添加启用的参数
|
||||
const parameterMappings = {
|
||||
temperature: 'temperature',
|
||||
top_p: 'top_p',
|
||||
max_tokens: 'max_tokens',
|
||||
frequency_penalty: 'frequency_penalty',
|
||||
presence_penalty: 'presence_penalty',
|
||||
seed: 'seed'
|
||||
};
|
||||
|
||||
Object.entries(parameterMappings).forEach(([key, param]) => {
|
||||
if (parameterEnabled[key] && inputs[param] !== undefined && inputs[param] !== null) {
|
||||
payload[param] = inputs[param];
|
||||
}
|
||||
});
|
||||
|
||||
return payload;
|
||||
};
|
||||
Reference in New Issue
Block a user