|
@@ -196,7 +196,7 @@
|
|
|
<!-- 完整的AI回复内容 -->
|
|
<!-- 完整的AI回复内容 -->
|
|
|
<div class="ai-response-content">
|
|
<div class="ai-response-content">
|
|
|
<!-- 状态统计卡片 - 优先显示(不等待后端数据) -->
|
|
<!-- 状态统计卡片 - 优先显示(不等待后端数据) -->
|
|
|
- <div v-if="message.showStats"
|
|
|
|
|
|
|
+ <div v-if="shouldShowMessageStatsCard(message)"
|
|
|
class="files-stats-white"
|
|
class="files-stats-white"
|
|
|
:class="{ 'sticky': messageScrollStates[index]?.isSticky && messageScrollStates[index]?.initialized && !messageScrollStates[index]?.isInitializing }"
|
|
:class="{ 'sticky': messageScrollStates[index]?.isSticky && messageScrollStates[index]?.initialized && !messageScrollStates[index]?.isInitializing }"
|
|
|
:style="(messageScrollStates[index]?.isSticky && messageScrollStates[index]?.initialized && !messageScrollStates[index]?.isInitializing && messageScrollStates[index]?.initialLeft > 0 && messageScrollStates[index]?.initialWidth > 0) ? {
|
|
:style="(messageScrollStates[index]?.isSticky && messageScrollStates[index]?.initialized && !messageScrollStates[index]?.isInitializing && messageScrollStates[index]?.initialLeft > 0 && messageScrollStates[index]?.initialWidth > 0) ? {
|
|
@@ -207,7 +207,7 @@
|
|
|
zIndex: 999
|
|
zIndex: 999
|
|
|
} : {}"
|
|
} : {}"
|
|
|
:data-message-index="index">
|
|
:data-message-index="index">
|
|
|
- <div class="stats-left">
|
|
|
|
|
|
|
+ <div v-if="shouldShowProgressStatus(message)" class="stats-left">
|
|
|
<StatusAvatar
|
|
<StatusAvatar
|
|
|
:status="getAvatarStatus(message.currentStatus, message.progress)"
|
|
:status="getAvatarStatus(message.currentStatus, message.progress)"
|
|
|
:size="36"
|
|
:size="36"
|
|
@@ -217,7 +217,7 @@
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<!-- 进度条胶囊 - 在白色卡片内 -->
|
|
<!-- 进度条胶囊 - 在白色卡片内 -->
|
|
|
- <div v-if="message.progress < 100" class="progress-capsule-inline">
|
|
|
|
|
|
|
+ <div v-if="shouldShowMessageProgress(message)" class="progress-capsule-inline">
|
|
|
<div class="progress-bar-mini">
|
|
<div class="progress-bar-mini">
|
|
|
<div class="progress-fill" :style="{ width: message.progress + '%' }"></div>
|
|
<div class="progress-fill" :style="{ width: message.progress + '%' }"></div>
|
|
|
<div class="progress-dot" :style="{ left: message.progress + '%' }"></div>
|
|
<div class="progress-dot" :style="{ left: message.progress + '%' }"></div>
|
|
@@ -508,7 +508,6 @@
|
|
|
v-model="messageText"
|
|
v-model="messageText"
|
|
|
@keydown.enter="handleMessageInputEnterKey"
|
|
@keydown.enter="handleMessageInputEnterKey"
|
|
|
@input="handleInput"
|
|
@input="handleInput"
|
|
|
- :disabled="isSending || hasTypingMessage"
|
|
|
|
|
maxlength="2000"
|
|
maxlength="2000"
|
|
|
rows="3"
|
|
rows="3"
|
|
|
></textarea>
|
|
></textarea>
|
|
@@ -752,6 +751,20 @@ import {
|
|
|
buildDocumentGenerationUserMessage,
|
|
buildDocumentGenerationUserMessage,
|
|
|
shouldAttachDocumentToRequest
|
|
shouldAttachDocumentToRequest
|
|
|
} from '@/utils/aiWritingRequest.js'
|
|
} from '@/utils/aiWritingRequest.js'
|
|
|
|
|
+import {
|
|
|
|
|
+ clearMessageOutputRenders,
|
|
|
|
|
+ getOutputtingProgress,
|
|
|
|
|
+ hideMessageProgressStatus,
|
|
|
|
|
+ markMessageProgressComplete,
|
|
|
|
|
+ shouldApplyMessageProgressStatus,
|
|
|
|
|
+ shouldFinalizeMessageOnSSEComplete,
|
|
|
|
|
+ shouldHideStatsForStreamingAnswer,
|
|
|
|
|
+ shouldShowMessageStatsCard,
|
|
|
|
|
+ shouldShowProgressStatus,
|
|
|
|
|
+ shouldShowMessageProgress,
|
|
|
|
|
+ trackMessageOutputRender,
|
|
|
|
|
+ waitForMessageOutputRenders
|
|
|
|
|
+} from '@/utils/aiProgressState.js'
|
|
|
import { buildUploadedDocumentPayload } from '@/utils/attachmentContext.js'
|
|
import { buildUploadedDocumentPayload } from '@/utils/attachmentContext.js'
|
|
|
import { getAttachmentCardIcon } from '@/utils/attachmentFile.js'
|
|
import { getAttachmentCardIcon } from '@/utils/attachmentFile.js'
|
|
|
import { prepareAIWritingEditorHtml } from '@/utils/aiWritingContent.js'
|
|
import { prepareAIWritingEditorHtml } from '@/utils/aiWritingContent.js'
|
|
@@ -1600,6 +1613,32 @@ const clearAllTypeIntervals = () => {
|
|
|
clearInterval(interval)
|
|
clearInterval(interval)
|
|
|
})
|
|
})
|
|
|
reportTypewriters.clear()
|
|
reportTypewriters.clear()
|
|
|
|
|
+ chatMessages.value.forEach(message => clearMessageOutputRenders(message))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const waitForProgressCompletionFrame = () => new Promise(resolve => setTimeout(resolve, 180))
|
|
|
|
|
+
|
|
|
|
|
+const completeAIMessageAfterRendered = async (message) => {
|
|
|
|
|
+ if (!message || message._stopped) return
|
|
|
|
|
+
|
|
|
|
|
+ if (message.showStats && Number(message.progress || 0) < 100) {
|
|
|
|
|
+ updateMessageStatus(message, 'completed')
|
|
|
|
|
+ }
|
|
|
|
|
+ markMessageProgressComplete(message)
|
|
|
|
|
+
|
|
|
|
|
+ await waitForMessageOutputRenders(message)
|
|
|
|
|
+
|
|
|
|
|
+ if (message._stopped) return
|
|
|
|
|
+
|
|
|
|
|
+ if (shouldShowProgressStatus(message)) {
|
|
|
|
|
+ message.isTyping = true
|
|
|
|
|
+ await waitForProgressCompletionFrame()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (message._stopped) return
|
|
|
|
|
+
|
|
|
|
|
+ hideMessageProgressStatus(message)
|
|
|
|
|
+ clearMessageOutputRenders(message)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 处理文件标签格式的回显
|
|
// 处理文件标签格式的回显
|
|
@@ -2777,6 +2816,10 @@ const getAvatarStatus = (currentStatus, progress) => {
|
|
|
|
|
|
|
|
// 更新AI消息状态和进度
|
|
// 更新AI消息状态和进度
|
|
|
const updateMessageStatus = (aiMessage, status, customMessage = null) => {
|
|
const updateMessageStatus = (aiMessage, status, customMessage = null) => {
|
|
|
|
|
+ if (!shouldApplyMessageProgressStatus(aiMessage, status)) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
const statusConfig = {
|
|
const statusConfig = {
|
|
|
querying_kb: {
|
|
querying_kb: {
|
|
|
message: '🔍 <span class="ai-name">蜀道安全管理AI智能助手</span>正在为您分析知识库……',
|
|
message: '🔍 <span class="ai-name">蜀道安全管理AI智能助手</span>正在为您分析知识库……',
|
|
@@ -2840,7 +2883,9 @@ const updateMessageStatus = (aiMessage, status, customMessage = null) => {
|
|
|
aiMessage.statusMessage = customMessage || config.message
|
|
aiMessage.statusMessage = customMessage || config.message
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- aiMessage.progress = config.progress
|
|
|
|
|
|
|
+ aiMessage.progress = status === 'outputting'
|
|
|
|
|
+ ? getOutputtingProgress(aiMessage.progress)
|
|
|
|
|
+ : config.progress
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -2913,8 +2958,10 @@ const handleSSEMessage = (data, aiMessageIndex) => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if ((data.message || '').includes('基础回答模式')) {
|
|
if ((data.message || '').includes('基础回答模式')) {
|
|
|
- aiMessage.currentStatus = 'outputting'
|
|
|
|
|
- aiMessage.progress = Math.max(aiMessage.progress || 0, 90)
|
|
|
|
|
|
|
+ if (shouldApplyMessageProgressStatus(aiMessage, 'outputting')) {
|
|
|
|
|
+ aiMessage.currentStatus = 'outputting'
|
|
|
|
|
+ aiMessage.progress = getOutputtingProgress(aiMessage.progress)
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
break
|
|
break
|
|
|
|
|
|
|
@@ -2949,12 +2996,12 @@ const handleSSEMessage = (data, aiMessageIndex) => {
|
|
|
aiMessage.summary = ''
|
|
aiMessage.summary = ''
|
|
|
|
|
|
|
|
// 使用打字机效果显示问题总结
|
|
// 使用打字机效果显示问题总结
|
|
|
- startReportFieldTypewriter(
|
|
|
|
|
|
|
+ trackMessageOutputRender(aiMessage, startReportFieldTypewriter(
|
|
|
{ file_index: 'summary', report: aiMessage, _typewriterStates: {} },
|
|
{ file_index: 'summary', report: aiMessage, _typewriterStates: {} },
|
|
|
'summary',
|
|
'summary',
|
|
|
fullSummary,
|
|
fullSummary,
|
|
|
50
|
|
50
|
|
|
- ).catch(err => {
|
|
|
|
|
|
|
+ )).catch(err => {
|
|
|
console.error('问题总结打字机效果失败:', err)
|
|
console.error('问题总结打字机效果失败:', err)
|
|
|
aiMessage.summary = fullSummary
|
|
aiMessage.summary = fullSummary
|
|
|
})
|
|
})
|
|
@@ -2962,7 +3009,11 @@ const handleSSEMessage = (data, aiMessageIndex) => {
|
|
|
break
|
|
break
|
|
|
|
|
|
|
|
case 'online_answer': {
|
|
case 'online_answer': {
|
|
|
- aiMessage.showStats = false
|
|
|
|
|
|
|
+ if (shouldHideStatsForStreamingAnswer(aiMessage)) {
|
|
|
|
|
+ aiMessage.showStats = false
|
|
|
|
|
+ } else {
|
|
|
|
|
+ updateMessageStatus(aiMessage, 'outputting')
|
|
|
|
|
+ }
|
|
|
if (shouldClearSummaryForOnlineAnswer(aiMessage)) {
|
|
if (shouldClearSummaryForOnlineAnswer(aiMessage)) {
|
|
|
aiMessage.summary = ''
|
|
aiMessage.summary = ''
|
|
|
aiMessage._fullSummary = ''
|
|
aiMessage._fullSummary = ''
|
|
@@ -2981,7 +3032,7 @@ const handleSSEMessage = (data, aiMessageIndex) => {
|
|
|
const processedReply = processAIResponse(finalContent)
|
|
const processedReply = processAIResponse(finalContent)
|
|
|
const renderedReply = renderMarkdownContent(processedReply)
|
|
const renderedReply = renderMarkdownContent(processedReply)
|
|
|
|
|
|
|
|
- startTypewriterEffect(aiMessage, renderedReply, 200)
|
|
|
|
|
|
|
+ trackMessageOutputRender(aiMessage, startTypewriterEffect(aiMessage, renderedReply, 200))
|
|
|
.catch(err => {
|
|
.catch(err => {
|
|
|
console.error('在线回答打字机效果失败:', err)
|
|
console.error('在线回答打字机效果失败:', err)
|
|
|
aiMessage.displayContent = renderedReply
|
|
aiMessage.displayContent = renderedReply
|
|
@@ -3178,7 +3229,7 @@ const handleSSEMessage = (data, aiMessageIndex) => {
|
|
|
targetReport._typewriterStarted = true
|
|
targetReport._typewriterStarted = true
|
|
|
|
|
|
|
|
// 先打概述(速度200 = 每次20个字符,极快)
|
|
// 先打概述(速度200 = 每次20个字符,极快)
|
|
|
- startReportFieldTypewriter(targetReport, 'summary', targetReport._fullContent.summary || '', 200)
|
|
|
|
|
|
|
+ const reportRenderPromise = startReportFieldTypewriter(targetReport, 'summary', targetReport._fullContent.summary || '', 200)
|
|
|
.then(() => {
|
|
.then(() => {
|
|
|
// 概述完成后打解析
|
|
// 概述完成后打解析
|
|
|
return startReportFieldTypewriter(targetReport, 'analysis', targetReport._fullContent.analysis || '', 200)
|
|
return startReportFieldTypewriter(targetReport, 'analysis', targetReport._fullContent.analysis || '', 200)
|
|
@@ -3193,6 +3244,7 @@ const handleSSEMessage = (data, aiMessageIndex) => {
|
|
|
// 全部完成,标记为已完成
|
|
// 全部完成,标记为已完成
|
|
|
targetReport._typewriterCompleted = true
|
|
targetReport._typewriterCompleted = true
|
|
|
})
|
|
})
|
|
|
|
|
+ trackMessageOutputRender(aiMessage, reportRenderPromise)
|
|
|
.catch(err => {
|
|
.catch(err => {
|
|
|
console.error('报告打字机效果失败:', err)
|
|
console.error('报告打字机效果失败:', err)
|
|
|
// 失败时直接显示完整内容
|
|
// 失败时直接显示完整内容
|
|
@@ -3272,7 +3324,7 @@ const handleSSEMessage = (data, aiMessageIndex) => {
|
|
|
_typewriterStates: {}
|
|
_typewriterStates: {}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- startReportFieldTypewriter(summaryReport, 'webSearchSummary', data.summary, 200)
|
|
|
|
|
|
|
+ trackMessageOutputRender(aiMessage, startReportFieldTypewriter(summaryReport, 'webSearchSummary', data.summary, 200))
|
|
|
.then(() => {
|
|
.then(() => {
|
|
|
// 标记为已完成,防止重复触发
|
|
// 标记为已完成,防止重复触发
|
|
|
aiMessage._webSearchSummaryCompleted = true
|
|
aiMessage._webSearchSummaryCompleted = true
|
|
@@ -3309,26 +3361,30 @@ const handleSSEMessage = (data, aiMessageIndex) => {
|
|
|
.then((response) => {
|
|
.then((response) => {
|
|
|
console.log('[网络搜索] AI消息保存成功,更新为完成状态')
|
|
console.log('[网络搜索] AI消息保存成功,更新为完成状态')
|
|
|
// 保存成功后,更新状态为completed
|
|
// 保存成功后,更新状态为completed
|
|
|
- updateMessageStatus(aiMessage, 'completed')
|
|
|
|
|
- aiMessage.isTyping = false
|
|
|
|
|
- isSending.value = false
|
|
|
|
|
- streamingReports.value.clear()
|
|
|
|
|
|
|
+ completeAIMessageAfterRendered(aiMessage).then(() => {
|
|
|
|
|
+ isSending.value = false
|
|
|
|
|
+ streamingReports.value.clear()
|
|
|
|
|
+ isAIReplyProcessComplete.value = true
|
|
|
|
|
+ })
|
|
|
|
|
|
|
|
// 重置AI回复流程状态
|
|
// 重置AI回复流程状态
|
|
|
- isAIReplyProcessComplete.value = true
|
|
|
|
|
})
|
|
})
|
|
|
.catch(err => {
|
|
.catch(err => {
|
|
|
console.error('[网络搜索] AI消息保存失败:', err)
|
|
console.error('[网络搜索] AI消息保存失败:', err)
|
|
|
// 即使保存失败,也更新为完成状态
|
|
// 即使保存失败,也更新为完成状态
|
|
|
- updateMessageStatus(aiMessage, 'completed')
|
|
|
|
|
- aiMessage.isTyping = false
|
|
|
|
|
- isSending.value = false
|
|
|
|
|
|
|
+ completeAIMessageAfterRendered(aiMessage).then(() => {
|
|
|
|
|
+ isSending.value = false
|
|
|
|
|
+ streamingReports.value.clear()
|
|
|
|
|
+ isAIReplyProcessComplete.value = true
|
|
|
|
|
+ })
|
|
|
})
|
|
})
|
|
|
} else {
|
|
} else {
|
|
|
// 没有ai_message_id时,直接更新为完成状态
|
|
// 没有ai_message_id时,直接更新为完成状态
|
|
|
- updateMessageStatus(aiMessage, 'completed')
|
|
|
|
|
- aiMessage.isTyping = false
|
|
|
|
|
- isSending.value = false
|
|
|
|
|
|
|
+ completeAIMessageAfterRendered(aiMessage).then(() => {
|
|
|
|
|
+ isSending.value = false
|
|
|
|
|
+ streamingReports.value.clear()
|
|
|
|
|
+ isAIReplyProcessComplete.value = true
|
|
|
|
|
+ })
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
break
|
|
break
|
|
@@ -3340,21 +3396,19 @@ const handleSSEMessage = (data, aiMessageIndex) => {
|
|
|
|
|
|
|
|
case 'completed':
|
|
case 'completed':
|
|
|
console.log('[SSE] 收到completed事件')
|
|
console.log('[SSE] 收到completed事件')
|
|
|
- isSending.value = false
|
|
|
|
|
- streamingReports.value.clear()
|
|
|
|
|
- aiMessage.isTyping = false
|
|
|
|
|
-
|
|
|
|
|
- // 只有在还未完成时才更新状态
|
|
|
|
|
- if (aiMessage.progress < 100) {
|
|
|
|
|
- updateMessageStatus(aiMessage, 'completed')
|
|
|
|
|
- }
|
|
|
|
|
- const completedPayload = buildAIMessageUpdatePayload(aiMessage)
|
|
|
|
|
- if (completedPayload) {
|
|
|
|
|
- updateAIMessageContent(completedPayload.aiMessageId, completedPayload.content)
|
|
|
|
|
- .catch(err => console.error('completed事件回写AI消息失败:', err))
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- ElMessage.success('报告生成完成')
|
|
|
|
|
|
|
+ completeAIMessageAfterRendered(aiMessage).then(() => {
|
|
|
|
|
+ isSending.value = false
|
|
|
|
|
+ streamingReports.value.clear()
|
|
|
|
|
+ isAIReplyProcessComplete.value = true
|
|
|
|
|
+
|
|
|
|
|
+ const completedPayload = buildAIMessageUpdatePayload(aiMessage)
|
|
|
|
|
+ if (completedPayload) {
|
|
|
|
|
+ updateAIMessageContent(completedPayload.aiMessageId, completedPayload.content)
|
|
|
|
|
+ .catch(err => console.error('completed事件回写AI消息失败:', err))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ ElMessage.success('报告生成完成')
|
|
|
|
|
+ })
|
|
|
break
|
|
break
|
|
|
|
|
|
|
|
case 'interrupted':
|
|
case 'interrupted':
|
|
@@ -3392,8 +3446,10 @@ const handleSSEError = (error) => {
|
|
|
ElMessage.error('连接已断开')
|
|
ElMessage.error('连接已断开')
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-const handleSSEComplete = () => {
|
|
|
|
|
|
|
+const handleSSEComplete = async () => {
|
|
|
isSending.value = false
|
|
isSending.value = false
|
|
|
|
|
+ const renderingMessages = chatMessages.value.filter(message => shouldFinalizeMessageOnSSEComplete(message))
|
|
|
|
|
+ await Promise.all(renderingMessages.map(message => completeAIMessageAfterRendered(message)))
|
|
|
|
|
|
|
|
chatMessages.value.forEach(message => {
|
|
chatMessages.value.forEach(message => {
|
|
|
if (message.type === 'ai' && message.isTyping) {
|
|
if (message.type === 'ai' && message.isTyping) {
|