消息体渲染优化
This commit is contained in:
parent
d4277e003a
commit
059b93ddbb
|
@ -281,6 +281,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div ref="bottomAnchor"></div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 输入区域 -->
|
<!-- 输入区域 -->
|
||||||
|
@ -382,11 +384,12 @@ import { ref, reactive, nextTick, watch, onMounted } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import VueOfficePdf from '@vue-office/pdf'
|
import VueOfficePdf from '@vue-office/pdf'
|
||||||
import VabChart from '@/plugins/VabChart/index.vue'
|
import VabChart from '@/plugins/VabChart/index.vue'
|
||||||
|
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/vue/24/solid'
|
||||||
import { sendChatMessage, type ChatMessageResponse, type ChatMessageSendRequest, stopMessagesStream, getFilePathList, type TraceFile, type TraceData } from '@/api/chat'
|
import { sendChatMessage, type ChatMessageResponse, type ChatMessageSendRequest, stopMessagesStream, getFilePathList, type TraceFile, type TraceData } from '@/api/chat'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { throttle } from 'lodash'
|
import { throttle } from 'lodash'
|
||||||
|
|
||||||
|
const expanded = ref(false)
|
||||||
const { t, locale } = useI18n()
|
const { t, locale } = useI18n()
|
||||||
// Props 定义
|
// Props 定义
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@ -441,11 +444,12 @@ interface Message {
|
||||||
isOpenRemark?: boolean // 是否为开场白消息
|
isOpenRemark?: boolean // 是否为开场白消息
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 消息体底部锚点
|
||||||
|
const bottomAnchor = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
const messages = ref<Message[]>([])
|
const messages = ref<Message[]>([])
|
||||||
const inputMessage = ref('')
|
const inputMessage = ref('')
|
||||||
const messagesContainer = ref<HTMLElement>()
|
const messagesContainer = ref<HTMLElement | null>(null)
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
const currentTaskId = ref('')
|
const currentTaskId = ref('')
|
||||||
const showStopButton = ref(false)
|
const showStopButton = ref(false)
|
||||||
|
@ -563,7 +567,7 @@ const createNewChat = async () => {
|
||||||
|
|
||||||
// 滚动到底部显示新消息
|
// 滚动到底部显示新消息
|
||||||
await nextTick()
|
await nextTick()
|
||||||
await scrollToBottom()
|
throttledScrollToBottom()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换会话
|
// 切换会话
|
||||||
|
@ -632,6 +636,10 @@ watch(messages, (newMessages) => {
|
||||||
}
|
}
|
||||||
}, { deep: true })
|
}, { deep: true })
|
||||||
|
|
||||||
|
|
||||||
|
// 用户是否正在拖动滚动条
|
||||||
|
let isUserScrolling = false
|
||||||
|
|
||||||
// 初始化时加载会话历史
|
// 初始化时加载会话历史
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
loadChatHistory()
|
loadChatHistory()
|
||||||
|
@ -639,6 +647,15 @@ onMounted(async () => {
|
||||||
if (chatHistory.value.length === 0) {
|
if (chatHistory.value.length === 0) {
|
||||||
await createNewChat()
|
await createNewChat()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//监听用户是否拖动滚动条
|
||||||
|
messagesContainer.value?.addEventListener('scroll', () => {
|
||||||
|
const el = messagesContainer.value
|
||||||
|
if (!el) return
|
||||||
|
const threshold = 100
|
||||||
|
isUserScrolling = el.scrollHeight - el.scrollTop - el.clientHeight > threshold
|
||||||
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 解析ECharts代码块
|
// 解析ECharts代码块
|
||||||
|
@ -683,10 +700,15 @@ const formatAnswer = async (text: string) => {
|
||||||
// 为表格添加容器包装和强制样式属性
|
// 为表格添加容器包装和强制样式属性
|
||||||
const wrappedHtml = html.replace(
|
const wrappedHtml = html.replace(
|
||||||
/<table>/g,
|
/<table>/g,
|
||||||
'<div class="table-container"><table class="excel-table" border="1" width="auto" cellspacing="0" cellpadding="0">'
|
`<details class="table-wrapper" open>
|
||||||
|
<summary>
|
||||||
|
Expand / Collapse table
|
||||||
|
</summary>
|
||||||
|
<div class="table-container"><table class="excel-table" border="1" width="auto" cellspacing="0" cellpadding="0">`
|
||||||
).replace(
|
).replace(
|
||||||
/<\/table>/g,
|
/<\/table>/g,
|
||||||
'</table></div>'
|
'</table></div></details>'
|
||||||
|
|
||||||
).replace(
|
).replace(
|
||||||
/<th>/g,
|
/<th>/g,
|
||||||
'<th style="border: 1px solid #cbd5e1; padding: 12px; background: #f1f5f9;">'
|
'<th style="border: 1px solid #cbd5e1; padding: 12px; background: #f1f5f9;">'
|
||||||
|
@ -700,9 +722,9 @@ const formatAnswer = async (text: string) => {
|
||||||
'strong', 'em', 'code', 'pre', 'a', 'br', 'p',
|
'strong', 'em', 'code', 'pre', 'a', 'br', 'p',
|
||||||
'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'span',
|
'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'span',
|
||||||
'blockquote', 'hr', 'table', 'thead', 'tbody',
|
'blockquote', 'hr', 'table', 'thead', 'tbody',
|
||||||
'tr', 'th', 'td', 'div'
|
'tr', 'th', 'td', 'div', 'details', 'summary'
|
||||||
],
|
],
|
||||||
ALLOWED_ATTR: ['href', 'target', 'class', 'style', 'data-chart-id', 'border', 'cellspacing', 'cellpadding']
|
ALLOWED_ATTR: ['href', 'target', 'class', 'style', 'data-chart-id', 'border', 'cellspacing', 'cellpadding', 'open']
|
||||||
}) as string
|
}) as string
|
||||||
|
|
||||||
console.log('Markdown输入:', processedText)
|
console.log('Markdown输入:', processedText)
|
||||||
|
@ -801,9 +823,6 @@ const sendMessage = async () => {
|
||||||
currentTaskId.value = data.taskId
|
currentTaskId.value = data.taskId
|
||||||
}
|
}
|
||||||
|
|
||||||
// messages.value = [...messages.value]
|
|
||||||
// botMessage.text += data.answer
|
|
||||||
// await scrollToBottom()
|
|
||||||
throttledScrollToBottom()
|
throttledScrollToBottom()
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -824,7 +843,6 @@ const sendMessage = async () => {
|
||||||
const formatted = await formatAnswer(parseThink(botMessage.text).answer)
|
const formatted = await formatAnswer(parseThink(botMessage.text).answer)
|
||||||
botMessage.formattedText = formatted.html
|
botMessage.formattedText = formatted.html
|
||||||
botMessage.echarts = formatted.echarts
|
botMessage.echarts = formatted.echarts
|
||||||
// messages.value = [...messages.value]
|
|
||||||
|
|
||||||
if (data.conversationId) {
|
if (data.conversationId) {
|
||||||
newConversationId = data.conversationId
|
newConversationId = data.conversationId
|
||||||
|
@ -852,7 +870,6 @@ const sendMessage = async () => {
|
||||||
botMessage.sources = data
|
botMessage.sources = data
|
||||||
botMessage.conversationId = newConversationId
|
botMessage.conversationId = newConversationId
|
||||||
botMessage.messageId = newMessageId
|
botMessage.messageId = newMessageId
|
||||||
// messages.value = [...messages.value]
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('获取文件来源失败:', error)
|
console.log('获取文件来源失败:', error)
|
||||||
|
@ -885,20 +902,35 @@ const sendMessage = async () => {
|
||||||
if (messages.value[messages.value.length - 1]?.isLoading) {
|
if (messages.value[messages.value.length - 1]?.isLoading) {
|
||||||
messages.value[messages.value.length - 1].isLoading = false
|
messages.value[messages.value.length - 1].isLoading = false
|
||||||
}
|
}
|
||||||
await scrollToBottom()
|
scrollToBottomForce()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrollToBottom = async () => {
|
const scrollToBottomForce = async () => {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
if (messagesContainer.value) {
|
if (messagesContainer.value) {
|
||||||
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
|
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//将锚点滚动到最底部
|
||||||
|
const scrollToBottom = async () => {
|
||||||
|
await nextTick()
|
||||||
|
setTimeout(() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (messagesContainer.value && !isUserScrolling) {
|
||||||
|
bottomAnchor.value.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'start'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},60)
|
||||||
|
}
|
||||||
|
|
||||||
const throttledScrollToBottom = throttle(() => {
|
const throttledScrollToBottom = throttle(() => {
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
}, 150)
|
}, 300,{ leading: true, trailing: true })
|
||||||
|
|
||||||
// 解析方法
|
// 解析方法
|
||||||
const parseThink = (text: string) => {
|
const parseThink = (text: string) => {
|
||||||
|
@ -1227,7 +1259,7 @@ const regenerateResponse = async (userQuery: string) => {
|
||||||
formattedText: ''
|
formattedText: ''
|
||||||
})
|
})
|
||||||
messages.value.push(botMessage)
|
messages.value.push(botMessage)
|
||||||
await scrollToBottom()
|
throttledScrollToBottom()
|
||||||
|
|
||||||
// 获取当前会话的会话ID
|
// 获取当前会话的会话ID
|
||||||
const currentSession = chatHistory.value[currentChatIndex.value]
|
const currentSession = chatHistory.value[currentChatIndex.value]
|
||||||
|
@ -1287,8 +1319,7 @@ const regenerateResponse = async (userQuery: string) => {
|
||||||
currentTaskId.value = data.taskId
|
currentTaskId.value = data.taskId
|
||||||
}
|
}
|
||||||
|
|
||||||
// messages.value = [...messages.value]
|
throttledScrollToBottom()
|
||||||
await scrollToBottom()
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('解析错误:', e)
|
console.error('解析错误:', e)
|
||||||
|
@ -1308,7 +1339,6 @@ const regenerateResponse = async (userQuery: string) => {
|
||||||
const formatted = await formatAnswer(parseThink(botMessage.text).answer)
|
const formatted = await formatAnswer(parseThink(botMessage.text).answer)
|
||||||
botMessage.formattedText = formatted.html
|
botMessage.formattedText = formatted.html
|
||||||
botMessage.echarts = formatted.echarts
|
botMessage.echarts = formatted.echarts
|
||||||
// messages.value = [...messages.value]
|
|
||||||
|
|
||||||
if (data.conversationId) {
|
if (data.conversationId) {
|
||||||
newConversationId = data.conversationId
|
newConversationId = data.conversationId
|
||||||
|
@ -1336,7 +1366,6 @@ const regenerateResponse = async (userQuery: string) => {
|
||||||
botMessage.sources = data
|
botMessage.sources = data
|
||||||
botMessage.conversationId = newConversationId
|
botMessage.conversationId = newConversationId
|
||||||
botMessage.messageId = newMessageId
|
botMessage.messageId = newMessageId
|
||||||
// messages.value = [...messages.value]
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('获取文件来源失败:', error)
|
console.log('获取文件来源失败:', error)
|
||||||
|
@ -1369,7 +1398,7 @@ const regenerateResponse = async (userQuery: string) => {
|
||||||
if (messages.value[messages.value.length - 1]?.isLoading) {
|
if (messages.value[messages.value.length - 1]?.isLoading) {
|
||||||
messages.value[messages.value.length - 1].isLoading = false
|
messages.value[messages.value.length - 1].isLoading = false
|
||||||
}
|
}
|
||||||
await scrollToBottom()
|
scrollToBottomForce()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1399,7 +1428,7 @@ const handleRecommendQuestionClick = (question: string) => {
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.chat-container {
|
.chat-container {
|
||||||
height: 90%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -2130,6 +2159,20 @@ const handleRecommendQuestionClick = (question: string) => {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::v-deep .table-wrapper summary {
|
||||||
|
cursor: pointer;
|
||||||
|
display: list-item;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: #f8fafc;
|
||||||
|
/* border: 1px solid #e2e8f0; */
|
||||||
|
/* border-radius: 6px; */
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* .excel-table th,
|
/* .excel-table th,
|
||||||
.excel-table td {
|
.excel-table td {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
@ -2705,6 +2748,14 @@ const handleRecommendQuestionClick = (question: string) => {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::v-deep .icon-chevron {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
color: #333; /* 让箭头颜色和文字颜色一致 */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.chart-wrapper {
|
.chart-wrapper {
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
|
|
Loading…
Reference in New Issue