|
@@ -139,10 +139,10 @@
|
|
|
<div class="ai-response-content">
|
|
<div class="ai-response-content">
|
|
|
<!-- 进度统计卡片 -->
|
|
<!-- 进度统计卡片 -->
|
|
|
<div
|
|
<div
|
|
|
- v-if="shouldShowProgressStatus(message)"
|
|
|
|
|
|
|
+ v-if="shouldShowDatabaseFiles(message) && shouldShowProgressStatus(message)"
|
|
|
class="stats-card"
|
|
class="stats-card"
|
|
|
>
|
|
>
|
|
|
- <div v-if="shouldShowProgressStatus(message)" class="stats-left">
|
|
|
|
|
|
|
+ <div v-if="shouldShowDatabaseFiles(message) && shouldShowProgressStatus(message)" class="stats-left">
|
|
|
<StatusAvatar
|
|
<StatusAvatar
|
|
|
:status="getAvatarStatus(message.currentStatus, message.progress)"
|
|
:status="getAvatarStatus(message.currentStatus, message.progress)"
|
|
|
:size="28"
|
|
:size="28"
|
|
@@ -196,8 +196,12 @@
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<!-- 报告列表 -->
|
|
<!-- 报告列表 -->
|
|
|
- <div v-if="message.reports && message.reports.length > 0" class="reports-list">
|
|
|
|
|
- <template v-for="(report, rIndex) in dedupeReportsByFileAndScene(message.reports)" :key="`${report.source_file}-${report.file_index}-${rIndex}`">
|
|
|
|
|
|
|
+ <div v-if="shouldShowDatabaseFiles(message)" class="reports-list">
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="(report, rIndex) in getVisibleDatabaseReports(message)"
|
|
|
|
|
+ :key="`${report.source_file}-${report.file_index}-${rIndex}`"
|
|
|
|
|
+ class="database-report-reveal-item"
|
|
|
|
|
+ >
|
|
|
<!-- 类别标题 -->
|
|
<!-- 类别标题 -->
|
|
|
<CategoryTitle
|
|
<CategoryTitle
|
|
|
v-if="report.type === 'category_title'"
|
|
v-if="report.type === 'category_title'"
|
|
@@ -211,7 +215,7 @@
|
|
|
:report="report"
|
|
:report="report"
|
|
|
@preview-file="handleFilePreview"
|
|
@preview-file="handleFilePreview"
|
|
|
/>
|
|
/>
|
|
|
- </template>
|
|
|
|
|
|
|
+ </div>
|
|
|
|
|
|
|
|
<!-- 分类下的Loading动画 -->
|
|
<!-- 分类下的Loading动画 -->
|
|
|
<div v-if="message.isTyping && hasOnlyCategoryTitles(message.reports)" class="report-loading">
|
|
<div v-if="message.isTyping && hasOnlyCategoryTitles(message.reports)" class="report-loading">
|
|
@@ -225,7 +229,7 @@
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<!-- 查询结果总结 -->
|
|
<!-- 查询结果总结 -->
|
|
|
- <div v-if="message.summary" class="question-summary result-summary-card">
|
|
|
|
|
|
|
+ <div v-if="shouldShowDatabaseFiles(message) && message._databaseReportsRevealComplete && message.summary" class="question-summary result-summary-card">
|
|
|
<StreamMarkdown :content="message.summary" :streaming="false" />
|
|
<StreamMarkdown :content="message.summary" :streaming="false" />
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
@@ -485,14 +489,21 @@ import { createSSEConnection, closeSSEConnection } from '@/utils/sse'
|
|
|
import { getApiPrefix, BACKEND_API_PREFIX } from '@/utils/apiConfig'
|
|
import { getApiPrefix, BACKEND_API_PREFIX } from '@/utils/apiConfig'
|
|
|
import { renderMarkdown } from '@/utils/markdown'
|
|
import { renderMarkdown } from '@/utils/markdown'
|
|
|
import { stopSSEStream, updateAIMessageContent } from '@/utils/api.js'
|
|
import { stopSSEStream, updateAIMessageContent } from '@/utils/api.js'
|
|
|
|
|
+import { buildPreviewUrl } from '@/utils/filePreviewUrl'
|
|
|
import {
|
|
import {
|
|
|
applyReportChunkToMessage,
|
|
applyReportChunkToMessage,
|
|
|
|
|
+ buildDisplayThinkingSummary,
|
|
|
buildAIMessageUpdatePayload,
|
|
buildAIMessageUpdatePayload,
|
|
|
dedupeReportsByFileAndScene,
|
|
dedupeReportsByFileAndScene,
|
|
|
extractRelatedQuestions,
|
|
extractRelatedQuestions,
|
|
|
|
|
+ hasRawThinkingLeak,
|
|
|
hydratePersistedReports,
|
|
hydratePersistedReports,
|
|
|
|
|
+ normalizeDisplayThinkingContent,
|
|
|
normalizeReportsForPersistence,
|
|
normalizeReportsForPersistence,
|
|
|
- shouldClearSummaryForOnlineAnswer
|
|
|
|
|
|
|
+ sanitizeThinkingContentForPersistence,
|
|
|
|
|
+ shouldClearSummaryForOnlineAnswer,
|
|
|
|
|
+ splitHtmlIntoTypewriterChunks,
|
|
|
|
|
+ THINKING_FALLBACK_TEXT
|
|
|
} from '@/utils/chatHistoryPersistence.js'
|
|
} from '@/utils/chatHistoryPersistence.js'
|
|
|
import {
|
|
import {
|
|
|
clearMessageOutputRenders,
|
|
clearMessageOutputRenders,
|
|
@@ -697,6 +708,12 @@ const currentQuestion = ref('') // 当前问题
|
|
|
const streamingReports = ref(new Map()) // 流式报告缓存
|
|
const streamingReports = ref(new Map()) // 流式报告缓存
|
|
|
const reportTypewriters = new Map() // 存储每个报告字段的打字机定时器
|
|
const reportTypewriters = new Map() // 存储每个报告字段的打字机定时器
|
|
|
const typewriterIntervals = new Map() // 存储打字机定时器
|
|
const typewriterIntervals = new Map() // 存储打字机定时器
|
|
|
|
|
+const streamingAnswerTypewriters = new Map() // 存储流式回答的平滑渲染定时器
|
|
|
|
|
+const databaseFileRevealTimers = new Map() // 存储召回文件逐条显现定时器
|
|
|
|
|
+const STREAMING_ANSWER_CHARS_PER_FRAME = 2
|
|
|
|
|
+const STREAMING_ANSWER_FRAME_MS = 1000 / 36
|
|
|
|
|
+const FINAL_ANSWER_TYPEWRITER_SPEED = 80
|
|
|
|
|
+const DATABASE_FILE_REVEAL_INTERVAL_MS = 320
|
|
|
|
|
|
|
|
// 功能卡片图标计数器
|
|
// 功能卡片图标计数器
|
|
|
let functionCardIconIndex = 0
|
|
let functionCardIconIndex = 0
|
|
@@ -1374,6 +1391,18 @@ const getConversationMessages = async (conversationId) => {
|
|
|
if (parsedContent.thinkingContent) {
|
|
if (parsedContent.thinkingContent) {
|
|
|
thinkingContent = parsedContent.thinkingContent
|
|
thinkingContent = parsedContent.thinkingContent
|
|
|
}
|
|
}
|
|
|
|
|
+ if (parsedContent.answer || parsedContent.content) {
|
|
|
|
|
+ const answerContent = parsedContent.answer || parsedContent.content || ''
|
|
|
|
|
+ const processedContent = String(answerContent)
|
|
|
|
|
+ .replace(/\\n/g, '\n')
|
|
|
|
|
+ .replace(/\\t/g, '\t')
|
|
|
|
|
+ .replace(/\\r/g, '\r')
|
|
|
|
|
+ displayContent = processedContent.trim()
|
|
|
|
|
+ ? renderMarkdownContent(processedContent)
|
|
|
|
|
+ : ''
|
|
|
|
|
+ } else {
|
|
|
|
|
+ displayContent = ''
|
|
|
|
|
+ }
|
|
|
} else if (parsedContent.answer || parsedContent.content || parsedContent.thinkingContent) {
|
|
} else if (parsedContent.answer || parsedContent.content || parsedContent.thinkingContent) {
|
|
|
if (parsedContent.thinkingContent) {
|
|
if (parsedContent.thinkingContent) {
|
|
|
thinkingContent = parsedContent.thinkingContent
|
|
thinkingContent = parsedContent.thinkingContent
|
|
@@ -1387,6 +1416,7 @@ const getConversationMessages = async (conversationId) => {
|
|
|
} else if (Array.isArray(parsedContent)) {
|
|
} else if (Array.isArray(parsedContent)) {
|
|
|
// 旧格式,直接是reports数组
|
|
// 旧格式,直接是reports数组
|
|
|
reports = hydratePersistedReports(parsedContent)
|
|
reports = hydratePersistedReports(parsedContent)
|
|
|
|
|
+ displayContent = ''
|
|
|
} else {
|
|
} else {
|
|
|
throw new Error('Not an array or valid format')
|
|
throw new Error('Not an array or valid format')
|
|
|
}
|
|
}
|
|
@@ -1426,6 +1456,14 @@ const getConversationMessages = async (conversationId) => {
|
|
|
completedCount = actualReports.filter(r => r.status === 'completed').length
|
|
completedCount = actualReports.filter(r => r.status === 'completed').length
|
|
|
progress = totalFiles > 0 ? Math.round((completedCount / totalFiles) * 100) : 100
|
|
progress = totalFiles > 0 ? Math.round((completedCount / totalFiles) * 100) : 100
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ if (message.type === 'ai' && thinkingContent) {
|
|
|
|
|
+ thinkingContent = sanitizeThinkingContentForPersistence(thinkingContent, {
|
|
|
|
|
+ rawThinking: thinkingContent,
|
|
|
|
|
+ userQuestion: userQuestion || '',
|
|
|
|
|
+ summary
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
return {
|
|
return {
|
|
|
type: message.type, // 'user' 或 'ai'
|
|
type: message.type, // 'user' 或 'ai'
|
|
@@ -1437,6 +1475,9 @@ const getConversationMessages = async (conversationId) => {
|
|
|
showThinking: Boolean(thinkingContent),
|
|
showThinking: Boolean(thinkingContent),
|
|
|
thinkingStreaming: false,
|
|
thinkingStreaming: false,
|
|
|
answerStreaming: false,
|
|
answerStreaming: false,
|
|
|
|
|
+ _databaseReportsRevealComplete: true,
|
|
|
|
|
+ _databaseReportsRevealLength: dedupeReportsByFileAndScene(reports).length,
|
|
|
|
|
+ _visibleDatabaseReportCount: dedupeReportsByFileAndScene(reports).length,
|
|
|
totalFiles: totalFiles, // 总文件数
|
|
totalFiles: totalFiles, // 总文件数
|
|
|
completedCount: completedCount, // 完成数
|
|
completedCount: completedCount, // 完成数
|
|
|
progress: progress, // 进度
|
|
progress: progress, // 进度
|
|
@@ -1525,6 +1566,121 @@ const getConversationMessages = async (conversationId) => {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+const getDatabaseDisplayReports = (message) => dedupeReportsByFileAndScene(message?.reports || [])
|
|
|
|
|
+
|
|
|
|
|
+const shouldWaitForDatabaseFileReveal = (message) => (
|
|
|
|
|
+ shouldShowDatabaseFiles(message)
|
|
|
|
|
+ && getDatabaseDisplayReports(message).length > 0
|
|
|
|
|
+ && !message._databaseReportsRevealComplete
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+const triggerRelatedQuestionsForMessage = (message) => {
|
|
|
|
|
+ if (!message || message._relatedQuestionsRequested) return
|
|
|
|
|
+
|
|
|
|
|
+ const messageIndex = chatMessages.value.findIndex(item => item === message)
|
|
|
|
|
+ const previousMessages = messageIndex >= 0
|
|
|
|
|
+ ? chatMessages.value.slice(0, messageIndex)
|
|
|
|
|
+ : chatMessages.value
|
|
|
|
|
+ const userMessage = previousMessages.filter(msg => msg.type === 'user').pop()
|
|
|
|
|
+
|
|
|
|
|
+ if (!userMessage || !message.ai_message_id) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let aiReplyContent = ''
|
|
|
|
|
+ if (message.summary) {
|
|
|
|
|
+ aiReplyContent = message.summary
|
|
|
|
|
+ } else if (message.content) {
|
|
|
|
|
+ aiReplyContent = message.content
|
|
|
|
|
+ } else if (message.reports && message.reports.length > 0) {
|
|
|
|
|
+ aiReplyContent = message.reports
|
|
|
|
|
+ .filter(report => report.report && report.report.summary)
|
|
|
|
|
+ .map(report => report.report.summary)
|
|
|
|
|
+ .slice(0, 3)
|
|
|
|
|
+ .join('\n\n')
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!aiReplyContent.trim()) return
|
|
|
|
|
+
|
|
|
|
|
+ message._relatedQuestionsRequested = true
|
|
|
|
|
+ getAIRelatedQuestions(userMessage.content, aiReplyContent, message.ai_message_id)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const completePostAnswerUiOnce = (message) => {
|
|
|
|
|
+ if (!message || message._postAnswerUiCompleted) return
|
|
|
|
|
+
|
|
|
|
|
+ if (shouldWaitForDatabaseFileReveal(message)) {
|
|
|
|
|
+ message._postAnswerUiPending = true
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ message._postAnswerUiPending = false
|
|
|
|
|
+ message._postAnswerUiCompleted = true
|
|
|
|
|
+ isAIReplyProcessComplete.value = !shouldWaitForDatabaseFileReveal(lastAIMessageForPersistence)
|
|
|
|
|
+ if (getDatabaseDisplayReports(message).length === 0) {
|
|
|
|
|
+ triggerRelatedQuestionsForMessage(message)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const stopDatabaseFilesReveal = (message) => {
|
|
|
|
|
+ const messageId = message?.id
|
|
|
|
|
+ if (!messageId) return
|
|
|
|
|
+
|
|
|
|
|
+ const timer = databaseFileRevealTimers.get(messageId)
|
|
|
|
|
+ if (timer) {
|
|
|
|
|
+ clearInterval(timer)
|
|
|
|
|
+ databaseFileRevealTimers.delete(messageId)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const startDatabaseFilesReveal = (message) => {
|
|
|
|
|
+ if (!message || !shouldShowDatabaseFiles(message)) return
|
|
|
|
|
+
|
|
|
|
|
+ const reports = getDatabaseDisplayReports(message)
|
|
|
|
|
+ if (reports.length === 0) return
|
|
|
|
|
+
|
|
|
|
|
+ if (message._databaseReportsRevealComplete && message._databaseReportsRevealLength === reports.length) {
|
|
|
|
|
+ message._visibleDatabaseReportCount = reports.length
|
|
|
|
|
+ completePostAnswerUiOnce(message)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (message._databaseReportsRevealLength !== reports.length) {
|
|
|
|
|
+ message._databaseReportsRevealLength = reports.length
|
|
|
|
|
+ message._databaseReportsRevealComplete = false
|
|
|
|
|
+ message._visibleDatabaseReportCount = Math.min(
|
|
|
|
|
+ Math.max(Number(message._visibleDatabaseReportCount || 0), 0),
|
|
|
|
|
+ reports.length
|
|
|
|
|
+ )
|
|
|
|
|
+ } else if (!Number.isFinite(Number(message._visibleDatabaseReportCount))) {
|
|
|
|
|
+ message._visibleDatabaseReportCount = 0
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (databaseFileRevealTimers.has(message.id)) return
|
|
|
|
|
+
|
|
|
|
|
+ const timer = setInterval(() => {
|
|
|
|
|
+ const latestReports = getDatabaseDisplayReports(message)
|
|
|
|
|
+ const currentCount = Number(message._visibleDatabaseReportCount || 0)
|
|
|
|
|
+
|
|
|
|
|
+ if (!shouldShowDatabaseFiles(message) || latestReports.length === 0) {
|
|
|
|
|
+ stopDatabaseFilesReveal(message)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const nextCount = Math.min(currentCount + 1, latestReports.length)
|
|
|
|
|
+ message._visibleDatabaseReportCount = nextCount
|
|
|
|
|
+
|
|
|
|
|
+ if (nextCount >= latestReports.length) {
|
|
|
|
|
+ message._databaseReportsRevealComplete = true
|
|
|
|
|
+ message._databaseReportsRevealLength = latestReports.length
|
|
|
|
|
+ stopDatabaseFilesReveal(message)
|
|
|
|
|
+ completePostAnswerUiOnce(message)
|
|
|
|
|
+ }
|
|
|
|
|
+ }, DATABASE_FILE_REVEAL_INTERVAL_MS)
|
|
|
|
|
+
|
|
|
|
|
+ databaseFileRevealTimers.set(message.id, timer)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// 清除所有打字机定时器
|
|
// 清除所有打字机定时器
|
|
|
const clearAllTypeIntervals = () => {
|
|
const clearAllTypeIntervals = () => {
|
|
|
typewriterIntervals.forEach((interval, messageId) => {
|
|
typewriterIntervals.forEach((interval, messageId) => {
|
|
@@ -1536,6 +1692,14 @@ const clearAllTypeIntervals = () => {
|
|
|
clearInterval(interval)
|
|
clearInterval(interval)
|
|
|
})
|
|
})
|
|
|
reportTypewriters.clear()
|
|
reportTypewriters.clear()
|
|
|
|
|
+ streamingAnswerTypewriters.forEach((interval) => {
|
|
|
|
|
+ clearInterval(interval)
|
|
|
|
|
+ })
|
|
|
|
|
+ streamingAnswerTypewriters.clear()
|
|
|
|
|
+ databaseFileRevealTimers.forEach((timer) => {
|
|
|
|
|
+ clearInterval(timer)
|
|
|
|
|
+ })
|
|
|
|
|
+ databaseFileRevealTimers.clear()
|
|
|
chatMessages.value.forEach(message => clearMessageOutputRenders(message))
|
|
chatMessages.value.forEach(message => clearMessageOutputRenders(message))
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1544,6 +1708,9 @@ const waitForProgressCompletionFrame = () => new Promise(resolve => setTimeout(r
|
|
|
const completeAIMessageAfterRendered = async (message) => {
|
|
const completeAIMessageAfterRendered = async (message) => {
|
|
|
if (!message || message._stopped) return
|
|
if (!message || message._stopped) return
|
|
|
|
|
|
|
|
|
|
+ finalizeStreamingThinkingIfNeeded(message)
|
|
|
|
|
+ finalizeAnswerStreamIfNeeded(message)
|
|
|
|
|
+
|
|
|
if (message.showStats && Number(message.progress || 0) < 100) {
|
|
if (message.showStats && Number(message.progress || 0) < 100) {
|
|
|
updateMessageStatus(message, 'completed')
|
|
updateMessageStatus(message, 'completed')
|
|
|
}
|
|
}
|
|
@@ -1562,6 +1729,7 @@ const completeAIMessageAfterRendered = async (message) => {
|
|
|
|
|
|
|
|
hideMessageProgressStatus(message)
|
|
hideMessageProgressStatus(message)
|
|
|
clearMessageOutputRenders(message)
|
|
clearMessageOutputRenders(message)
|
|
|
|
|
+ startDatabaseFilesReveal(message)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 获取报告的完整内容(优先使用_fullContent,解决打字机效果未完成时内容为空的问题)
|
|
// 获取报告的完整内容(优先使用_fullContent,解决打字机效果未完成时内容为空的问题)
|
|
@@ -2122,17 +2290,17 @@ const openInNewTab = () => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 处理文件预览
|
|
// 处理文件预览
|
|
|
-const handleFilePreview = (data) => {
|
|
|
|
|
|
|
+const handleFilePreview = async (data) => {
|
|
|
// 重置状态
|
|
// 重置状态
|
|
|
fileError.value = ''
|
|
fileError.value = ''
|
|
|
fileLoading.value = false
|
|
fileLoading.value = false
|
|
|
|
|
|
|
|
// 处理不同类型的输入参数
|
|
// 处理不同类型的输入参数
|
|
|
if (typeof data === 'string') {
|
|
if (typeof data === 'string') {
|
|
|
- previewFilePath.value = data
|
|
|
|
|
|
|
+ previewFilePath.value = await buildPreviewUrl(data)
|
|
|
previewFileName.value = data
|
|
previewFileName.value = data
|
|
|
} else if (data && data.filePath) {
|
|
} else if (data && data.filePath) {
|
|
|
- previewFilePath.value = data.filePath
|
|
|
|
|
|
|
+ previewFilePath.value = await buildPreviewUrl(data.filePath)
|
|
|
previewFileName.value = data.fileName || data.filePath
|
|
previewFileName.value = data.fileName || data.filePath
|
|
|
} else {
|
|
} else {
|
|
|
fileError.value = '文件路径为空'
|
|
fileError.value = '文件路径为空'
|
|
@@ -2437,6 +2605,164 @@ const updateMessageStatus = (aiMessage, status, customMessage = null) => {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+const renderAnswerPrefix = (message, visibleLength) => {
|
|
|
|
|
+ const rawContent = String(message.content || '')
|
|
|
|
|
+ const visibleContent = rawContent.slice(0, Math.min(visibleLength, rawContent.length))
|
|
|
|
|
+ message.displayContent = visibleContent
|
|
|
|
|
+ ? renderMarkdownContent(processAIResponse(visibleContent))
|
|
|
|
|
+ : ''
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const stopStreamingAnswerTypewriter = (message, { complete = false } = {}) => {
|
|
|
|
|
+ if (!message) return
|
|
|
|
|
+
|
|
|
|
|
+ const interval = streamingAnswerTypewriters.get(message.id)
|
|
|
|
|
+ if (interval) {
|
|
|
|
|
+ clearInterval(interval)
|
|
|
|
|
+ streamingAnswerTypewriters.delete(message.id)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (complete) {
|
|
|
|
|
+ message._visibleAnswerLength = String(message.content || '').length
|
|
|
|
|
+ renderAnswerPrefix(message, message._visibleAnswerLength)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const markAnswerDisplayComplete = (message) => {
|
|
|
|
|
+ if (!message) return
|
|
|
|
|
+
|
|
|
|
|
+ message._answerDisplayComplete = true
|
|
|
|
|
+ startDatabaseFilesReveal(message)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const canRenderAnswerContent = (message) => !message?.thinkingStreaming
|
|
|
|
|
+
|
|
|
|
|
+const markAnswerContentStarted = (message, { resetDisplay = false } = {}) => {
|
|
|
|
|
+ if (!message) return
|
|
|
|
|
+
|
|
|
|
|
+ message._answerContentStarted = true
|
|
|
|
|
+ message._answerDisplayComplete = false
|
|
|
|
|
+ message._visibleAnswerLength = Number(message._visibleAnswerLength || 0)
|
|
|
|
|
+ if (!message.content) {
|
|
|
|
|
+ message.content = ''
|
|
|
|
|
+ }
|
|
|
|
|
+ if (resetDisplay && !message.displayContent && !message._visibleAnswerLength) {
|
|
|
|
|
+ message.displayContent = ''
|
|
|
|
|
+ message._visibleAnswerLength = 0
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const startAnswerRenderingIfReady = (message) => {
|
|
|
|
|
+ if (!message || !canRenderAnswerContent(message)) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (message._answerContentStarted || message.content) {
|
|
|
|
|
+ message._answerContentStarted = true
|
|
|
|
|
+ message.answerStreaming = message._answerContentDone !== true
|
|
|
|
|
+ message.isTyping = true
|
|
|
|
|
+ ensureStreamingAnswerTypewriter(message)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const finalizeStreamingThinkingIfNeeded = (message) => {
|
|
|
|
|
+ if (!message) return
|
|
|
|
|
+
|
|
|
|
|
+ if (message.thinkingStreaming) {
|
|
|
|
|
+ message.thinkingStreaming = false
|
|
|
|
|
+ finalizeThinkingContent(message)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ ensureThinkingFallback(message)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const finalizeAnswerStreamIfNeeded = (message) => {
|
|
|
|
|
+ if (!message) return
|
|
|
|
|
+
|
|
|
|
|
+ if (message.content) {
|
|
|
|
|
+ if (!message._answerContentStarted) {
|
|
|
|
|
+ markAnswerContentStarted(message)
|
|
|
|
|
+ }
|
|
|
|
|
+ message._answerContentDone = true
|
|
|
|
|
+ message.answerStreaming = false
|
|
|
|
|
+ startAnswerRenderingIfReady(message)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ message.answerStreaming = false
|
|
|
|
|
+ message._answerContentDone = true
|
|
|
|
|
+ message._answerDisplayComplete = true
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const ensureStreamingAnswerTypewriter = (message) => {
|
|
|
|
|
+ if (!message || streamingAnswerTypewriters.has(message.id)) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ message._visibleAnswerLength = Number(message._visibleAnswerLength || 0)
|
|
|
|
|
+ const interval = setInterval(() => {
|
|
|
|
|
+ const rawContent = String(message.content || '')
|
|
|
|
|
+ const targetLength = rawContent.length
|
|
|
|
|
+
|
|
|
|
|
+ if (message._visibleAnswerLength < targetLength) {
|
|
|
|
|
+ message._visibleAnswerLength = Math.min(
|
|
|
|
|
+ targetLength,
|
|
|
|
|
+ message._visibleAnswerLength + STREAMING_ANSWER_CHARS_PER_FRAME
|
|
|
|
|
+ )
|
|
|
|
|
+ renderAnswerPrefix(message, message._visibleAnswerLength)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!message.answerStreaming) {
|
|
|
|
|
+ stopStreamingAnswerTypewriter(message, { complete: true })
|
|
|
|
|
+ message.isTyping = false
|
|
|
|
|
+ markAnswerDisplayComplete(message)
|
|
|
|
|
+ }
|
|
|
|
|
+ }, STREAMING_ANSWER_FRAME_MS)
|
|
|
|
|
+
|
|
|
|
|
+ streamingAnswerTypewriters.set(message.id, interval)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const startTypewriterEffect = (message, fullContent, speed = 30) => {
|
|
|
|
|
+ return new Promise((resolve) => {
|
|
|
|
|
+ if (typewriterIntervals.has(message.id)) {
|
|
|
|
|
+ clearInterval(typewriterIntervals.get(message.id))
|
|
|
|
|
+ typewriterIntervals.delete(message.id)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const chunks = splitHtmlIntoTypewriterChunks(fullContent)
|
|
|
|
|
+ let currentIndex = 0
|
|
|
|
|
+ message.displayContent = ''
|
|
|
|
|
+ message.isTyping = true
|
|
|
|
|
+
|
|
|
|
|
+ const interval = setInterval(() => {
|
|
|
|
|
+ if (currentIndex < chunks.length) {
|
|
|
|
|
+ const charsToAdd = Math.max(1, Math.floor(speed / 10))
|
|
|
|
|
+ let charsAdded = 0
|
|
|
|
|
+
|
|
|
|
|
+ while (currentIndex < chunks.length && charsAdded < charsToAdd) {
|
|
|
|
|
+ const chunk = chunks[currentIndex]
|
|
|
|
|
+ message.displayContent += chunk
|
|
|
|
|
+ currentIndex += 1
|
|
|
|
|
+
|
|
|
|
|
+ if (!(chunk.startsWith('<') && chunk.endsWith('>'))) {
|
|
|
|
|
+ charsAdded += 1
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ clearInterval(interval)
|
|
|
|
|
+ typewriterIntervals.delete(message.id)
|
|
|
|
|
+ message.isTyping = false
|
|
|
|
|
+ message.displayContent = fullContent
|
|
|
|
|
+ resolve()
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 1000 / 60)
|
|
|
|
|
+
|
|
|
|
|
+ typewriterIntervals.set(message.id, interval)
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// 打字机效果函数
|
|
// 打字机效果函数
|
|
|
const startReportFieldTypewriter = (report, field, fullContent, speed = 50) => {
|
|
const startReportFieldTypewriter = (report, field, fullContent, speed = 50) => {
|
|
|
return new Promise((resolve) => {
|
|
return new Promise((resolve) => {
|
|
@@ -2478,7 +2804,6 @@ const startReportFieldTypewriter = (report, field, fullContent, speed = 50) => {
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-const THINKING_FALLBACK_TEXT = '正在结合检索结果梳理回答重点,请稍等……'
|
|
|
|
|
const THINKING_HEADING_REPLACEMENTS = [
|
|
const THINKING_HEADING_REPLACEMENTS = [
|
|
|
[/^\s*\d+\.\s*Analyze the Request:?\s*$/i, '### 问题理解'],
|
|
[/^\s*\d+\.\s*Analyze the Request:?\s*$/i, '### 问题理解'],
|
|
|
[/^\s*Analyze the Request:?\s*$/i, '### 问题理解'],
|
|
[/^\s*Analyze the Request:?\s*$/i, '### 问题理解'],
|
|
@@ -2488,7 +2813,13 @@ const THINKING_HEADING_REPLACEMENTS = [
|
|
|
[/^\s*Key Points:?\s*$/i, '### 回答重点']
|
|
[/^\s*Key Points:?\s*$/i, '### 回答重点']
|
|
|
]
|
|
]
|
|
|
const THINKING_BLOCKLIST_PATTERNS = [
|
|
const THINKING_BLOCKLIST_PATTERNS = [
|
|
|
|
|
+ /^\s*Here'?s a thinking process:?\s*$/i,
|
|
|
/^\s*Thinking Process:?\s*$/i,
|
|
/^\s*Thinking Process:?\s*$/i,
|
|
|
|
|
+ /^\s*Analyze User Input:?\s*$/i,
|
|
|
|
|
+ /^\s*User Question:\s*/i,
|
|
|
|
|
+ /^\s*Context:\s*/i,
|
|
|
|
|
+ /^\s*Intent:\s*/i,
|
|
|
|
|
+ /^\s*Is Professional Question:\s*/i,
|
|
|
/^\s*[-*•]?\s*Role:\s*/i,
|
|
/^\s*[-*•]?\s*Role:\s*/i,
|
|
|
/^\s*[-*•]?\s*Task:\s*/i,
|
|
/^\s*[-*•]?\s*Task:\s*/i,
|
|
|
/^\s*[-*•]?\s*Input:\s*/i,
|
|
/^\s*[-*•]?\s*Input:\s*/i,
|
|
@@ -2504,7 +2835,7 @@ const getDisplayThinkingContent = (content) => {
|
|
|
const rawContent = String(content || '')
|
|
const rawContent = String(content || '')
|
|
|
if (!rawContent.trim()) return ''
|
|
if (!rawContent.trim()) return ''
|
|
|
|
|
|
|
|
- const hasMetaBoilerplate = /Thinking Process:|Analyze the Request|Role:|Task:|Input:|Constraints:/i.test(rawContent)
|
|
|
|
|
|
|
+ const hasMetaBoilerplate = hasRawThinkingLeak(rawContent)
|
|
|
const normalizedLines = rawContent
|
|
const normalizedLines = rawContent
|
|
|
.split('\n')
|
|
.split('\n')
|
|
|
.map(line => line.replace(/\r/g, ''))
|
|
.map(line => line.replace(/\r/g, ''))
|
|
@@ -2524,17 +2855,42 @@ const getDisplayThinkingContent = (content) => {
|
|
|
return !THINKING_BLOCKLIST_PATTERNS.some(pattern => pattern.test(trimmed))
|
|
return !THINKING_BLOCKLIST_PATTERNS.some(pattern => pattern.test(trimmed))
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- const normalizedContent = normalizedLines.join('\n').trim()
|
|
|
|
|
- if (normalizedContent) {
|
|
|
|
|
|
|
+ const normalizedContent = normalizeDisplayThinkingContent(normalizedLines.join('\n'))
|
|
|
|
|
+ if (normalizedContent && !hasMetaBoilerplate) {
|
|
|
return normalizedContent
|
|
return normalizedContent
|
|
|
}
|
|
}
|
|
|
- return hasMetaBoilerplate ? THINKING_FALLBACK_TEXT : rawContent.trim()
|
|
|
|
|
|
|
+ return hasMetaBoilerplate
|
|
|
|
|
+ ? buildDisplayThinkingSummary({ rawThinking: rawContent })
|
|
|
|
|
+ : normalizeDisplayThinkingContent(rawContent)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const ensureThinkingFallback = (aiMessage) => {
|
|
|
|
|
+ if (!aiMessage.thinkingContent || !aiMessage.thinkingContent.trim()) {
|
|
|
|
|
+ aiMessage.thinkingContent = buildDisplayThinkingSummary({
|
|
|
|
|
+ rawThinking: aiMessage.rawThinkingContent || aiMessage.rawIntentThinkingContent || '',
|
|
|
|
|
+ userQuestion: aiMessage.userQuestion || currentQuestion.value || '',
|
|
|
|
|
+ summary: aiMessage._fullSummary || aiMessage.summary || ''
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ aiMessage.showThinking = true
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const appendThinkingContent = (aiMessage, sectionTitle, content) => {
|
|
const appendThinkingContent = (aiMessage, sectionTitle, content) => {
|
|
|
- const normalized = (content || '').trim()
|
|
|
|
|
|
|
+ const normalized = normalizeDisplayThinkingContent(content)
|
|
|
if (!normalized) return
|
|
if (!normalized) return
|
|
|
|
|
|
|
|
|
|
+ if (hasRawThinkingLeak(normalized)) {
|
|
|
|
|
+ aiMessage.rawThinkingContent = `${aiMessage.rawThinkingContent || ''}${normalized}`
|
|
|
|
|
+ aiMessage.thinkingContent = buildDisplayThinkingSummary({
|
|
|
|
|
+ rawThinking: aiMessage.rawThinkingContent,
|
|
|
|
|
+ existingThinking: aiMessage.thinkingContent === THINKING_FALLBACK_TEXT ? '' : normalizeDisplayThinkingContent(aiMessage.thinkingContent),
|
|
|
|
|
+ userQuestion: aiMessage.userQuestion || currentQuestion.value || '',
|
|
|
|
|
+ summary: aiMessage._fullSummary || aiMessage.summary || ''
|
|
|
|
|
+ })
|
|
|
|
|
+ aiMessage.showThinking = true
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
if (!aiMessage.thinkingContent) {
|
|
if (!aiMessage.thinkingContent) {
|
|
|
aiMessage.thinkingContent = normalized
|
|
aiMessage.thinkingContent = normalized
|
|
|
} else if (!aiMessage.thinkingContent.includes(normalized)) {
|
|
} else if (!aiMessage.thinkingContent.includes(normalized)) {
|
|
@@ -2547,14 +2903,68 @@ const appendThinkingContent = (aiMessage, sectionTitle, content) => {
|
|
|
const appendThinkingDelta = (aiMessage, chunk) => {
|
|
const appendThinkingDelta = (aiMessage, chunk) => {
|
|
|
const normalized = chunk || ''
|
|
const normalized = chunk || ''
|
|
|
if (!normalized) return
|
|
if (!normalized) return
|
|
|
- aiMessage.thinkingContent = `${aiMessage.thinkingContent || ''}${normalized}`
|
|
|
|
|
|
|
+ aiMessage.rawThinkingContent = `${aiMessage.rawThinkingContent || ''}${normalized}`
|
|
|
|
|
+ aiMessage.thinkingContent = buildDisplayThinkingSummary({
|
|
|
|
|
+ rawThinking: aiMessage.rawThinkingContent,
|
|
|
|
|
+ existingThinking: aiMessage.thinkingContent === THINKING_FALLBACK_TEXT ? '' : normalizeDisplayThinkingContent(aiMessage.thinkingContent),
|
|
|
|
|
+ userQuestion: aiMessage.userQuestion || currentQuestion.value || '',
|
|
|
|
|
+ summary: aiMessage._fullSummary || aiMessage.summary || ''
|
|
|
|
|
+ })
|
|
|
aiMessage.showThinking = true
|
|
aiMessage.showThinking = true
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+const finalizeThinkingContent = (aiMessage) => {
|
|
|
|
|
+ aiMessage.thinkingContent = buildDisplayThinkingSummary({
|
|
|
|
|
+ rawThinking: aiMessage.rawThinkingContent || aiMessage.rawIntentThinkingContent || '',
|
|
|
|
|
+ existingThinking: hasRawThinkingLeak(aiMessage.thinkingContent) || aiMessage.thinkingContent === THINKING_FALLBACK_TEXT
|
|
|
|
|
+ ? ''
|
|
|
|
|
+ : normalizeDisplayThinkingContent(aiMessage.thinkingContent),
|
|
|
|
|
+ userQuestion: aiMessage.userQuestion || currentQuestion.value || '',
|
|
|
|
|
+ summary: aiMessage._fullSummary || aiMessage.summary || ''
|
|
|
|
|
+ })
|
|
|
|
|
+ aiMessage.thinkingContent = normalizeDisplayThinkingContent(aiMessage.thinkingContent)
|
|
|
|
|
+ aiMessage.showThinking = Boolean(aiMessage.thinkingContent)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
const toggleThinkingPanel = (message) => {
|
|
const toggleThinkingPanel = (message) => {
|
|
|
message.showThinking = message.showThinking === false
|
|
message.showThinking = message.showThinking === false
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+const shouldShowDatabaseFiles = (message) => {
|
|
|
|
|
+ if (!message?.reports || message.reports.length === 0) {
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!message.thinkingContent || message.thinkingStreaming) {
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (message.answerStreaming || message.isTyping || message._answerDisplayComplete === false) {
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return Boolean(
|
|
|
|
|
+ (message.displayContent && message.displayContent.length > 0)
|
|
|
|
|
+ || message._answerDisplayComplete
|
|
|
|
|
+ || message.currentStatus === 'completed'
|
|
|
|
|
+ || Number(message.progress || 0) >= 100
|
|
|
|
|
+ )
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const getVisibleDatabaseReports = (message) => {
|
|
|
|
|
+ if (!shouldShowDatabaseFiles(message)) {
|
|
|
|
|
+ return []
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const reports = getDatabaseDisplayReports(message)
|
|
|
|
|
+ if (message._databaseReportsRevealComplete) {
|
|
|
|
|
+ return reports
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const visibleCount = Math.max(0, Number(message._visibleDatabaseReportCount || 0))
|
|
|
|
|
+ return reports.slice(0, Math.min(visibleCount, reports.length))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// SSE消息处理函数
|
|
// SSE消息处理函数
|
|
|
const handleSSEMessage = (data, aiMessageIndex) => {
|
|
const handleSSEMessage = (data, aiMessageIndex) => {
|
|
|
const aiMessage = chatMessages.value[aiMessageIndex]
|
|
const aiMessage = chatMessages.value[aiMessageIndex]
|
|
@@ -2617,6 +3027,10 @@ const handleSSEMessage = (data, aiMessageIndex) => {
|
|
|
// 专业问题:意图识别完成,更新为查询知识库状态
|
|
// 专业问题:意图识别完成,更新为查询知识库状态
|
|
|
updateMessageStatus(aiMessage, 'querying_kb')
|
|
updateMessageStatus(aiMessage, 'querying_kb')
|
|
|
|
|
|
|
|
|
|
+ if (data.thinking_content) {
|
|
|
|
|
+ aiMessage.rawIntentThinkingContent = data.thinking_content
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// 如果启用联网搜索,稍后会更新为web_searching状态
|
|
// 如果启用联网搜索,稍后会更新为web_searching状态
|
|
|
// (当收到web_search_raw事件时)
|
|
// (当收到web_search_raw事件时)
|
|
|
|
|
|
|
@@ -2642,6 +3056,7 @@ const handleSSEMessage = (data, aiMessageIndex) => {
|
|
|
case 'answer_thinking_start':
|
|
case 'answer_thinking_start':
|
|
|
aiMessage.thinkingStreaming = true
|
|
aiMessage.thinkingStreaming = true
|
|
|
aiMessage.showThinking = true
|
|
aiMessage.showThinking = true
|
|
|
|
|
+ stopStreamingAnswerTypewriter(aiMessage)
|
|
|
if (shouldApplyMessageProgressStatus(aiMessage, 'deep_thinking')) {
|
|
if (shouldApplyMessageProgressStatus(aiMessage, 'deep_thinking')) {
|
|
|
updateMessageStatus(aiMessage, 'deep_thinking')
|
|
updateMessageStatus(aiMessage, 'deep_thinking')
|
|
|
}
|
|
}
|
|
@@ -2653,9 +3068,11 @@ const handleSSEMessage = (data, aiMessageIndex) => {
|
|
|
|
|
|
|
|
case 'answer_thinking_done':
|
|
case 'answer_thinking_done':
|
|
|
aiMessage.thinkingStreaming = false
|
|
aiMessage.thinkingStreaming = false
|
|
|
|
|
+ finalizeThinkingContent(aiMessage)
|
|
|
if (!shouldHideStatsForStreamingAnswer(aiMessage) && aiMessage.currentStatus === 'deep_thinking') {
|
|
if (!shouldHideStatsForStreamingAnswer(aiMessage) && aiMessage.currentStatus === 'deep_thinking') {
|
|
|
updateMessageStatus(aiMessage, 'outputting', '正在整理回答内容……')
|
|
updateMessageStatus(aiMessage, 'outputting', '正在整理回答内容……')
|
|
|
}
|
|
}
|
|
|
|
|
+ startAnswerRenderingIfReady(aiMessage)
|
|
|
break
|
|
break
|
|
|
|
|
|
|
|
case 'answer_content_start':
|
|
case 'answer_content_start':
|
|
@@ -2668,32 +3085,36 @@ const handleSSEMessage = (data, aiMessageIndex) => {
|
|
|
aiMessage.summary = ''
|
|
aiMessage.summary = ''
|
|
|
aiMessage._fullSummary = ''
|
|
aiMessage._fullSummary = ''
|
|
|
}
|
|
}
|
|
|
- aiMessage.answerStreaming = true
|
|
|
|
|
|
|
+ aiMessage._answerContentDone = false
|
|
|
|
|
+ aiMessage.answerStreaming = canRenderAnswerContent(aiMessage)
|
|
|
aiMessage.isTyping = true
|
|
aiMessage.isTyping = true
|
|
|
- if (!aiMessage.content) {
|
|
|
|
|
- aiMessage.content = ''
|
|
|
|
|
- }
|
|
|
|
|
- if (!aiMessage.displayContent) {
|
|
|
|
|
- aiMessage.displayContent = ''
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ markAnswerContentStarted(aiMessage, { resetDisplay: true })
|
|
|
|
|
+ startAnswerRenderingIfReady(aiMessage)
|
|
|
break
|
|
break
|
|
|
|
|
|
|
|
case 'answer_content_delta': {
|
|
case 'answer_content_delta': {
|
|
|
const delta = data.chunk || ''
|
|
const delta = data.chunk || ''
|
|
|
if (!delta) break
|
|
if (!delta) break
|
|
|
|
|
+ if (!aiMessage._answerContentStarted) {
|
|
|
|
|
+ markAnswerContentStarted(aiMessage)
|
|
|
|
|
+ }
|
|
|
aiMessage.content = `${aiMessage.content || ''}${delta}`
|
|
aiMessage.content = `${aiMessage.content || ''}${delta}`
|
|
|
- const processedReply = processAIResponse(aiMessage.content)
|
|
|
|
|
- aiMessage.displayContent = renderMarkdownContent(processedReply)
|
|
|
|
|
aiMessage.isTyping = true
|
|
aiMessage.isTyping = true
|
|
|
|
|
+ startAnswerRenderingIfReady(aiMessage)
|
|
|
break
|
|
break
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
case 'answer_content_done':
|
|
case 'answer_content_done':
|
|
|
|
|
+ if (!aiMessage._answerContentStarted && aiMessage.content) {
|
|
|
|
|
+ markAnswerContentStarted(aiMessage)
|
|
|
|
|
+ }
|
|
|
|
|
+ aiMessage._answerContentDone = true
|
|
|
aiMessage.answerStreaming = false
|
|
aiMessage.answerStreaming = false
|
|
|
- aiMessage.isTyping = false
|
|
|
|
|
|
|
+ startAnswerRenderingIfReady(aiMessage)
|
|
|
break
|
|
break
|
|
|
|
|
|
|
|
case 'online_answer': {
|
|
case 'online_answer': {
|
|
|
|
|
+ stopStreamingAnswerTypewriter(aiMessage)
|
|
|
aiMessage.answerStreaming = false
|
|
aiMessage.answerStreaming = false
|
|
|
aiMessage.thinkingStreaming = false
|
|
aiMessage.thinkingStreaming = false
|
|
|
if (shouldHideStatsForStreamingAnswer(aiMessage)) {
|
|
if (shouldHideStatsForStreamingAnswer(aiMessage)) {
|
|
@@ -2717,8 +3138,19 @@ const handleSSEMessage = (data, aiMessageIndex) => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const processedReply = processAIResponse(finalContent)
|
|
const processedReply = processAIResponse(finalContent)
|
|
|
- aiMessage.displayContent = renderMarkdownContent(processedReply)
|
|
|
|
|
- aiMessage.isTyping = false
|
|
|
|
|
|
|
+ const renderedReply = renderMarkdownContent(processedReply)
|
|
|
|
|
+
|
|
|
|
|
+ aiMessage._answerDisplayComplete = false
|
|
|
|
|
+ trackMessageOutputRender(aiMessage, startTypewriterEffect(aiMessage, renderedReply, FINAL_ANSWER_TYPEWRITER_SPEED))
|
|
|
|
|
+ .then(() => {
|
|
|
|
|
+ markAnswerDisplayComplete(aiMessage)
|
|
|
|
|
+ })
|
|
|
|
|
+ .catch(err => {
|
|
|
|
|
+ console.error('在线回答打字机效果失败:', err)
|
|
|
|
|
+ aiMessage.displayContent = renderedReply
|
|
|
|
|
+ aiMessage.isTyping = false
|
|
|
|
|
+ markAnswerDisplayComplete(aiMessage)
|
|
|
|
|
+ })
|
|
|
break
|
|
break
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -2730,6 +3162,7 @@ const handleSSEMessage = (data, aiMessageIndex) => {
|
|
|
break
|
|
break
|
|
|
|
|
|
|
|
case 'documents':
|
|
case 'documents':
|
|
|
|
|
+ finalizeStreamingThinkingIfNeeded(aiMessage)
|
|
|
aiMessage.totalFiles = data.total
|
|
aiMessage.totalFiles = data.total
|
|
|
aiMessage.completedCount = 0
|
|
aiMessage.completedCount = 0
|
|
|
|
|
|
|
@@ -2745,6 +3178,7 @@ const handleSSEMessage = (data, aiMessageIndex) => {
|
|
|
break
|
|
break
|
|
|
|
|
|
|
|
case 'category_title':
|
|
case 'category_title':
|
|
|
|
|
+ finalizeStreamingThinkingIfNeeded(aiMessage)
|
|
|
// 第一个分类标题时,说明开始分析文件
|
|
// 第一个分类标题时,说明开始分析文件
|
|
|
if (aiMessage.reports.length === 0) {
|
|
if (aiMessage.reports.length === 0) {
|
|
|
// 只有在当前状态进度 >= data_retrieved的进度时,才更新为analyzing_files
|
|
// 只有在当前状态进度 >= data_retrieved的进度时,才更新为analyzing_files
|
|
@@ -2776,6 +3210,7 @@ const handleSSEMessage = (data, aiMessageIndex) => {
|
|
|
break
|
|
break
|
|
|
|
|
|
|
|
case 'report_start':
|
|
case 'report_start':
|
|
|
|
|
+ finalizeStreamingThinkingIfNeeded(aiMessage)
|
|
|
// 调试日志:查看完整的数据结构
|
|
// 调试日志:查看完整的数据结构
|
|
|
console.log('🔍 [DEBUG] report_start 数据:', {
|
|
console.log('🔍 [DEBUG] report_start 数据:', {
|
|
|
file_index: data.file_index,
|
|
file_index: data.file_index,
|
|
@@ -2814,6 +3249,7 @@ const handleSSEMessage = (data, aiMessageIndex) => {
|
|
|
break
|
|
break
|
|
|
|
|
|
|
|
case 'report':
|
|
case 'report':
|
|
|
|
|
+ finalizeStreamingThinkingIfNeeded(aiMessage)
|
|
|
// 第一个报告开始时,更新到深度思考状态
|
|
// 第一个报告开始时,更新到深度思考状态
|
|
|
if (aiMessage.reports.filter(r => r.status === 'completed').length === 0) {
|
|
if (aiMessage.reports.filter(r => r.status === 'completed').length === 0) {
|
|
|
updateMessageStatus(aiMessage, 'deep_thinking')
|
|
updateMessageStatus(aiMessage, 'deep_thinking')
|
|
@@ -2850,9 +3286,9 @@ const handleSSEMessage = (data, aiMessageIndex) => {
|
|
|
...reportData, // 保留所有原始字段,包括可能的链接字段
|
|
...reportData, // 保留所有原始字段,包括可能的链接字段
|
|
|
report: {
|
|
report: {
|
|
|
display_name: fullDisplayName, // 直接显示
|
|
display_name: fullDisplayName, // 直接显示
|
|
|
- summary: hadStreamingContent ? fullSummary : '',
|
|
|
|
|
- analysis: hadStreamingContent ? fullAnalysis : '',
|
|
|
|
|
- clauses: hadStreamingContent ? fullClauses : ''
|
|
|
|
|
|
|
+ summary: fullSummary,
|
|
|
|
|
+ analysis: fullAnalysis,
|
|
|
|
|
+ clauses: fullClauses
|
|
|
},
|
|
},
|
|
|
status: 'completed',
|
|
status: 'completed',
|
|
|
metadata: {
|
|
metadata: {
|
|
@@ -2865,7 +3301,7 @@ const handleSSEMessage = (data, aiMessageIndex) => {
|
|
|
analysis: fullAnalysis,
|
|
analysis: fullAnalysis,
|
|
|
clauses: fullClauses
|
|
clauses: fullClauses
|
|
|
},
|
|
},
|
|
|
- _typewriterCompleted: hadStreamingContent || existingReport?._typewriterCompleted || false
|
|
|
|
|
|
|
+ _typewriterCompleted: true
|
|
|
}
|
|
}
|
|
|
targetReport = aiMessage.reports[idx]
|
|
targetReport = aiMessage.reports[idx]
|
|
|
streamingReports.value.delete(reportData.file_index)
|
|
streamingReports.value.delete(reportData.file_index)
|
|
@@ -2879,9 +3315,9 @@ const handleSSEMessage = (data, aiMessageIndex) => {
|
|
|
...reportData, // 保留所有原始字段,包括可能的链接字段
|
|
...reportData, // 保留所有原始字段,包括可能的链接字段
|
|
|
report: {
|
|
report: {
|
|
|
display_name: fullDisplayName, // 直接显示
|
|
display_name: fullDisplayName, // 直接显示
|
|
|
- summary: '',
|
|
|
|
|
- analysis: '',
|
|
|
|
|
- clauses: ''
|
|
|
|
|
|
|
+ summary: fullSummary,
|
|
|
|
|
+ analysis: fullAnalysis,
|
|
|
|
|
+ clauses: fullClauses
|
|
|
},
|
|
},
|
|
|
status: 'completed',
|
|
status: 'completed',
|
|
|
metadata: {
|
|
metadata: {
|
|
@@ -2893,50 +3329,14 @@ const handleSSEMessage = (data, aiMessageIndex) => {
|
|
|
summary: fullSummary,
|
|
summary: fullSummary,
|
|
|
analysis: fullAnalysis,
|
|
analysis: fullAnalysis,
|
|
|
clauses: fullClauses
|
|
clauses: fullClauses
|
|
|
- }
|
|
|
|
|
|
|
+ },
|
|
|
|
|
+ _typewriterCompleted: true
|
|
|
}
|
|
}
|
|
|
aiMessage.reports.push(newReport)
|
|
aiMessage.reports.push(newReport)
|
|
|
targetReport = newReport
|
|
targetReport = newReport
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 使用顺序打字机效果:概述 -> 解析 -> 相关条款
|
|
|
|
|
- if (targetReport._fullContent && !targetReport._typewriterCompleted) {
|
|
|
|
|
- // 标记打字机已启动,防止重复触发
|
|
|
|
|
- targetReport._typewriterStarted = true
|
|
|
|
|
-
|
|
|
|
|
- // 先打概述(速度200 = 每次20个字符,极快)
|
|
|
|
|
- const reportRenderPromise = startReportFieldTypewriter(targetReport, 'summary', targetReport._fullContent.summary || '', 200)
|
|
|
|
|
- .then(() => {
|
|
|
|
|
- // 概述完成后打解析
|
|
|
|
|
- return startReportFieldTypewriter(targetReport, 'analysis', targetReport._fullContent.analysis || '', 200)
|
|
|
|
|
- })
|
|
|
|
|
- .then(() => {
|
|
|
|
|
- // 解析完成后打相关条款
|
|
|
|
|
- if (targetReport._fullContent.clauses) {
|
|
|
|
|
- return startReportFieldTypewriter(targetReport, 'clauses', targetReport._fullContent.clauses || '', 200)
|
|
|
|
|
- }
|
|
|
|
|
- })
|
|
|
|
|
- .then(() => {
|
|
|
|
|
- // 全部完成,标记为已完成
|
|
|
|
|
- targetReport._typewriterCompleted = true
|
|
|
|
|
- })
|
|
|
|
|
- trackMessageOutputRender(aiMessage, reportRenderPromise)
|
|
|
|
|
- .catch(err => {
|
|
|
|
|
- console.error('报告打字机效果失败:', err)
|
|
|
|
|
- // 失败时直接显示完整内容
|
|
|
|
|
- targetReport.report.summary = targetReport._fullContent.summary || ''
|
|
|
|
|
- targetReport.report.analysis = targetReport._fullContent.analysis || ''
|
|
|
|
|
- targetReport.report.clauses = targetReport._fullContent.clauses || ''
|
|
|
|
|
- targetReport._typewriterCompleted = true
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- console.log('📝 [DEBUG] 报告打字机已启动:', {
|
|
|
|
|
- file_index: targetReport.file_index,
|
|
|
|
|
- summary_length: targetReport._fullContent.summary?.length || 0,
|
|
|
|
|
- analysis_length: targetReport._fullContent.analysis?.length || 0,
|
|
|
|
|
- clauses_length: targetReport._fullContent.clauses?.length || 0
|
|
|
|
|
- })
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // 文件区在正文之后才展示,报告内容直接缓存完整值,避免隐藏区域后台打字造成卡顿。
|
|
|
|
|
|
|
|
// 更新进度
|
|
// 更新进度
|
|
|
aiMessage.completedCount = aiMessage.reports.filter(r => r.status === 'completed' && r.type !== 'category_title').length
|
|
aiMessage.completedCount = aiMessage.reports.filter(r => r.status === 'completed' && r.type !== 'category_title').length
|
|
@@ -3144,19 +3544,34 @@ const handleSSEComplete = async () => {
|
|
|
if (message.ai_message_id) {
|
|
if (message.ai_message_id) {
|
|
|
// 构建完整的内容数据,包含报告、网络搜索结果和summary
|
|
// 构建完整的内容数据,包含报告、网络搜索结果和summary
|
|
|
// 使用getReportsWithFullContent确保报告内容完整(解决打字机效果未完成时内容为空的问题)
|
|
// 使用getReportsWithFullContent确保报告内容完整(解决打字机效果未完成时内容为空的问题)
|
|
|
|
|
+ const safeThinkingContent = sanitizeThinkingContentForPersistence(
|
|
|
|
|
+ message.thinkingContent || message.rawThinkingContent || '',
|
|
|
|
|
+ {
|
|
|
|
|
+ rawThinking: message.rawThinkingContent || '',
|
|
|
|
|
+ userQuestion: message.userQuestion || '',
|
|
|
|
|
+ summary: message._fullSummary || message.summary || ''
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
const contentData = {
|
|
const contentData = {
|
|
|
reports: getReportsWithFullContent(message.reports),
|
|
reports: getReportsWithFullContent(message.reports),
|
|
|
webSearchRaw: message.webSearchRaw || null,
|
|
webSearchRaw: message.webSearchRaw || null,
|
|
|
// 使用完整的webSearchSummary,而不是打字机过程中的部分内容
|
|
// 使用完整的webSearchSummary,而不是打字机过程中的部分内容
|
|
|
webSearchSummary: message._fullWebSearchSummary || message.webSearchSummary || null,
|
|
webSearchSummary: message._fullWebSearchSummary || message.webSearchSummary || null,
|
|
|
hasWebSearchResults: message.hasWebSearchResults || false,
|
|
hasWebSearchResults: message.hasWebSearchResults || false,
|
|
|
|
|
+ thinkingContent: safeThinkingContent,
|
|
|
// ===== 🔧 修复:将summary也包含在content JSON中 =====
|
|
// ===== 🔧 修复:将summary也包含在content JSON中 =====
|
|
|
summary: message._fullSummary || message.summary || ''
|
|
summary: message._fullSummary || message.summary || ''
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const collectedContent = message.reports && message.reports.length > 0
|
|
const collectedContent = message.reports && message.reports.length > 0
|
|
|
? JSON.stringify(contentData)
|
|
? JSON.stringify(contentData)
|
|
|
- : (message.content || message._fullSummary || message.summary || '')
|
|
|
|
|
|
|
+ : (safeThinkingContent
|
|
|
|
|
+ ? JSON.stringify({
|
|
|
|
|
+ answer: message.content || message._fullSummary || message.summary || '',
|
|
|
|
|
+ thinkingContent: safeThinkingContent
|
|
|
|
|
+ })
|
|
|
|
|
+ : (message.content || message._fullSummary || message.summary || ''))
|
|
|
|
|
|
|
|
if (collectedContent) {
|
|
if (collectedContent) {
|
|
|
// 同时保存summary字段(作为单独字段)
|
|
// 同时保存summary字段(作为单独字段)
|
|
@@ -3268,11 +3683,13 @@ const handleSSEComplete = async () => {
|
|
|
if (aiReplyContent && aiReplyContent.trim()) {
|
|
if (aiReplyContent && aiReplyContent.trim()) {
|
|
|
console.log('📝 AI回复内容长度:', aiReplyContent.length)
|
|
console.log('📝 AI回复内容长度:', aiReplyContent.length)
|
|
|
// 获取AI相关推荐问题
|
|
// 获取AI相关推荐问题
|
|
|
- getAIRelatedQuestions(
|
|
|
|
|
- lastUserMessage.content,
|
|
|
|
|
- aiReplyContent,
|
|
|
|
|
- lastAIMessage.ai_message_id
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ if (getDatabaseDisplayReports(lastAIMessage).length === 0) {
|
|
|
|
|
+ getAIRelatedQuestions(
|
|
|
|
|
+ lastUserMessage.content,
|
|
|
|
|
+ aiReplyContent,
|
|
|
|
|
+ lastAIMessage.ai_message_id
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
} else {
|
|
} else {
|
|
|
console.warn('⚠️ AI回复内容为空,跳过推荐问题获取')
|
|
console.warn('⚠️ AI回复内容为空,跳过推荐问题获取')
|
|
|
}
|
|
}
|
|
@@ -5136,6 +5553,22 @@ onActivated(async () => {
|
|
|
.reports-list {
|
|
.reports-list {
|
|
|
margin-top: 12px;
|
|
margin-top: 12px;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ .database-report-reveal-item {
|
|
|
|
|
+ animation: database-report-fade-in 320ms ease-out both;
|
|
|
|
|
+ will-change: opacity, transform;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @keyframes database-report-fade-in {
|
|
|
|
|
+ from {
|
|
|
|
|
+ opacity: 0;
|
|
|
|
|
+ transform: translateY(8px);
|
|
|
|
|
+ }
|
|
|
|
|
+ to {
|
|
|
|
|
+ opacity: 1;
|
|
|
|
|
+ transform: translateY(0);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
// AI文本内容
|
|
// AI文本内容
|
|
|
.ai-text {
|
|
.ai-text {
|
|
@@ -6039,6 +6472,22 @@ onActivated(async () => {
|
|
|
color: #374151;
|
|
color: #374151;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+.ai-markdown-content :deep(ul),
|
|
|
|
|
+.ai-markdown-content :deep(ol) {
|
|
|
|
|
+ margin: 8px 0 10px;
|
|
|
|
|
+ padding-left: 0;
|
|
|
|
|
+ list-style: none;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+ max-width: 100%;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.ai-markdown-content :deep(li) {
|
|
|
|
|
+ margin: 6px 0;
|
|
|
|
|
+ padding-left: 0;
|
|
|
|
|
+ overflow-wrap: anywhere;
|
|
|
|
|
+ word-break: break-word;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/* 网络搜索弹窗样式 */
|
|
/* 网络搜索弹窗样式 */
|
|
|
.web-search-modal-overlay {
|
|
.web-search-modal-overlay {
|
|
|
position: fixed;
|
|
position: fixed;
|
|
@@ -6494,4 +6943,28 @@ onActivated(async () => {
|
|
|
100% { transform: rotate(360deg); }
|
|
100% { transform: rotate(360deg); }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+:deep(.brief-answer-card .ai-markdown-content ul),
|
|
|
|
|
+:deep(.brief-answer-card .ai-markdown-content ol),
|
|
|
|
|
+:deep(.brief-answer-card .ai-markdown-content div > ul),
|
|
|
|
|
+:deep(.brief-answer-card .ai-markdown-content div > ol) {
|
|
|
|
|
+ margin: 8px 0 10px !important;
|
|
|
|
|
+ padding-left: 0 !important;
|
|
|
|
|
+ list-style: none !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+:deep(.brief-answer-card .ai-markdown-content li) {
|
|
|
|
|
+ display: block !important;
|
|
|
|
|
+ margin: 6px 0 !important;
|
|
|
|
|
+ padding-left: 0 !important;
|
|
|
|
|
+ list-style: none !important;
|
|
|
|
|
+ overflow-wrap: anywhere !important;
|
|
|
|
|
+ word-break: break-word !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+:deep(.brief-answer-card .ai-markdown-content li::marker) {
|
|
|
|
|
+ content: '' !important;
|
|
|
|
|
+ display: none !important;
|
|
|
|
|
+ font-size: 0 !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
</style>
|
|
</style>
|