AI问答组件

import {
defineComponent,
reactive,
ref,
onMounted,
onUnmounted,
nextTick
} from '@vue/composition-api'
// import { useSelector } from '@/utils/bhooks'
import { marked } from 'marked'
import request from '@/utils/request'
import { getHistoryMessages, getConversationMessages } from '@/services/newProductPerformance'
import HistoryPopup from './HistoryPopup'
import './index.scss'
import { trackView } from '../../pages/commandCenter/utils/sensors'

// 配置 marked (4.x 兼容移动端)
marked.setOptions({
breaks: true,
gfm: true
})

// 性能日志 - 记录关键性能指标(生产测试环境都显示)
const perfLog = (...args: any[]) => {
console.log(...args)
}
// 数据日志 - 记录分析数据和最终数据(生产测试环境都显示)
const dataLog = (...args: any[]) => {
console.log(...args)
}

interface Message {
id: string
type: 'user' | 'ai'
content: string
timestamp: number
options?: string[] // 选项列表
streaming?: boolean // 是否正在流式输出
thinkingProcess?: string // 思考过程(保留兼容)
thinkingTime?: number // 思考用时(秒)
showThinking?: boolean // 是否展开思考过程
thinkingStartTime?: number // 开始思考的时间戳
technicalData?: string // 技术数据(outputs.result)
finalResult?: string // 最终结果(outputs.answer)
typeWriterTimer?: any // 打字机效果定时器
}

interface HistoryMessage {
id: string
conversation_id: string
query: string
answer: string
created_at: number
status: string
}

interface HistorySession {
id: string
title: string
lastMessage: string
timestamp: number
}

interface AIRequestBody {
inputs: {
user_query: string
}
conversation_id: string
query: string
files: Array<{
transfer_method: string
upload_file_id: string
type: string
url: string
}>
user: string
}

