diff --git a/chat-client/src/views/chatweb/components/ChatBox.vue b/chat-client/src/views/chatweb/components/ChatBox.vue
index dc8dfcc..69b4df4 100644
--- a/chat-client/src/views/chatweb/components/ChatBox.vue
+++ b/chat-client/src/views/chatweb/components/ChatBox.vue
@@ -281,6 +281,8 @@
+
+
@@ -382,11 +384,12 @@ import { ref, reactive, nextTick, watch, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import VueOfficePdf from '@vue-office/pdf'
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 { useI18n } from 'vue-i18n'
import { throttle } from 'lodash'
+const expanded = ref(false)
const { t, locale } = useI18n()
// Props 定义
interface Props {
@@ -441,11 +444,12 @@ interface Message {
isOpenRemark?: boolean // 是否为开场白消息
}
-
+// 消息体底部锚点
+const bottomAnchor = ref(null)
const messages = ref([])
const inputMessage = ref('')
-const messagesContainer = ref()
+const messagesContainer = ref(null)
const isLoading = ref(false)
const currentTaskId = ref('')
const showStopButton = ref(false)
@@ -563,7 +567,7 @@ const createNewChat = async () => {
// 滚动到底部显示新消息
await nextTick()
- await scrollToBottom()
+ throttledScrollToBottom()
}
// 切换会话
@@ -632,6 +636,10 @@ watch(messages, (newMessages) => {
}
}, { deep: true })
+
+// 用户是否正在拖动滚动条
+let isUserScrolling = false
+
// 初始化时加载会话历史
onMounted(async () => {
loadChatHistory()
@@ -639,6 +647,15 @@ onMounted(async () => {
if (chatHistory.value.length === 0) {
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代码块
@@ -683,10 +700,15 @@ const formatAnswer = async (text: string) => {
// 为表格添加容器包装和强制样式属性
const wrappedHtml = html.replace(
//g,
- ''
+ `
+
+ Expand / Collapse table
+
+ `
).replace(
- /<\/table>/g,
- '
'
+ /<\/table>/g,
+ '
'
+
).replace(
//g,
' | '
@@ -700,9 +722,9 @@ const formatAnswer = async (text: string) => {
'strong', 'em', 'code', 'pre', 'a', 'br', 'p',
'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'span',
'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
console.log('Markdown输入:', processedText)
@@ -801,9 +823,6 @@ const sendMessage = async () => {
currentTaskId.value = data.taskId
}
- // messages.value = [...messages.value]
- // botMessage.text += data.answer
- // await scrollToBottom()
throttledScrollToBottom()
}
} catch (e) {
@@ -824,7 +843,6 @@ const sendMessage = async () => {
const formatted = await formatAnswer(parseThink(botMessage.text).answer)
botMessage.formattedText = formatted.html
botMessage.echarts = formatted.echarts
- // messages.value = [...messages.value]
if (data.conversationId) {
newConversationId = data.conversationId
@@ -852,7 +870,6 @@ const sendMessage = async () => {
botMessage.sources = data
botMessage.conversationId = newConversationId
botMessage.messageId = newMessageId
- // messages.value = [...messages.value]
}
} catch (error) {
console.log('获取文件来源失败:', error)
@@ -885,20 +902,35 @@ const sendMessage = async () => {
if (messages.value[messages.value.length - 1]?.isLoading) {
messages.value[messages.value.length - 1].isLoading = false
}
- await scrollToBottom()
+ scrollToBottomForce()
}
}
-const scrollToBottom = async () => {
+const scrollToBottomForce = async () => {
await nextTick()
if (messagesContainer.value) {
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(() => {
- scrollToBottom()
-}, 150)
+ scrollToBottom()
+}, 300,{ leading: true, trailing: true })
// 解析方法
const parseThink = (text: string) => {
@@ -1227,7 +1259,7 @@ const regenerateResponse = async (userQuery: string) => {
formattedText: ''
})
messages.value.push(botMessage)
- await scrollToBottom()
+ throttledScrollToBottom()
// 获取当前会话的会话ID
const currentSession = chatHistory.value[currentChatIndex.value]
@@ -1287,8 +1319,7 @@ const regenerateResponse = async (userQuery: string) => {
currentTaskId.value = data.taskId
}
- // messages.value = [...messages.value]
- await scrollToBottom()
+ throttledScrollToBottom()
}
} catch (e) {
console.error('解析错误:', e)
@@ -1308,7 +1339,6 @@ const regenerateResponse = async (userQuery: string) => {
const formatted = await formatAnswer(parseThink(botMessage.text).answer)
botMessage.formattedText = formatted.html
botMessage.echarts = formatted.echarts
- // messages.value = [...messages.value]
if (data.conversationId) {
newConversationId = data.conversationId
@@ -1336,7 +1366,6 @@ const regenerateResponse = async (userQuery: string) => {
botMessage.sources = data
botMessage.conversationId = newConversationId
botMessage.messageId = newMessageId
- // messages.value = [...messages.value]
}
} catch (error) {
console.log('获取文件来源失败:', error)
@@ -1369,7 +1398,7 @@ const regenerateResponse = async (userQuery: string) => {
if (messages.value[messages.value.length - 1]?.isLoading) {
messages.value[messages.value.length - 1].isLoading = false
}
- await scrollToBottom()
+ scrollToBottomForce()
}
}
@@ -1399,7 +1428,7 @@ const handleRecommendQuestionClick = (question: string) => {
|