|
|
@@ -6,60 +6,26 @@ import {
|
|
|
getUserIdFromLocalStorage,
|
|
|
showError,
|
|
|
getLogo,
|
|
|
+ isMobile,
|
|
|
} from '../../helpers/index.js';
|
|
|
import {
|
|
|
- Card,
|
|
|
- Chat,
|
|
|
- Input,
|
|
|
Layout,
|
|
|
- Select,
|
|
|
- Slider,
|
|
|
- TextArea,
|
|
|
- Typography,
|
|
|
- Button,
|
|
|
- MarkdownRender,
|
|
|
- Tag,
|
|
|
- Tabs,
|
|
|
- TabPane,
|
|
|
Toast,
|
|
|
- Tooltip,
|
|
|
Modal,
|
|
|
} from '@douyinfe/semi-ui';
|
|
|
import { SSE } from 'sse';
|
|
|
-import {
|
|
|
- Settings,
|
|
|
- Sparkles,
|
|
|
- ChevronRight,
|
|
|
- ChevronUp,
|
|
|
- Brain,
|
|
|
- Zap,
|
|
|
- MessageSquare,
|
|
|
- SlidersHorizontal,
|
|
|
- Hash,
|
|
|
- Thermometer,
|
|
|
- Type,
|
|
|
- Users,
|
|
|
- Loader2,
|
|
|
- Target,
|
|
|
- Repeat,
|
|
|
- Ban,
|
|
|
- Shuffle,
|
|
|
- ToggleLeft,
|
|
|
- Code,
|
|
|
- Eye,
|
|
|
- EyeOff,
|
|
|
- FileText,
|
|
|
- Clock,
|
|
|
- Check,
|
|
|
- X,
|
|
|
- Copy,
|
|
|
- RefreshCw,
|
|
|
- Trash2,
|
|
|
-} from 'lucide-react';
|
|
|
import { StyleContext } from '../../context/Style/index.js';
|
|
|
import { useTranslation } from 'react-i18next';
|
|
|
-import { renderGroupOption, truncateText, stringToColor } from '../../helpers/render.js';
|
|
|
-import { IconSend } from '@douyinfe/semi-icons';
|
|
|
+import { stringToColor } from '../../helpers/render.js';
|
|
|
+
|
|
|
+import SettingsPanel from '../../components/playground/SettingsPanel';
|
|
|
+import ChatArea from '../../components/playground/ChatArea';
|
|
|
+import DebugPanel from '../../components/playground/DebugPanel';
|
|
|
+import MessageContent from '../../components/playground/MessageContent';
|
|
|
+import MessageActions from '../../components/playground/MessageActions';
|
|
|
+import FloatingButtons from '../../components/playground/FloatingButtons';
|
|
|
+
|
|
|
+import { saveConfig, loadConfig } from '../../components/playground/configStorage';
|
|
|
|
|
|
let id = 4;
|
|
|
function getId() {
|
|
|
@@ -118,36 +84,19 @@ const Playground = () => {
|
|
|
},
|
|
|
];
|
|
|
|
|
|
- const defaultModel = 'deepseek-r1';
|
|
|
- const [inputs, setInputs] = useState({
|
|
|
- model: defaultModel,
|
|
|
- group: '',
|
|
|
- max_tokens: 0,
|
|
|
- temperature: 0,
|
|
|
- top_p: 1,
|
|
|
- frequency_penalty: 0,
|
|
|
- presence_penalty: 0,
|
|
|
- seed: null,
|
|
|
- stream: true,
|
|
|
- });
|
|
|
- const [parameterEnabled, setParameterEnabled] = useState({
|
|
|
- max_tokens: true,
|
|
|
- temperature: true,
|
|
|
- top_p: false,
|
|
|
- frequency_penalty: false,
|
|
|
- presence_penalty: false,
|
|
|
- seed: false,
|
|
|
- });
|
|
|
+ const savedConfig = loadConfig();
|
|
|
+
|
|
|
+ const [inputs, setInputs] = useState(savedConfig.inputs);
|
|
|
+ const [parameterEnabled, setParameterEnabled] = useState(savedConfig.parameterEnabled);
|
|
|
+ const [systemPrompt, setSystemPrompt] = useState(savedConfig.systemPrompt);
|
|
|
+ const [showDebugPanel, setShowDebugPanel] = useState(savedConfig.showDebugPanel);
|
|
|
+
|
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
|
const [status, setStatus] = useState({});
|
|
|
- const [systemPrompt, setSystemPrompt] = useState(
|
|
|
- 'You are a helpful assistant. You can help me by answering my questions. You can also ask me questions.',
|
|
|
- );
|
|
|
const [message, setMessage] = useState(defaultMessage);
|
|
|
const [models, setModels] = useState([]);
|
|
|
const [groups, setGroups] = useState([]);
|
|
|
- const [showSettings, setShowSettings] = useState(true);
|
|
|
- const [showDebugPanel, setShowDebugPanel] = useState(true);
|
|
|
+ const [showSettings, setShowSettings] = useState(false);
|
|
|
const [debugData, setDebugData] = useState({
|
|
|
request: null,
|
|
|
response: null,
|
|
|
@@ -156,6 +105,35 @@ const Playground = () => {
|
|
|
const [activeDebugTab, setActiveDebugTab] = useState('request');
|
|
|
const [styleState, styleDispatch] = useContext(StyleContext);
|
|
|
const sseSourceRef = useRef(null);
|
|
|
+ const chatRef = useRef(null);
|
|
|
+
|
|
|
+ const saveConfigTimeoutRef = useRef(null);
|
|
|
+
|
|
|
+ const debouncedSaveConfig = useCallback(() => {
|
|
|
+ if (saveConfigTimeoutRef.current) {
|
|
|
+ clearTimeout(saveConfigTimeoutRef.current);
|
|
|
+ }
|
|
|
+
|
|
|
+ saveConfigTimeoutRef.current = setTimeout(() => {
|
|
|
+ const configToSave = {
|
|
|
+ inputs,
|
|
|
+ parameterEnabled,
|
|
|
+ systemPrompt,
|
|
|
+ showDebugPanel,
|
|
|
+ };
|
|
|
+ saveConfig(configToSave);
|
|
|
+ }, 1000);
|
|
|
+ }, [inputs, parameterEnabled, systemPrompt, showDebugPanel]);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ debouncedSaveConfig();
|
|
|
+
|
|
|
+ return () => {
|
|
|
+ if (saveConfigTimeoutRef.current) {
|
|
|
+ clearTimeout(saveConfigTimeoutRef.current);
|
|
|
+ }
|
|
|
+ };
|
|
|
+ }, [debouncedSaveConfig]);
|
|
|
|
|
|
const handleInputChange = (name, value) => {
|
|
|
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
|
|
@@ -168,6 +146,38 @@ const Playground = () => {
|
|
|
}));
|
|
|
};
|
|
|
|
|
|
+ const handleConfigImport = useCallback((importedConfig) => {
|
|
|
+ if (importedConfig.inputs) {
|
|
|
+ setInputs(prev => ({
|
|
|
+ ...prev,
|
|
|
+ ...importedConfig.inputs,
|
|
|
+ }));
|
|
|
+ }
|
|
|
+
|
|
|
+ if (importedConfig.parameterEnabled) {
|
|
|
+ setParameterEnabled(prev => ({
|
|
|
+ ...prev,
|
|
|
+ ...importedConfig.parameterEnabled,
|
|
|
+ }));
|
|
|
+ }
|
|
|
+
|
|
|
+ if (importedConfig.systemPrompt) {
|
|
|
+ setSystemPrompt(importedConfig.systemPrompt);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (typeof importedConfig.showDebugPanel === 'boolean') {
|
|
|
+ setShowDebugPanel(importedConfig.showDebugPanel);
|
|
|
+ }
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ const handleConfigReset = useCallback(() => {
|
|
|
+ const defaultConfig = loadConfig();
|
|
|
+ setInputs(defaultConfig.inputs);
|
|
|
+ setParameterEnabled(defaultConfig.parameterEnabled);
|
|
|
+ setSystemPrompt(defaultConfig.systemPrompt);
|
|
|
+ setShowDebugPanel(defaultConfig.showDebugPanel);
|
|
|
+ }, []);
|
|
|
+
|
|
|
useEffect(() => {
|
|
|
if (searchParams.get('expired')) {
|
|
|
showError(t('未登录或登录已过期,请重新登录!'));
|
|
|
@@ -181,6 +191,23 @@ const Playground = () => {
|
|
|
loadGroups();
|
|
|
}, [searchParams, t]);
|
|
|
|
|
|
+ useEffect(() => {
|
|
|
+ const handleResize = () => {
|
|
|
+ styleDispatch({
|
|
|
+ type: 'set_is_mobile',
|
|
|
+ payload: isMobile(),
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ handleResize();
|
|
|
+
|
|
|
+ window.addEventListener('resize', handleResize);
|
|
|
+
|
|
|
+ return () => {
|
|
|
+ window.removeEventListener('resize', handleResize);
|
|
|
+ };
|
|
|
+ }, [styleDispatch]);
|
|
|
+
|
|
|
const loadModels = async () => {
|
|
|
let res = await API.get(`/api/user/models`);
|
|
|
const { success, message, data } = res.data;
|
|
|
@@ -190,9 +217,10 @@ const Playground = () => {
|
|
|
value: model,
|
|
|
}));
|
|
|
setModels(localModelOptions);
|
|
|
- const hasDefault = localModelOptions.some(option => option.value === defaultModel);
|
|
|
- if (!hasDefault && localModelOptions.length > 0) {
|
|
|
- setInputs((inputs) => ({ ...inputs, model: localModelOptions[0].value }));
|
|
|
+
|
|
|
+ const hasCurrentModel = localModelOptions.some(option => option.value === inputs.model);
|
|
|
+ if (!hasCurrentModel && localModelOptions.length > 0) {
|
|
|
+ handleInputChange('model', localModelOptions[0].value);
|
|
|
}
|
|
|
} else {
|
|
|
showError(t(message));
|
|
|
@@ -204,7 +232,7 @@ const Playground = () => {
|
|
|
const { success, message, data } = res.data;
|
|
|
if (success) {
|
|
|
let localGroupOptions = Object.entries(data).map(([group, info]) => ({
|
|
|
- label: truncateText(info.desc, '50%'),
|
|
|
+ label: info.desc.length > 20 ? info.desc.substring(0, 20) + '...' : info.desc,
|
|
|
value: group,
|
|
|
ratio: info.ratio,
|
|
|
fullLabel: info.desc,
|
|
|
@@ -239,7 +267,11 @@ const Playground = () => {
|
|
|
}
|
|
|
|
|
|
setGroups(localGroupOptions);
|
|
|
- handleInputChange('group', localGroupOptions[0].value);
|
|
|
+
|
|
|
+ const hasCurrentGroup = localGroupOptions.some(option => option.value === inputs.group);
|
|
|
+ if (!hasCurrentGroup) {
|
|
|
+ handleInputChange('group', localGroupOptions[0].value);
|
|
|
+ }
|
|
|
} else {
|
|
|
showError(t(message));
|
|
|
}
|
|
|
@@ -533,11 +565,31 @@ const Playground = () => {
|
|
|
(content, attachment) => {
|
|
|
console.log('attachment: ', attachment);
|
|
|
setMessage((prevMessage) => {
|
|
|
+ let messageContent;
|
|
|
+ const validImageUrls = inputs.imageUrls.filter(url => url.trim() !== '');
|
|
|
+
|
|
|
+ if (validImageUrls.length > 0) {
|
|
|
+ messageContent = [
|
|
|
+ {
|
|
|
+ type: 'text',
|
|
|
+ text: content,
|
|
|
+ },
|
|
|
+ ...validImageUrls.map(url => ({
|
|
|
+ type: 'image_url',
|
|
|
+ image_url: {
|
|
|
+ url: url.trim(),
|
|
|
+ },
|
|
|
+ })),
|
|
|
+ ];
|
|
|
+ } else {
|
|
|
+ messageContent = content;
|
|
|
+ }
|
|
|
+
|
|
|
const newMessage = [
|
|
|
...prevMessage,
|
|
|
{
|
|
|
role: 'user',
|
|
|
- content: content,
|
|
|
+ content: messageContent,
|
|
|
createAt: Date.now(),
|
|
|
id: getId(),
|
|
|
},
|
|
|
@@ -603,7 +655,7 @@ const Playground = () => {
|
|
|
return newMessage;
|
|
|
});
|
|
|
},
|
|
|
- [getSystemMessage, inputs, setMessage],
|
|
|
+ [getSystemMessage, inputs, setMessage, parameterEnabled],
|
|
|
);
|
|
|
|
|
|
const completeMessage = useCallback((status = 'complete') => {
|
|
|
@@ -860,762 +912,159 @@ const Playground = () => {
|
|
|
}
|
|
|
}, [setMessage]);
|
|
|
|
|
|
- const DebugToggle = () => {
|
|
|
- return (
|
|
|
- <Button
|
|
|
- icon={showDebugPanel ? <EyeOff size={14} /> : <Eye size={14} />}
|
|
|
- onClick={() => setShowDebugPanel(!showDebugPanel)}
|
|
|
- theme="borderless"
|
|
|
- type="tertiary"
|
|
|
- size="small"
|
|
|
- className="!rounded-lg !text-gray-600 hover:!text-purple-600 hover:!bg-purple-50"
|
|
|
- >
|
|
|
- {showDebugPanel ? t('隐藏调试') : t('显示调试')}
|
|
|
- </Button>
|
|
|
- );
|
|
|
- };
|
|
|
-
|
|
|
- const SettingsToggle = () => {
|
|
|
- if (!styleState.isMobile) return null;
|
|
|
- return (
|
|
|
- <Button
|
|
|
- icon={<Settings size={16} />}
|
|
|
- style={{
|
|
|
- position: 'absolute',
|
|
|
- left: showSettings ? -10 : -20,
|
|
|
- top: '50%',
|
|
|
- transform: 'translateY(-50%)',
|
|
|
- zIndex: 1000,
|
|
|
- width: 40,
|
|
|
- height: 40,
|
|
|
- borderRadius: '0 20px 20px 0',
|
|
|
- padding: 0,
|
|
|
- boxShadow: '2px 0 8px rgba(0, 0, 0, 0.15)',
|
|
|
- }}
|
|
|
- onClick={() => setShowSettings(!showSettings)}
|
|
|
- theme='solid'
|
|
|
- type='primary'
|
|
|
- />
|
|
|
+ const toggleReasoningExpansion = (messageId) => {
|
|
|
+ setMessage(prevMessages =>
|
|
|
+ prevMessages.map(msg =>
|
|
|
+ msg.id === messageId && msg.role === 'assistant'
|
|
|
+ ? { ...msg, isReasoningExpanded: !msg.isReasoningExpanded }
|
|
|
+ : msg
|
|
|
+ )
|
|
|
);
|
|
|
};
|
|
|
|
|
|
- function CustomInputRender(props) {
|
|
|
- const { detailProps } = props;
|
|
|
- const { clearContextNode, uploadNode, inputNode, sendNode, onClick } =
|
|
|
- detailProps;
|
|
|
-
|
|
|
- return (
|
|
|
- <div className="p-4">
|
|
|
- <div
|
|
|
- className="flex items-end gap-3 p-4 bg-gray-50 rounded-2xl shadow-sm hover:shadow-md transition-shadow"
|
|
|
- style={{ border: '1px solid var(--semi-color-border)' }}
|
|
|
- onClick={onClick}
|
|
|
- >
|
|
|
- <div className="flex-1">
|
|
|
- {inputNode}
|
|
|
- </div>
|
|
|
- <Button
|
|
|
- theme="solid"
|
|
|
- type="primary"
|
|
|
- className="!rounded-lg !bg-purple-500 hover:!bg-purple-600 flex-shrink-0"
|
|
|
- icon={<IconSend />}
|
|
|
- >
|
|
|
- {t('发送')}
|
|
|
- </Button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- const renderInputArea = useCallback((props) => {
|
|
|
- return <CustomInputRender {...props} />;
|
|
|
- }, []);
|
|
|
-
|
|
|
- const renderChatBoxAction = useCallback((props) => {
|
|
|
- const { message } = props;
|
|
|
-
|
|
|
- const isLoading = message.status === 'loading' || message.status === 'incomplete';
|
|
|
-
|
|
|
- return (
|
|
|
- <div className="flex items-center gap-0.5">
|
|
|
- {!isLoading && (
|
|
|
- <Tooltip content={t('重试')} position="top">
|
|
|
- <Button
|
|
|
- theme="borderless"
|
|
|
- type="tertiary"
|
|
|
- size="small"
|
|
|
- icon={<RefreshCw size={14} />}
|
|
|
- onClick={() => handleMessageReset(message)}
|
|
|
- className="!rounded-md !text-gray-400 hover:!text-blue-600 hover:!bg-blue-50 !w-7 !h-7 !p-0 transition-all"
|
|
|
- aria-label={t('重试')}
|
|
|
- />
|
|
|
- </Tooltip>
|
|
|
- )}
|
|
|
-
|
|
|
- {message.content && (
|
|
|
- <Tooltip content={t('复制')} position="top">
|
|
|
- <Button
|
|
|
- theme="borderless"
|
|
|
- type="tertiary"
|
|
|
- size="small"
|
|
|
- icon={<Copy size={14} />}
|
|
|
- onClick={() => handleMessageCopy(message)}
|
|
|
- className="!rounded-md !text-gray-400 hover:!text-green-600 hover:!bg-green-50 !w-7 !h-7 !p-0 transition-all"
|
|
|
- aria-label={t('复制')}
|
|
|
- />
|
|
|
- </Tooltip>
|
|
|
- )}
|
|
|
-
|
|
|
- {!isLoading && (
|
|
|
- <Tooltip content={t('删除')} position="top">
|
|
|
- <Button
|
|
|
- theme="borderless"
|
|
|
- type="tertiary"
|
|
|
- size="small"
|
|
|
- icon={<Trash2 size={14} />}
|
|
|
- onClick={() => handleMessageDelete(message)}
|
|
|
- className="!rounded-md !text-gray-400 hover:!text-red-600 hover:!bg-red-50 !w-7 !h-7 !p-0 transition-all"
|
|
|
- aria-label={t('删除')}
|
|
|
- />
|
|
|
- </Tooltip>
|
|
|
- )}
|
|
|
- </div>
|
|
|
- );
|
|
|
- }, [handleMessageReset, handleMessageCopy, handleMessageDelete, t]);
|
|
|
-
|
|
|
const renderCustomChatContent = useCallback(
|
|
|
({ message, className }) => {
|
|
|
- if (message.status === 'error') {
|
|
|
- return (
|
|
|
- <div className={`${className} flex items-center p-4 bg-red-50 rounded-xl`}>
|
|
|
- <Typography.Text type="danger" className="text-sm">
|
|
|
- {message.content || t('请求发生错误')}
|
|
|
- </Typography.Text>
|
|
|
- </div>
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- const toggleReasoningExpansion = (messageId) => {
|
|
|
- setMessage(prevMessages =>
|
|
|
- prevMessages.map(msg =>
|
|
|
- msg.id === messageId && msg.role === 'assistant'
|
|
|
- ? { ...msg, isReasoningExpanded: !msg.isReasoningExpanded }
|
|
|
- : msg
|
|
|
- )
|
|
|
- );
|
|
|
- };
|
|
|
-
|
|
|
- const isThinkingStatus = message.status === 'loading' || message.status === 'incomplete';
|
|
|
- let currentExtractedThinkingContent = null;
|
|
|
- let currentDisplayableFinalContent = message.content || "";
|
|
|
- let thinkingSource = null;
|
|
|
-
|
|
|
- if (message.role === 'assistant') {
|
|
|
- let baseContentForDisplay = message.content || "";
|
|
|
- let combinedThinkingContent = "";
|
|
|
-
|
|
|
- if (message.reasoningContent) {
|
|
|
- combinedThinkingContent = message.reasoningContent;
|
|
|
- thinkingSource = 'reasoningContent';
|
|
|
- }
|
|
|
-
|
|
|
- if (baseContentForDisplay.includes('<think>')) {
|
|
|
- const thinkTagRegex = /<think>([\s\S]*?)<\/think>/g;
|
|
|
- let match;
|
|
|
- let thoughtsFromPairedTags = [];
|
|
|
- let replyParts = [];
|
|
|
- let lastIndex = 0;
|
|
|
-
|
|
|
- while ((match = thinkTagRegex.exec(baseContentForDisplay)) !== null) {
|
|
|
- replyParts.push(baseContentForDisplay.substring(lastIndex, match.index));
|
|
|
- thoughtsFromPairedTags.push(match[1]);
|
|
|
- lastIndex = match.index + match[0].length;
|
|
|
- }
|
|
|
- replyParts.push(baseContentForDisplay.substring(lastIndex));
|
|
|
-
|
|
|
- if (thoughtsFromPairedTags.length > 0) {
|
|
|
- const pairedThoughtsStr = thoughtsFromPairedTags.join('\n\n---\n\n');
|
|
|
- if (combinedThinkingContent) {
|
|
|
- combinedThinkingContent += '\n\n---\n\n' + pairedThoughtsStr;
|
|
|
- } else {
|
|
|
- combinedThinkingContent = pairedThoughtsStr;
|
|
|
- }
|
|
|
- thinkingSource = thinkingSource ? thinkingSource + ' & <think> tags' : '<think> tags';
|
|
|
- }
|
|
|
-
|
|
|
- baseContentForDisplay = replyParts.join('');
|
|
|
- }
|
|
|
-
|
|
|
- if (isThinkingStatus) {
|
|
|
- const lastOpenThinkIndex = baseContentForDisplay.lastIndexOf('<think>');
|
|
|
- if (lastOpenThinkIndex !== -1) {
|
|
|
- const fragmentAfterLastOpen = baseContentForDisplay.substring(lastOpenThinkIndex);
|
|
|
- if (!fragmentAfterLastOpen.includes('</think>')) {
|
|
|
- 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>';
|
|
|
- }
|
|
|
- baseContentForDisplay = baseContentForDisplay.substring(0, lastOpenThinkIndex);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- currentExtractedThinkingContent = combinedThinkingContent || null;
|
|
|
- currentDisplayableFinalContent = baseContentForDisplay.replace(/<\/?think>/g, '').trim();
|
|
|
- }
|
|
|
-
|
|
|
- const headerText = isThinkingStatus ? t('思考中...') : t('思考过程');
|
|
|
- const finalExtractedThinkingContent = currentExtractedThinkingContent;
|
|
|
- const finalDisplayableFinalContent = currentDisplayableFinalContent;
|
|
|
-
|
|
|
- if (message.role === 'assistant' &&
|
|
|
- isThinkingStatus &&
|
|
|
- !finalExtractedThinkingContent &&
|
|
|
- (!finalDisplayableFinalContent || finalDisplayableFinalContent.trim() === '')) {
|
|
|
- return (
|
|
|
- <div className={`${className} flex items-center gap-4 p-6 bg-gradient-to-r from-purple-50 to-indigo-50 rounded-2xl`}>
|
|
|
- <div className="w-10 h-10 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={20} />
|
|
|
- </div>
|
|
|
- <div className="flex flex-col">
|
|
|
- <Typography.Text strong className="text-gray-800 text-base">
|
|
|
- {t('正在思考...')}
|
|
|
- </Typography.Text>
|
|
|
- <Typography.Text className="text-gray-500 text-sm">
|
|
|
- AI 正在分析您的问题
|
|
|
- </Typography.Text>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
return (
|
|
|
- <div className={className}>
|
|
|
- {message.role === 'assistant' && finalExtractedThinkingContent && (
|
|
|
- <div className="bg-gradient-to-br from-indigo-50 via-purple-50 to-pink-50 rounded-2xl mb-4 overflow-hidden shadow-sm backdrop-blur-sm">
|
|
|
- <div
|
|
|
- className="flex items-center justify-between p-5 cursor-pointer hover:bg-gradient-to-r hover:from-white/40 hover:to-purple-50/60 transition-all"
|
|
|
- onClick={() => toggleReasoningExpansion(message.id)}
|
|
|
- >
|
|
|
- <div className="flex items-center gap-4">
|
|
|
- <div className="w-8 h-8 rounded-full bg-gradient-to-br from-purple-500 to-indigo-600 flex items-center justify-center shadow-lg">
|
|
|
- <Brain className="text-white" size={16} />
|
|
|
- </div>
|
|
|
- <div className="flex flex-col">
|
|
|
- <Typography.Text strong className="text-gray-800 text-base">
|
|
|
- {headerText}
|
|
|
- </Typography.Text>
|
|
|
- {thinkingSource && (
|
|
|
- <Typography.Text className="text-gray-500 text-xs mt-0.5">
|
|
|
- 来源: {thinkingSource}
|
|
|
- </Typography.Text>
|
|
|
- )}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div className="flex items-center gap-3">
|
|
|
- {isThinkingStatus && (
|
|
|
- <div className="flex items-center gap-2">
|
|
|
- <Loader2 className="animate-spin text-purple-500" size={18} />
|
|
|
- <Typography.Text className="text-purple-600 text-sm font-medium">
|
|
|
- 思考中
|
|
|
- </Typography.Text>
|
|
|
- </div>
|
|
|
- )}
|
|
|
- {!isThinkingStatus && (
|
|
|
- <div className="w-6 h-6 rounded-full bg-purple-100 flex items-center justify-center">
|
|
|
- {message.isReasoningExpanded ?
|
|
|
- <ChevronUp size={16} className="text-purple-600" /> :
|
|
|
- <ChevronRight size={16} className="text-purple-600" />
|
|
|
- }
|
|
|
- </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`}
|
|
|
- >
|
|
|
- {message.isReasoningExpanded && (
|
|
|
- <div className="p-5 pt-4">
|
|
|
- <div className="bg-white/70 backdrop-blur-sm rounded-xl p-4 shadow-inner overflow-x-auto max-h-50 overflow-y-auto">
|
|
|
- <div className="prose prose-sm prose-purple max-w-none">
|
|
|
- <MarkdownRender raw={finalExtractedThinkingContent} />
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- )}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- )}
|
|
|
-
|
|
|
- {(finalDisplayableFinalContent && finalDisplayableFinalContent.trim() !== '') && (
|
|
|
- <div className="prose prose-sm prose-gray max-w-none overflow-x-auto">
|
|
|
- <MarkdownRender raw={finalDisplayableFinalContent} />
|
|
|
- </div>
|
|
|
- )}
|
|
|
- </div>
|
|
|
+ <MessageContent
|
|
|
+ message={message}
|
|
|
+ className={className}
|
|
|
+ styleState={styleState}
|
|
|
+ onToggleReasoningExpansion={toggleReasoningExpansion}
|
|
|
+ />
|
|
|
);
|
|
|
},
|
|
|
- [t, setMessage],
|
|
|
+ [styleState],
|
|
|
);
|
|
|
|
|
|
+ const renderChatBoxAction = useCallback((props) => {
|
|
|
+ const { message: currentMessage } = props;
|
|
|
+
|
|
|
+ const isAnyMessageGenerating = message.some(msg => msg.status === 'loading' || msg.status === 'incomplete');
|
|
|
+
|
|
|
+ return (
|
|
|
+ <MessageActions
|
|
|
+ message={currentMessage}
|
|
|
+ styleState={styleState}
|
|
|
+ onMessageReset={handleMessageReset}
|
|
|
+ onMessageCopy={handleMessageCopy}
|
|
|
+ onMessageDelete={handleMessageDelete}
|
|
|
+ isAnyMessageGenerating={isAnyMessageGenerating}
|
|
|
+ />
|
|
|
+ );
|
|
|
+ }, [handleMessageReset, handleMessageCopy, handleMessageDelete, styleState, message]);
|
|
|
+
|
|
|
return (
|
|
|
- <div className="min-h-screen bg-gray-50">
|
|
|
- <Layout style={{ height: '100vh', background: 'transparent' }}>
|
|
|
+ <div className="h-full bg-gray-50">
|
|
|
+ <Layout style={{ height: '100%', background: 'transparent' }} className="flex flex-col md:flex-row">
|
|
|
{(showSettings || !styleState.isMobile) && (
|
|
|
<Layout.Sider
|
|
|
style={{
|
|
|
background: 'transparent',
|
|
|
borderRight: 'none',
|
|
|
flexShrink: 0,
|
|
|
- minWidth: 320,
|
|
|
- maxWidth: 320,
|
|
|
- height: 'calc(100vh - 100px)',
|
|
|
+ minWidth: styleState.isMobile ? '100%' : 320,
|
|
|
+ maxWidth: styleState.isMobile ? '100%' : 320,
|
|
|
+ height: styleState.isMobile ? 'auto' : 'calc(100vh - 100px)',
|
|
|
+ overflow: 'auto',
|
|
|
+ position: styleState.isMobile ? 'fixed' : 'relative',
|
|
|
+ zIndex: styleState.isMobile ? 1000 : 1,
|
|
|
+ width: '100%',
|
|
|
+ top: 0,
|
|
|
+ left: 0,
|
|
|
+ right: 0,
|
|
|
+ bottom: 0,
|
|
|
}}
|
|
|
- width={320}
|
|
|
+ width={styleState.isMobile ? '100%' : 320}
|
|
|
+ className={styleState.isMobile ? 'bg-white shadow-lg' : ''}
|
|
|
>
|
|
|
- <Card className="!rounded-2xl h-full flex flex-col" bodyStyle={{ padding: '24px', height: '100%', display: 'flex', 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-blue-500 flex items-center justify-center mr-3">
|
|
|
- <SlidersHorizontal size={20} className="text-white" />
|
|
|
- </div>
|
|
|
- <Typography.Title heading={5} className="mb-0">
|
|
|
- {t('模型设置')}
|
|
|
- </Typography.Title>
|
|
|
- </div>
|
|
|
- <DebugToggle />
|
|
|
- </div>
|
|
|
-
|
|
|
- <div className="space-y-6 overflow-y-auto flex-1 pr-2 model-settings-scroll">
|
|
|
- {/* 分组选择 */}
|
|
|
- <div>
|
|
|
- <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>
|
|
|
- </div>
|
|
|
- <Select
|
|
|
- placeholder={t('请选择分组')}
|
|
|
- name='group'
|
|
|
- required
|
|
|
- selection
|
|
|
- onChange={(value) => handleInputChange('group', value)}
|
|
|
- value={inputs.group}
|
|
|
- autoComplete='new-password'
|
|
|
- optionList={groups}
|
|
|
- renderOptionItem={renderGroupOption}
|
|
|
- style={{ width: '100%' }}
|
|
|
- className="!rounded-lg"
|
|
|
- />
|
|
|
- </div>
|
|
|
-
|
|
|
- {/* 模型选择 */}
|
|
|
- <div>
|
|
|
- <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>
|
|
|
- </div>
|
|
|
- <Select
|
|
|
- placeholder={t('请选择模型')}
|
|
|
- name='model'
|
|
|
- required
|
|
|
- selection
|
|
|
- searchPosition='dropdown'
|
|
|
- filter
|
|
|
- onChange={(value) => handleInputChange('model', value)}
|
|
|
- value={inputs.model}
|
|
|
- autoComplete='new-password'
|
|
|
- optionList={models}
|
|
|
- className="!rounded-lg"
|
|
|
- />
|
|
|
- </div>
|
|
|
-
|
|
|
- {/* Temperature */}
|
|
|
- <div className={`transition-opacity duration-200 ${!parameterEnabled.temperature ? '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" className="!rounded-full">
|
|
|
- {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} />}
|
|
|
- onClick={() => handleParameterToggle('temperature')}
|
|
|
- className="!rounded-full !w-6 !h-6 !p-0 !min-w-0"
|
|
|
- />
|
|
|
- </div>
|
|
|
- <Typography.Text className="text-xs text-gray-500 mb-2">
|
|
|
- 控制输出的随机性和创造性
|
|
|
- </Typography.Text>
|
|
|
- <Slider
|
|
|
- step={0.1}
|
|
|
- min={0.1}
|
|
|
- max={1}
|
|
|
- value={inputs.temperature}
|
|
|
- onChange={(value) => handleInputChange('temperature', value)}
|
|
|
- className="mt-2"
|
|
|
- disabled={!parameterEnabled.temperature}
|
|
|
- />
|
|
|
- </div>
|
|
|
-
|
|
|
- {/* Top P */}
|
|
|
- <div className={`transition-opacity duration-200 ${!parameterEnabled.top_p ? '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" className="!rounded-full">
|
|
|
- {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} />}
|
|
|
- onClick={() => handleParameterToggle('top_p')}
|
|
|
- className="!rounded-full !w-6 !h-6 !p-0 !min-w-0"
|
|
|
- />
|
|
|
- </div>
|
|
|
- <Typography.Text className="text-xs text-gray-500 mb-2">
|
|
|
- 核采样,控制词汇选择的多样性
|
|
|
- </Typography.Text>
|
|
|
- <Slider
|
|
|
- step={0.1}
|
|
|
- min={0.1}
|
|
|
- max={1}
|
|
|
- value={inputs.top_p}
|
|
|
- onChange={(value) => handleInputChange('top_p', value)}
|
|
|
- className="mt-2"
|
|
|
- disabled={!parameterEnabled.top_p}
|
|
|
- />
|
|
|
- </div>
|
|
|
-
|
|
|
- {/* Frequency Penalty */}
|
|
|
- <div className={`transition-opacity duration-200 ${!parameterEnabled.frequency_penalty ? '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" className="!rounded-full">
|
|
|
- {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} />}
|
|
|
- onClick={() => handleParameterToggle('frequency_penalty')}
|
|
|
- className="!rounded-full !w-6 !h-6 !p-0 !min-w-0"
|
|
|
- />
|
|
|
- </div>
|
|
|
- <Typography.Text className="text-xs text-gray-500 mb-2">
|
|
|
- 频率惩罚,减少重复词汇的出现
|
|
|
- </Typography.Text>
|
|
|
- <Slider
|
|
|
- step={0.1}
|
|
|
- min={-2}
|
|
|
- max={2}
|
|
|
- value={inputs.frequency_penalty}
|
|
|
- onChange={(value) => handleInputChange('frequency_penalty', value)}
|
|
|
- className="mt-2"
|
|
|
- disabled={!parameterEnabled.frequency_penalty}
|
|
|
- />
|
|
|
- </div>
|
|
|
-
|
|
|
- {/* Presence Penalty */}
|
|
|
- <div className={`transition-opacity duration-200 ${!parameterEnabled.presence_penalty ? '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" className="!rounded-full">
|
|
|
- {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} />}
|
|
|
- onClick={() => handleParameterToggle('presence_penalty')}
|
|
|
- className="!rounded-full !w-6 !h-6 !p-0 !min-w-0"
|
|
|
- />
|
|
|
- </div>
|
|
|
- <Typography.Text className="text-xs text-gray-500 mb-2">
|
|
|
- 存在惩罚,鼓励讨论新话题
|
|
|
- </Typography.Text>
|
|
|
- <Slider
|
|
|
- step={0.1}
|
|
|
- min={-2}
|
|
|
- max={2}
|
|
|
- value={inputs.presence_penalty}
|
|
|
- onChange={(value) => handleInputChange('presence_penalty', value)}
|
|
|
- className="mt-2"
|
|
|
- disabled={!parameterEnabled.presence_penalty}
|
|
|
- />
|
|
|
- </div>
|
|
|
-
|
|
|
- {/* MaxTokens */}
|
|
|
- <div className={`transition-opacity duration-200 ${!parameterEnabled.max_tokens ? '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} />}
|
|
|
- onClick={() => handleParameterToggle('max_tokens')}
|
|
|
- className="!rounded-full !w-6 !h-6 !p-0 !min-w-0"
|
|
|
- />
|
|
|
- </div>
|
|
|
- <Input
|
|
|
- placeholder='MaxTokens'
|
|
|
- name='max_tokens'
|
|
|
- required
|
|
|
- autoComplete='new-password'
|
|
|
- defaultValue={0}
|
|
|
- value={inputs.max_tokens}
|
|
|
- onChange={(value) => handleInputChange('max_tokens', value)}
|
|
|
- className="!rounded-lg"
|
|
|
- disabled={!parameterEnabled.max_tokens}
|
|
|
- />
|
|
|
- </div>
|
|
|
-
|
|
|
- {/* Seed */}
|
|
|
- <div className={`transition-opacity duration-200 ${!parameterEnabled.seed ? '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>
|
|
|
- </div>
|
|
|
- <Button
|
|
|
- theme={parameterEnabled.seed ? 'solid' : 'borderless'}
|
|
|
- type={parameterEnabled.seed ? 'primary' : 'tertiary'}
|
|
|
- size="small"
|
|
|
- icon={parameterEnabled.seed ? <Check size={10} /> : <X size={10} />}
|
|
|
- onClick={() => handleParameterToggle('seed')}
|
|
|
- className="!rounded-full !w-6 !h-6 !p-0 !min-w-0"
|
|
|
- />
|
|
|
- </div>
|
|
|
- <Input
|
|
|
- placeholder='随机种子 (留空为随机)'
|
|
|
- name='seed'
|
|
|
- autoComplete='new-password'
|
|
|
- value={inputs.seed || ''}
|
|
|
- onChange={(value) => handleInputChange('seed', value === '' ? null : value)}
|
|
|
- className="!rounded-lg"
|
|
|
- disabled={!parameterEnabled.seed}
|
|
|
- />
|
|
|
- </div>
|
|
|
-
|
|
|
- {/* Stream Toggle */}
|
|
|
- <div>
|
|
|
- <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>
|
|
|
- </div>
|
|
|
- <Button
|
|
|
- theme={inputs.stream ? 'solid' : 'borderless'}
|
|
|
- type={inputs.stream ? 'primary' : 'tertiary'}
|
|
|
- size="small"
|
|
|
- onClick={() => handleInputChange('stream', !inputs.stream)}
|
|
|
- className="!rounded-full"
|
|
|
- >
|
|
|
- {inputs.stream ? '开启' : '关闭'}
|
|
|
- </Button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- {/* System Prompt */}
|
|
|
- <div>
|
|
|
- <div className="flex items-center gap-2 mb-2">
|
|
|
- <Type size={16} className="text-gray-500" />
|
|
|
- <Typography.Text strong className="text-sm">
|
|
|
- System Prompt
|
|
|
- </Typography.Text>
|
|
|
- </div>
|
|
|
- <TextArea
|
|
|
- placeholder='System Prompt'
|
|
|
- name='system'
|
|
|
- required
|
|
|
- autoComplete='new-password'
|
|
|
- autosize
|
|
|
- defaultValue={systemPrompt}
|
|
|
- onChange={(value) => setSystemPrompt(value)}
|
|
|
- className="!rounded-lg"
|
|
|
- maxHeight={200}
|
|
|
- />
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </Card>
|
|
|
+ <SettingsPanel
|
|
|
+ inputs={inputs}
|
|
|
+ parameterEnabled={parameterEnabled}
|
|
|
+ models={models}
|
|
|
+ groups={groups}
|
|
|
+ systemPrompt={systemPrompt}
|
|
|
+ styleState={styleState}
|
|
|
+ showSettings={showSettings}
|
|
|
+ showDebugPanel={showDebugPanel}
|
|
|
+ onInputChange={handleInputChange}
|
|
|
+ onParameterToggle={handleParameterToggle}
|
|
|
+ onSystemPromptChange={setSystemPrompt}
|
|
|
+ onCloseSettings={() => setShowSettings(false)}
|
|
|
+ onConfigImport={handleConfigImport}
|
|
|
+ onConfigReset={handleConfigReset}
|
|
|
+ />
|
|
|
</Layout.Sider>
|
|
|
)}
|
|
|
|
|
|
<Layout.Content className="relative flex-1 overflow-hidden">
|
|
|
- <div className="px-4 overflow-hidden flex gap-4" style={{ height: 'calc(100vh - 100px)' }}>
|
|
|
+ <div className="sm:px-4 overflow-hidden flex flex-col lg:flex-row gap-2 sm:gap-4 h-[calc(100vh-100px)]">
|
|
|
<div className="flex-1 flex flex-col">
|
|
|
- <SettingsToggle />
|
|
|
- <Card
|
|
|
- className="!rounded-2xl h-full"
|
|
|
- bodyStyle={{ padding: 0, height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
|
|
|
- >
|
|
|
- {/* 聊天头部 */}
|
|
|
- <div className="px-6 py-4 bg-gradient-to-r from-purple-500 to-blue-500 rounded-t-2xl">
|
|
|
- <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">
|
|
|
- {t('AI 对话')}
|
|
|
- </Typography.Title>
|
|
|
- <Typography.Text className="!text-white/80 text-sm">
|
|
|
- {inputs.model || t('选择模型开始对话')}
|
|
|
- </Typography.Text>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- {/* 聊天内容区域 */}
|
|
|
- <div className="flex-1 overflow-hidden">
|
|
|
- <Chat
|
|
|
- chatBoxRenderConfig={{
|
|
|
- renderChatBoxContent: renderCustomChatContent,
|
|
|
- renderChatBoxAction: renderChatBoxAction,
|
|
|
- }}
|
|
|
- renderInputArea={renderInputArea}
|
|
|
- roleConfig={roleInfo}
|
|
|
- style={{
|
|
|
- height: '100%',
|
|
|
- maxWidth: '100%',
|
|
|
- overflow: 'hidden'
|
|
|
- }}
|
|
|
- chats={message}
|
|
|
- onMessageSend={onMessageSend}
|
|
|
- onMessageCopy={handleMessageCopy}
|
|
|
- onMessageReset={handleMessageReset}
|
|
|
- onMessageDelete={handleMessageDelete}
|
|
|
- showClearContext
|
|
|
- showStopGenerate
|
|
|
- onStopGenerator={onStopGenerator}
|
|
|
- onClear={() => setMessage([])}
|
|
|
- className="h-full"
|
|
|
- placeholder={t('请输入您的问题...')}
|
|
|
- />
|
|
|
- </div>
|
|
|
- </Card>
|
|
|
+ <ChatArea
|
|
|
+ chatRef={chatRef}
|
|
|
+ message={message}
|
|
|
+ inputs={inputs}
|
|
|
+ styleState={styleState}
|
|
|
+ showDebugPanel={showDebugPanel}
|
|
|
+ roleInfo={roleInfo}
|
|
|
+ onMessageSend={onMessageSend}
|
|
|
+ onMessageCopy={handleMessageCopy}
|
|
|
+ onMessageReset={handleMessageReset}
|
|
|
+ onMessageDelete={handleMessageDelete}
|
|
|
+ onStopGenerator={onStopGenerator}
|
|
|
+ onClearMessages={() => setMessage([])}
|
|
|
+ onToggleDebugPanel={() => setShowDebugPanel(!showDebugPanel)}
|
|
|
+ renderCustomChatContent={renderCustomChatContent}
|
|
|
+ renderChatBoxAction={renderChatBoxAction}
|
|
|
+ />
|
|
|
</div>
|
|
|
|
|
|
- {/* 调试面板 */}
|
|
|
- {showDebugPanel && (
|
|
|
- <div className="w-96 flex-shrink-0">
|
|
|
- <Card className="!rounded-2xl h-full flex flex-col" bodyStyle={{ padding: '24px', height: '100%', display: 'flex', flexDirection: 'column' }}>
|
|
|
- <div className="flex items-center mb-6 flex-shrink-0">
|
|
|
- <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">
|
|
|
- {t('调试信息')}
|
|
|
- </Typography.Title>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div className="flex-1 overflow-hidden debug-panel">
|
|
|
- <Tabs
|
|
|
- type="line"
|
|
|
- className="h-full"
|
|
|
- style={{ height: '100%', display: 'flex', flexDirection: 'column' }}
|
|
|
- activeKey={activeDebugTab}
|
|
|
- onChange={setActiveDebugTab}
|
|
|
- >
|
|
|
- <TabPane tab={
|
|
|
- <div className="flex items-center gap-2">
|
|
|
- <FileText size={16} />
|
|
|
- {t('请求体')}
|
|
|
- </div>
|
|
|
- } itemKey="request">
|
|
|
- <div className="h-full overflow-y-auto bg-gray-50 rounded-lg p-4 model-settings-scroll">
|
|
|
- {debugData.request ? (
|
|
|
- <pre className="debug-code text-gray-700 whitespace-pre-wrap break-words">
|
|
|
- {JSON.stringify(debugData.request, null, 2)}
|
|
|
- </pre>
|
|
|
- ) : (
|
|
|
- <Typography.Text type="secondary" className="text-sm">
|
|
|
- {t('暂无请求数据')}
|
|
|
- </Typography.Text>
|
|
|
- )}
|
|
|
- </div>
|
|
|
- </TabPane>
|
|
|
-
|
|
|
- <TabPane tab={
|
|
|
- <div className="flex items-center gap-2">
|
|
|
- <Zap size={16} />
|
|
|
- {t('响应内容')}
|
|
|
- </div>
|
|
|
- } itemKey="response">
|
|
|
- <div className="h-full overflow-y-auto bg-gray-50 rounded-lg p-4 model-settings-scroll">
|
|
|
- {debugData.response ? (
|
|
|
- <pre className="debug-code text-gray-700 whitespace-pre-wrap break-words">
|
|
|
- {debugData.response}
|
|
|
- </pre>
|
|
|
- ) : (
|
|
|
- <Typography.Text type="secondary" className="text-sm">
|
|
|
- {t('暂无响应数据')}
|
|
|
- </Typography.Text>
|
|
|
- )}
|
|
|
- </div>
|
|
|
- </TabPane>
|
|
|
- </Tabs>
|
|
|
- </div>
|
|
|
-
|
|
|
- {debugData.timestamp && (
|
|
|
- <div className="flex items-center gap-2 mt-4 pt-4 flex-shrink-0">
|
|
|
- <Clock size={14} className="text-gray-500" />
|
|
|
- <Typography.Text className="text-xs text-gray-500">
|
|
|
- {t('最后更新')}: {new Date(debugData.timestamp).toLocaleString()}
|
|
|
- </Typography.Text>
|
|
|
- </div>
|
|
|
- )}
|
|
|
- </Card>
|
|
|
+ {/* 调试面板 - 桌面端 */}
|
|
|
+ {showDebugPanel && !styleState.isMobile && (
|
|
|
+ <div className="w-96 flex-shrink-0 h-full">
|
|
|
+ <DebugPanel
|
|
|
+ debugData={debugData}
|
|
|
+ activeDebugTab={activeDebugTab}
|
|
|
+ onActiveDebugTabChange={setActiveDebugTab}
|
|
|
+ styleState={styleState}
|
|
|
+ />
|
|
|
</div>
|
|
|
)}
|
|
|
</div>
|
|
|
+
|
|
|
+ {/* 调试面板 - 移动端覆盖层 */}
|
|
|
+ {showDebugPanel && styleState.isMobile && (
|
|
|
+ <div
|
|
|
+ style={{
|
|
|
+ position: 'fixed',
|
|
|
+ top: 0,
|
|
|
+ left: 0,
|
|
|
+ right: 0,
|
|
|
+ bottom: 0,
|
|
|
+ zIndex: 1000,
|
|
|
+ backgroundColor: 'white',
|
|
|
+ overflow: 'auto',
|
|
|
+ }}
|
|
|
+ className="shadow-lg"
|
|
|
+ >
|
|
|
+ <DebugPanel
|
|
|
+ debugData={debugData}
|
|
|
+ activeDebugTab={activeDebugTab}
|
|
|
+ onActiveDebugTabChange={setActiveDebugTab}
|
|
|
+ styleState={styleState}
|
|
|
+ showDebugPanel={showDebugPanel}
|
|
|
+ onCloseDebugPanel={() => setShowDebugPanel(false)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {/* 浮动按钮 */}
|
|
|
+ <FloatingButtons
|
|
|
+ styleState={styleState}
|
|
|
+ showSettings={showSettings}
|
|
|
+ showDebugPanel={showDebugPanel}
|
|
|
+ onToggleSettings={() => setShowSettings(!showSettings)}
|
|
|
+ onToggleDebugPanel={() => setShowDebugPanel(!showDebugPanel)}
|
|
|
+ />
|
|
|
</Layout.Content>
|
|
|
</Layout>
|
|
|
</div>
|