export default defineComponent({
name: 'AIChat',
props: {
visible: {
type: Boolean,
default: false
},
handleClose: {
type: Function,
default: undefined
}
},
setup(props) {
// const { op } = useSelector(({ op }) => ({ op }))

const state = reactive<{
messages: Message[]
inputValue: string
loading: boolean
copiedId: string | null
showHistory: boolean
historySessions: HistorySession[]
conversationId: string
historyMessagesMap: Record<string, HistoryMessage>
showQuickQuestions: boolean
}>({
messages: [],
inputValue: '',
loading: false,
copiedId: null,
showHistory: false,
conversationId: '',
historySessions: [],
historyMessagesMap: {},
showQuickQuestions: true
})

const chatContainer = ref()
const abortControllerRef = ref<AbortController | null>(null)
const timeoutAbortFlag = ref(false) // 标记是否是超时中断
const userAbortFlag = ref(false) // 标记是否是用户主动中断(点击新建对话)
const userScrolled = ref(false) // 标记用户是否手动滚动
const lastScrollTop = ref(0) // 记录上次滚动位置
const lastScrollHeight = ref(0) // 记录上次滚动时的内容高度

// 流式渲染优化 - 为每个请求创建独立的节流器
const throttleTimers = new Map<string, any>()
const pendingContents = new Map<string, string>()
const markdownCache = new Map<string, string>() // Markdown解析缓存
const renderCache = new Map<string, any>() // 渲染结果缓存(非响应式)

// 滚动节流(使用节流而不是防抖,减少触发频率)
let scrollTimeout: any = null
let lastScrollTime = 0 // 上次滚动时间

// 复制消息内容
const copyMessage = (content: string, messageId: string) => {
// 方法1:尝试使用 Clipboard API
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard
.writeText(content)
.then(() => {
state.copiedId = messageId
setTimeout(() => {
state.copiedId = null
}, 2000)
})
.catch(() => {
// 降级到方法2
fallbackCopy(content, messageId)
})
} else {
// 直接使用降级方案
fallbackCopy(content, messageId)
}
}

// 降级复制方案
const fallbackCopy = (content: string, messageId: string) => {
const textarea = document.createElement('textarea')
textarea.value = content
textarea.style.position = 'fixed'
textarea.style.opacity = '0'

// 检查 document.body 是否存在
if (!document.body) {
return
}

document.body.appendChild(textarea)
textarea.select()

try {
const success = document.execCommand('copy')
if (success) {
state.copiedId = messageId
setTimeout(() => {
state.copiedId = null
}, 2000)
}
} catch (err) {
// 复制失败,静默处理
} finally {
// 确保元素存在再移除
if (document.body.contains(textarea)) {
document.body.removeChild(textarea)
}
}
}

// 刷新消息(重新生成)
const refreshMessage = async (messageId: string) => {
const messageIndex = state.messages.findIndex((m) => m.id === messageId)
if (messageIndex === -1 || messageIndex === 0) return

// 获取用户的问题(前一条消息)
const userMessage = state.messages[messageIndex - 1]
if (!userMessage || userMessage.type !== 'user') return

// 确保不是中断状态,允许正常刷新
userAbortFlag.value = false

// 删除旧消息
state.messages.splice(messageIndex, 1)
state.loading = true

try {
// 确保消息ID唯一性,使用更高精度时间戳
const newMessageId = ${Date.now()}-${Math.random().toString(36).substr(2, 9)}-${performance.now().toString().replace('.', '')}
let messageCreated = false
const thinkingStartTime = Date.now()

// 流式更新函数 - 立即逐字显示,类似DeepSeek的流畅效果
const throttledUpdate = (chunk: string) => {
// 使用 requestAnimationFrame 优化更新,提高流畅度
requestAnimationFrame(() => {
if (!messageCreated) {
// 双重检查:确保消息不存在才创建,避免重复
const existingMessage = state.messages.find((m) => m.id === newMessageId)
if (existingMessage) {
// 消息已存在,直接更新
if (existingMessage.typeWriterTimer) {
clearTimeout(existingMessage.typeWriterTimer)
existingMessage.typeWriterTimer = null
}
existingMessage.content = chunk
existingMessage.finalResult = chunk
messageCreated = true
return
}

const newAiMessage: Message = {
id: newMessageId,
type: 'ai',
content: chunk,
timestamp: Date.now(),
streaming: true,
showThinking: true,
thinkingStartTime,
finalResult: chunk
}
state.messages.splice(messageIndex, 0, newAiMessage)
messageCreated = true
// 创建流式消息后立即隐藏loading,避免重复显示
state.loading = false
scrollToBottom(false, true)
} else {
const message = state.messages.find(
(m) => m.id === newMessageId
)
if (message) {
// 清理定时器
if (message.typeWriterTimer) {
clearTimeout(message.typeWriterTimer)
message.typeWriterTimer = null
}
// 直接更新,立即显示
message.content = chunk
message.finalResult = chunk
}
}
})
}

const result = await callAIApiStream(
userMessage.content,
throttledUpdate,
(errorContent) => {
// 如果是用户主动中断(点击新建对话),不更新错误信息
if (userAbortFlag.value) {
return
}

// 错误处理:立即结束流式状态
const streamingMessage = state.messages.find(m => m.type === 'ai' && m.streaming)
if (streamingMessage) {
streamingMessage.streaming = false
streamingMessage.content = errorContent
}
}
)

// 查找已存在的消息(可能在流式更新时已创建)
let message = state.messages.find((m) => m.id === newMessageId)
const thinkingTime = result?.thinkingTime || 0

// 如果消息已创建(messageCreated为true)或找到了消息,就更新它,不要创建新的
if (messageCreated || message) {
// 如果messageCreated为true但找不到消息,尝试再次查找(可能Vue响应式更新延迟)
if (!message && messageCreated) {
message = state.messages.find((m) => m.id === newMessageId)
}

if (message) {
// 更新已存在的消息
message.streaming = false
message.thinkingTime = thinkingTime
message.technicalData = result?.technicalData || ''
message.finalResult = result?.finalResult || ''

// 清理打字机定时器
if (message.typeWriterTimer) {
clearTimeout(message.typeWriterTimer)
message.typeWriterTimer = null
}
}
// 如果messageCreated为true但找不到消息,不创建新消息,避免重复
} else {
// 只有在既没有创建也没有找到消息时,才创建新消息(超时等情况)
if (result?.content) {
const newAiMessage: Message = {
id: newMessageId,
type: 'ai',
content: result.content,
timestamp: Date.now(),
streaming: false,
thinkingTime,
showThinking: true,
thinkingStartTime,
technicalData: result.technicalData || '',
finalResult: result.finalResult || ''
}
state.messages.splice(messageIndex, 0, newAiMessage)
} else {
// 如果是用户主动中断(点击新建对话),不显示错误信息
if (userAbortFlag.value) {
return
}

const newAiMessage: Message = {
id: newMessageId,
type: 'ai',
content: '抱歉,服务暂时不可用,请稍后再试。',
timestamp: Date.now(),
streaming: false,
showThinking: true,
thinkingStartTime
}
state.messages.splice(messageIndex, 0, newAiMessage)
// 未收到任何响应,显示默认提示
}
}

// refreshMessage 中不记录性能指标(性能指标在 callAIApiStream 中记录)
} catch (error: any) {
// 刷新失败,静默处理

// 如果是用户主动中断(点击新建对话),不显示错误信息
if (userAbortFlag.value) {
return
}

// 根据错误类型提供不同的错误信息
let errorContent = '抱歉,服务暂时不可用,请稍后再试。'

if (error.message?.includes('timeout') || error.code === 'ECONNABORTED') {
errorContent = '抱歉,请求超时,请稍后重试。'
} else if (error.message?.includes('Network Error') || error.code === 'NETWORK_ERROR') {
errorContent = '抱歉,网络连接失败,请检查网络设置后重试。'
} else if (error.response?.status >= 500) {
errorContent = '抱歉,服务器暂时不可用,请稍后重试。'
} else if (error.response?.status === 401 || error.response?.status === 403) {
errorContent = '抱歉,认证失败,请重新登录后重试。'
}

const errorMessage: Message = {
id: ${Date.now()}-${Math.random().toString(36).substr(2, 9)}-${performance.now().toString().replace('.', '')},
type: 'ai',
content: errorContent,
timestamp: Date.now()
}
state.messages.splice(messageIndex, 0, errorMessage)
} finally {
state.loading = false
debugLoadingState()
scrollToBottom()
}
}

// 点赞/点踩
// const likeMessage = (messageId: string, isLike: boolean) => {
// const message = state.messages.find((m) => m.id === messageId)
// if (message) {
// // 记录反馈(实际项目中发送到后端)
// console.log(消息 ${messageId} ${isLike ? '点赞' : '点踩'})
// }
// }

// 渲染 Markdown 内容
const renderContent = (content: string, isStreaming?: boolean) => {
if (!content) return null

// 流式时统一使用纯文本显示,保持 DOM 结构一致,避免闪烁
// 流式结束后再解析 Markdown,获得完整样式
if (isStreaming) {
// 使用内容hash作为缓存key(前200字符+长度),避免内存过大
const hashKey = content.length > 200
? ${content.substring(0, 200)}_${content.length}
: content
const cacheKey = streaming_${hashKey}
const cached = renderCache.get(cacheKey)
if (cached) return cached

// 流式时统一使用纯文本,保持 DOM 结构一致
// 使用 pre-wrap 保持格式,但避免频繁解析导致的闪烁
const result = (


{content}

)
renderCache.set(cacheKey, result)
return result
}

try {
// 使用内容hash作为缓存key(简单hash,避免内存过大)
const cacheKey = content.length > 1000 ? content.substring(0, 100) + content.length : content
let html = markdownCache.get(cacheKey)

if (!html) {
html = marked(content) as string

// 为表格添加滚动容器和优化
html = html.replace(/

/gi, '
')
html = html.replace(/</table>/gi, '
')

// 优化表格列宽 - 检测数字列
html = html.replace(/<td([^>])>(\d+)</td>/gi, '<td$1 data-numeric="true">$2')
html = html.replace(/<th([^>]
)>(\d+)</th>/gi, '<th$1 data-numeric="true">$2')

// 缓存结果(限制缓存大小,避免内存泄漏)
if (markdownCache.size > 50) {
const firstKey = markdownCache.keys().next().value
markdownCache.delete(firstKey)
}
markdownCache.set(cacheKey, html)
}

const result = (


)
// 使用非响应式缓存,避免无限更新循环
if (renderCache.size > 100) {
const firstKey = renderCache.keys().next().value
renderCache.delete(firstKey)
}
renderCache.set(cacheKey, result)
return result
} catch (error) {
// Markdown 渲染错误,降级处理
// 降级:使用简单格式化显示
const lines = content.split('\n')
return (

{lines.map((line, idx) => (
<div key={idx} style={{ marginBottom: '8px' }}>
{line ||
}

))}

)
}
}

// 切换思考过程显示
const toggleThinking = (messageId: string) => {
const index = state.messages.findIndex((m) => m.id === messageId)
if (index !== -1) {
const message = state.messages[index]
// 使用 Vue 2 响应式更新
state.messages.splice(index, 1, {
...message,
showThinking: !message.showThinking
})
}
}

// 流式调用 AI 接口
const callAIApiStream = async (
question: string,
onChunk: (chunk: string) => void,
onError?: (error: string) => void
) => {
const apiPath = 'ipdHwCockpit/ai/streaming-messages'

// SSE 流式处理状态(提升到 try 外部)
let accumulatedContent = ''
let technicalData = '' // 技术数据(outputs.result)
let finalResult = '' // 最终结果(outputs.answer)
let displayedLength = 0 // 已显示的长度,用于逐字显示
let currentEvent = ''
let buffer = ''
let lastResponseLength = 0
let hasContentNode = false // 是否收到内容节点
let startTime = Date.now() // 请求开始时间
let firstCharTime = 0 // 收到第一个字符的时间
let renderStartTime = 0 // 开始渲染的时间
let timeoutCheckInterval: any = null
let typeWriterTimer: any = null // 打字机效果定时器

try {
// 清理之前的 AbortController
if (abortControllerRef.value) {
abortControllerRef.value.abort()
}
// 创建新的 AbortController
abortControllerRef.value = new AbortController()

// 重置显示状态
displayedLength = 0
accumulatedContent = ''
technicalData = ''
finalResult = ''
lastResponseLength = 0
hasContentNode = false
firstCharTime = 0
renderStartTime = 0
if (typeWriterTimer) {
clearTimeout(typeWriterTimer)
typeWriterTimer = null
}

const userId = localStorage.getItem('usercode') || 'W9065236'

// 构建请求体
const requestBody: AIRequestBody = {
inputs: {
user_query: question
},
conversation_id: state.conversationId,
query: question,
files: [
{
transfer_method: '',
upload_file_id: '',
type: '',
url: ''
}
],
user: userId
}

// 记录请求开始时间(startTime 已在外部定义,用于性能分析)

const processStreamData = (responseText: string) => {
// 只处理新增的部分
const newChunk = responseText.substring(lastResponseLength)
lastResponseLength = responseText.length
buffer += newChunk

const lines = buffer.split('\n')
buffer = lines.pop() || ''

for (const line of lines) {
const trimmedLine = line.trim()
if (!trimmedLine) continue

if (trimmedLine.startsWith('event:')) {
currentEvent = trimmedLine.substring(6).trim()
continue
}

if (trimmedLine.startsWith('data:')) {
try {
const jsonStr = trimmedLine.substring(5).trim()
if (!jsonStr || jsonStr === '[DONE]') continue

if (jsonStr.startsWith('Error:')) {
const errorMsg = jsonStr.substring(6).trim()

// 如果是用户主动中断(点击新建对话),不显示错误信息
if (userAbortFlag.value) {
return
}

// 根据错误类型提供不同的提示
if (errorMsg.includes('credentials is not initialized')) {
accumulatedContent = '抱歉,AI服务配置异常,请联系管理员检查模型凭据设置。'
} else if (errorMsg.includes('timeout') || errorMsg.includes('超时')) {
accumulatedContent = '抱歉,AI服务响应超时,请稍后重试。'
} else if (errorMsg.includes('network') || errorMsg.includes('网络')) {
accumulatedContent = '抱歉,网络连接异常,请检查网络设置后重试。'
} else if (errorMsg.includes('server') || errorMsg.includes('服务器')) {
accumulatedContent = '抱歉,服务器暂时不可用,请稍后重试。'
} else {
accumulatedContent = 服务异常:${errorMsg}
}

onChunk(accumulatedContent)
return
}

let data
try {
data = JSON.parse(jsonStr)
} catch (parseError) {
continue
}

if (data.conversation_id && !state.conversationId) {
state.conversationId = data.conversation_id
}

const eventType = currentEvent || data.event || ''

if (eventType === 'message' || eventType === 'agent_message') {
// message 事件包含最终结果,逐字显示
const answer = data.answer || ''
if (answer) {
// 记录收到第一个字符的时间
if (firstCharTime === 0 && answer.length > 0) {
firstCharTime = Date.now()
const firstCharResponseTime = firstCharTime - startTime
perfLog('⏱️ 请求到首字渲染时间:', ${firstCharResponseTime}ms)
// 记录开始渲染时间
if (renderStartTime === 0) {
renderStartTime = performance.now()
}
}

// 累积到 finalResult
finalResult += answer
accumulatedContent = finalResult

// 如果 finalResult 变长了,继续逐字显示新增部分
if (finalResult.length > displayedLength) {
// 逐字显示:从已显示位置开始,逐字符显示新内容
const getCurrentFinalResult = () => finalResult

const displayNextChunk = () => {
const currentFinalResult = getCurrentFinalResult()
const currentLength = currentFinalResult.length

// 关键:只有在有新数据时才继续显示,避免大块渲染
if (displayedLength < currentLength) {
// 检测是否包含表格,优化渲染策略
const hasTable = currentFinalResult.includes('|') || currentFinalResult.includes('<table')

// 计算未显示的数据量
const pendingLength = currentLength - displayedLength

// 动态调整显示速度:类似DeepSeek的流畅效果
let chunkSize = 1
let delay = 5 // 减少延迟,提高流畅度

if (hasTable) {
// 表格内容:根据未显示数据量动态调整
if (pendingLength > 100) {
chunkSize = 15
delay = 8
} else if (pendingLength > 50) {
chunkSize = 8
delay = 8
} else {
chunkSize = 4
delay = 10
}
} else if (currentLength > 1000) {
// 长文本:根据未显示数据量动态调整
if (pendingLength > 100) {
chunkSize = 12
delay = 5
} else if (pendingLength > 50) {
chunkSize = 6
delay = 5
} else {
chunkSize = 2
delay = 6
}
} else {
// 普通文本:根据未显示数据量动态调整,追求流畅度
if (pendingLength > 50) {
chunkSize = 8
delay = 4
} else if (pendingLength > 20) {
chunkSize = 4
delay = 4
} else {
chunkSize = 1
delay = 5
}
}

// 只显示少量字符,避免大块渲染
const nextLength = Math.min(displayedLength + chunkSize, currentLength)
const chunkToShow = currentFinalResult.substring(0, nextLength)

displayedLength = nextLength

// 使用 requestAnimationFrame 优化Vue更新,类似DeepSeek的流畅效果
requestAnimationFrame(() => {
onChunk(chunkToShow)
// 流式更新时不自动滚动,只在流式结束时滚动一次
// 避免频繁滚动导致屏幕晃动
})

// 重新获取最新的长度(可能已经增长)
const updatedLength = getCurrentFinalResult().length

// 如果还有未显示的内容,继续显示
if (displayedLength < updatedLength) {
// 清除之前的定时器(如果有),确保使用最新延迟
if (typeWriterTimer) {
clearTimeout(typeWriterTimer)
typeWriterTimer = null
}
// 继续逐字显示
typeWriterTimer = setTimeout(() => {
typeWriterTimer = null
try {
displayNextChunk()
} catch (error: any) {
// 静默处理渲染错误
}
}, delay)
} else {
// 当前已接收的数据已全部显示完
// 暂停渲染,等待新数据到达
if (typeWriterTimer) {
clearTimeout(typeWriterTimer)
typeWriterTimer = null
}
// 使用稍长的延迟检查新数据,避免频繁检查
typeWriterTimer = setTimeout(() => {
typeWriterTimer = null
try {
// 再次检查是否有新数据到达
const checkLength = getCurrentFinalResult().length
if (displayedLength < checkLength) {
// 有新数据到达,立即继续逐字显示
displayNextChunk()
} else {
// 确实没有新数据了,完全停止渲染
scrollToBottom(false, true)
}
} catch (error: any) {
// 静默处理检查错误
}
}, delay * 2) // 使用2倍延迟检查新数据
}
} else {
// 没有新数据,完全停止渲染
if (typeWriterTimer) {
clearTimeout(typeWriterTimer)
typeWriterTimer = null
}
}
}

// 如果有定时器在运行,检查它是否在等待新数据
// 如果正在等待新数据,立即清除并开始显示,避免延迟
if (typeWriterTimer) {
// 清除等待中的定时器,立即开始显示新数据
clearTimeout(typeWriterTimer)
typeWriterTimer = null
}
// 立即开始显示,确保新数据到达时能及时响应
displayNextChunk()
}
}
} else if (eventType === 'message_replace') {
accumulatedContent = data.answer || ''
onChunk(accumulatedContent)
// 替换时立即滚动
scrollToBottom(true)
} else if (eventType === 'node_finished') {
if (data.data?.status === 'succeeded' && data.data?.outputs) {
const outputs = data.data.outputs
const nodeType = data.data.node_type

// 标记是否收到内容节点
const contentNodes = [
'llm',
'tool',
'code',
'template-transform',
'http-request'
]
if (contentNodes.includes(nodeType)) {
hasContentNode = true
}

// 分别提取技术数据和最终结果
const currentTechnical = outputs.result || ''
const currentFinal = outputs.answer || ''

// 只累积技术数据,不触发显示
// 因为 message 事件已经在处理显示了,避免重复输出
if (currentTechnical) {
technicalData += currentTechnical
}
// 如果 message 事件不存在,才累积 finalResult(兼容旧接口)
// 但不再触发显示,避免与 message 事件重复
if (currentFinal && !accumulatedContent) {
finalResult += currentFinal
accumulatedContent = finalResult
}
}
} else if (eventType === 'error') {
const errorMsg = data.message || '请求失败,请稍后重试'
// 关键错误,静默处理

// 如果是用户主动中断(点击新建对话),不显示错误信息
if (userAbortFlag.value) {
return
}

// 特殊处理模型凭据未初始化错误
if (errorMsg.includes('credentials is not initialized')) {
accumulatedContent = '抱歉,AI服务配置异常,请联系管理员检查模型凭据设置。'
} else if (errorMsg.includes('Model') && errorMsg.includes('not available')) {
accumulatedContent = '抱歉,AI模型暂时不可用,请稍后重试或联系管理员。'
} else {
accumulatedContent = 抱歉,服务异常:${errorMsg}
}

onChunk(accumulatedContent)
// 错误时立即滚动到底部
scrollToBottom(false)

// 立即中断请求,防止继续处理
if (abortControllerRef.value) {
abortControllerRef.value.abort()
}

// 标记为错误状态,不再处理后续数据
hasContentNode = true

// 立即结束流式状态,防止继续显示"正在生成回答"
if (onError) {
onError(accumulatedContent)
}

return
} else if (
eventType === 'message_end' ||
eventType === 'workflow_finished'
) {
if (accumulatedContent.length === 0) {
if (!hasContentNode) {
// 后台工作流未执行到内容节点
accumulatedContent =
'抱歉,AI服务响应超时,请稍后重试或联系管理员。'
onChunk(accumulatedContent)
}
}
} else if (eventType === 'ping') {
// 静默处理
} else if (
eventType === 'node_started' ||
eventType === 'workflow_started'
) {
// 静默处理
}
} catch (e) {
// 静默处理解析错误
}
}
}
}

// 启动超时检测(300秒无内容节点则中断)
timeoutAbortFlag.value = false
timeoutCheckInterval = setInterval(() => {
const elapsed = Date.now() - startTime

// 300秒无内容节点超时
if (
elapsed > 300000 &&
!hasContentNode &&
accumulatedContent.length === 0
) {
// 300秒未收到内容节点,超时中断
timeoutAbortFlag.value = true
clearInterval(timeoutCheckInterval)
if (abortControllerRef.value) {
abortControllerRef.value.abort()
}
}

// 300秒总超时(兜底保护)
if (elapsed > 300000) {
// 300秒总超时,兜底保护
timeoutAbortFlag.value = true
clearInterval(timeoutCheckInterval)
if (abortControllerRef.value) {
abortControllerRef.value.abort()
}
}
}, 5000) // 每5秒检查一次

try {
await request.post(apiPath, requestBody, {
timeout: 300000, // AI 流式请求超时 300s
responseType: 'text',
headers: {
Accept: 'text/event-stream',
'Cache-Control': 'no-cache'
},
signal: abortControllerRef.value.signal,
onDownloadProgress: (progressEvent) => {
const response =
(progressEvent.currentTarget as any)?.response ||
(progressEvent.target as any)?.response
if (response) {
processStreamData(response)
}
}
})
} finally {
clearInterval(timeoutCheckInterval)
// 请求完成后重置 AbortController
abortControllerRef.value = null
// 注意:不清理 typeWriterTimer,让它继续完成逐字显示
}

const thinkingTime = Math.round((Date.now() - startTime) / 1000)

// 计算首字到最终渲染时间
if (firstCharTime > 0) {
const finalRenderTime = Date.now()
const firstCharToFinalTime = finalRenderTime - firstCharTime
perfLog('⏱️ 首字到最终渲染时间:', ${firstCharToFinalTime}ms)
}

// 记录分析数据和最终数据
if (technicalData || finalResult) {
dataLog('📊 分析数据 (technicalData):', technicalData)
dataLog('📝 最终数据 (finalResult):', finalResult)
}

return {
content: accumulatedContent,
thinkingTime,
technicalData,
finalResult
}
} catch (error: any) {
clearInterval(timeoutCheckInterval) // 清理定时器

const thinkingTime = Math.round((Date.now() - startTime) / 1000)

// 关键错误,静默处理

// 如果是用户主动中断(点击新建对话),不显示错误信息
if (userAbortFlag.value) {
return {
content: '',
thinkingTime,
technicalData: '',
finalResult: ''
}
}

if (error.name === 'AbortError') {
// 检查是否是超时导致的中断
if (timeoutAbortFlag.value) {
return {
content:
'抱歉,AI服务响应超时,请稍后重试或联系管理员。\n\n可能原因:后台工作流未执行到内容节点(tool/llm),请检查 Dify 工作流配置。',
thinkingTime,
technicalData: '',
finalResult: ''
}
}
return {
content: '',
thinkingTime,
technicalData: '',
finalResult: ''
}
}

// axios 超时
if (
error.code === 'ECONNABORTED' ||
error.message?.includes('timeout')
) {
// 请求超时,静默处理
return {
content: '抱歉,AI服务响应超时,请稍后重试。',
thinkingTime,
technicalData: '',
finalResult: ''
}
}

// 处理网络连接错误
if (error.code === 'NETWORK_ERROR' || error.message?.includes('Network Error')) {
// 网络连接失败,静默处理
return {
content: '抱歉,网络连接失败,请检查网络设置后重试。',
thinkingTime,
technicalData: '',
finalResult: ''
}
}

// 处理服务器错误
if (error.response?.status >= 500) {
// 服务器内部错误,静默处理
// 尝试从错误响应中提取更详细的错误信息
let errorMsg = '抱歉,服务器暂时不可用,请稍后重试。'
if (error.response.data) {
if (typeof error.response.data === 'string') {
errorMsg = 服务器错误: ${error.response.data}
} else if (error.response.data.message) {
errorMsg = 服务器错误: ${error.response.data.message}
} else if (error.response.data.error) {
errorMsg = 服务器错误: ${error.response.data.error}
}
}
return {
content: errorMsg,
thinkingTime,
technicalData: '',
finalResult: ''
}
}

// 处理认证错误
if (error.response?.status === 401 || error.response?.status === 403) {
// 认证失败,静默处理
return {
content: '抱歉,认证失败,请重新登录后重试。',
thinkingTime,
technicalData: '',
finalResult: ''
}
}

// 处理模型凭据相关错误
if (error.message?.includes('credentials is not initialized')) {
// 模型凭据未初始化,静默处理
return {
content: '抱歉,AI服务配置异常,请联系管理员检查模型凭据设置。',
thinkingTime,
technicalData: '',
finalResult: ''
}
}

// 处理请求被取消
if (error.name === 'CanceledError' || error.code === 'ERR_CANCELED') {
// 请求被取消,静默处理
return {
content: '',
thinkingTime,
technicalData: '',
finalResult: ''
}
}

// 处理其他未知错误,静默处理
return {
content: '抱歉,服务暂时不可用,请稍后再试。',
thinkingTime,
technicalData: '',
finalResult: ''
}
} finally {
// 确保清理定时器和重置状态
clearInterval(timeoutCheckInterval)
// 注意:不清理 typeWriterTimer,让它继续完成逐字显示
// 只有在用户主动中断或组件卸载时才清理
if (abortControllerRef.value) {
abortControllerRef.value = null
}
// 清理完成
}
}

// 检测用户是否手动滚动
const handleScroll = () => {
const container = chatContainer.value || document.querySelector('.chat-messages') as HTMLElement
if (!container) return

const currentScrollTop = container.scrollTop
const scrollHeight = container.scrollHeight
const clientHeight = container.clientHeight
const distanceFromBottom = scrollHeight - currentScrollTop - clientHeight

// 如果用户向上滚动(scrollTop增加且不在底部),标记为用户手动滚动
// 排除内容增长导致的自动滚动到底部的情况
if (currentScrollTop > lastScrollTop.value && currentScrollTop > 0 && distanceFromBottom > 50) {
userScrolled.value = true
}

// 如果用户滚动到底部附近(50px内),恢复自动滚动
if (distanceFromBottom <= 50) {
userScrolled.value = false
}

lastScrollTop.value = currentScrollTop
}

// 滚动到底部 - 智能滚动(使用节流减少触发频率)
const scrollToBottom = (smooth: boolean = true, force: boolean = false) => {
// 检查组件是否可见
if (!props.visible) {
return
}

// 如果用户手动滚动且不是强制滚动,则不自动滚动
if (!force && userScrolled.value) {
return
}

// 使用节流,限制滚动频率(至少间隔500ms才滚动一次)
const now = Date.now()
const throttleDelay = 500 // 节流间隔500ms,大幅减少滚动
if (!force && now - lastScrollTime < throttleDelay) {
// 如果距离上次滚动时间太短,取消本次滚动
if (scrollTimeout) {
clearTimeout(scrollTimeout)
}
// 延迟执行,确保最后一次滚动能执行
scrollTimeout = setTimeout(() => {
scrollToBottom(smooth, force)
}, throttleDelay - (now - lastScrollTime))
return
}

// 清除之前的定时器
if (scrollTimeout) {
clearTimeout(scrollTimeout)
}

// 使用 requestAnimationFrame 优化滚动性能
requestAnimationFrame(() => {
// 使用DOM查询获取滚动容器
const getScrollContainer = () => {
// 先尝试使用ref
if (chatContainer.value) {
return chatContainer.value
}

// 如果ref不存在,使用DOM查询
const container = document.querySelector('.chat-messages')
if (container) {
return container as HTMLElement
}

return null
}

// 使用更稳定的滚动方式
const container = getScrollContainer()
if (container) {
const scrollHeight = container.scrollHeight
const currentScrollTop = container.scrollTop
const clientHeight = container.clientHeight
const distanceFromBottom = scrollHeight - currentScrollTop - clientHeight

// 检测内容高度是否明显增加(至少增加200px才滚动,大幅减少触发)
// 或者距离底部超过300px时才滚动
const heightDiff = scrollHeight - lastScrollHeight.value
if (!force && heightDiff < 200 && distanceFromBottom < 300) {
// 内容增加不明显且距离底部不远,不滚动
return
}

// 强制滚动时,重置用户滚动状态
if (force) {
userScrolled.value = false
} else if (userScrolled.value) {
// 再次检查用户是否手动滚动(双重检查)
// 如果不在底部附近,不自动滚动
if (distanceFromBottom > 50) {
return
}
// 如果在底部附近,恢复自动滚动
userScrolled.value = false
}

// 流式更新时始终使用立即滚动,避免平滑滚动导致的晃动
// 只在非流式且非强制时使用平滑滚动
const isStreaming = state.messages.some(m => m.type === 'ai' && m.streaming)
if (smooth && !force && !isStreaming) {
// 平滑滚动(仅非流式时)
container.scrollTo({
top: scrollHeight,
behavior: 'smooth'
})
} else {
// 立即滚动,使用 scrollTop 避免重排和晃动
container.scrollTop = scrollHeight
}

// 更新滚动位置和高度记录
lastScrollTop.value = container.scrollTop
lastScrollHeight.value = scrollHeight
lastScrollTime = now
}
})
}

// 发送消息
const sendMessage = async () => {
if (!state.inputValue.trim() || state.loading) return

// 确保不是中断状态,允许正常提问
userAbortFlag.value = false

// 重置滚动状态,新消息时允许自动滚动
userScrolled.value = false

// 隐藏快速问题区域
state.showQuickQuestions = false

// 检查网络状态
if (!checkNetworkStatus()) {
const errorMessage: Message = {
id: ${Date.now()}-${Math.random().toString(36).substr(2, 9)}-${performance.now().toString().replace('.', '')},
type: 'ai',
content: '抱歉,网络连接已断开,请检查网络设置后重试。',
timestamp: Date.now()
}
state.messages.push(errorMessage)
return
}

const userMessage: Message = {
id: ${Date.now()}-${Math.random().toString(36).substr(2, 9)}-${performance.now().toString().replace('.', '')},
type: 'user',
content: state.inputValue.trim(),
timestamp: Date.now()
}

state.messages.push(userMessage)
const question = state.inputValue
state.inputValue = ''
state.loading = true
scrollToBottom(false)

try {
// 确保消息ID唯一性,使用更高精度时间戳
const aiMessageId = ${Date.now()}-${Math.random().toString(36).substr(2, 9)}-${performance.now().toString().replace('.', '')}
let messageCreated = false

const thinkingStartTime = Date.now()

// 流式更新函数 - 立即逐字显示,类似DeepSeek的流畅效果
const throttledUpdate = (chunk: string) => {
// 使用 requestAnimationFrame 优化更新,提高流畅度
requestAnimationFrame(() => {
if (!messageCreated) {
// 双重检查:确保消息不存在才创建,避免重复
const existingMessage = state.messages.find((m) => m.id === aiMessageId)
if (existingMessage) {
// 消息已存在,直接更新
if (existingMessage.typeWriterTimer) {
clearTimeout(existingMessage.typeWriterTimer)
existingMessage.typeWriterTimer = null
}
existingMessage.content = chunk
existingMessage.finalResult = chunk
messageCreated = true
return
}

const aiMessage: Message = {
id: aiMessageId,
type: 'ai',
content: chunk,
timestamp: Date.now(),
streaming: true,
showThinking: true,
thinkingStartTime,
finalResult: chunk
}
state.messages.push(aiMessage)
messageCreated = true
// 创建流式消息后立即隐藏loading,避免重复显示
state.loading = false
scrollToBottom(false, true)
} else {
// 更新消息内容
const message = state.messages.find((m) => m.id === aiMessageId)
if (message) {
// 清理定时器
if (message.typeWriterTimer) {
clearTimeout(message.typeWriterTimer)
message.typeWriterTimer = null
}
// 直接更新,立即显示
message.content = chunk
message.finalResult = chunk
}
}
})
}

const result = await callAIApiStream(question, throttledUpdate, (errorContent) => {
// 如果是用户主动中断(点击新建对话),不更新错误信息
if (userAbortFlag.value) {
return
}

// 错误处理:立即结束流式状态
const streamingMessage = state.messages.find(m => m.type === 'ai' && m.streaming)
if (streamingMessage) {
streamingMessage.streaming = false
streamingMessage.content = errorContent
// 流式错误:已结束流式状态
}
})

// 流式结束,标记完成
const thinkingTime = result?.thinkingTime || 0

// 数据日志已在 callAIApiStream 中记录,此处不重复记录

// 查找已存在的消息(可能在流式更新时已创建)
let message = state.messages.find((m) => m.id === aiMessageId)

// 如果消息已创建(messageCreated为true)或找到了消息,就更新它,不要创建新的
if (messageCreated || message) {
// 如果messageCreated为true但找不到消息,尝试再次查找(可能Vue响应式更新延迟)
if (!message && messageCreated) {
message = state.messages.find((m) => m.id === aiMessageId)
}

if (message) {
// 更新已存在的消息
message.streaming = false
message.thinkingTime = thinkingTime
message.technicalData = result?.technicalData || ''
message.finalResult = result?.finalResult || ''

// 清理打字机定时器
if (message.typeWriterTimer) {
clearTimeout(message.typeWriterTimer)
message.typeWriterTimer = null
}
}
// 如果messageCreated为true但找不到消息,不创建新消息,避免重复
} else {
// 只有在既没有创建也没有找到消息时,才创建新消息(超时等情况)
if (result?.content) {
const aiMessage: Message = {
id: aiMessageId,
type: 'ai',
content: result.content,
timestamp: Date.now(),
streaming: false,
thinkingTime,
showThinking: true,
thinkingStartTime,
technicalData: result.technicalData || '',
finalResult: result.finalResult || ''
}
state.messages.push(aiMessage)
} else {
// 如果是用户主动中断(点击新建对话),不显示错误信息
if (userAbortFlag.value) {
return
}

// 兜底:如果连 result 都没有,显示默认提示
const aiMessage: Message = {
id: aiMessageId,
type: 'ai',
content: '抱歉,服务暂时不可用,请稍后再试。',
timestamp: Date.now(),
streaming: false,
showThinking: true,
thinkingStartTime
}
state.messages.push(aiMessage)
// 未收到任何响应,显示默认提示
}
}

} catch (error: any) {
// 发送失败,静默处理

// 如果是用户主动中断(点击新建对话),不显示错误信息
if (userAbortFlag.value) {
return
}

// 根据错误类型提供不同的错误信息
let errorContent = '抱歉,服务暂时不可用,请稍后再试。'

if (error.message?.includes('timeout') || error.code === 'ECONNABORTED') {
errorContent = '抱歉,请求超时,请稍后重试。'
} else if (error.message?.includes('Network Error') || error.code === 'NETWORK_ERROR') {
errorContent = '抱歉,网络连接失败,请检查网络设置后重试。'
} else if (error.response?.status >= 500) {
errorContent = '抱歉,服务器暂时不可用,请稍后重试。'
} else if (error.response?.status === 401 || error.response?.status === 403) {
errorContent = '抱歉,认证失败,请重新登录后重试。'
} else if (error.message?.includes('credentials is not initialized')) {
errorContent = '抱歉,AI服务配置异常,请联系管理员检查模型凭据设置。'
}

const errorMessage: Message = {
id: ${Date.now()}-${Math.random().toString(36).substr(2, 9)}-${performance.now().toString().replace('.', '')},
type: 'ai',
content: errorContent,
timestamp: Date.now()
}
state.messages.push(errorMessage)
} finally {
state.loading = false
debugLoadingState()
scrollToBottom(false)
}
}

// 快捷问题
const quickQuestions = [
'今天吃什么',
'天气怎么样',
'你是不是很帅'
]

const askQuickQuestion = (question: string) => {
state.inputValue = question
sendMessage()
}

// 清空对话
const clearChat = () => {
// 清空对话

// 立即清空消息,避免显示任何错误信息
state.messages = []

// 立即设置用户主动中断标志,避免显示任何错误信息
userAbortFlag.value = true

// 中断正在进行的请求
if (abortControllerRef.value) {
// 中断正在进行的请求
abortControllerRef.value.abort()
abortControllerRef.value = null
}

// 清理定时器
throttleTimers.forEach(timer => {
if (timer) clearTimeout(timer)
})
throttleTimers.clear()
pendingContents.clear()

// 清理滚动防抖定时器
if (scrollTimeout) {
clearTimeout(scrollTimeout)
scrollTimeout = null
}

// 重置状态
state.conversationId = ''
state.loading = false
state.showQuickQuestions = true // 显示快速问题区域

// 重置滚动状态
userScrolled.value = false
lastScrollTop.value = 0

// 延迟重置中断标志,确保所有异步操作都能检测到
setTimeout(() => {
userAbortFlag.value = false
// 对话清空完成
}, 200)

scrollToBottom(false, true) // 强制滚动到底部
}

// 打开历史记录
const openHistory = async () => {
state.showHistory = true

const userId = localStorage.getItem('usercode') || 'W9065236'
// if (!state.conversationId) return

try {
const res = await getHistoryMessages({
user: userId,
limit: 50,
// sort_by:'-updated_at'
})

// 检查数据结构 - 可能是直接返回数组,而不是 {data: []} 格式
let dataArray: any[] | null = null
if (Array.isArray(res?.data)) {
dataArray = res.data
} else if (res?.data?.data && Array.isArray(res.data.data)) {
dataArray = res.data.data
}

if (dataArray && dataArray.length > 0) {
// 存储完整会话数据
state.historyMessagesMap = {}
dataArray.forEach((item: any) => {
state.historyMessagesMap[item.id] = item
})

state.historySessions = dataArray.map((item: any) => ({
id: item.id,
title:
item.name.slice(0, 30) + (item.name.length > 30 ? '...' : ''),
lastMessage: item.introduction || '',
timestamp: item.updated_at > 1000000000000 ? item.updated_at : item.updated_at * 1000
}))
} else {
// 历史记录数据为空
}
} catch (error) {
// 获取历史记录失败,静默处理
}
}

// 关闭历史记录
const closeHistory = () => {
state.showHistory = false
}

// 加载历史会话
const loadHistorySession = async (sessionId: string) => {
const userId = localStorage.getItem('usercode') || 'W9065236'

// 确保不是中断状态,允许正常加载历史
userAbortFlag.value = false

// 隐藏快速问题区域
state.showQuickQuestions = false

try {
// 设置当前会话ID
state.conversationId = sessionId

// 加载历史会话

// 先关闭历史弹窗
closeHistory()

// 清空当前消息
state.messages = []
state.loading = true

// 调用接口获取历史消息列表
const res = await getConversationMessages({
conversation_id: sessionId,
user: userId,
limit: 20
})

// 检查数据结构
let messagesData: any[] = []
if (Array.isArray(res?.data?.data)) {
messagesData = res.data.data
} else if (Array.isArray(res?.data)) {
messagesData = res.data
}

if (messagesData && messagesData.length > 0) {
// 将接口返回的消息转换为 Message 格式
const formattedMessages: Message[] = []

messagesData.forEach((item: any) => {
// 转换时间戳:如果是秒级则转为毫秒,并加8小时转换为本地时间
const timezoneOffset = 8 * 60 * 60 * 1000 // 8小时时区偏移
const timestamp = (item.created_at > 1000000000000
? item.created_at
: item.created_at * 1000) + timezoneOffset

// 用户消息(query)
if (item.query) {
formattedMessages.push({
id: ${item.id}-user,
type: 'user',
content: item.query,
timestamp
})
}

// AI消息(answer)
if (item.answer) {
formattedMessages.push({
id: ${item.id}-ai,
type: 'ai',
content: item.answer,
timestamp,
streaming: false,
showThinking: true,
finalResult: item.answer
})
}
})

// 按时间排序,确保消息顺序正确
formattedMessages.sort((a, b) => a.timestamp - b.timestamp)

// 添加到消息列表
state.messages = formattedMessages
// 加载历史消息成功
} else {
// 历史消息数据为空
}

nextTick(() => {
scrollToBottom(false)
})
} catch (error: any) {
// 加载历史会话失败,静默处理

// 如果是用户主动中断(点击新建对话),不显示错误信息
if (userAbortFlag.value) {
return
}

// 根据错误类型提供不同的错误信息
let errorContent = '抱歉,无法加载历史消息,请稍后再试。'

if (error.message?.includes('timeout') || error.code === 'ECONNABORTED') {
errorContent = '抱歉,请求超时,请稍后重试。'
} else if (error.message?.includes('Network Error') || error.code === 'NETWORK_ERROR') {
errorContent = '抱歉,网络连接失败,请检查网络设置后重试。'
} else if (error.response?.status >= 500) {
errorContent = '抱歉,服务器暂时不可用,请稍后重试。'
} else if (error.response?.status === 401 || error.response?.status === 403) {
errorContent = '抱歉,认证失败,请重新登录后重试。'
}

const errorMessage: Message = {
id: ${Date.now()}-${Math.random().toString(36).substr(2, 9)}-${performance.now().toString().replace('.', '')},
type: 'ai',
content: errorContent,
timestamp: Date.now()
}
state.messages.push(errorMessage)
} finally {
state.loading = false
debugLoadingState()
}
}

// 处理回车发送
const handleKeyPress = (event: KeyboardEvent) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault()
sendMessage()
}
}

// 调试函数:检查loading状态(已禁用日志)
const debugLoadingState = () => {
// 静默处理
}

// 重试机制(预留功能)
// const retryWithBackoff = async (
// fn: () => Promise,
// maxRetries: number = 3,
// baseDelay: number = 1000
// ) => {
// for (let attempt = 1; attempt <= maxRetries; attempt++) {
// try {
// return await fn()
// } catch (error: any) {
// console.warn(⚠️ 第${attempt}次尝试失败:, error.message)
//
// // 如果是最后一次尝试,抛出错误
// if (attempt === maxRetries) {
// throw error
// }
//
// // 指数退避延迟
// const delay = baseDelay * Math.pow(2, attempt - 1)
// console.log(⏳ ${delay}ms后重试...)
// await new Promise(resolve => setTimeout(resolve, delay))
// }
// }
// }

// 清理函数
const cleanup = () => {
// 清理所有定时器
throttleTimers.forEach(timer => {
if (timer) clearTimeout(timer)
})
throttleTimers.clear()
pendingContents.clear()
markdownCache.clear() // 清理Markdown缓存
renderCache.clear() // 清理渲染缓存

// 清理滚动防抖定时器
if (scrollTimeout) {
clearTimeout(scrollTimeout)
scrollTimeout = null
}

// 清理 AbortController
if (abortControllerRef.value) {
abortControllerRef.value.abort()
abortControllerRef.value = null
}

// 强制重置 loading 状态
state.loading = false
}

// 网络状态检测
const checkNetworkStatus = () => {
if (!navigator.onLine) {
// 网络离线
return false
}
return true
}

// 网络状态变化监听
const handleOnline = () => {
// 网络已连接
}

const handleOffline = () => {
// 网络已断开
// 如果正在加载,显示网络错误
if (state.loading) {
state.loading = false
const errorMessage: Message = {
id: ${Date.now()}-${Math.random().toString(36).substr(2, 9)}-${performance.now().toString().replace('.', '')},
type: 'ai',
content: '抱歉,网络连接已断开,请检查网络设置后重试。',
timestamp: Date.now()
}
state.messages.push(errorMessage)
}
}

onMounted(() => {
// 延迟滚动,确保组件完全渲染
setTimeout(() => {
scrollToBottom(false, true) // 强制滚动到底部
}, 100)

// 绑定滚动事件监听器
nextTick(() => {
const container = chatContainer.value || document.querySelector('.chat-messages')
if (container) {
container.addEventListener('scroll', handleScroll, { passive: true })
}
})

// 监听网络状态
window.addEventListener('online', handleOnline)
window.addEventListener('offline', handleOffline)
trackView({
level2Directory: 'AI智能报告',
level3Directory: 'AI智能报告'
})
})

onUnmounted(() => {
cleanup()

// 移除滚动事件监听器
const container = chatContainer.value || document.querySelector('.chat-messages')
if (container) {
container.removeEventListener('scroll', handleScroll)
}

// 移除网络状态监听
window.removeEventListener('online', handleOnline)
window.removeEventListener('offline', handleOffline)
})

const handleClose = () => {
// 中断正在进行的请求(避免资源浪费)
if (abortControllerRef.value) {
abortControllerRef.value.abort()
abortControllerRef.value = null
}

// 清理定时器(避免内存泄漏)
throttleTimers.forEach(timer => {
if (timer) clearTimeout(timer)
})
throttleTimers.clear()
pendingContents.clear()

if (scrollTimeout) {
clearTimeout(scrollTimeout)
scrollTimeout = null
}

// 重置loading状态
state.loading = false

// 关闭弹窗
if (props.handleClose) {
props.handleClose()
}
}

return () => {
if (!props.visible) return null

return (







{/* 你好, /}
你好,我是你的AI质量助手

{/
基于当前数据上下文的智能问答
*/}






{state.showQuickQuestions ? (





我是
AI质量助手

质量最亲密的AI工作搭子,问答无顾虑


你可以对我说:


{quickQuestions.map((question: string, index: number) => (
<div
key={index}
class="quick-question-item"
onClick={() => askQuickQuestion(question)}>
💬
{question}

))}



{/*


🤖


/}


) : (

{state.messages.map((message) => (
<div key={message.id} class={message ${message.type}}>
{message.type === 'ai' && (
🤖AI分析助手

)}

{/
AI 消息统一使用深度思考模式 /}
{message.type === 'ai' ? (

{/
流式时只显示最终结果,不显示思考过程 /}
{message.streaming ? (

{message.finalResult ? (
renderContent(message.finalResult, true)
) : null}

) : (

<div
class="thinking-header"
onClick={() => toggleThinking(message.id)}>


已深度思考{message.thinkingTime ? (${message.thinkingTime}秒) : ''}


{message.showThinking ? '▼' : '▶'}


{message.showThinking && (

{/
非流式时才显示技术数据 /}

{message.technicalData || message.content}


)}
{/
最终结果显示 */}

{renderContent(message.finalResult || message.content, false)}


)}

) : (
{message.content}

)}
{message.options && message.options.length > 0 && (

{message.options.map((option: string, idx: number) => (
<button
key={idx}
class="option-btn"
onClick={() => askQuickQuestion(option)}>
{option}

))}

)}



))}

{state.loading && !state.messages.some(m => m.type === 'ai' && m.streaming) && (


🤖质量分析助手





AI正在思考中









)}

)}


<input
value={state.inputValue}
onInput={(e: any) => {
state.inputValue = e.target.value
}}
placeholder="请输入你的问题..."
onKeypress={handleKeyPress}
disabled={state.loading}
/>
<button
class="send-btn"
onClick={sendMessage}
disabled={state.loading || !state.inputValue.trim()}>
发送

{/* 历史记录弹窗 */}



)
}
}
})

posted @ 2025-12-16 17:08  XiaoZhengTou  阅读(53)  评论(0)    收藏  举报