|  |  |  |  | import React, { useState, useRef, useEffect } from 'react' | 
					
						
							|  |  |  |  | import ReactMarkdown from 'react-markdown' | 
					
						
							|  |  |  |  | import remarkGfm from 'remark-gfm' | 
					
						
							|  |  |  |  | import { | 
					
						
							|  |  |  |  |   Input, | 
					
						
							|  |  |  |  |   Button, | 
					
						
							|  |  |  |  |   Typography, | 
					
						
							|  |  |  |  |   Spin, | 
					
						
							|  |  |  |  |   message, | 
					
						
							|  |  |  |  |   Layout, | 
					
						
							|  |  |  |  |   Tooltip, | 
					
						
							|  |  |  |  |   Input as AntInput, | 
					
						
							|  |  |  |  |   Modal, | 
					
						
							|  |  |  |  |   Upload, | 
					
						
							|  |  |  |  |   Dropdown, | 
					
						
							|  |  |  |  |   Space, | 
					
						
							|  |  |  |  |   Select, | 
					
						
							|  |  |  |  | } from 'antd' | 
					
						
							|  |  |  |  | import { | 
					
						
							|  |  |  |  |   ArrowUpOutlined, | 
					
						
							|  |  |  |  |   LinuxOutlined, | 
					
						
							|  |  |  |  |   RobotOutlined, | 
					
						
							|  |  |  |  |   LoadingOutlined, | 
					
						
							|  |  |  |  |   CopyOutlined, | 
					
						
							|  |  |  |  |   ReloadOutlined, | 
					
						
							|  |  |  |  |   PaperClipOutlined, | 
					
						
							|  |  |  |  | } from '@ant-design/icons' | 
					
						
							|  |  |  |  | import { callLocalChatAPI, handleAPIError } from '@/config/api' | 
					
						
							|  |  |  |  | import { conversationStore } from '@/utils/pageConversationStore' | 
					
						
							|  |  |  |  | import './ChatConversation.less' | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | const { Text } = Typography | 
					
						
							|  |  |  |  | const { TextArea } = Input | 
					
						
							|  |  |  |  | const { Sider, Content } = Layout | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | function ChatConversation() { | 
					
						
							|  |  |  |  |   // 会话相关状态 - 使用独立的存储
 | 
					
						
							|  |  |  |  |   const [conversations, setConversations] = useState(conversationStore.getConversations()) | 
					
						
							|  |  |  |  |   const [currentConversationId, setCurrentConversationId] = useState(conversationStore.getCurrentConversationId()) | 
					
						
							|  |  |  |  |   const [inputValue, setInputValue] = useState('') | 
					
						
							|  |  |  |  |   const [loading, setLoading] = useState(false) | 
					
						
							|  |  |  |  |   const [sidebarExpanded, setSidebarExpanded] = useState(false) | 
					
						
							|  |  |  |  |   const [editModalVisible, setEditModalVisible] = useState(false) | 
					
						
							|  |  |  |  |   const [editingConversation, setEditingConversation] = useState(null) | 
					
						
							|  |  |  |  |   const [newTitle, setNewTitle] = useState('') | 
					
						
							|  |  |  |  |   const [deepThinkingEnabled, setDeepThinkingEnabled] = useState(false) | 
					
						
							|  |  |  |  |   const [typingMessage, setTypingMessage] = useState('') | 
					
						
							|  |  |  |  |   const [isTyping, setIsTyping] = useState(false) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   const [streamingContent, setStreamingContent] = useState('') | 
					
						
							|  |  |  |  | const [isStreaming, setIsStreaming] = useState(false) | 
					
						
							|  |  |  |  | const [currentStreamingMessageId, setCurrentStreamingMessageId] = useState(null) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   // 流式输出状态 - 已移除
 | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   const messagesEndRef = useRef(null) | 
					
						
							|  |  |  |  |   const inputRef = useRef(null) | 
					
						
							|  |  |  |  |   const typewriterTimeoutRef = useRef(null) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   // 获取当前会话
 | 
					
						
							|  |  |  |  |   const currentConversation = conversations.find(conv => conv.id === currentConversationId) | 
					
						
							|  |  |  |  |   const currentMessages = currentConversation?.messages || []    // 当前会话消息
 | 
					
						
							|  |  |  |  |    | 
					
						
							|  |  |  |  |   // 调试信息
 | 
					
						
							|  |  |  |  |   console.log('Current conversation state:', { | 
					
						
							|  |  |  |  |     conversations: conversations.length, | 
					
						
							|  |  |  |  |     currentConversationId, | 
					
						
							|  |  |  |  |     currentConversation: !!currentConversation, | 
					
						
							|  |  |  |  |     currentMessages: currentMessages.length | 
					
						
							|  |  |  |  |   }) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   // console.log(currentMessages,"=========================333333")
 | 
					
						
							|  |  |  |  |   const chatScrollRef = useRef(null); | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   const scrollToBottom = () => { | 
					
						
							|  |  |  |  |     messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }) | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   // 流式输出相关函数 - 已移除
 | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   const handleChange = value => { | 
					
						
							|  |  |  |  |     console.log(`selected ${value}`); | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   // 订阅对话状态变化
 | 
					
						
							|  |  |  |  |   useEffect(() => { | 
					
						
							|  |  |  |  |     const unsubscribe = conversationStore.subscribe(({ conversations, currentConversationId }) => { | 
					
						
							|  |  |  |  |       setConversations(conversations) | 
					
						
							|  |  |  |  |       setCurrentConversationId(currentConversationId) | 
					
						
							|  |  |  |  |     }) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     return unsubscribe | 
					
						
							|  |  |  |  |   }, []) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   // 清理定时器
 | 
					
						
							|  |  |  |  |   useEffect(() => { | 
					
						
							|  |  |  |  |     return () => { | 
					
						
							|  |  |  |  |       if (typewriterTimeoutRef.current) { | 
					
						
							|  |  |  |  |         clearTimeout(typewriterTimeoutRef.current) | 
					
						
							|  |  |  |  |       } | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  |   }, []) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   // 打字机效果函数 - 简化版本用于测试
 | 
					
						
							|  |  |  |  |   const typewriterEffect = (text, onComplete) => { | 
					
						
							|  |  |  |  |     console.log('Typewriter effect started with text:', text) | 
					
						
							|  |  |  |  |     let index = 0 | 
					
						
							|  |  |  |  |     setTypingMessage('') | 
					
						
							|  |  |  |  |     setIsTyping(true) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     const typeNextWords = () => { | 
					
						
							|  |  |  |  |       if (index < text.length) { | 
					
						
							|  |  |  |  |         // 简化:每次显示3个字符
 | 
					
						
							|  |  |  |  |         const nextIndex = Math.min(index + 3, text.length) | 
					
						
							|  |  |  |  |         const currentText = text.substring(0, nextIndex) | 
					
						
							|  |  |  |  |         console.log('Typing progress:', currentText) | 
					
						
							|  |  |  |  |         setTypingMessage(currentText) | 
					
						
							|  |  |  |  |         index = nextIndex | 
					
						
							|  |  |  |  |          | 
					
						
							|  |  |  |  |         // 500ms间隔
 | 
					
						
							|  |  |  |  |         typewriterTimeoutRef.current = setTimeout(typeNextWords, 500) | 
					
						
							|  |  |  |  |       } else { | 
					
						
							|  |  |  |  |         console.log('Typewriter effect completed') | 
					
						
							|  |  |  |  |         setIsTyping(false) | 
					
						
							|  |  |  |  |         onComplete(text) | 
					
						
							|  |  |  |  |       } | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     typeNextWords() | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   // 模拟流式效果
 | 
					
						
							|  |  |  |  |   const simulateStreamingEffect = (fullContent, messageId) => { | 
					
						
							|  |  |  |  |     setIsStreaming(true) | 
					
						
							|  |  |  |  |     setStreamingContent('') | 
					
						
							|  |  |  |  |      | 
					
						
							|  |  |  |  |     let currentIndex = 0 | 
					
						
							|  |  |  |  |     const chunkSize = 3 // 每次显示3个字符
 | 
					
						
							|  |  |  |  |     const delay = 50 // 50ms间隔,可以调整速度
 | 
					
						
							|  |  |  |  |      | 
					
						
							|  |  |  |  |     const streamNext = () => { | 
					
						
							|  |  |  |  |       if (currentIndex < fullContent.length) { | 
					
						
							|  |  |  |  |         const nextIndex = Math.min(currentIndex + chunkSize, fullContent.length) | 
					
						
							|  |  |  |  |         const currentContent = fullContent.substring(0, nextIndex) | 
					
						
							|  |  |  |  |          | 
					
						
							|  |  |  |  |         setStreamingContent(currentContent) | 
					
						
							|  |  |  |  |         conversationStore.updateMessage(messageId, currentContent) | 
					
						
							|  |  |  |  |          | 
					
						
							|  |  |  |  |         currentIndex = nextIndex | 
					
						
							|  |  |  |  |         setTimeout(streamNext, delay) | 
					
						
							|  |  |  |  |       } else { | 
					
						
							|  |  |  |  |         // 流式效果完成
 | 
					
						
							|  |  |  |  |         setIsStreaming(false) | 
					
						
							|  |  |  |  |         setStreamingContent('') | 
					
						
							|  |  |  |  |         setCurrentStreamingMessageId(null) | 
					
						
							|  |  |  |  |       } | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  |      | 
					
						
							|  |  |  |  |     streamNext() | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   // 创建新对话
 | 
					
						
							|  |  |  |  |   const createNewConversation = () => { | 
					
						
							|  |  |  |  |     conversationStore.createNewConversation() | 
					
						
							|  |  |  |  |     setInputValue('') | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   // 切换对话
 | 
					
						
							|  |  |  |  |   const switchConversation = (conversationId) => { | 
					
						
							|  |  |  |  |     conversationStore.switchConversation(conversationId) | 
					
						
							|  |  |  |  |     setInputValue('') | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   // 删除对话
 | 
					
						
							|  |  |  |  |   const deleteConversation = (conversationId) => { | 
					
						
							|  |  |  |  |     if (conversations.length <= 1) { | 
					
						
							|  |  |  |  |       message.warning('至少需要保留一个对话') | 
					
						
							|  |  |  |  |       return | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     conversationStore.deleteConversation(conversationId) | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   // 编辑对话标题
 | 
					
						
							|  |  |  |  |   const editConversationTitle = (conversation) => { | 
					
						
							|  |  |  |  |     setEditingConversation(conversation) | 
					
						
							|  |  |  |  |     setNewTitle(conversation.title) | 
					
						
							|  |  |  |  |     setEditModalVisible(true) | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   // 保存对话标题
 | 
					
						
							|  |  |  |  |   const saveConversationTitle = () => { | 
					
						
							|  |  |  |  |     if (!newTitle.trim()) { | 
					
						
							|  |  |  |  |       message.warning('标题不能为空') | 
					
						
							|  |  |  |  |       return | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     conversationStore.updateConversationTitle(editingConversation.id, newTitle.trim()) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     setEditModalVisible(false) | 
					
						
							|  |  |  |  |     setEditingConversation(null) | 
					
						
							|  |  |  |  |     setNewTitle('') | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   // 发送消息
 | 
					
						
							|  |  |  |  |   const sendMessage = async () => { | 
					
						
							|  |  |  |  |     const messageText = inputValue.trim() | 
					
						
							|  |  |  |  |     if (!messageText || loading) return | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     // 添加用户消息到独立存储
 | 
					
						
							|  |  |  |  |     conversationStore.addMessage('user', messageText) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     setInputValue('') | 
					
						
							|  |  |  |  |     setLoading(true) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     try { | 
					
						
							|  |  |  |  |       // 调用本地后端接口
 | 
					
						
							|  |  |  |  |       const result = await callLocalChatAPI(messageText) | 
					
						
							|  |  |  |  |       console.log('API result:', result) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |       // 直接处理结果
 | 
					
						
							|  |  |  |  |       const thought = result?.thought || '' | 
					
						
							|  |  |  |  |       const message = result?.message || '' | 
					
						
							|  |  |  |  |       console.log('Processed content:', { thought, message, deepThinkingEnabled }) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |       // 根据深度思考按钮状态决定是否包含思考内容
 | 
					
						
							|  |  |  |  |       const finalContent = deepThinkingEnabled  | 
					
						
							|  |  |  |  |         ? thought + (message ? '\n\n' + message : '') | 
					
						
							|  |  |  |  |         : message | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |       console.log('Final content:', finalContent) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |       if (finalContent.trim()) { | 
					
						
							|  |  |  |  |         // 先添加一个空的助手消息
 | 
					
						
							|  |  |  |  |         const assistantMessageId = conversationStore.addMessage('assistant', '') | 
					
						
							|  |  |  |  |         setCurrentStreamingMessageId(assistantMessageId) | 
					
						
							|  |  |  |  |          | 
					
						
							|  |  |  |  |         // 开始模拟流式效果
 | 
					
						
							|  |  |  |  |         simulateStreamingEffect(finalContent, assistantMessageId) | 
					
						
							|  |  |  |  |       } else { | 
					
						
							|  |  |  |  |         console.log('No content, adding fallback message') | 
					
						
							|  |  |  |  |         conversationStore.addMessage('assistant', '抱歉,我无法生成回复内容。') | 
					
						
							|  |  |  |  |       } | 
					
						
							|  |  |  |  |     } catch (error) { | 
					
						
							|  |  |  |  |       const errorMessage = handleAPIError(error) | 
					
						
							|  |  |  |  |       message.error(errorMessage) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |       // 添加错误消息到独立存储
 | 
					
						
							|  |  |  |  |       conversationStore.addMessage('assistant', `抱歉,我遇到了一个问题:${errorMessage}`) | 
					
						
							|  |  |  |  |     } finally { | 
					
						
							|  |  |  |  |       setLoading(false) | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   // 处理回车键发送
 | 
					
						
							|  |  |  |  |   const handleKeyPress = (e) => { | 
					
						
							|  |  |  |  |     if (e.key === 'Enter' && !e.shiftKey) { | 
					
						
							|  |  |  |  |       e.preventDefault() | 
					
						
							|  |  |  |  |       sendMessage() | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   // 清空当前对话
 | 
					
						
							|  |  |  |  |   const clearCurrentChat = () => { | 
					
						
							|  |  |  |  |     conversationStore.clearCurrentConversation() | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   //  兼容剪贴板
 | 
					
						
							|  |  |  |  |   const copyToClipboard = async (text) => { | 
					
						
							|  |  |  |  |     if (!text) return Promise.reject(new Error("exmpty text")) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     // 现代 API,需 HTTPS 或 localhost(window.isSecureContext)
 | 
					
						
							|  |  |  |  |     if (navigator.clipboard && window.isSecureContext) { | 
					
						
							|  |  |  |  |       try { | 
					
						
							|  |  |  |  |         await navigator.clipboard.writeText(text) | 
					
						
							|  |  |  |  |         return | 
					
						
							|  |  |  |  |       } catch (e) { | 
					
						
							|  |  |  |  |         // 若失败,继续使用回退方案
 | 
					
						
							|  |  |  |  |         console.warn('navigator.clipboard failed, fallback to execCommand', e) | 
					
						
							|  |  |  |  |       } | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     // 回退方案
 | 
					
						
							|  |  |  |  |     return new Promise((resolve, reject) => { | 
					
						
							|  |  |  |  |       try { | 
					
						
							|  |  |  |  |         const ta = document.createElement('textarea') | 
					
						
							|  |  |  |  |         ta.value = text | 
					
						
							|  |  |  |  |         ta.setAttribute('readonly', '') | 
					
						
							|  |  |  |  |         ta.style.position = 'fixed' | 
					
						
							|  |  |  |  |         ta.style.left = '-9999px' | 
					
						
							|  |  |  |  |         document.body.appendChild(ta) | 
					
						
							|  |  |  |  |         ta.select() | 
					
						
							|  |  |  |  |         ta.setSelectionRange(0, ta.value.length) | 
					
						
							|  |  |  |  |         const ok = document.execCommand('copy') | 
					
						
							|  |  |  |  |         document.body.removeChild(ta) | 
					
						
							|  |  |  |  |         if (ok) resolve() | 
					
						
							|  |  |  |  |         else reject(new Error('execCommand failed')) | 
					
						
							|  |  |  |  |       } catch (err) { | 
					
						
							|  |  |  |  |         reject(err) | 
					
						
							|  |  |  |  |       } | 
					
						
							|  |  |  |  |     }) | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   // 渲染单条消息 ==================================
 | 
					
						
							|  |  |  |  |   const renderMessage = (msg, index) => { | 
					
						
							|  |  |  |  |     const isUser = msg.role === 'user' | 
					
						
							|  |  |  |  |      | 
					
						
							|  |  |  |  |     // 如果是当前正在流式输出的消息,显示流式内容
 | 
					
						
							|  |  |  |  |     const isCurrentStreaming = isStreaming &&  | 
					
						
							|  |  |  |  |       currentStreamingMessageId === msg.id &&  | 
					
						
							|  |  |  |  |       msg.role === 'assistant' | 
					
						
							|  |  |  |  |      | 
					
						
							|  |  |  |  |     const displayContent = isCurrentStreaming ? streamingContent : (msg.content || '') | 
					
						
							|  |  |  |  |      | 
					
						
							|  |  |  |  |     // 调试信息
 | 
					
						
							|  |  |  |  |     if (msg.role === 'assistant') { | 
					
						
							|  |  |  |  |       console.log('Rendering assistant message:', {  | 
					
						
							|  |  |  |  |         index,  | 
					
						
							|  |  |  |  |         content: msg.content,  | 
					
						
							|  |  |  |  |         displayContent, | 
					
						
							|  |  |  |  |         isCurrentStreaming, | 
					
						
							|  |  |  |  |         streamingContent | 
					
						
							|  |  |  |  |       }) | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  |      | 
					
						
							|  |  |  |  |     return ( | 
					
						
							|  |  |  |  |       <div | 
					
						
							|  |  |  |  |         key={index} | 
					
						
							|  |  |  |  |         className={`ds-message-row ${isUser ? 'from-user' : 'from-assistant'}`} | 
					
						
							|  |  |  |  |       > | 
					
						
							|  |  |  |  |         {!isUser && ( | 
					
						
							|  |  |  |  |           <div className='ds-avatar'> | 
					
						
							|  |  |  |  |             <RobotOutlined />    | 
					
						
							|  |  |  |  |           </div> | 
					
						
							|  |  |  |  |         )} | 
					
						
							|  |  |  |  |         {isUser && <div className='ds-avatar user'><LinuxOutlined /></div>} | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         <div className='ds-message-body'> | 
					
						
							|  |  |  |  |           <div className='ds-message-meta'> | 
					
						
							|  |  |  |  |             <span className='role'>{isUser ? '' : 'AI对话'}</span> | 
					
						
							|  |  |  |  |             <span className='time'> | 
					
						
							|  |  |  |  |               {msg.timestamp | 
					
						
							|  |  |  |  |                 ? (typeof msg.timestamp === 'string' | 
					
						
							|  |  |  |  |                   ? msg.timestamp | 
					
						
							|  |  |  |  |                   : msg.timestamp.toLocaleString())     // 时间
 | 
					
						
							|  |  |  |  |                 : ''} | 
					
						
							|  |  |  |  |             </span> | 
					
						
							|  |  |  |  |             {!isUser && ( | 
					
						
							|  |  |  |  |               <span className='actions'>   | 
					
						
							|  |  |  |  |                 {/* 复制按钮 */} | 
					
						
							|  |  |  |  |                 <Tooltip title='复制'>      | 
					
						
							|  |  |  |  |                   <Button | 
					
						
							|  |  |  |  |                     size='small' | 
					
						
							|  |  |  |  |                     type='text' | 
					
						
							|  |  |  |  |                     icon={<CopyOutlined />} | 
					
						
							|  |  |  |  |                     onClick={async () => { | 
					
						
							|  |  |  |  |                       try { | 
					
						
							|  |  |  |  |                         await copyToClipboard(msg.content || '') | 
					
						
							|  |  |  |  |                         message.success('已复制') | 
					
						
							|  |  |  |  |                       } catch (err) { | 
					
						
							|  |  |  |  |                         console.error('copy failed', err) | 
					
						
							|  |  |  |  |                         message.error('复制失败,请在 HTTPS / localhost 或允许剪贴板权限的环境下重试') | 
					
						
							|  |  |  |  |                       } | 
					
						
							|  |  |  |  |                     }} | 
					
						
							|  |  |  |  |                   /> | 
					
						
							|  |  |  |  |                 </Tooltip> | 
					
						
							|  |  |  |  |                 {/* 重新生成按钮 */} | 
					
						
							|  |  |  |  |                 <Tooltip title='重新生成(示例)'>   | 
					
						
							|  |  |  |  |                   <Button | 
					
						
							|  |  |  |  |                     size='small' | 
					
						
							|  |  |  |  |                     type='text' | 
					
						
							|  |  |  |  |                     icon={<ReloadOutlined />} | 
					
						
							|  |  |  |  |                     disabled={loading} | 
					
						
							|  |  |  |  |                     onClick={() => { | 
					
						
							|  |  |  |  |                       // 可复用最后一条用户消息再请求
 | 
					
						
							|  |  |  |  |                       const lastUser = [...currentMessages].reverse().find(m => m.role === 'user') | 
					
						
							|  |  |  |  |                       if (lastUser) { | 
					
						
							|  |  |  |  |                         setInputValue(lastUser.content) | 
					
						
							|  |  |  |  |                         sendMessage() | 
					
						
							|  |  |  |  |                       } | 
					
						
							|  |  |  |  |                     }} | 
					
						
							|  |  |  |  |                   /> | 
					
						
							|  |  |  |  |                 </Tooltip> | 
					
						
							|  |  |  |  |               </span> | 
					
						
							|  |  |  |  |             )} | 
					
						
							|  |  |  |  |           </div> | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |           {/* 回答框 */} | 
					
						
							|  |  |  |  |           <div className='ds-message-content'> | 
					
						
							|  |  |  |  |             {displayContent ? ( | 
					
						
							|  |  |  |  |               <ReactMarkdown | 
					
						
							|  |  |  |  |                 remarkPlugins={[remarkGfm]} | 
					
						
							|  |  |  |  |               > | 
					
						
							|  |  |  |  |                 {displayContent} | 
					
						
							|  |  |  |  |               </ReactMarkdown> | 
					
						
							|  |  |  |  |             ) : ( | 
					
						
							|  |  |  |  |               <div style={{ color: '#999', fontStyle: 'italic' }}> | 
					
						
							|  |  |  |  |                 暂无内容 | 
					
						
							|  |  |  |  |               </div> | 
					
						
							|  |  |  |  |             )} | 
					
						
							|  |  |  |  |           </div> | 
					
						
							|  |  |  |  |         </div> | 
					
						
							|  |  |  |  |       </div> | 
					
						
							|  |  |  |  |     ) | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   //  =================================================
 | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   return ( | 
					
						
							|  |  |  |  |     <div className='ds-chat-page'> | 
					
						
							|  |  |  |  |       <div className='ds-chat-scroll' ref={chatScrollRef}> | 
					
						
							|  |  |  |  |         <div className='ds-chat-inner'> | 
					
						
							|  |  |  |  |           {currentMessages.length === 0 && !loading && ( | 
					
						
							|  |  |  |  |             //  已有默认初始化对话,无需此处提示
 | 
					
						
							|  |  |  |  |             <div className='ds-empty'> | 
					
						
							|  |  |  |  |               <h3>开始对话吧</h3> | 
					
						
							|  |  |  |  |               <p>提出一个问题,比如:请帮我总结一段文本。</p> | 
					
						
							|  |  |  |  |             </div> | 
					
						
							|  |  |  |  |           )} | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |           {/* 历史消息 */} | 
					
						
							|  |  |  |  |           {currentMessages.map(renderMessage)} | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |           {/* 加载状态 */} | 
					
						
							|  |  |  |  |           {loading && ( | 
					
						
							|  |  |  |  |             <div className='ds-message-row from-assistant thinking'> | 
					
						
							|  |  |  |  |               <div className='ds-avatar'> | 
					
						
							|  |  |  |  |                 <RobotOutlined /> | 
					
						
							|  |  |  |  |               </div> | 
					
						
							|  |  |  |  |               <div className='ds-message-body'> | 
					
						
							|  |  |  |  |                 <div className='ds-message-meta'> | 
					
						
							|  |  |  |  |                   <span className='role'>AI</span> | 
					
						
							|  |  |  |  |                   <span className='time'>正在思考…</span> | 
					
						
							|  |  |  |  |                 </div> | 
					
						
							|  |  |  |  |                 <div className='ds-message-content typing'> | 
					
						
							|  |  |  |  |                   <Spin | 
					
						
							|  |  |  |  |                     indicator={<LoadingOutlined style={{ fontSize: 16 }} spin />} | 
					
						
							|  |  |  |  |                     size='small' | 
					
						
							|  |  |  |  |                   /> | 
					
						
							|  |  |  |  |                   <span className='typing-dots'> | 
					
						
							|  |  |  |  |                     <i /><i /><i /> | 
					
						
							|  |  |  |  |                   </span> | 
					
						
							|  |  |  |  |                 </div> | 
					
						
							|  |  |  |  |               </div> | 
					
						
							|  |  |  |  |             </div> | 
					
						
							|  |  |  |  |           )} | 
					
						
							|  |  |  |  |           <div ref={messagesEndRef} /> | 
					
						
							|  |  |  |  |         </div> | 
					
						
							|  |  |  |  |       </div> | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |       {/* 问答输入框 */} | 
					
						
							|  |  |  |  |       <div className='ds-input-bar'> | 
					
						
							|  |  |  |  |         <div className='ds-input-inner'> | 
					
						
							|  |  |  |  |           <div className='ds-textarea-wrap'> | 
					
						
							|  |  |  |  |             <TextArea | 
					
						
							|  |  |  |  |               ref={inputRef} | 
					
						
							|  |  |  |  |               value={inputValue} | 
					
						
							|  |  |  |  |               onChange={(e) => setInputValue(e.target.value)} | 
					
						
							|  |  |  |  |               onKeyDown={(e) => { | 
					
						
							|  |  |  |  |                 if (e.key === 'Enter' && !e.shiftKey) { | 
					
						
							|  |  |  |  |                   e.preventDefault() | 
					
						
							|  |  |  |  |                   sendMessage() | 
					
						
							|  |  |  |  |                 } | 
					
						
							|  |  |  |  |               }} | 
					
						
							|  |  |  |  |               placeholder='给AI助手发送消息' | 
					
						
							|  |  |  |  |               autoSize={{ minRows: 3, maxRows: 8 }} | 
					
						
							|  |  |  |  |               disabled={loading} | 
					
						
							|  |  |  |  |               bordered={false} | 
					
						
							|  |  |  |  |             /> | 
					
						
							|  |  |  |  |           </div> | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |           {/* 深度思考按钮 */} | 
					
						
							|  |  |  |  |           <Button | 
					
						
							|  |  |  |  |             type={deepThinkingEnabled ? 'primary' : 'default'} | 
					
						
							|  |  |  |  |             style={{ | 
					
						
							|  |  |  |  |               minWidth: 100, maxWidth: 120, marginRight: 8 | 
					
						
							|  |  |  |  |             }} | 
					
						
							|  |  |  |  |             onClick={() => setDeepThinkingEnabled(!deepThinkingEnabled)} | 
					
						
							|  |  |  |  |           > | 
					
						
							|  |  |  |  |             深度思考 | 
					
						
							|  |  |  |  |           </Button> | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |           {/* 上传文件 */} | 
					
						
							|  |  |  |  |           <Upload className='ds-uploading'> | 
					
						
							|  |  |  |  |             <Button | 
					
						
							|  |  |  |  |               shape='circle' | 
					
						
							|  |  |  |  |               icon={<PaperClipOutlined />} | 
					
						
							|  |  |  |  |             /> | 
					
						
							|  |  |  |  |           </Upload> | 
					
						
							|  |  |  |  |           {/* 发送按钮 */} | 
					
						
							|  |  |  |  |           <div className='ds-input-actions'> | 
					
						
							|  |  |  |  |             <Button | 
					
						
							|  |  |  |  |               type='primary' | 
					
						
							|  |  |  |  |               shape='circle' | 
					
						
							|  |  |  |  |               icon={<ArrowUpOutlined />} | 
					
						
							|  |  |  |  |               onClick={sendMessage} | 
					
						
							|  |  |  |  |               disabled={!inputValue.trim() || loading} | 
					
						
							|  |  |  |  |             > | 
					
						
							|  |  |  |  |             </Button> | 
					
						
							|  |  |  |  |           </div> | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         </div> | 
					
						
							|  |  |  |  |         <div className='ds-input-hint'> | 
					
						
							|  |  |  |  |           Enter 发送 · Shift+Enter 换行 | 
					
						
							|  |  |  |  |           <span className='sep'>|</span> | 
					
						
							|  |  |  |  |           <a onClick={clearCurrentChat}>清空当前对话</a> | 
					
						
							|  |  |  |  |           <Select | 
					
						
							|  |  |  |  |             className='ds-select' | 
					
						
							|  |  |  |  |             dropdownClassName="ds-select-dropdown" | 
					
						
							|  |  |  |  |             defaultValue="模型反馈" | 
					
						
							|  |  |  |  |             style={{ | 
					
						
							|  |  |  |  |               minWidth: 120, maxWidth: 200, marginLeft: 'auto', marginRight: 8 | 
					
						
							|  |  |  |  |             }} | 
					
						
							|  |  |  |  |             onchange={handleChange} | 
					
						
							|  |  |  |  |             options={[ | 
					
						
							|  |  |  |  |               { value: "正面反馈", label: "正面反馈" }, | 
					
						
							|  |  |  |  |               { value: "负面反馈", label: "负面反馈" }, | 
					
						
							|  |  |  |  |             ]}> | 
					
						
							|  |  |  |  |           </Select> | 
					
						
							|  |  |  |  |         </div> | 
					
						
							|  |  |  |  |       </div> | 
					
						
							|  |  |  |  |     </div> | 
					
						
							|  |  |  |  |   ) | 
					
						
							|  |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | export default ChatConversation |