🎨 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:
t0ng7u
2025-08-30 21:15:10 +08:00
parent 41cf516ec5
commit 0d57b1acd4
274 changed files with 11025 additions and 7659 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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
);
},
);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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));
}
});
};
};

View File

@@ -36,4 +36,4 @@ export {
getConfigTimestamp,
exportConfig,
importConfig,
} from './configStorage';
} from './configStorage';