|
|
|
|
@ -43,75 +43,36 @@ function ChatConversation() {
|
|
|
|
|
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 [deepThinkingEnabled, setDeepThinkingEnabled] = useState(false)
|
|
|
|
|
const [typingMessage, setTypingMessage] = 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 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);
|
|
|
|
|
// 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 => {
|
|
|
|
|
@ -138,9 +99,34 @@ function ChatConversation() {
|
|
|
|
|
}
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
// useEffect(() => {
|
|
|
|
|
// scrollToBottom()
|
|
|
|
|
// }, [currentMessages])
|
|
|
|
|
// 打字机效果函数 - 简化版本用于测试
|
|
|
|
|
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 createNewConversation = () => {
|
|
|
|
|
@ -195,60 +181,31 @@ function ChatConversation() {
|
|
|
|
|
|
|
|
|
|
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', '抱歉,我无法生成回复内容。')
|
|
|
|
|
}
|
|
|
|
|
// 调用本地后端接口(非流式版本)
|
|
|
|
|
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()) {
|
|
|
|
|
// 暂时直接添加消息,不使用打字机效果
|
|
|
|
|
console.log('Adding message directly:', finalContent)
|
|
|
|
|
conversationStore.addMessage('assistant', finalContent)
|
|
|
|
|
} else {
|
|
|
|
|
console.log('No content, adding fallback message')
|
|
|
|
|
conversationStore.addMessage('assistant', '抱歉,我无法生成回复内容。')
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
const errorMessage = handleAPIError(error)
|
|
|
|
|
@ -256,11 +213,6 @@ function ChatConversation() {
|
|
|
|
|
|
|
|
|
|
// 添加错误消息到独立存储
|
|
|
|
|
conversationStore.addMessage('assistant', `抱歉,我遇到了一个问题:${errorMessage}`)
|
|
|
|
|
setIsStreaming(false)
|
|
|
|
|
setStreamingThought('')
|
|
|
|
|
setStreamingMessage('')
|
|
|
|
|
setDisplayedThought('')
|
|
|
|
|
setDisplayedMessage('')
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false)
|
|
|
|
|
}
|
|
|
|
|
@ -318,6 +270,17 @@ function ChatConversation() {
|
|
|
|
|
// 渲染单条消息 ==================================
|
|
|
|
|
const renderMessage = (msg, index) => {
|
|
|
|
|
const isUser = msg.role === 'user'
|
|
|
|
|
const displayContent = msg.content || ''
|
|
|
|
|
|
|
|
|
|
// 调试信息
|
|
|
|
|
if (msg.role === 'assistant') {
|
|
|
|
|
console.log('Rendering assistant message:', {
|
|
|
|
|
index,
|
|
|
|
|
content: msg.content,
|
|
|
|
|
displayContent
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
key={index}
|
|
|
|
|
@ -325,7 +288,7 @@ function ChatConversation() {
|
|
|
|
|
>
|
|
|
|
|
{!isUser && (
|
|
|
|
|
<div className='ds-avatar'>
|
|
|
|
|
<RobotOutlined />
|
|
|
|
|
<RobotOutlined />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{isUser && <div className='ds-avatar user'><LinuxOutlined /></div>}
|
|
|
|
|
@ -337,12 +300,13 @@ function ChatConversation() {
|
|
|
|
|
{msg.timestamp
|
|
|
|
|
? (typeof msg.timestamp === 'string'
|
|
|
|
|
? msg.timestamp
|
|
|
|
|
: msg.timestamp.toLocaleString())
|
|
|
|
|
: msg.timestamp.toLocaleString()) // 时间
|
|
|
|
|
: ''}
|
|
|
|
|
</span>
|
|
|
|
|
{!isUser && (
|
|
|
|
|
<span className='actions'>
|
|
|
|
|
<Tooltip title='复制'>
|
|
|
|
|
<span className='actions'>
|
|
|
|
|
{/* 复制按钮 */}
|
|
|
|
|
<Tooltip title='复制'>
|
|
|
|
|
<Button
|
|
|
|
|
size='small'
|
|
|
|
|
type='text'
|
|
|
|
|
@ -358,7 +322,8 @@ function ChatConversation() {
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
<Tooltip title='重新生成(示例)'>
|
|
|
|
|
{/* 重新生成按钮 */}
|
|
|
|
|
<Tooltip title='重新生成(示例)'>
|
|
|
|
|
<Button
|
|
|
|
|
size='small'
|
|
|
|
|
type='text'
|
|
|
|
|
@ -380,69 +345,15 @@ function ChatConversation() {
|
|
|
|
|
|
|
|
|
|
{/* 回答框 */}
|
|
|
|
|
<div className='ds-message-content'>
|
|
|
|
|
<ReactMarkdown
|
|
|
|
|
remarkPlugins={[remarkGfm]}
|
|
|
|
|
>
|
|
|
|
|
{msg.content || ''}
|
|
|
|
|
</ReactMarkdown>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 渲染流式输出消息
|
|
|
|
|
const renderStreamingMessage = () => {
|
|
|
|
|
if (!isStreaming && !displayedThought && !displayedMessage) return null
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className='ds-message-row from-assistant streaming'>
|
|
|
|
|
<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'>
|
|
|
|
|
{/* 思考内容 - 浅灰色 */}
|
|
|
|
|
{displayedThought && (
|
|
|
|
|
<div className='ds-thought-content'>
|
|
|
|
|
<ReactMarkdown
|
|
|
|
|
remarkPlugins={[remarkGfm]}
|
|
|
|
|
>
|
|
|
|
|
{displayedThought}
|
|
|
|
|
</ReactMarkdown>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* 回复内容 - 正常颜色 */}
|
|
|
|
|
{displayedMessage && (
|
|
|
|
|
<div className='ds-response-content'>
|
|
|
|
|
<ReactMarkdown
|
|
|
|
|
remarkPlugins={[remarkGfm]}
|
|
|
|
|
>
|
|
|
|
|
{displayedMessage}
|
|
|
|
|
</ReactMarkdown>
|
|
|
|
|
{/* 流式输入光标 */}
|
|
|
|
|
{isTyping && isStreaming && (
|
|
|
|
|
<span className='ds-typing-cursor'>|</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* 流式输出指示器 */}
|
|
|
|
|
{isStreaming && (
|
|
|
|
|
<div className='ds-streaming-indicator'>
|
|
|
|
|
<Spin
|
|
|
|
|
indicator={<LoadingOutlined style={{ fontSize: 14 }} spin />}
|
|
|
|
|
size='small'
|
|
|
|
|
/>
|
|
|
|
|
<span className='streaming-text'>
|
|
|
|
|
{isTyping ? '正在输入...' : '正在思考...'}
|
|
|
|
|
</span>
|
|
|
|
|
{displayContent ? (
|
|
|
|
|
<ReactMarkdown
|
|
|
|
|
remarkPlugins={[remarkGfm]}
|
|
|
|
|
>
|
|
|
|
|
{displayContent}
|
|
|
|
|
</ReactMarkdown>
|
|
|
|
|
) : (
|
|
|
|
|
<div style={{ color: '#999', fontStyle: 'italic' }}>
|
|
|
|
|
暂无内容
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
@ -450,6 +361,7 @@ function ChatConversation() {
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// =================================================
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
@ -467,11 +379,8 @@ function ChatConversation() {
|
|
|
|
|
{/* 历史消息 */}
|
|
|
|
|
{currentMessages.map(renderMessage)}
|
|
|
|
|
|
|
|
|
|
{/* 流式输出消息 */}
|
|
|
|
|
{renderStreamingMessage()}
|
|
|
|
|
|
|
|
|
|
{/* 加载状态(当没有流式输出时显示) */}
|
|
|
|
|
{loading && !isStreaming && (
|
|
|
|
|
{/* 加载状态 */}
|
|
|
|
|
{loading && (
|
|
|
|
|
<div className='ds-message-row from-assistant thinking'>
|
|
|
|
|
<div className='ds-avatar'>
|
|
|
|
|
<RobotOutlined />
|
|
|
|
|
@ -519,20 +428,16 @@ function ChatConversation() {
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 模型切换 */}
|
|
|
|
|
{/* <Select
|
|
|
|
|
defaultValue="智能定性"
|
|
|
|
|
{/* 深度思考按钮 */}
|
|
|
|
|
<Button
|
|
|
|
|
type={deepThinkingEnabled ? 'primary' : 'default'}
|
|
|
|
|
style={{
|
|
|
|
|
minWidth: 120, maxWidth: 200, marginRight: 8
|
|
|
|
|
minWidth: 100, maxWidth: 120, marginRight: 8
|
|
|
|
|
}}
|
|
|
|
|
onchange={handleChange}
|
|
|
|
|
options={[
|
|
|
|
|
{ value: "智能定性", label: "智能定性" },
|
|
|
|
|
// { value: "GPT-5 mini", label: "GPT-5 mini" },
|
|
|
|
|
// { value: "Claude Sonnet 4", label: "Claude Sonnet 4" },
|
|
|
|
|
// { value: "Kimi Senior", label: "Kimi Senior" },
|
|
|
|
|
]}>
|
|
|
|
|
</Select> */}
|
|
|
|
|
onClick={() => setDeepThinkingEnabled(!deepThinkingEnabled)}
|
|
|
|
|
>
|
|
|
|
|
深度思考
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
{/* 上传文件 */}
|
|
|
|
|
<Upload className='ds-uploading'>
|
|
|
|
|
@ -561,9 +466,9 @@ function ChatConversation() {
|
|
|
|
|
<Select
|
|
|
|
|
className='ds-select'
|
|
|
|
|
dropdownClassName="ds-select-dropdown"
|
|
|
|
|
defaultValue="模型反馈"
|
|
|
|
|
defaultValue="模型反馈"
|
|
|
|
|
style={{
|
|
|
|
|
minWidth: 120, maxWidth: 200, marginLeft: 'auto',marginRight: 8
|
|
|
|
|
minWidth: 120, maxWidth: 200, marginLeft: 'auto', marginRight: 8
|
|
|
|
|
}}
|
|
|
|
|
onchange={handleChange}
|
|
|
|
|
options={[
|
|
|
|
|
|