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 [streamingThought, setStreamingThought] = useState('') const [streamingMessage, setStreamingMessage] = useState('') const [isStreaming, setIsStreaming] = useState(false) const [displayedThought, setDisplayedThought] = useState('') const [displayedMessage, setDisplayedMessage] = useState('') // 流式显示控制 const [isTyping, setIsTyping] = useState(false) const messagesEndRef = useRef(null) const inputRef = useRef(null) const typewriterTimeoutRef = useRef(null) // 获取当前会话 const currentConversation = conversations.find(conv => conv.id === currentConversationId) const currentMessages = currentConversation?.messages || [] // 自动滚动到底部 const chatScrollRef = useRef(null); useEffect(() => { if (chatScrollRef.current) { chatScrollRef.current.scrollTop = chatScrollRef.current.scrollHeight; } }, [currentMessages, loading]); // currentMessages 是你的消息数组 const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }) } // 打字机效果函数 const typewriterEffect = (targetText, setDisplayedText, delay = 20) => { let currentIndex = 0 const currentDisplayed = displayedThought + displayedMessage const typeNextChar = () => { if (currentIndex < targetText.length) { setDisplayedText(targetText.substring(0, currentIndex + 1)) currentIndex++ typewriterTimeoutRef.current = setTimeout(typeNextChar, delay) } } typeNextChar() } // 监听流式内容变化,实现真正的流式显示 useEffect(() => { if (streamingThought && streamingThought !== displayedThought) { // 直接显示新内容,实现真正的流式显示 setDisplayedThought(streamingThought) setIsTyping(true) } }, [streamingThought]) useEffect(() => { if (streamingMessage && streamingMessage !== displayedMessage) { // 直接显示新内容,实现真正的流式显示 setDisplayedMessage(streamingMessage) setIsTyping(true) } }, [streamingMessage]) // 当流式输出停止时,停止打字状态 useEffect(() => { if (!isStreaming) { setIsTyping(false) } }, [isStreaming]) 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) } } }, []) useEffect(() => { scrollToBottom() }, [currentMessages]) // 创建新对话 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) setIsStreaming(true) setStreamingThought('') setStreamingMessage('') setDisplayedThought('') setDisplayedMessage('') try { // 调用本地后端接口(流式版本) const result = await callLocalChatAPI(messageText, (streamData) => { console.log('Stream data received:', streamData) if (streamData.type === 'thought') { console.log('Setting streamingThought:', streamData.content) // 使用totalContent作为完整内容,content作为新增内容 setStreamingThought(streamData.totalContent || streamData.content) } else if (streamData.type === 'message') { console.log('Setting streamingMessage:', streamData.content) // 使用totalContent作为完整内容,content作为新增内容 setStreamingMessage(streamData.totalContent || streamData.content) } else if (streamData.type === 'complete') { // 流式输出完成,将最终内容添加到对话存储 // 使用complete事件中传递的最终内容 const finalThought = streamData.thoughtContent || streamingThought const finalMessage = streamData.messageContent || streamingMessage const finalContent = finalThought + (finalMessage ? '\n\n' + finalMessage : '') console.log('Complete event - finalThought:', finalThought) console.log('Complete event - finalMessage:', finalMessage) console.log('Complete event - finalContent:', finalContent) if (finalContent.trim()) { conversationStore.addMessage('assistant', finalContent) } else { conversationStore.addMessage('assistant', '抱歉,我无法生成回复内容。') } setIsStreaming(false) setStreamingThought('') setStreamingMessage('') setDisplayedThought('') setDisplayedMessage('') } }) // 如果API没有返回流式数据,直接处理结果 if (result && !isStreaming) { const thought = result?.thought || '' const message = result?.message || '' const finalContent = thought + (message ? '\n\n' + message : '') if (finalContent.trim()) { conversationStore.addMessage('assistant', finalContent) } else { conversationStore.addMessage('assistant', '抱歉,我无法生成回复内容。') } } } catch (error) { const errorMessage = handleAPIError(error) message.error(errorMessage) // 添加错误消息到独立存储 conversationStore.addMessage('assistant', `抱歉,我遇到了一个问题:${errorMessage}`) setIsStreaming(false) setStreamingThought('') setStreamingMessage('') setDisplayedThought('') setDisplayedMessage('') } 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' return (
提出一个问题,比如:请帮我总结一段文本。