|
|
@@ -7,7 +7,16 @@
|
|
|
<!-- 消息列表 -->
|
|
|
<div class="message-list" v-else>
|
|
|
<el-scrollbar ref="scrollbar" @scroll="handleScroll">
|
|
|
- <div class="messages">
|
|
|
+ <!-- 加载更多指示器 -->
|
|
|
+ <div v-if="isLoadingMore" class="loading-more-indicator">
|
|
|
+ <div class="loading-spinner"></div>
|
|
|
+ <span>加载更多消息...</span>
|
|
|
+ </div>
|
|
|
+ // 添加一个 ref 用于获取消息列表容器
|
|
|
+ const messagesContainer = ref(null)
|
|
|
+
|
|
|
+ // 修改模板中的消息列表 div,添加 ref
|
|
|
+ <div class="messages" ref="messagesContainer">
|
|
|
<div v-for="(message, index) in messages" :key="index"
|
|
|
:class="['message-item', message.role === 'user' ? 'self' : 'other']">
|
|
|
<el-avatar :size="32" :src="message.role === 'user' ? userAvatar : avatar" />
|
|
|
@@ -32,7 +41,7 @@
|
|
|
<span class="dot"></span>
|
|
|
</span>
|
|
|
</div>
|
|
|
- <div class="timestamp ">{{ moment(message.timestamp).format('MM-DD HH:mm') }}
|
|
|
+ <div class="timestamp ">{{ moment(message.sortKey).format('MM-DD HH:mm') }}
|
|
|
<span v-if="message.add" style="cursor: pointer;" @click="handleInput">填充</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -100,7 +109,6 @@
|
|
|
import { ref, onMounted, nextTick, inject, useTemplateRef, reactive } from 'vue'
|
|
|
import { ElScrollbar, ElAvatar, ElInput, ElButton } from 'element-plus'
|
|
|
import moment from 'moment'
|
|
|
-import { cloneDeep } from 'lodash'
|
|
|
import fileLogo from '@/assets/svg/file.svg'
|
|
|
import {
|
|
|
buildExcelUnderstandingPrompt,
|
|
|
@@ -125,8 +133,14 @@ import { mockData, startMsg, mockData2, options, FunctionList } from '@/entrypoi
|
|
|
import { useAutoResizeTextarea } from '@/entrypoints/sidepanel/hook/useAutoResizeTextarea.ts'
|
|
|
import { getPageInfo, getXlsxValue, handleInput } from './utils/index.js'
|
|
|
import { useMsgStore } from '@/store/modules/msg.ts'
|
|
|
+import { useUserStore } from '@/store/modules/user'
|
|
|
+import { putChat } from "@/api/index.js";
|
|
|
+import {debounce} from 'lodash'
|
|
|
+const userStore = useUserStore()
|
|
|
import { getChatDetail } from '@/api/index.js'
|
|
|
-
|
|
|
+// 在其他状态变量附近添加
|
|
|
+const isLoadingMore = ref(false)
|
|
|
+import { v4 as uuidv4 } from 'uuid';
|
|
|
// 滚动条引用
|
|
|
const scrollbar = ref(null)
|
|
|
const scrollToBottomRef = ref(null)
|
|
|
@@ -135,9 +149,8 @@ const isShowPage = ref(false)
|
|
|
const tareRef = useTemplateRef('textareaRef')
|
|
|
const drawerRef = useTemplateRef('historyComponentRef')
|
|
|
// 获取父组件提供的 Hook 实例
|
|
|
-const { registerStore, useStore } = inject('indexedDBHook')
|
|
|
const msgStore = useMsgStore()
|
|
|
-const { pageInfoList, messages, msgUuid, AIModel } = storeToRefs(useMsgStore())
|
|
|
+const { pageInfoList, messages, msgUuid, AIModel,page,hasNext } = storeToRefs(useMsgStore())
|
|
|
const formInfo = ref('')
|
|
|
|
|
|
const {
|
|
|
@@ -164,13 +177,44 @@ function handleStopAsk() {
|
|
|
sendLoading.value = false
|
|
|
}
|
|
|
|
|
|
-function handleScroll({ scrollTop }) {
|
|
|
- // scrollTop 滚动条的位置
|
|
|
+function handleScroll(a) {
|
|
|
const { wrapRef } = scrollbar.value
|
|
|
const { scrollHeight, clientHeight } = wrapRef
|
|
|
- scrollToBottomRef.value.showButton = scrollHeight - scrollTop - clientHeight >= 350
|
|
|
+ scrollToBottomRef.value.showButton = scrollHeight - a.scrollTop - clientHeight >= 350
|
|
|
+
|
|
|
+ // 检测是否滚动到顶部
|
|
|
+ if (a.scrollTop <= 10 && hasNext.value) {
|
|
|
+ handleScrollToTop()
|
|
|
+ }
|
|
|
}
|
|
|
-
|
|
|
+const messagesContainer = ref(null)
|
|
|
+let innerText = ''
|
|
|
+// 处理滚动到顶部的事件
|
|
|
+const handleScrollToTop = debounce(async function () {
|
|
|
+ console.log('滚动到顶部,可以加载更多历史消息')
|
|
|
+ if (!sendLoading.value && !isLoadingMore.value) {
|
|
|
+ isLoadingMore.value = true
|
|
|
+ try {
|
|
|
+ // 记录当前第一条消息的位置
|
|
|
+ const firstMessage = messagesContainer.value?.firstElementChild.querySelector('.timestamp')
|
|
|
+ innerText = firstMessage.innerText
|
|
|
+ const oldHeight = firstMessage?.offsetTop || 0
|
|
|
+ await msgStore.changePage()
|
|
|
+ setTimeout(() => {
|
|
|
+ if (firstMessage) {
|
|
|
+ console.log([...messagesContainer.value.querySelectorAll('.timestamp')].find(_ => _.innerText === innerText).offsetTop, innerText, 778);
|
|
|
+ const newHeight = [...messagesContainer.value.querySelectorAll('.timestamp')].find(_ => _.innerText === innerText).offsetTop
|
|
|
+ const scrollOffset = newHeight - oldHeight
|
|
|
+ scrollbar.value?.setScrollTop(scrollOffset)
|
|
|
+ }
|
|
|
+ }, 400);
|
|
|
+ // 恢复滚动位置
|
|
|
+
|
|
|
+ } finally {
|
|
|
+ isLoadingMore.value = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+}, 500, { leading: false, trailing: true })
|
|
|
/**
|
|
|
* @param {string} msg
|
|
|
* @param {string} raw
|
|
|
@@ -178,31 +222,24 @@ function handleScroll({ scrollTop }) {
|
|
|
* **/
|
|
|
async function addMessage(msg, raw, type) {
|
|
|
console.log(msg);
|
|
|
-
|
|
|
- // 添加indexDB Store配置
|
|
|
- if (msgUuid.value === '') {
|
|
|
- msgUuid.value = 'D' + Date.now().toString()
|
|
|
- await registerStore({
|
|
|
- name: msgUuid.value,
|
|
|
- keyPath: 'id'
|
|
|
- })
|
|
|
- }
|
|
|
const newMessage = reactive({
|
|
|
+ id:+new Date(),
|
|
|
type: type || '',
|
|
|
rawContent: raw ?? msg,
|
|
|
+ senderId: userStore.token,
|
|
|
+ receiverId:-1, //大模型
|
|
|
content: msg,
|
|
|
- timestamp: moment().format('YYYY-MM-DD HH:mm:ss'),
|
|
|
- role:'user',
|
|
|
- addToHistory: !taklToHtml.value
|
|
|
+ sortKey: moment().valueOf(),
|
|
|
+ role: 'user',
|
|
|
+ conversationId:msgUuid.value,
|
|
|
+ addToHistory: `${!taklToHtml.value}`
|
|
|
})
|
|
|
if (type === 'document') {
|
|
|
newMessage.content = msg
|
|
|
newMessage.rawContent = raw
|
|
|
}
|
|
|
if (!msg) return
|
|
|
-
|
|
|
messages.value.push(newMessage)
|
|
|
- useStore(msgUuid.value).add(cloneDeep(newMessage))
|
|
|
await nextTick(() => {
|
|
|
scrollbar.value?.setScrollTop(99999)
|
|
|
})
|
|
|
@@ -231,21 +268,18 @@ const handleSummary = async () => {
|
|
|
})
|
|
|
params = [{
|
|
|
role: 'user',
|
|
|
- content: `请根据以下这几段内容综合总结出结果:
|
|
|
+ content: `请根据以下这几个内容综合总结出结果:
|
|
|
${tempStr}要求:
|
|
|
1. 用简洁清晰的语言提取所有的核心要点
|
|
|
2. 保持客观中立的语气
|
|
|
3. 按重要性排序
|
|
|
4. 返回内容做好换行,以及展示样式
|
|
|
5. 请以"以下是对该文件内容的总结:"开头,然后用要点的形式列出主要内容。
|
|
|
- 6. 对这几段内容进行综合分析及联想`
|
|
|
+ 6. 对这几个内容进行综合分析及联想`
|
|
|
}]
|
|
|
}
|
|
|
const msg = await addMessage(JSON.stringify(pageInfoList.value), JSON.stringify(params.map(_ => _.content)), 'document')
|
|
|
- // putChat({
|
|
|
- // id: msgUuid.value,
|
|
|
- // msg: msg
|
|
|
- // })
|
|
|
+ putChat(msg)
|
|
|
if (requestFlowFn) {
|
|
|
isShowPage.value = false
|
|
|
taklToHtml.value = false
|
|
|
@@ -279,28 +313,18 @@ function handleCurrentChange(e) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-function handleCurrentData(e) {
|
|
|
+async function handleCurrentData(e) {
|
|
|
drawerRef.value.drawer = false
|
|
|
if (!e) {
|
|
|
addNewDialogue()
|
|
|
return
|
|
|
}
|
|
|
- // 添加indexDB Store配置
|
|
|
+ messages.value = []
|
|
|
+ page.value = 1
|
|
|
msgUuid.value = e
|
|
|
- // getChatDetail(e).then(res => {
|
|
|
- // messages.value = reds
|
|
|
- // nextTick(() => {
|
|
|
- // scrollbar.value?.setScrollTop(99999)
|
|
|
- // })
|
|
|
- // }).finally(res => {
|
|
|
-
|
|
|
- // })
|
|
|
- useStore(e).getAll().then((res) => {
|
|
|
- messages.value = res
|
|
|
- nextTick(() => {
|
|
|
- scrollbar.value?.setScrollTop(99999)
|
|
|
- })
|
|
|
- })
|
|
|
+ chrome.storage.local.set({ msgUuid: msgUuid.value })
|
|
|
+ await msgStore.initMsg()
|
|
|
+ scrollbar.value?.setScrollTop(99999)
|
|
|
}
|
|
|
|
|
|
async function readClick() {
|
|
|
@@ -314,11 +338,7 @@ async function readClick() {
|
|
|
ElMessage.warning(`最多添加${import.meta.env.VITE_MAX_FILE_NUMBER}个文件`)
|
|
|
return
|
|
|
}
|
|
|
- console.log(45454);
|
|
|
-
|
|
|
const tempPageInfo = await getPageInfo()
|
|
|
- console.log(tempPageInfo);
|
|
|
-
|
|
|
if (AIModel.value.file === true) {
|
|
|
const blob = new Blob([tempPageInfo.content.mainContent], { type: 'text/plain' })
|
|
|
const file = new File([blob], tempPageInfo.title + '.txt', { type: 'text/plain' })
|
|
|
@@ -338,19 +358,17 @@ function deletePageInfo(i) {
|
|
|
}
|
|
|
|
|
|
function addNewDialogue() {
|
|
|
- // if (messages.value.length === 0) {
|
|
|
- // ElMessage.warning('已经是新对话')
|
|
|
- // return
|
|
|
- // }
|
|
|
- if (msgUuid.value === '') {
|
|
|
+ if (messages.value.length === 0) {
|
|
|
ElMessage.warning('已经是新对话')
|
|
|
return
|
|
|
}
|
|
|
isShowPage.value = false
|
|
|
taklToHtml.value = false
|
|
|
messages.value = []
|
|
|
+ page.value = 1
|
|
|
pageInfoList.value = []
|
|
|
- msgUuid.value = ''
|
|
|
+ msgUuid.value = uuidv4()
|
|
|
+ chrome.storage.local.set({ msgUuid: msgUuid.value })
|
|
|
}
|
|
|
|
|
|
async function handleAsk() {
|
|
|
@@ -358,14 +376,9 @@ async function handleAsk() {
|
|
|
inputMessage.value = ''
|
|
|
if (sendLoading.value) return
|
|
|
const msg = await addMessage(str)
|
|
|
- // putChat({
|
|
|
- // id: msgUuid.value,
|
|
|
- // msg: msg
|
|
|
- // })
|
|
|
+ putChat(msg)
|
|
|
if (type.value === FunctionList.Intelligent_Form_filling) {
|
|
|
const res = await fetchRes(str)
|
|
|
- console.log(res);
|
|
|
-
|
|
|
if (res.status === 'ok') {
|
|
|
formInfo.value = res.data
|
|
|
console.log(res, 55558)
|
|
|
@@ -411,10 +424,7 @@ const handleUpload = async (file) => {
|
|
|
xlsxData.value[header] = readData[1][i]
|
|
|
})
|
|
|
const msg = addMessage(`已上传文件:${file.name}`, buildExcelUnderstandingPrompt(readData[0], file?.name, formInfo.value))
|
|
|
- // putChat({
|
|
|
- // id: msgUuid.value,
|
|
|
- // msg: msg
|
|
|
- // })
|
|
|
+ putChat(msg)
|
|
|
const { rawContent, status } = await streamRes()
|
|
|
if (status === 'ok') {
|
|
|
let form = []
|
|
|
@@ -426,6 +436,7 @@ const handleUpload = async (file) => {
|
|
|
try {
|
|
|
sendLoading.value = true
|
|
|
const msg = await addMessage(`文件上传中`)
|
|
|
+ putChat(msg)
|
|
|
const data = await getFileValue(file)
|
|
|
msg.content = `已上传文件:${file.name}`
|
|
|
msg.rawContent = data
|
|
|
@@ -476,9 +487,11 @@ async function getFileValue(file) {
|
|
|
const res = await getFileContent(formData)
|
|
|
return res.data
|
|
|
}
|
|
|
+let a = null
|
|
|
// 组件挂载时滚动到底部
|
|
|
-onMounted(() => {
|
|
|
+onMounted(async () => {
|
|
|
msgStore.updateAIModel(options[0].options[0])
|
|
|
+ await msgStore.initMsg()
|
|
|
useAutoResizeTextarea(tareRef, inputMessage)
|
|
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
|
if (message.type === 'TO_SIDE_PANEL_PAGE_INFO') {
|
|
|
@@ -501,4 +514,29 @@ onMounted(() => {
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
@use '@/entrypoints/sidepanel/css/chat.scss';
|
|
|
+
|
|
|
+.loading-more-indicator {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ padding: 10px 0;
|
|
|
+ color: #909399;
|
|
|
+ font-size: 14px;
|
|
|
+
|
|
|
+ .loading-spinner {
|
|
|
+ width: 20px;
|
|
|
+ height: 20px;
|
|
|
+ margin-right: 8px;
|
|
|
+ border: 2px solid #e6e6e6;
|
|
|
+ border-top-color: #4d6bfe;
|
|
|
+ border-radius: 50%;
|
|
|
+ animation: spin 1s linear infinite;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes spin {
|
|
|
+ to {
|
|
|
+ transform: rotate(360deg);
|
|
|
+ }
|
|
|
+}
|
|
|
</style>
|