🎨 chore(web): apply ESLint and Prettier auto-fixes (baseline)
- Ran: bun run eslint:fix && bun run lint:fix - Inserted AGPL license header via eslint-plugin-header - Enforced no-multiple-empty-lines and other lint rules - Formatted code using Prettier v3 (@so1ve/prettier-config) - No functional changes; formatting-only baseline across JS/JSX files
This commit is contained in:
@@ -18,17 +18,8 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Card,
|
||||
Chat,
|
||||
Typography,
|
||||
Button,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
MessageSquare,
|
||||
Eye,
|
||||
EyeOff,
|
||||
} from 'lucide-react';
|
||||
import { Card, Chat, Typography, Button } from '@douyinfe/semi-ui';
|
||||
import { MessageSquare, Eye, EyeOff } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import CustomInputRender from './CustomInputRender';
|
||||
|
||||
@@ -57,37 +48,43 @@ const ChatArea = ({
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="h-full"
|
||||
className='h-full'
|
||||
bordered={false}
|
||||
bodyStyle={{ padding: 0, height: 'calc(100vh - 66px)', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
|
||||
bodyStyle={{
|
||||
padding: 0,
|
||||
height: 'calc(100vh - 66px)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{/* 聊天头部 */}
|
||||
{styleState.isMobile ? (
|
||||
<div className="pt-4"></div>
|
||||
<div className='pt-4'></div>
|
||||
) : (
|
||||
<div className="px-6 py-4 bg-gradient-to-r from-purple-500 to-blue-500 rounded-t-2xl">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-white/20 backdrop-blur flex items-center justify-center">
|
||||
<MessageSquare size={20} className="text-white" />
|
||||
<div className='px-6 py-4 bg-gradient-to-r from-purple-500 to-blue-500 rounded-t-2xl'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<div className='w-10 h-10 rounded-full bg-white/20 backdrop-blur flex items-center justify-center'>
|
||||
<MessageSquare size={20} className='text-white' />
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Title heading={5} className="!text-white mb-0">
|
||||
<Typography.Title heading={5} className='!text-white mb-0'>
|
||||
{t('AI 对话')}
|
||||
</Typography.Title>
|
||||
<Typography.Text className="!text-white/80 text-sm hidden sm:inline">
|
||||
<Typography.Text className='!text-white/80 text-sm hidden sm:inline'>
|
||||
{inputs.model || t('选择模型开始对话')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className='flex items-center gap-2'>
|
||||
<Button
|
||||
icon={showDebugPanel ? <EyeOff size={14} /> : <Eye size={14} />}
|
||||
onClick={onToggleDebugPanel}
|
||||
theme="borderless"
|
||||
type="primary"
|
||||
size="small"
|
||||
className="!rounded-lg !text-white/80 hover:!text-white hover:!bg-white/10"
|
||||
theme='borderless'
|
||||
type='primary'
|
||||
size='small'
|
||||
className='!rounded-lg !text-white/80 hover:!text-white hover:!bg-white/10'
|
||||
>
|
||||
{showDebugPanel ? t('隐藏调试') : t('显示调试')}
|
||||
</Button>
|
||||
@@ -97,7 +94,7 @@ const ChatArea = ({
|
||||
)}
|
||||
|
||||
{/* 聊天内容区域 */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<div className='flex-1 overflow-hidden'>
|
||||
<Chat
|
||||
ref={chatRef}
|
||||
chatBoxRenderConfig={{
|
||||
@@ -110,7 +107,7 @@ const ChatArea = ({
|
||||
style={{
|
||||
height: '100%',
|
||||
maxWidth: '100%',
|
||||
overflow: 'hidden'
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
chats={message}
|
||||
onMessageSend={onMessageSend}
|
||||
@@ -121,7 +118,7 @@ const ChatArea = ({
|
||||
showStopGenerate
|
||||
onStopGenerator={onStopGenerator}
|
||||
onClear={onClearMessages}
|
||||
className="h-full"
|
||||
className='h-full'
|
||||
placeholder={t('请输入您的问题...')}
|
||||
/>
|
||||
</div>
|
||||
@@ -129,4 +126,4 @@ const ChatArea = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatArea;
|
||||
export default ChatArea;
|
||||
|
||||
@@ -102,15 +102,17 @@ const highlightJson = (str) => {
|
||||
color = '#569cd6';
|
||||
}
|
||||
return `<span style="color: ${color}">${match}</span>`;
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const isJsonLike = (content, language) => {
|
||||
if (language === 'json') return true;
|
||||
const trimmed = content.trim();
|
||||
return (trimmed.startsWith('{') && trimmed.endsWith('}')) ||
|
||||
(trimmed.startsWith('[') && trimmed.endsWith(']'));
|
||||
return (
|
||||
(trimmed.startsWith('{') && trimmed.endsWith('}')) ||
|
||||
(trimmed.startsWith('[') && trimmed.endsWith(']'))
|
||||
);
|
||||
};
|
||||
|
||||
const formatContent = (content) => {
|
||||
@@ -148,7 +150,10 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
|
||||
const contentMetrics = useMemo(() => {
|
||||
const length = formattedContent.length;
|
||||
const isLarge = length > PERFORMANCE_CONFIG.MAX_DISPLAY_LENGTH;
|
||||
const isVeryLarge = length > PERFORMANCE_CONFIG.MAX_DISPLAY_LENGTH * PERFORMANCE_CONFIG.VERY_LARGE_MULTIPLIER;
|
||||
const isVeryLarge =
|
||||
length >
|
||||
PERFORMANCE_CONFIG.MAX_DISPLAY_LENGTH *
|
||||
PERFORMANCE_CONFIG.VERY_LARGE_MULTIPLIER;
|
||||
return { length, isLarge, isVeryLarge };
|
||||
}, [formattedContent.length]);
|
||||
|
||||
@@ -156,8 +161,10 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
|
||||
if (!contentMetrics.isLarge || isExpanded) {
|
||||
return formattedContent;
|
||||
}
|
||||
return formattedContent.substring(0, PERFORMANCE_CONFIG.PREVIEW_LENGTH) +
|
||||
'\n\n// ... 内容被截断以提升性能 ...';
|
||||
return (
|
||||
formattedContent.substring(0, PERFORMANCE_CONFIG.PREVIEW_LENGTH) +
|
||||
'\n\n// ... 内容被截断以提升性能 ...'
|
||||
);
|
||||
}, [formattedContent, contentMetrics.isLarge, isExpanded]);
|
||||
|
||||
const highlightedContent = useMemo(() => {
|
||||
@@ -174,9 +181,10 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
|
||||
|
||||
const handleCopy = useCallback(async () => {
|
||||
try {
|
||||
const textToCopy = typeof content === 'object' && content !== null
|
||||
? JSON.stringify(content, null, 2)
|
||||
: content;
|
||||
const textToCopy =
|
||||
typeof content === 'object' && content !== null
|
||||
? JSON.stringify(content, null, 2)
|
||||
: content;
|
||||
|
||||
const success = await copy(textToCopy);
|
||||
setCopied(true);
|
||||
@@ -205,11 +213,12 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
|
||||
}, [isExpanded, contentMetrics.isVeryLarge]);
|
||||
|
||||
if (!content) {
|
||||
const placeholderText = {
|
||||
preview: t('正在构造请求体预览...'),
|
||||
request: t('暂无请求数据'),
|
||||
response: t('暂无响应数据')
|
||||
}[title] || t('暂无数据');
|
||||
const placeholderText =
|
||||
{
|
||||
preview: t('正在构造请求体预览...'),
|
||||
request: t('暂无请求数据'),
|
||||
response: t('暂无响应数据'),
|
||||
}[title] || t('暂无数据');
|
||||
|
||||
return (
|
||||
<div style={codeThemeStyles.noContent}>
|
||||
@@ -222,7 +231,7 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
|
||||
const contentPadding = contentMetrics.isLarge ? '52px' : '16px';
|
||||
|
||||
return (
|
||||
<div style={codeThemeStyles.container} className="h-full">
|
||||
<div style={codeThemeStyles.container} className='h-full'>
|
||||
{/* 性能警告 */}
|
||||
{contentMetrics.isLarge && (
|
||||
<div style={codeThemeStyles.performanceWarning}>
|
||||
@@ -250,8 +259,8 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
|
||||
<Button
|
||||
icon={<Copy size={14} />}
|
||||
onClick={handleCopy}
|
||||
size="small"
|
||||
theme="borderless"
|
||||
size='small'
|
||||
theme='borderless'
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
@@ -268,25 +277,29 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
|
||||
...codeThemeStyles.content,
|
||||
paddingTop: contentPadding,
|
||||
}}
|
||||
className="model-settings-scroll"
|
||||
className='model-settings-scroll'
|
||||
>
|
||||
{isProcessing ? (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '200px',
|
||||
color: '#888'
|
||||
}}>
|
||||
<div style={{
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
border: '2px solid #444',
|
||||
borderTop: '2px solid #888',
|
||||
borderRadius: '50%',
|
||||
animation: 'spin 1s linear infinite',
|
||||
marginRight: '8px'
|
||||
}} />
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '200px',
|
||||
color: '#888',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
border: '2px solid #444',
|
||||
borderTop: '2px solid #888',
|
||||
borderRadius: '50%',
|
||||
animation: 'spin 1s linear infinite',
|
||||
marginRight: '8px',
|
||||
}}
|
||||
/>
|
||||
{t('正在处理大内容...')}
|
||||
</div>
|
||||
) : (
|
||||
@@ -296,18 +309,22 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
|
||||
|
||||
{/* 展开/收起按钮 */}
|
||||
{contentMetrics.isLarge && !isProcessing && (
|
||||
<div style={{
|
||||
...codeThemeStyles.actionButton,
|
||||
bottom: '12px',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
...codeThemeStyles.actionButton,
|
||||
bottom: '12px',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
}}
|
||||
>
|
||||
<Tooltip content={isExpanded ? t('收起内容') : t('显示完整内容')}>
|
||||
<Button
|
||||
icon={isExpanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
|
||||
icon={
|
||||
isExpanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />
|
||||
}
|
||||
onClick={handleToggleExpand}
|
||||
size="small"
|
||||
theme="borderless"
|
||||
size='small'
|
||||
theme='borderless'
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
@@ -317,8 +334,16 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
|
||||
>
|
||||
{isExpanded ? t('收起') : t('展开')}
|
||||
{!isExpanded && (
|
||||
<span style={{ fontSize: '11px', opacity: 0.7, marginLeft: '4px' }}>
|
||||
(+{Math.round((contentMetrics.length - PERFORMANCE_CONFIG.PREVIEW_LENGTH) / 1000)}K)
|
||||
<span
|
||||
style={{ fontSize: '11px', opacity: 0.7, marginLeft: '4px' }}
|
||||
>
|
||||
(+
|
||||
{Math.round(
|
||||
(contentMetrics.length -
|
||||
PERFORMANCE_CONFIG.PREVIEW_LENGTH) /
|
||||
1000,
|
||||
)}
|
||||
K)
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
@@ -329,4 +354,4 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeViewer;
|
||||
export default CodeViewer;
|
||||
|
||||
@@ -18,21 +18,16 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Typography,
|
||||
Toast,
|
||||
Modal,
|
||||
Dropdown,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
Download,
|
||||
Upload,
|
||||
RotateCcw,
|
||||
Settings2,
|
||||
} from 'lucide-react';
|
||||
import { Button, Typography, Toast, Modal, Dropdown } from '@douyinfe/semi-ui';
|
||||
import { Download, Upload, RotateCcw, Settings2 } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { exportConfig, importConfig, clearConfig, hasStoredConfig, getConfigTimestamp } from './configStorage';
|
||||
import {
|
||||
exportConfig,
|
||||
importConfig,
|
||||
clearConfig,
|
||||
hasStoredConfig,
|
||||
getConfigTimestamp,
|
||||
} from './configStorage';
|
||||
|
||||
const ConfigManager = ({
|
||||
currentConfig,
|
||||
@@ -51,7 +46,10 @@ const ConfigManager = ({
|
||||
...currentConfig,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
localStorage.setItem('playground_config', JSON.stringify(configWithTimestamp));
|
||||
localStorage.setItem(
|
||||
'playground_config',
|
||||
JSON.stringify(configWithTimestamp),
|
||||
);
|
||||
|
||||
exportConfig(currentConfig, messages);
|
||||
Toast.success({
|
||||
@@ -104,7 +102,9 @@ const ConfigManager = ({
|
||||
const handleReset = () => {
|
||||
Modal.confirm({
|
||||
title: t('重置配置'),
|
||||
content: t('将清除所有保存的配置并恢复默认设置,此操作不可撤销。是否继续?'),
|
||||
content: t(
|
||||
'将清除所有保存的配置并恢复默认设置,此操作不可撤销。是否继续?',
|
||||
),
|
||||
okText: t('确定重置'),
|
||||
cancelText: t('取消'),
|
||||
okButtonProps: {
|
||||
@@ -114,7 +114,9 @@ const ConfigManager = ({
|
||||
// 询问是否同时重置消息
|
||||
Modal.confirm({
|
||||
title: t('重置选项'),
|
||||
content: t('是否同时重置对话消息?选择"是"将清空所有对话记录并恢复默认示例;选择"否"将保留当前对话记录。'),
|
||||
content: t(
|
||||
'是否同时重置对话消息?选择"是"将清空所有对话记录并恢复默认示例;选择"否"将保留当前对话记录。',
|
||||
),
|
||||
okText: t('同时重置消息'),
|
||||
cancelText: t('仅重置配置'),
|
||||
okButtonProps: {
|
||||
@@ -159,7 +161,7 @@ const ConfigManager = ({
|
||||
name: 'export',
|
||||
onClick: handleExport,
|
||||
children: (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className='flex items-center gap-2'>
|
||||
<Download size={14} />
|
||||
{t('导出配置')}
|
||||
</div>
|
||||
@@ -170,7 +172,7 @@ const ConfigManager = ({
|
||||
name: 'import',
|
||||
onClick: handleImportClick,
|
||||
children: (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className='flex items-center gap-2'>
|
||||
<Upload size={14} />
|
||||
{t('导入配置')}
|
||||
</div>
|
||||
@@ -184,7 +186,7 @@ const ConfigManager = ({
|
||||
name: 'reset',
|
||||
onClick: handleReset,
|
||||
children: (
|
||||
<div className="flex items-center gap-2 text-red-600">
|
||||
<div className='flex items-center gap-2 text-red-600'>
|
||||
<RotateCcw size={14} />
|
||||
{t('重置配置')}
|
||||
</div>
|
||||
@@ -197,24 +199,24 @@ const ConfigManager = ({
|
||||
return (
|
||||
<>
|
||||
<Dropdown
|
||||
trigger="click"
|
||||
position="bottomLeft"
|
||||
trigger='click'
|
||||
position='bottomLeft'
|
||||
showTick
|
||||
menu={dropdownItems}
|
||||
>
|
||||
<Button
|
||||
icon={<Settings2 size={14} />}
|
||||
theme="borderless"
|
||||
type="tertiary"
|
||||
size="small"
|
||||
className="!rounded-lg !text-gray-600 hover:!text-blue-600 hover:!bg-blue-50"
|
||||
theme='borderless'
|
||||
type='tertiary'
|
||||
size='small'
|
||||
className='!rounded-lg !text-gray-600 hover:!text-blue-600 hover:!bg-blue-50'
|
||||
/>
|
||||
</Dropdown>
|
||||
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".json"
|
||||
type='file'
|
||||
accept='.json'
|
||||
onChange={handleFileChange}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
@@ -224,42 +226,42 @@ const ConfigManager = ({
|
||||
|
||||
// 桌面端显示紧凑的按钮组
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className='space-y-3'>
|
||||
{/* 配置状态信息和重置按钮 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Typography.Text className="text-xs text-gray-500">
|
||||
<div className='flex items-center justify-between'>
|
||||
<Typography.Text className='text-xs text-gray-500'>
|
||||
{getConfigStatus()}
|
||||
</Typography.Text>
|
||||
<Button
|
||||
icon={<RotateCcw size={12} />}
|
||||
size="small"
|
||||
theme="borderless"
|
||||
type="danger"
|
||||
size='small'
|
||||
theme='borderless'
|
||||
type='danger'
|
||||
onClick={handleReset}
|
||||
className="!rounded-full !text-xs !px-2"
|
||||
className='!rounded-full !text-xs !px-2'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 导出和导入按钮 */}
|
||||
<div className="flex gap-2">
|
||||
<div className='flex gap-2'>
|
||||
<Button
|
||||
icon={<Download size={12} />}
|
||||
size="small"
|
||||
theme="solid"
|
||||
type="primary"
|
||||
size='small'
|
||||
theme='solid'
|
||||
type='primary'
|
||||
onClick={handleExport}
|
||||
className="!rounded-lg flex-1 !text-xs !h-7"
|
||||
className='!rounded-lg flex-1 !text-xs !h-7'
|
||||
>
|
||||
{t('导出')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
icon={<Upload size={12} />}
|
||||
size="small"
|
||||
theme="outline"
|
||||
type="primary"
|
||||
size='small'
|
||||
theme='outline'
|
||||
type='primary'
|
||||
onClick={handleImportClick}
|
||||
className="!rounded-lg flex-1 !text-xs !h-7"
|
||||
className='!rounded-lg flex-1 !text-xs !h-7'
|
||||
>
|
||||
{t('导入')}
|
||||
</Button>
|
||||
@@ -267,8 +269,8 @@ const ConfigManager = ({
|
||||
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".json"
|
||||
type='file'
|
||||
accept='.json'
|
||||
onChange={handleFileChange}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
@@ -276,4 +278,4 @@ const ConfigManager = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfigManager;
|
||||
export default ConfigManager;
|
||||
|
||||
@@ -21,23 +21,24 @@ import React from 'react';
|
||||
|
||||
const CustomInputRender = (props) => {
|
||||
const { detailProps } = props;
|
||||
const { clearContextNode, uploadNode, inputNode, sendNode, onClick } = detailProps;
|
||||
const { clearContextNode, uploadNode, inputNode, sendNode, onClick } =
|
||||
detailProps;
|
||||
|
||||
// 清空按钮
|
||||
const styledClearNode = clearContextNode
|
||||
? React.cloneElement(clearContextNode, {
|
||||
className: `!rounded-full !bg-gray-100 hover:!bg-red-500 hover:!text-white flex-shrink-0 transition-all ${clearContextNode.props.className || ''}`,
|
||||
style: {
|
||||
...clearContextNode.props.style,
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
minWidth: '32px',
|
||||
padding: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}
|
||||
})
|
||||
className: `!rounded-full !bg-gray-100 hover:!bg-red-500 hover:!text-white flex-shrink-0 transition-all ${clearContextNode.props.className || ''}`,
|
||||
style: {
|
||||
...clearContextNode.props.style,
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
minWidth: '32px',
|
||||
padding: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
})
|
||||
: null;
|
||||
|
||||
// 发送按钮
|
||||
@@ -52,21 +53,19 @@ const CustomInputRender = (props) => {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="p-2 sm:p-4">
|
||||
<div className='p-2 sm:p-4'>
|
||||
<div
|
||||
className="flex items-center gap-2 sm:gap-3 p-2 bg-gray-50 rounded-xl sm:rounded-2xl shadow-sm hover:shadow-md transition-shadow"
|
||||
className='flex items-center gap-2 sm:gap-3 p-2 bg-gray-50 rounded-xl sm:rounded-2xl shadow-sm hover:shadow-md transition-shadow'
|
||||
style={{ border: '1px solid var(--semi-color-border)' }}
|
||||
onClick={onClick}
|
||||
>
|
||||
{/* 清空对话按钮 - 左边 */}
|
||||
{styledClearNode}
|
||||
<div className="flex-1">
|
||||
{inputNode}
|
||||
</div>
|
||||
<div className='flex-1'>{inputNode}</div>
|
||||
{/* 发送按钮 - 右边 */}
|
||||
{styledSendNode}
|
||||
</div>
|
||||
@@ -74,4 +73,4 @@ const CustomInputRender = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomInputRender;
|
||||
export default CustomInputRender;
|
||||
|
||||
@@ -25,13 +25,7 @@ import {
|
||||
Switch,
|
||||
Banner,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
Code,
|
||||
Edit,
|
||||
Check,
|
||||
X,
|
||||
AlertTriangle,
|
||||
} from 'lucide-react';
|
||||
import { Code, Edit, Check, X, AlertTriangle } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const CustomRequestEditor = ({
|
||||
@@ -48,12 +42,22 @@ const CustomRequestEditor = ({
|
||||
|
||||
// 当切换到自定义模式时,用默认payload初始化
|
||||
useEffect(() => {
|
||||
if (customRequestMode && (!customRequestBody || customRequestBody.trim() === '')) {
|
||||
const defaultJson = defaultPayload ? JSON.stringify(defaultPayload, null, 2) : '';
|
||||
if (
|
||||
customRequestMode &&
|
||||
(!customRequestBody || customRequestBody.trim() === '')
|
||||
) {
|
||||
const defaultJson = defaultPayload
|
||||
? JSON.stringify(defaultPayload, null, 2)
|
||||
: '';
|
||||
setLocalValue(defaultJson);
|
||||
onCustomRequestBodyChange(defaultJson);
|
||||
}
|
||||
}, [customRequestMode, defaultPayload, customRequestBody, onCustomRequestBodyChange]);
|
||||
}, [
|
||||
customRequestMode,
|
||||
defaultPayload,
|
||||
customRequestBody,
|
||||
onCustomRequestBodyChange,
|
||||
]);
|
||||
|
||||
// 同步外部传入的customRequestBody到本地状态
|
||||
useEffect(() => {
|
||||
@@ -113,21 +117,21 @@ const CustomRequestEditor = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
<Switch
|
||||
checked={customRequestMode}
|
||||
onChange={handleModeToggle}
|
||||
checkedText="开"
|
||||
uncheckedText="关"
|
||||
size="small"
|
||||
checkedText='开'
|
||||
uncheckedText='关'
|
||||
size='small'
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -135,43 +139,43 @@ const CustomRequestEditor = ({
|
||||
<>
|
||||
{/* 提示信息 */}
|
||||
<Banner
|
||||
type="warning"
|
||||
description="启用此模式后,将使用您自定义的请求体发送API请求,模型配置面板的参数设置将被忽略。"
|
||||
type='warning'
|
||||
description='启用此模式后,将使用您自定义的请求体发送API请求,模型配置面板的参数设置将被忽略。'
|
||||
icon={<AlertTriangle size={16} />}
|
||||
className="!rounded-lg"
|
||||
className='!rounded-lg'
|
||||
closeIcon={null}
|
||||
/>
|
||||
|
||||
{/* JSON编辑器 */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Typography.Text strong className="text-sm">
|
||||
<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">
|
||||
<div className='flex items-center gap-2'>
|
||||
{isValid ? (
|
||||
<div className="flex items-center gap-1 text-green-600">
|
||||
<div className='flex items-center gap-1 text-green-600'>
|
||||
<Check size={14} />
|
||||
<Typography.Text className="text-xs">
|
||||
<Typography.Text className='text-xs'>
|
||||
格式正确
|
||||
</Typography.Text>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1 text-red-600">
|
||||
<div className='flex items-center gap-1 text-red-600'>
|
||||
<X size={14} />
|
||||
<Typography.Text className="text-xs">
|
||||
<Typography.Text className='text-xs'>
|
||||
格式错误
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
theme="borderless"
|
||||
type="tertiary"
|
||||
size="small"
|
||||
theme='borderless'
|
||||
type='tertiary'
|
||||
size='small'
|
||||
icon={<Edit size={14} />}
|
||||
onClick={formatJson}
|
||||
disabled={!isValid}
|
||||
className="!rounded-lg"
|
||||
className='!rounded-lg'
|
||||
>
|
||||
格式化
|
||||
</Button>
|
||||
@@ -191,12 +195,12 @@ const CustomRequestEditor = ({
|
||||
/>
|
||||
|
||||
{!isValid && errorMessage && (
|
||||
<Typography.Text type="danger" className="text-xs mt-1 block">
|
||||
<Typography.Text type='danger' className='text-xs mt-1 block'>
|
||||
{errorMessage}
|
||||
</Typography.Text>
|
||||
)}
|
||||
|
||||
<Typography.Text className="text-xs text-gray-500 mt-2 block">
|
||||
<Typography.Text className='text-xs text-gray-500 mt-2 block'>
|
||||
请输入有效的JSON格式的请求体。您可以参考预览面板中的默认请求体格式。
|
||||
</Typography.Text>
|
||||
</div>
|
||||
@@ -206,4 +210,4 @@ const CustomRequestEditor = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomRequestEditor;
|
||||
export default CustomRequestEditor;
|
||||
|
||||
@@ -26,14 +26,7 @@ import {
|
||||
Button,
|
||||
Dropdown,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
Code,
|
||||
Zap,
|
||||
Clock,
|
||||
X,
|
||||
Eye,
|
||||
Send,
|
||||
} from 'lucide-react';
|
||||
import { Code, Zap, Clock, X, Eye, Send } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import CodeViewer from './CodeViewer';
|
||||
|
||||
@@ -76,7 +69,7 @@ const DebugPanel = ({
|
||||
<Dropdown
|
||||
render={
|
||||
<Dropdown.Menu>
|
||||
{items.map(item => {
|
||||
{items.map((item) => {
|
||||
return (
|
||||
<Dropdown.Item
|
||||
key={item.itemKey}
|
||||
@@ -104,21 +97,21 @@ const DebugPanel = ({
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="h-full flex flex-col"
|
||||
className='h-full flex flex-col'
|
||||
bordered={false}
|
||||
bodyStyle={{
|
||||
padding: styleState.isMobile ? '16px' : '24px',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-6 flex-shrink-0">
|
||||
<div className="flex items-center">
|
||||
<div className="w-10 h-10 rounded-full bg-gradient-to-r from-green-500 to-blue-500 flex items-center justify-center mr-3">
|
||||
<Code size={20} className="text-white" />
|
||||
<div className='flex items-center justify-between mb-6 flex-shrink-0'>
|
||||
<div className='flex items-center'>
|
||||
<div className='w-10 h-10 rounded-full bg-gradient-to-r from-green-500 to-blue-500 flex items-center justify-center mr-3'>
|
||||
<Code size={20} className='text-white' />
|
||||
</div>
|
||||
<Typography.Title heading={5} className="mb-0">
|
||||
<Typography.Title heading={5} className='mb-0'>
|
||||
{t('调试信息')}
|
||||
</Typography.Title>
|
||||
</div>
|
||||
@@ -127,75 +120,84 @@ const DebugPanel = ({
|
||||
<Button
|
||||
icon={<X size={16} />}
|
||||
onClick={onCloseDebugPanel}
|
||||
theme="borderless"
|
||||
type="tertiary"
|
||||
size="small"
|
||||
className="!rounded-lg"
|
||||
theme='borderless'
|
||||
type='tertiary'
|
||||
size='small'
|
||||
className='!rounded-lg'
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-hidden debug-panel">
|
||||
<div className='flex-1 overflow-hidden debug-panel'>
|
||||
<Tabs
|
||||
renderArrow={renderArrow}
|
||||
type="card"
|
||||
type='card'
|
||||
collapsible
|
||||
className="h-full"
|
||||
className='h-full'
|
||||
style={{ height: '100%', display: 'flex', flexDirection: 'column' }}
|
||||
activeKey={activeKey}
|
||||
onChange={handleTabChange}
|
||||
>
|
||||
<TabPane tab={
|
||||
<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">
|
||||
<TabPane
|
||||
tab={
|
||||
<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
|
||||
content={debugData.previewRequest}
|
||||
title="preview"
|
||||
language="json"
|
||||
title='preview'
|
||||
language='json'
|
||||
/>
|
||||
</TabPane>
|
||||
|
||||
<TabPane tab={
|
||||
<div className="flex items-center gap-2">
|
||||
<Send size={16} />
|
||||
{t('实际请求体')}
|
||||
</div>
|
||||
} itemKey="request">
|
||||
<TabPane
|
||||
tab={
|
||||
<div className='flex items-center gap-2'>
|
||||
<Send size={16} />
|
||||
{t('实际请求体')}
|
||||
</div>
|
||||
}
|
||||
itemKey='request'
|
||||
>
|
||||
<CodeViewer
|
||||
content={debugData.request}
|
||||
title="request"
|
||||
language="json"
|
||||
title='request'
|
||||
language='json'
|
||||
/>
|
||||
</TabPane>
|
||||
|
||||
<TabPane tab={
|
||||
<div className="flex items-center gap-2">
|
||||
<Zap size={16} />
|
||||
{t('响应')}
|
||||
</div>
|
||||
} itemKey="response">
|
||||
<TabPane
|
||||
tab={
|
||||
<div className='flex items-center gap-2'>
|
||||
<Zap size={16} />
|
||||
{t('响应')}
|
||||
</div>
|
||||
}
|
||||
itemKey='response'
|
||||
>
|
||||
<CodeViewer
|
||||
content={debugData.response}
|
||||
title="response"
|
||||
language="json"
|
||||
title='response'
|
||||
language='json'
|
||||
/>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between mt-4 pt-4 flex-shrink-0">
|
||||
<div className='flex items-center justify-between mt-4 pt-4 flex-shrink-0'>
|
||||
{(debugData.timestamp || debugData.previewTimestamp) && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock size={14} className="text-gray-500" />
|
||||
<Typography.Text className="text-xs text-gray-500">
|
||||
<div className='flex items-center gap-2'>
|
||||
<Clock size={14} className='text-gray-500' />
|
||||
<Typography.Text className='text-xs text-gray-500'>
|
||||
{activeKey === 'preview' && debugData.previewTimestamp
|
||||
? `${t('预览更新')}: ${new Date(debugData.previewTimestamp).toLocaleString()}`
|
||||
: debugData.timestamp
|
||||
@@ -209,4 +211,4 @@ const DebugPanel = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default DebugPanel;
|
||||
export default DebugPanel;
|
||||
|
||||
@@ -19,11 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
|
||||
import React from 'react';
|
||||
import { Button } from '@douyinfe/semi-ui';
|
||||
import {
|
||||
Settings,
|
||||
Eye,
|
||||
EyeOff,
|
||||
} from 'lucide-react';
|
||||
import { Settings, Eye, EyeOff } from 'lucide-react';
|
||||
|
||||
const FloatingButtons = ({
|
||||
styleState,
|
||||
@@ -55,7 +51,7 @@ const FloatingButtons = ({
|
||||
onClick={onToggleSettings}
|
||||
theme='solid'
|
||||
type='primary'
|
||||
className="lg:hidden"
|
||||
className='lg:hidden'
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -64,8 +60,8 @@ const FloatingButtons = ({
|
||||
<Button
|
||||
icon={showDebugPanel ? <EyeOff size={18} /> : <Eye size={18} />}
|
||||
onClick={onToggleDebugPanel}
|
||||
theme="solid"
|
||||
type={showDebugPanel ? "danger" : "primary"}
|
||||
theme='solid'
|
||||
type={showDebugPanel ? 'danger' : 'primary'}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
right: 16,
|
||||
@@ -80,11 +76,11 @@ const FloatingButtons = ({
|
||||
? 'linear-gradient(to right, #e11d48, #be123c)'
|
||||
: 'linear-gradient(to right, #4f46e5, #6366f1)',
|
||||
}}
|
||||
className="lg:hidden"
|
||||
className='lg:hidden'
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FloatingButtons;
|
||||
export default FloatingButtons;
|
||||
|
||||
@@ -18,21 +18,17 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Input,
|
||||
Typography,
|
||||
Button,
|
||||
Switch,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { Input, Typography, Button, Switch } from '@douyinfe/semi-ui';
|
||||
import { IconFile } from '@douyinfe/semi-icons';
|
||||
import {
|
||||
FileText,
|
||||
Plus,
|
||||
X,
|
||||
Image,
|
||||
} from 'lucide-react';
|
||||
import { FileText, Plus, X, Image } from 'lucide-react';
|
||||
|
||||
const ImageUrlInput = ({ imageUrls, imageEnabled, onImageUrlsChange, onImageEnabledChange, disabled = false }) => {
|
||||
const ImageUrlInput = ({
|
||||
imageUrls,
|
||||
imageEnabled,
|
||||
onImageUrlsChange,
|
||||
onImageEnabledChange,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const handleAddImageUrl = () => {
|
||||
const newUrls = [...imageUrls, ''];
|
||||
onImageUrlsChange(newUrls);
|
||||
@@ -51,75 +47,87 @@ const ImageUrlInput = ({ imageUrls, imageEnabled, onImageUrlsChange, onImageEnab
|
||||
|
||||
return (
|
||||
<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 && !disabled ? "text-blue-500" : "text-gray-400"} />
|
||||
<Typography.Text strong className="text-sm">
|
||||
<div className='flex items-center justify-between mb-2'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<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 className='text-xs text-orange-600'>
|
||||
(已在自定义模式中忽略)
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className='flex items-center gap-2'>
|
||||
<Switch
|
||||
checked={imageEnabled}
|
||||
onChange={onImageEnabledChange}
|
||||
checkedText="启用"
|
||||
uncheckedText="停用"
|
||||
size="small"
|
||||
className="flex-shrink-0"
|
||||
checkedText='启用'
|
||||
uncheckedText='停用'
|
||||
size='small'
|
||||
className='flex-shrink-0'
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Button
|
||||
icon={<Plus size={14} />}
|
||||
size="small"
|
||||
theme="solid"
|
||||
type="primary"
|
||||
size='small'
|
||||
theme='solid'
|
||||
type='primary'
|
||||
onClick={handleAddImageUrl}
|
||||
className="!rounded-full !w-4 !h-4 !p-0 !min-w-0"
|
||||
className='!rounded-full !w-4 !h-4 !p-0 !min-w-0'
|
||||
disabled={!imageEnabled || disabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!imageEnabled ? (
|
||||
<Typography.Text className="text-xs text-gray-500 mb-2 block">
|
||||
{disabled ? '图片功能在自定义请求体模式下不可用' : '启用后可添加图片URL进行多模态对话'}
|
||||
<Typography.Text className='text-xs text-gray-500 mb-2 block'>
|
||||
{disabled
|
||||
? '图片功能在自定义请求体模式下不可用'
|
||||
: '启用后可添加图片URL进行多模态对话'}
|
||||
</Typography.Text>
|
||||
) : imageUrls.length === 0 ? (
|
||||
<Typography.Text className="text-xs text-gray-500 mb-2 block">
|
||||
{disabled ? '图片功能在自定义请求体模式下不可用' : '点击 + 按钮添加图片URL进行多模态对话'}
|
||||
<Typography.Text className='text-xs text-gray-500 mb-2 block'>
|
||||
{disabled
|
||||
? '图片功能在自定义请求体模式下不可用'
|
||||
: '点击 + 按钮添加图片URL进行多模态对话'}
|
||||
</Typography.Text>
|
||||
) : (
|
||||
<Typography.Text className="text-xs text-gray-500 mb-2 block">
|
||||
已添加 {imageUrls.length} 张图片{disabled ? ' (自定义模式下不可用)' : ''}
|
||||
<Typography.Text className='text-xs text-gray-500 mb-2 block'>
|
||||
已添加 {imageUrls.length} 张图片
|
||||
{disabled ? ' (自定义模式下不可用)' : ''}
|
||||
</Typography.Text>
|
||||
)}
|
||||
|
||||
<div className={`space-y-2 max-h-32 overflow-y-auto image-list-scroll ${!imageEnabled || disabled ? 'opacity-50' : ''}`}>
|
||||
<div
|
||||
className={`space-y-2 max-h-32 overflow-y-auto image-list-scroll ${!imageEnabled || disabled ? 'opacity-50' : ''}`}
|
||||
>
|
||||
{imageUrls.map((url, index) => (
|
||||
<div key={index} className="flex items-center gap-2">
|
||||
<div className="flex-1">
|
||||
<div key={index} className='flex items-center gap-2'>
|
||||
<div className='flex-1'>
|
||||
<Input
|
||||
placeholder={`https://example.com/image${index + 1}.jpg`}
|
||||
value={url}
|
||||
onChange={(value) => handleUpdateImageUrl(index, value)}
|
||||
className="!rounded-lg"
|
||||
size="small"
|
||||
className='!rounded-lg'
|
||||
size='small'
|
||||
prefix={<IconFile size='small' />}
|
||||
disabled={!imageEnabled || disabled}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
icon={<X size={12} />}
|
||||
size="small"
|
||||
theme="borderless"
|
||||
type="danger"
|
||||
size='small'
|
||||
theme='borderless'
|
||||
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"
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
@@ -129,4 +137,4 @@ const ImageUrlInput = ({ imageUrls, imageEnabled, onImageUrlsChange, onImageEnab
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageUrlInput;
|
||||
export default ImageUrlInput;
|
||||
|
||||
@@ -18,17 +18,8 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Button,
|
||||
Tooltip,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
RefreshCw,
|
||||
Copy,
|
||||
Trash2,
|
||||
UserCheck,
|
||||
Edit,
|
||||
} from 'lucide-react';
|
||||
import { Button, Tooltip } from '@douyinfe/semi-ui';
|
||||
import { RefreshCw, Copy, Trash2, UserCheck, Edit } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const MessageActions = ({
|
||||
@@ -40,23 +31,32 @@ const MessageActions = ({
|
||||
onRoleToggle,
|
||||
onMessageEdit,
|
||||
isAnyMessageGenerating = false,
|
||||
isEditing = false
|
||||
isEditing = false,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isLoading = message.status === 'loading' || message.status === 'incomplete';
|
||||
const isLoading =
|
||||
message.status === 'loading' || message.status === 'incomplete';
|
||||
const shouldDisableActions = isAnyMessageGenerating || isEditing;
|
||||
const canToggleRole = message.role === 'assistant' || message.role === 'system';
|
||||
const canEdit = !isLoading && message.content && typeof onMessageEdit === 'function' && !isEditing;
|
||||
const canToggleRole =
|
||||
message.role === 'assistant' || message.role === 'system';
|
||||
const canEdit =
|
||||
!isLoading &&
|
||||
message.content &&
|
||||
typeof onMessageEdit === 'function' &&
|
||||
!isEditing;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-0.5">
|
||||
<div className='flex items-center gap-0.5'>
|
||||
{!isLoading && (
|
||||
<Tooltip content={shouldDisableActions ? t('操作暂时被禁用') : t('重试')} position="top">
|
||||
<Tooltip
|
||||
content={shouldDisableActions ? t('操作暂时被禁用') : t('重试')}
|
||||
position='top'
|
||||
>
|
||||
<Button
|
||||
theme="borderless"
|
||||
type="tertiary"
|
||||
size="small"
|
||||
theme='borderless'
|
||||
type='tertiary'
|
||||
size='small'
|
||||
icon={<RefreshCw size={styleState.isMobile ? 12 : 14} />}
|
||||
onClick={() => !shouldDisableActions && onMessageReset(message)}
|
||||
disabled={shouldDisableActions}
|
||||
@@ -67,11 +67,11 @@ const MessageActions = ({
|
||||
)}
|
||||
|
||||
{message.content && (
|
||||
<Tooltip content={t('复制')} position="top">
|
||||
<Tooltip content={t('复制')} position='top'>
|
||||
<Button
|
||||
theme="borderless"
|
||||
type="tertiary"
|
||||
size="small"
|
||||
theme='borderless'
|
||||
type='tertiary'
|
||||
size='small'
|
||||
icon={<Copy size={styleState.isMobile ? 12 : 14} />}
|
||||
onClick={() => onMessageCopy(message)}
|
||||
className={`!rounded-full !text-gray-400 hover:!text-green-600 hover:!bg-green-50 ${styleState.isMobile ? '!w-6 !h-6' : '!w-7 !h-7'} !p-0 transition-all`}
|
||||
@@ -81,11 +81,14 @@ const MessageActions = ({
|
||||
)}
|
||||
|
||||
{canEdit && (
|
||||
<Tooltip content={shouldDisableActions ? t('操作暂时被禁用') : t('编辑')} position="top">
|
||||
<Tooltip
|
||||
content={shouldDisableActions ? t('操作暂时被禁用') : t('编辑')}
|
||||
position='top'
|
||||
>
|
||||
<Button
|
||||
theme="borderless"
|
||||
type="tertiary"
|
||||
size="small"
|
||||
theme='borderless'
|
||||
type='tertiary'
|
||||
size='small'
|
||||
icon={<Edit size={styleState.isMobile ? 12 : 14} />}
|
||||
onClick={() => !shouldDisableActions && onMessageEdit(message)}
|
||||
disabled={shouldDisableActions}
|
||||
@@ -104,27 +107,36 @@ const MessageActions = ({
|
||||
? t('切换为System角色')
|
||||
: t('切换为Assistant角色')
|
||||
}
|
||||
position="top"
|
||||
position='top'
|
||||
>
|
||||
<Button
|
||||
theme="borderless"
|
||||
type="tertiary"
|
||||
size="small"
|
||||
theme='borderless'
|
||||
type='tertiary'
|
||||
size='small'
|
||||
icon={<UserCheck size={styleState.isMobile ? 12 : 14} />}
|
||||
onClick={() => !shouldDisableActions && onRoleToggle && onRoleToggle(message)}
|
||||
onClick={() =>
|
||||
!shouldDisableActions && onRoleToggle && onRoleToggle(message)
|
||||
}
|
||||
disabled={shouldDisableActions}
|
||||
className={`!rounded-full ${shouldDisableActions ? '!text-gray-300 !cursor-not-allowed' : message.role === 'system' ? '!text-purple-500 hover:!text-purple-700 hover:!bg-purple-50' : '!text-gray-400 hover:!text-purple-600 hover:!bg-purple-50'} ${styleState.isMobile ? '!w-6 !h-6' : '!w-7 !h-7'} !p-0 transition-all`}
|
||||
aria-label={message.role === 'assistant' ? t('切换为System角色') : t('切换为Assistant角色')}
|
||||
aria-label={
|
||||
message.role === 'assistant'
|
||||
? t('切换为System角色')
|
||||
: t('切换为Assistant角色')
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{!isLoading && (
|
||||
<Tooltip content={shouldDisableActions ? t('操作暂时被禁用') : t('删除')} position="top">
|
||||
<Tooltip
|
||||
content={shouldDisableActions ? t('操作暂时被禁用') : t('删除')}
|
||||
position='top'
|
||||
>
|
||||
<Button
|
||||
theme="borderless"
|
||||
type="tertiary"
|
||||
size="small"
|
||||
theme='borderless'
|
||||
type='tertiary'
|
||||
size='small'
|
||||
icon={<Trash2 size={styleState.isMobile ? 12 : 14} />}
|
||||
onClick={() => !shouldDisableActions && onMessageDelete(message)}
|
||||
disabled={shouldDisableActions}
|
||||
@@ -137,4 +149,4 @@ const MessageActions = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default MessageActions;
|
||||
export default MessageActions;
|
||||
|
||||
@@ -18,18 +18,10 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import {
|
||||
Typography,
|
||||
TextArea,
|
||||
Button,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { Typography, TextArea, Button } from '@douyinfe/semi-ui';
|
||||
import MarkdownRenderer from '../common/markdown/MarkdownRenderer';
|
||||
import ThinkingContent from './ThinkingContent';
|
||||
import {
|
||||
Loader2,
|
||||
Check,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { Loader2, Check, X } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const MessageContent = ({
|
||||
@@ -41,13 +33,14 @@ const MessageContent = ({
|
||||
onEditSave,
|
||||
onEditCancel,
|
||||
editValue,
|
||||
onEditValueChange
|
||||
onEditValueChange,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const previousContentLengthRef = useRef(0);
|
||||
const lastContentRef = useRef('');
|
||||
|
||||
const isThinkingStatus = message.status === 'loading' || message.status === 'incomplete';
|
||||
const isThinkingStatus =
|
||||
message.status === 'loading' || message.status === 'incomplete';
|
||||
|
||||
useEffect(() => {
|
||||
if (!isThinkingStatus) {
|
||||
@@ -60,10 +53,11 @@ const MessageContent = ({
|
||||
let errorText;
|
||||
|
||||
if (Array.isArray(message.content)) {
|
||||
const textContent = message.content.find(item => item.type === 'text');
|
||||
errorText = textContent && textContent.text && typeof textContent.text === 'string'
|
||||
? textContent.text
|
||||
: t('请求发生错误');
|
||||
const textContent = message.content.find((item) => item.type === 'text');
|
||||
errorText =
|
||||
textContent && textContent.text && typeof textContent.text === 'string'
|
||||
? textContent.text
|
||||
: t('请求发生错误');
|
||||
} else if (typeof message.content === 'string') {
|
||||
errorText = message.content;
|
||||
} else {
|
||||
@@ -72,21 +66,21 @@ const MessageContent = ({
|
||||
|
||||
return (
|
||||
<div className={`${className}`}>
|
||||
<Typography.Text className="text-white">
|
||||
{errorText}
|
||||
</Typography.Text>
|
||||
<Typography.Text className='text-white'>{errorText}</Typography.Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let currentExtractedThinkingContent = null;
|
||||
let currentDisplayableFinalContent = "";
|
||||
let currentDisplayableFinalContent = '';
|
||||
let thinkingSource = null;
|
||||
|
||||
const getTextContent = (content) => {
|
||||
if (Array.isArray(content)) {
|
||||
const textItem = content.find(item => item.type === 'text');
|
||||
return textItem && textItem.text && typeof textItem.text === 'string' ? textItem.text : '';
|
||||
const textItem = content.find((item) => item.type === 'text');
|
||||
return textItem && textItem.text && typeof textItem.text === 'string'
|
||||
? textItem.text
|
||||
: '';
|
||||
} else if (typeof content === 'string') {
|
||||
return content;
|
||||
}
|
||||
@@ -97,7 +91,7 @@ const MessageContent = ({
|
||||
|
||||
if (message.role === 'assistant') {
|
||||
let baseContentForDisplay = getTextContent(message.content);
|
||||
let combinedThinkingContent = "";
|
||||
let combinedThinkingContent = '';
|
||||
|
||||
if (message.reasoningContent) {
|
||||
combinedThinkingContent = message.reasoningContent;
|
||||
@@ -112,7 +106,9 @@ const MessageContent = ({
|
||||
let lastIndex = 0;
|
||||
|
||||
while ((match = thinkTagRegex.exec(baseContentForDisplay)) !== null) {
|
||||
replyParts.push(baseContentForDisplay.substring(lastIndex, match.index));
|
||||
replyParts.push(
|
||||
baseContentForDisplay.substring(lastIndex, match.index),
|
||||
);
|
||||
thoughtsFromPairedTags.push(match[1]);
|
||||
lastIndex = match.index + match[0].length;
|
||||
}
|
||||
@@ -125,7 +121,9 @@ const MessageContent = ({
|
||||
} else {
|
||||
combinedThinkingContent = pairedThoughtsStr;
|
||||
}
|
||||
thinkingSource = thinkingSource ? thinkingSource + ' & <think> tags' : '<think> tags';
|
||||
thinkingSource = thinkingSource
|
||||
? thinkingSource + ' & <think> tags'
|
||||
: '<think> tags';
|
||||
}
|
||||
|
||||
baseContentForDisplay = replyParts.join('');
|
||||
@@ -134,37 +132,55 @@ const MessageContent = ({
|
||||
if (isThinkingStatus) {
|
||||
const lastOpenThinkIndex = baseContentForDisplay.lastIndexOf('<think>');
|
||||
if (lastOpenThinkIndex !== -1) {
|
||||
const fragmentAfterLastOpen = baseContentForDisplay.substring(lastOpenThinkIndex);
|
||||
const fragmentAfterLastOpen =
|
||||
baseContentForDisplay.substring(lastOpenThinkIndex);
|
||||
if (!fragmentAfterLastOpen.includes('</think>')) {
|
||||
const unclosedThought = fragmentAfterLastOpen.substring('<think>'.length).trim();
|
||||
const unclosedThought = fragmentAfterLastOpen
|
||||
.substring('<think>'.length)
|
||||
.trim();
|
||||
if (unclosedThought) {
|
||||
if (combinedThinkingContent) {
|
||||
combinedThinkingContent += '\n\n---\n\n' + unclosedThought;
|
||||
} else {
|
||||
combinedThinkingContent = unclosedThought;
|
||||
}
|
||||
thinkingSource = thinkingSource ? thinkingSource + ' + streaming <think>' : 'streaming <think>';
|
||||
thinkingSource = thinkingSource
|
||||
? thinkingSource + ' + streaming <think>'
|
||||
: 'streaming <think>';
|
||||
}
|
||||
baseContentForDisplay = baseContentForDisplay.substring(0, lastOpenThinkIndex);
|
||||
baseContentForDisplay = baseContentForDisplay.substring(
|
||||
0,
|
||||
lastOpenThinkIndex,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentExtractedThinkingContent = combinedThinkingContent || null;
|
||||
currentDisplayableFinalContent = baseContentForDisplay.replace(/<\/?think>/g, '').trim();
|
||||
currentDisplayableFinalContent = baseContentForDisplay
|
||||
.replace(/<\/?think>/g, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
const finalExtractedThinkingContent = currentExtractedThinkingContent;
|
||||
const finalDisplayableFinalContent = currentDisplayableFinalContent;
|
||||
|
||||
if (message.role === 'assistant' &&
|
||||
if (
|
||||
message.role === 'assistant' &&
|
||||
isThinkingStatus &&
|
||||
!finalExtractedThinkingContent &&
|
||||
(!finalDisplayableFinalContent || finalDisplayableFinalContent.trim() === '')) {
|
||||
(!finalDisplayableFinalContent ||
|
||||
finalDisplayableFinalContent.trim() === '')
|
||||
) {
|
||||
return (
|
||||
<div className={`${className} flex items-center gap-2 sm:gap-4 bg-gradient-to-r from-purple-50 to-indigo-50`}>
|
||||
<div className="w-5 h-5 rounded-full bg-gradient-to-br from-purple-500 to-indigo-600 flex items-center justify-center shadow-lg">
|
||||
<Loader2 className="animate-spin text-white" size={styleState.isMobile ? 16 : 20} />
|
||||
<div
|
||||
className={`${className} flex items-center gap-2 sm:gap-4 bg-gradient-to-r from-purple-50 to-indigo-50`}
|
||||
>
|
||||
<div className='w-5 h-5 rounded-full bg-gradient-to-br from-purple-500 to-indigo-600 flex items-center justify-center shadow-lg'>
|
||||
<Loader2
|
||||
className='animate-spin text-white'
|
||||
size={styleState.isMobile ? 16 : 20}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -173,12 +189,17 @@ const MessageContent = ({
|
||||
return (
|
||||
<div className={className}>
|
||||
{message.role === 'system' && (
|
||||
<div className="mb-2 sm:mb-4">
|
||||
<div className="flex items-center gap-2 p-2 sm:p-3 bg-gradient-to-r from-amber-50 to-orange-50 rounded-lg" style={{ border: '1px solid var(--semi-color-border)' }}>
|
||||
<div className="w-4 h-4 sm:w-5 sm:h-5 rounded-full bg-gradient-to-br from-amber-500 to-orange-600 flex items-center justify-center shadow-sm">
|
||||
<Typography.Text className="text-white text-xs font-bold">S</Typography.Text>
|
||||
<div className='mb-2 sm:mb-4'>
|
||||
<div
|
||||
className='flex items-center gap-2 p-2 sm:p-3 bg-gradient-to-r from-amber-50 to-orange-50 rounded-lg'
|
||||
style={{ border: '1px solid var(--semi-color-border)' }}
|
||||
>
|
||||
<div className='w-4 h-4 sm:w-5 sm:h-5 rounded-full bg-gradient-to-br from-amber-500 to-orange-600 flex items-center justify-center shadow-sm'>
|
||||
<Typography.Text className='text-white text-xs font-bold'>
|
||||
S
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Typography.Text className="text-amber-700 text-xs sm:text-sm font-medium">
|
||||
<Typography.Text className='text-amber-700 text-xs sm:text-sm font-medium'>
|
||||
{t('系统消息')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
@@ -196,7 +217,7 @@ const MessageContent = ({
|
||||
)}
|
||||
|
||||
{isEditing ? (
|
||||
<div className="space-y-3">
|
||||
<div className='space-y-3'>
|
||||
<TextArea
|
||||
value={editValue}
|
||||
onChange={(value) => onEditValueChange(value)}
|
||||
@@ -207,27 +228,27 @@ const MessageContent = ({
|
||||
fontSize: styleState.isMobile ? '14px' : '15px',
|
||||
lineHeight: '1.6',
|
||||
}}
|
||||
className="!border-blue-200 focus:!border-blue-400 !bg-blue-50/50"
|
||||
className='!border-blue-200 focus:!border-blue-400 !bg-blue-50/50'
|
||||
/>
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<div className='flex items-center gap-2 w-full'>
|
||||
<Button
|
||||
size="small"
|
||||
type="danger"
|
||||
theme="light"
|
||||
size='small'
|
||||
type='danger'
|
||||
theme='light'
|
||||
icon={<X size={14} />}
|
||||
onClick={onEditCancel}
|
||||
className="flex-1"
|
||||
className='flex-1'
|
||||
>
|
||||
{t('取消')}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="warning"
|
||||
theme="solid"
|
||||
size='small'
|
||||
type='warning'
|
||||
theme='solid'
|
||||
icon={<Check size={14} />}
|
||||
onClick={onEditSave}
|
||||
disabled={!editValue || editValue.trim() === ''}
|
||||
className="flex-1"
|
||||
className='flex-1'
|
||||
>
|
||||
{t('保存')}
|
||||
</Button>
|
||||
@@ -236,19 +257,23 @@ const MessageContent = ({
|
||||
) : (
|
||||
(() => {
|
||||
if (Array.isArray(message.content)) {
|
||||
const textContent = message.content.find(item => item.type === 'text');
|
||||
const imageContents = message.content.filter(item => item.type === 'image_url');
|
||||
const textContent = message.content.find(
|
||||
(item) => item.type === 'text',
|
||||
);
|
||||
const imageContents = message.content.filter(
|
||||
(item) => item.type === 'image_url',
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{imageContents.length > 0 && (
|
||||
<div className="mb-3 space-y-2">
|
||||
<div className='mb-3 space-y-2'>
|
||||
{imageContents.map((imgItem, index) => (
|
||||
<div key={index} className="max-w-sm">
|
||||
<div key={index} className='max-w-sm'>
|
||||
<img
|
||||
src={imgItem.image_url.url}
|
||||
alt={`用户上传的图片 ${index + 1}`}
|
||||
className="rounded-lg max-w-full h-auto shadow-sm border"
|
||||
className='rounded-lg max-w-full h-auto shadow-sm border'
|
||||
style={{ maxHeight: '300px' }}
|
||||
onError={(e) => {
|
||||
e.target.style.display = 'none';
|
||||
@@ -256,7 +281,7 @@ const MessageContent = ({
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="text-red-500 text-sm p-2 bg-red-50 rounded-lg border border-red-200"
|
||||
className='text-red-500 text-sm p-2 bg-red-50 rounded-lg border border-red-200'
|
||||
style={{ display: 'none' }}
|
||||
>
|
||||
图片加载失败: {imgItem.image_url.url}
|
||||
@@ -266,28 +291,42 @@ const MessageContent = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{textContent && textContent.text && typeof textContent.text === 'string' && textContent.text.trim() !== '' && (
|
||||
<div className={`prose prose-xs sm:prose-sm prose-gray max-w-none overflow-x-auto text-xs sm:text-sm ${message.role === 'user' ? 'user-message' : ''}`}>
|
||||
<MarkdownRenderer
|
||||
content={textContent.text}
|
||||
className={message.role === 'user' ? 'user-message' : ''}
|
||||
animated={false}
|
||||
previousContentLength={0}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{textContent &&
|
||||
textContent.text &&
|
||||
typeof textContent.text === 'string' &&
|
||||
textContent.text.trim() !== '' && (
|
||||
<div
|
||||
className={`prose prose-xs sm:prose-sm prose-gray max-w-none overflow-x-auto text-xs sm:text-sm ${message.role === 'user' ? 'user-message' : ''}`}
|
||||
>
|
||||
<MarkdownRenderer
|
||||
content={textContent.text}
|
||||
className={
|
||||
message.role === 'user' ? 'user-message' : ''
|
||||
}
|
||||
animated={false}
|
||||
previousContentLength={0}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof message.content === 'string') {
|
||||
if (message.role === 'assistant') {
|
||||
if (finalDisplayableFinalContent && finalDisplayableFinalContent.trim() !== '') {
|
||||
if (
|
||||
finalDisplayableFinalContent &&
|
||||
finalDisplayableFinalContent.trim() !== ''
|
||||
) {
|
||||
// 获取上一次的内容长度
|
||||
let prevLength = 0;
|
||||
if (isThinkingStatus && lastContentRef.current) {
|
||||
// 只有当前内容包含上一次内容时,才使用上一次的长度
|
||||
if (finalDisplayableFinalContent.startsWith(lastContentRef.current)) {
|
||||
if (
|
||||
finalDisplayableFinalContent.startsWith(
|
||||
lastContentRef.current,
|
||||
)
|
||||
) {
|
||||
prevLength = lastContentRef.current.length;
|
||||
}
|
||||
}
|
||||
@@ -298,10 +337,10 @@ const MessageContent = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="prose prose-xs sm:prose-sm prose-gray max-w-none overflow-x-auto text-xs sm:text-sm">
|
||||
<div className='prose prose-xs sm:prose-sm prose-gray max-w-none overflow-x-auto text-xs sm:text-sm'>
|
||||
<MarkdownRenderer
|
||||
content={finalDisplayableFinalContent}
|
||||
className=""
|
||||
className=''
|
||||
animated={isThinkingStatus}
|
||||
previousContentLength={prevLength}
|
||||
/>
|
||||
@@ -310,7 +349,9 @@ const MessageContent = ({
|
||||
}
|
||||
} else {
|
||||
return (
|
||||
<div className={`prose prose-xs sm:prose-sm prose-gray max-w-none overflow-x-auto text-xs sm:text-sm ${message.role === 'user' ? 'user-message' : ''}`}>
|
||||
<div
|
||||
className={`prose prose-xs sm:prose-sm prose-gray max-w-none overflow-x-auto text-xs sm:text-sm ${message.role === 'user' ? 'user-message' : ''}`}
|
||||
>
|
||||
<MarkdownRenderer
|
||||
content={message.content}
|
||||
className={message.role === 'user' ? 'user-message' : ''}
|
||||
@@ -329,4 +370,4 @@ const MessageContent = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default MessageContent;
|
||||
export default MessageContent;
|
||||
|
||||
@@ -24,56 +24,74 @@ 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.role === nextProps.message.role &&
|
||||
prevProps.message.reasoningContent === nextProps.message.reasoningContent &&
|
||||
prevProps.message.isReasoningExpanded === nextProps.message.isReasoningExpanded &&
|
||||
prevProps.isEditing === nextProps.isEditing &&
|
||||
prevProps.editValue === nextProps.editValue &&
|
||||
prevProps.styleState.isMobile === nextProps.styleState.isMobile
|
||||
);
|
||||
});
|
||||
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.role === nextProps.message.role &&
|
||||
prevProps.message.reasoningContent ===
|
||||
nextProps.message.reasoningContent &&
|
||||
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.message.role === nextProps.message.role &&
|
||||
prevProps.isAnyMessageGenerating === nextProps.isAnyMessageGenerating &&
|
||||
prevProps.isEditing === nextProps.isEditing &&
|
||||
prevProps.onMessageReset === nextProps.onMessageReset
|
||||
);
|
||||
});
|
||||
export const OptimizedMessageActions = React.memo(
|
||||
MessageActions,
|
||||
(prevProps, nextProps) => {
|
||||
return (
|
||||
prevProps.message.id === nextProps.message.id &&
|
||||
prevProps.message.role === nextProps.message.role &&
|
||||
prevProps.isAnyMessageGenerating === nextProps.isAnyMessageGenerating &&
|
||||
prevProps.isEditing === nextProps.isEditing &&
|
||||
prevProps.onMessageReset === nextProps.onMessageReset
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// 优化的设置面板组件
|
||||
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 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
|
||||
);
|
||||
});
|
||||
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
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -18,13 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Input,
|
||||
Slider,
|
||||
Typography,
|
||||
Button,
|
||||
Tag,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { Input, Slider, Typography, Button, Tag } from '@douyinfe/semi-ui';
|
||||
import {
|
||||
Hash,
|
||||
Thermometer,
|
||||
@@ -46,28 +40,36 @@ const ParameterControl = ({
|
||||
return (
|
||||
<>
|
||||
{/* Temperature */}
|
||||
<div className={`transition-opacity duration-200 mb-4 ${!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" />
|
||||
<Typography.Text strong className="text-sm">
|
||||
<div
|
||||
className={`transition-opacity duration-200 mb-4 ${!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' />
|
||||
<Typography.Text strong className='text-sm'>
|
||||
Temperature
|
||||
</Typography.Text>
|
||||
<Tag size="small" shape='circle'>
|
||||
<Tag size='small' shape='circle'>
|
||||
{inputs.temperature}
|
||||
</Tag>
|
||||
</div>
|
||||
<Button
|
||||
theme={parameterEnabled.temperature ? 'solid' : 'borderless'}
|
||||
type={parameterEnabled.temperature ? 'primary' : 'tertiary'}
|
||||
size="small"
|
||||
icon={parameterEnabled.temperature ? <Check size={10} /> : <X size={10} />}
|
||||
size='small'
|
||||
icon={
|
||||
parameterEnabled.temperature ? (
|
||||
<Check size={10} />
|
||||
) : (
|
||||
<X size={10} />
|
||||
)
|
||||
}
|
||||
onClick={() => onParameterToggle('temperature')}
|
||||
className="!rounded-full !w-4 !h-4 !p-0 !min-w-0"
|
||||
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">
|
||||
<Typography.Text className='text-xs text-gray-500 mb-2'>
|
||||
控制输出的随机性和创造性
|
||||
</Typography.Text>
|
||||
<Slider
|
||||
@@ -76,34 +78,38 @@ const ParameterControl = ({
|
||||
max={1}
|
||||
value={inputs.temperature}
|
||||
onChange={(value) => onInputChange('temperature', value)}
|
||||
className="mt-2"
|
||||
className='mt-2'
|
||||
disabled={!parameterEnabled.temperature || disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Top P */}
|
||||
<div className={`transition-opacity duration-200 mb-4 ${!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" />
|
||||
<Typography.Text strong className="text-sm">
|
||||
<div
|
||||
className={`transition-opacity duration-200 mb-4 ${!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' />
|
||||
<Typography.Text strong className='text-sm'>
|
||||
Top P
|
||||
</Typography.Text>
|
||||
<Tag size="small" shape='circle'>
|
||||
<Tag size='small' shape='circle'>
|
||||
{inputs.top_p}
|
||||
</Tag>
|
||||
</div>
|
||||
<Button
|
||||
theme={parameterEnabled.top_p ? 'solid' : 'borderless'}
|
||||
type={parameterEnabled.top_p ? 'primary' : 'tertiary'}
|
||||
size="small"
|
||||
icon={parameterEnabled.top_p ? <Check size={10} /> : <X size={10} />}
|
||||
size='small'
|
||||
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"
|
||||
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">
|
||||
<Typography.Text className='text-xs text-gray-500 mb-2'>
|
||||
核采样,控制词汇选择的多样性
|
||||
</Typography.Text>
|
||||
<Slider
|
||||
@@ -112,34 +118,42 @@ const ParameterControl = ({
|
||||
max={1}
|
||||
value={inputs.top_p}
|
||||
onChange={(value) => onInputChange('top_p', value)}
|
||||
className="mt-2"
|
||||
className='mt-2'
|
||||
disabled={!parameterEnabled.top_p || disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Frequency Penalty */}
|
||||
<div className={`transition-opacity duration-200 mb-4 ${!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" />
|
||||
<Typography.Text strong className="text-sm">
|
||||
<div
|
||||
className={`transition-opacity duration-200 mb-4 ${!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' />
|
||||
<Typography.Text strong className='text-sm'>
|
||||
Frequency Penalty
|
||||
</Typography.Text>
|
||||
<Tag size="small" shape='circle'>
|
||||
<Tag size='small' shape='circle'>
|
||||
{inputs.frequency_penalty}
|
||||
</Tag>
|
||||
</div>
|
||||
<Button
|
||||
theme={parameterEnabled.frequency_penalty ? 'solid' : 'borderless'}
|
||||
type={parameterEnabled.frequency_penalty ? 'primary' : 'tertiary'}
|
||||
size="small"
|
||||
icon={parameterEnabled.frequency_penalty ? <Check size={10} /> : <X size={10} />}
|
||||
size='small'
|
||||
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"
|
||||
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">
|
||||
<Typography.Text className='text-xs text-gray-500 mb-2'>
|
||||
频率惩罚,减少重复词汇的出现
|
||||
</Typography.Text>
|
||||
<Slider
|
||||
@@ -148,34 +162,42 @@ const ParameterControl = ({
|
||||
max={2}
|
||||
value={inputs.frequency_penalty}
|
||||
onChange={(value) => onInputChange('frequency_penalty', value)}
|
||||
className="mt-2"
|
||||
className='mt-2'
|
||||
disabled={!parameterEnabled.frequency_penalty || disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Presence Penalty */}
|
||||
<div className={`transition-opacity duration-200 mb-4 ${!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" />
|
||||
<Typography.Text strong className="text-sm">
|
||||
<div
|
||||
className={`transition-opacity duration-200 mb-4 ${!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' />
|
||||
<Typography.Text strong className='text-sm'>
|
||||
Presence Penalty
|
||||
</Typography.Text>
|
||||
<Tag size="small" shape='circle'>
|
||||
<Tag size='small' shape='circle'>
|
||||
{inputs.presence_penalty}
|
||||
</Tag>
|
||||
</div>
|
||||
<Button
|
||||
theme={parameterEnabled.presence_penalty ? 'solid' : 'borderless'}
|
||||
type={parameterEnabled.presence_penalty ? 'primary' : 'tertiary'}
|
||||
size="small"
|
||||
icon={parameterEnabled.presence_penalty ? <Check size={10} /> : <X size={10} />}
|
||||
size='small'
|
||||
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"
|
||||
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">
|
||||
<Typography.Text className='text-xs text-gray-500 mb-2'>
|
||||
存在惩罚,鼓励讨论新话题
|
||||
</Typography.Text>
|
||||
<Slider
|
||||
@@ -184,27 +206,35 @@ const ParameterControl = ({
|
||||
max={2}
|
||||
value={inputs.presence_penalty}
|
||||
onChange={(value) => onInputChange('presence_penalty', value)}
|
||||
className="mt-2"
|
||||
className='mt-2'
|
||||
disabled={!parameterEnabled.presence_penalty || disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* MaxTokens */}
|
||||
<div className={`transition-opacity duration-200 mb-4 ${!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" />
|
||||
<Typography.Text strong className="text-sm">
|
||||
<div
|
||||
className={`transition-opacity duration-200 mb-4 ${!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' />
|
||||
<Typography.Text strong className='text-sm'>
|
||||
Max Tokens
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Button
|
||||
theme={parameterEnabled.max_tokens ? 'solid' : 'borderless'}
|
||||
type={parameterEnabled.max_tokens ? 'primary' : 'tertiary'}
|
||||
size="small"
|
||||
icon={parameterEnabled.max_tokens ? <Check size={10} /> : <X size={10} />}
|
||||
size='small'
|
||||
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"
|
||||
className='!rounded-full !w-4 !h-4 !p-0 !min-w-0'
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
@@ -216,30 +246,32 @@ const ParameterControl = ({
|
||||
defaultValue={0}
|
||||
value={inputs.max_tokens}
|
||||
onChange={(value) => onInputChange('max_tokens', value)}
|
||||
className="!rounded-lg"
|
||||
className='!rounded-lg'
|
||||
disabled={!parameterEnabled.max_tokens || disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Seed */}
|
||||
<div className={`transition-opacity duration-200 mb-4 ${!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" />
|
||||
<Typography.Text strong className="text-sm">
|
||||
<div
|
||||
className={`transition-opacity duration-200 mb-4 ${!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' />
|
||||
<Typography.Text strong className='text-sm'>
|
||||
Seed
|
||||
</Typography.Text>
|
||||
<Typography.Text className="text-xs text-gray-400">
|
||||
<Typography.Text className='text-xs text-gray-400'>
|
||||
(可选,用于复现结果)
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Button
|
||||
theme={parameterEnabled.seed ? 'solid' : 'borderless'}
|
||||
type={parameterEnabled.seed ? 'primary' : 'tertiary'}
|
||||
size="small"
|
||||
size='small'
|
||||
icon={parameterEnabled.seed ? <Check size={10} /> : <X size={10} />}
|
||||
onClick={() => onParameterToggle('seed')}
|
||||
className="!rounded-full !w-4 !h-4 !p-0 !min-w-0"
|
||||
className='!rounded-full !w-4 !h-4 !p-0 !min-w-0'
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
@@ -248,8 +280,10 @@ const ParameterControl = ({
|
||||
name='seed'
|
||||
autoComplete='new-password'
|
||||
value={inputs.seed || ''}
|
||||
onChange={(value) => onInputChange('seed', value === '' ? null : value)}
|
||||
className="!rounded-lg"
|
||||
onChange={(value) =>
|
||||
onInputChange('seed', value === '' ? null : value)
|
||||
}
|
||||
className='!rounded-lg'
|
||||
disabled={!parameterEnabled.seed || disabled}
|
||||
/>
|
||||
</div>
|
||||
@@ -257,4 +291,4 @@ const ParameterControl = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default ParameterControl;
|
||||
export default ParameterControl;
|
||||
|
||||
@@ -18,20 +18,8 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Card,
|
||||
Select,
|
||||
Typography,
|
||||
Button,
|
||||
Switch,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
Sparkles,
|
||||
Users,
|
||||
ToggleLeft,
|
||||
X,
|
||||
Settings,
|
||||
} from 'lucide-react';
|
||||
import { Card, Select, Typography, Button, Switch } from '@douyinfe/semi-ui';
|
||||
import { Sparkles, Users, ToggleLeft, X, Settings } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { renderGroupOption, selectFilter } from '../../helpers';
|
||||
import ParameterControl from './ParameterControl';
|
||||
@@ -70,22 +58,22 @@ const SettingsPanel = ({
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="h-full flex flex-col"
|
||||
className='h-full flex flex-col'
|
||||
bordered={false}
|
||||
bodyStyle={{
|
||||
padding: styleState.isMobile ? '16px' : '24px',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
{/* 标题区域 - 与调试面板保持一致 */}
|
||||
<div className="flex items-center justify-between mb-6 flex-shrink-0">
|
||||
<div className="flex items-center">
|
||||
<div className="w-10 h-10 rounded-full bg-gradient-to-r from-purple-500 to-pink-500 flex items-center justify-center mr-3">
|
||||
<Settings size={20} className="text-white" />
|
||||
<div className='flex items-center justify-between mb-6 flex-shrink-0'>
|
||||
<div className='flex items-center'>
|
||||
<div className='w-10 h-10 rounded-full bg-gradient-to-r from-purple-500 to-pink-500 flex items-center justify-center mr-3'>
|
||||
<Settings size={20} className='text-white' />
|
||||
</div>
|
||||
<Typography.Title heading={5} className="mb-0">
|
||||
<Typography.Title heading={5} className='mb-0'>
|
||||
{t('模型配置')}
|
||||
</Typography.Title>
|
||||
</div>
|
||||
@@ -94,17 +82,17 @@ const SettingsPanel = ({
|
||||
<Button
|
||||
icon={<X size={16} />}
|
||||
onClick={onCloseSettings}
|
||||
theme="borderless"
|
||||
type="tertiary"
|
||||
size="small"
|
||||
className="!rounded-lg"
|
||||
theme='borderless'
|
||||
type='tertiary'
|
||||
size='small'
|
||||
className='!rounded-lg'
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 移动端配置管理 */}
|
||||
{styleState.isMobile && (
|
||||
<div className="mb-4 flex-shrink-0">
|
||||
<div className='mb-4 flex-shrink-0'>
|
||||
<ConfigManager
|
||||
currentConfig={currentConfig}
|
||||
onConfigImport={onConfigImport}
|
||||
@@ -115,7 +103,7 @@ const SettingsPanel = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-6 overflow-y-auto flex-1 pr-2 model-settings-scroll">
|
||||
<div className='space-y-6 overflow-y-auto flex-1 pr-2 model-settings-scroll'>
|
||||
{/* 自定义请求体编辑器 */}
|
||||
<CustomRequestEditor
|
||||
customRequestMode={customRequestMode}
|
||||
@@ -127,13 +115,13 @@ const SettingsPanel = ({
|
||||
|
||||
{/* 分组选择 */}
|
||||
<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">
|
||||
<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 className='text-xs text-orange-600'>
|
||||
(已在自定义模式中忽略)
|
||||
</Typography.Text>
|
||||
)}
|
||||
@@ -152,20 +140,20 @@ const SettingsPanel = ({
|
||||
renderOptionItem={renderGroupOption}
|
||||
style={{ width: '100%' }}
|
||||
dropdownStyle={{ width: '100%', maxWidth: '100%' }}
|
||||
className="!rounded-lg"
|
||||
className='!rounded-lg'
|
||||
disabled={customRequestMode}
|
||||
/>
|
||||
</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">
|
||||
<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 className='text-xs text-orange-600'>
|
||||
(已在自定义模式中忽略)
|
||||
</Typography.Text>
|
||||
)}
|
||||
@@ -183,7 +171,7 @@ const SettingsPanel = ({
|
||||
optionList={models}
|
||||
style={{ width: '100%' }}
|
||||
dropdownStyle={{ width: '100%', maxWidth: '100%' }}
|
||||
className="!rounded-lg"
|
||||
className='!rounded-lg'
|
||||
disabled={customRequestMode}
|
||||
/>
|
||||
</div>
|
||||
@@ -194,7 +182,9 @@ const SettingsPanel = ({
|
||||
imageUrls={inputs.imageUrls}
|
||||
imageEnabled={inputs.imageEnabled}
|
||||
onImageUrlsChange={(urls) => onInputChange('imageUrls', urls)}
|
||||
onImageEnabledChange={(enabled) => onInputChange('imageEnabled', enabled)}
|
||||
onImageEnabledChange={(enabled) =>
|
||||
onInputChange('imageEnabled', enabled)
|
||||
}
|
||||
disabled={customRequestMode}
|
||||
/>
|
||||
</div>
|
||||
@@ -212,14 +202,14 @@ const SettingsPanel = ({
|
||||
|
||||
{/* 流式输出开关 */}
|
||||
<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">
|
||||
<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 className='text-xs text-orange-600'>
|
||||
(已在自定义模式中忽略)
|
||||
</Typography.Text>
|
||||
)}
|
||||
@@ -227,9 +217,9 @@ const SettingsPanel = ({
|
||||
<Switch
|
||||
checked={inputs.stream}
|
||||
onChange={(checked) => onInputChange('stream', checked)}
|
||||
checkedText="开"
|
||||
uncheckedText="关"
|
||||
size="small"
|
||||
checkedText='开'
|
||||
uncheckedText='关'
|
||||
size='small'
|
||||
disabled={customRequestMode}
|
||||
/>
|
||||
</div>
|
||||
@@ -238,7 +228,7 @@ const SettingsPanel = ({
|
||||
|
||||
{/* 桌面端的配置管理放在底部 */}
|
||||
{!styleState.isMobile && (
|
||||
<div className="flex-shrink-0 pt-3">
|
||||
<div className='flex-shrink-0 pt-3'>
|
||||
<ConfigManager
|
||||
currentConfig={currentConfig}
|
||||
onConfigImport={onConfigImport}
|
||||
@@ -252,4 +242,4 @@ const SettingsPanel = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsPanel;
|
||||
export default SettingsPanel;
|
||||
|
||||
@@ -28,17 +28,25 @@ const ThinkingContent = ({
|
||||
finalExtractedThinkingContent,
|
||||
thinkingSource,
|
||||
styleState,
|
||||
onToggleReasoningExpansion
|
||||
onToggleReasoningExpansion,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const scrollRef = useRef(null);
|
||||
const lastContentRef = useRef('');
|
||||
|
||||
const isThinkingStatus = message.status === 'loading' || message.status === 'incomplete';
|
||||
const headerText = (isThinkingStatus && !message.isThinkingComplete) ? t('思考中...') : t('思考过程');
|
||||
const isThinkingStatus =
|
||||
message.status === 'loading' || message.status === 'incomplete';
|
||||
const headerText =
|
||||
isThinkingStatus && !message.isThinkingComplete
|
||||
? t('思考中...')
|
||||
: t('思考过程');
|
||||
|
||||
useEffect(() => {
|
||||
if (scrollRef.current && finalExtractedThinkingContent && message.isReasoningExpanded) {
|
||||
if (
|
||||
scrollRef.current &&
|
||||
finalExtractedThinkingContent &&
|
||||
message.isReasoningExpanded
|
||||
) {
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
}
|
||||
}, [finalExtractedThinkingContent, message.isReasoningExpanded]);
|
||||
@@ -63,72 +71,100 @@ const ThinkingContent = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-xl sm:rounded-2xl mb-2 sm:mb-4 overflow-hidden shadow-sm backdrop-blur-sm">
|
||||
<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 cursor-pointer hover:bg-gradient-to-r hover:from-white/20 hover:to-purple-50/30 transition-all"
|
||||
className='flex items-center justify-between p-3 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'
|
||||
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 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 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">
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<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 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`}
|
||||
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='p-3 sm:p-5 pt-2 sm:pt-4'>
|
||||
<div
|
||||
ref={scrollRef}
|
||||
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"
|
||||
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',
|
||||
scrollbarWidth: 'thin',
|
||||
scrollbarColor: 'rgba(0, 0, 0, 0.3) transparent'
|
||||
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">
|
||||
<div className='prose prose-xs sm:prose-sm prose-purple max-w-none text-xs sm:text-sm'>
|
||||
<MarkdownRenderer
|
||||
content={finalExtractedThinkingContent}
|
||||
className=""
|
||||
className=''
|
||||
animated={isThinkingStatus}
|
||||
previousContentLength={prevLength}
|
||||
/>
|
||||
@@ -141,4 +177,4 @@ const ThinkingContent = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default ThinkingContent;
|
||||
export default ThinkingContent;
|
||||
|
||||
@@ -17,7 +17,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import { STORAGE_KEYS, DEFAULT_CONFIG } from '../../constants/playground.constants';
|
||||
import {
|
||||
STORAGE_KEYS,
|
||||
DEFAULT_CONFIG,
|
||||
} from '../../constants/playground.constants';
|
||||
|
||||
const MESSAGES_STORAGE_KEY = 'playground_messages';
|
||||
|
||||
@@ -72,9 +75,12 @@ export const loadConfig = () => {
|
||||
...DEFAULT_CONFIG.parameterEnabled,
|
||||
...parsedConfig.parameterEnabled,
|
||||
},
|
||||
showDebugPanel: parsedConfig.showDebugPanel || DEFAULT_CONFIG.showDebugPanel,
|
||||
customRequestMode: parsedConfig.customRequestMode || DEFAULT_CONFIG.customRequestMode,
|
||||
customRequestBody: parsedConfig.customRequestBody || DEFAULT_CONFIG.customRequestBody,
|
||||
showDebugPanel:
|
||||
parsedConfig.showDebugPanel || DEFAULT_CONFIG.showDebugPanel,
|
||||
customRequestMode:
|
||||
parsedConfig.customRequestMode || DEFAULT_CONFIG.customRequestMode,
|
||||
customRequestBody:
|
||||
parsedConfig.customRequestBody || DEFAULT_CONFIG.customRequestBody,
|
||||
};
|
||||
|
||||
return mergedConfig;
|
||||
@@ -180,7 +186,6 @@ export const exportConfig = (config, messages = null) => {
|
||||
link.click();
|
||||
|
||||
URL.revokeObjectURL(link.href);
|
||||
|
||||
} catch (error) {
|
||||
console.error('导出配置失败:', error);
|
||||
}
|
||||
@@ -201,7 +206,10 @@ export const importConfig = (file) => {
|
||||
|
||||
if (importedConfig.inputs && importedConfig.parameterEnabled) {
|
||||
// 如果导入的配置包含消息,也一起导入
|
||||
if (importedConfig.messages && Array.isArray(importedConfig.messages)) {
|
||||
if (
|
||||
importedConfig.messages &&
|
||||
Array.isArray(importedConfig.messages)
|
||||
) {
|
||||
saveMessages(importedConfig.messages);
|
||||
}
|
||||
|
||||
@@ -219,4 +227,4 @@ export const importConfig = (file) => {
|
||||
reject(new Error('导入配置失败: ' + error.message));
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
@@ -36,4 +36,4 @@ export {
|
||||
getConfigTimestamp,
|
||||
exportConfig,
|
||||
importConfig,
|
||||
} from './configStorage';
|
||||
} from './configStorage';
|
||||
|
||||
Reference in New Issue
Block a user