|
@@ -7,12 +7,22 @@
|
|
|
<!-- 消息列表 -->
|
|
|
<div class="message-list w-full" 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.isSelf ? 'self' : 'other']">
|
|
|
- <el-avatar :size="32" :src="message.avatar" />
|
|
|
+ :class="['message-item', message.role === 'user' ? 'self' : 'other']">
|
|
|
+ <el-avatar :size="32" :src="message.role === 'user' ? userAvatar : avatar" />
|
|
|
<div class="message-content">
|
|
|
- <div class="content" v-if="!message.isSelf" :class="{ 'loading-content': message.content === '' }">
|
|
|
+ <div class="content" v-if="message.role === 'system'"
|
|
|
+ :class="{ 'loading-content': message.content === '' }">
|
|
|
<formTable v-if="message.type === 'form'" :content="message.rawContent" />
|
|
|
<span v-else v-html="message.content"></span>
|
|
|
<span class="loading-indicator" v-if="sendLoading && index === messages.length - 1">
|
|
@@ -21,17 +31,17 @@
|
|
|
<span class="dot"></span>
|
|
|
</span>
|
|
|
</div>
|
|
|
- <document v-else-if="message.type === 'document' && message.isSelf" :content="message.content"
|
|
|
- :rawContent="message.rawContent" />
|
|
|
+ <document v-else-if="message.type === 'document' && message.role === 'user'" :content="message.content"
|
|
|
+ :rawContent="message.rawContent" />
|
|
|
<div v-else class="content">
|
|
|
<span v-if="message.type === ''">{{ message.content }}</span>
|
|
|
<span class="loading-indicator" v-if="sendLoading && index === messages.length - 1">
|
|
|
- <span class="dot"></span>
|
|
|
- <span class="dot"></span>
|
|
|
- <span class="dot"></span>
|
|
|
- </span>
|
|
|
+ <span class="dot"></span>
|
|
|
+ <span class="dot"></span>
|
|
|
+ <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>
|
|
@@ -41,32 +51,19 @@
|
|
|
<ScrollToBottom :target="scrollbar" ref="scrollToBottomRef" />
|
|
|
</div>
|
|
|
|
|
|
- <Tools @read-click="readClick"
|
|
|
- @upload-file="handleUpload"
|
|
|
- @handle-capture="handleCapture"
|
|
|
- @his-records="hisRecords"
|
|
|
- @add-new-dialogue="addNewDialogue"
|
|
|
- @handle-current-change="handleCurrentChange"
|
|
|
- @handel-intelligent-filling-click="handelIntelligentFillingClick" />
|
|
|
+ <Tools @read-click="readClick" @upload-file="handleUpload" @handle-capture="handleCapture" @his-records="hisRecords"
|
|
|
+ @add-new-dialogue="addNewDialogue" @handle-current-change="handleCurrentChange"
|
|
|
+ @handel-intelligent-filling-click="handelIntelligentFillingClick" />
|
|
|
|
|
|
<div class="w-full max-w-[720px] p-[0_12px_12px]">
|
|
|
<!-- 输入区域 -->
|
|
|
<div class="input-area w-full">
|
|
|
<div v-show="isShowPage" style="border-bottom: 1px solid #F0F0F0;">
|
|
|
<div class="card_list">
|
|
|
- <template v-for="(v,i) in pageInfoList" :key="i">
|
|
|
+ <template v-for="(v, i) in pageInfoList" :key="i">
|
|
|
<div class="card_image " v-if="v.type === 'image'">
|
|
|
- <el-image
|
|
|
- v-loading="v.isUpload"
|
|
|
- class="w-full h-full p-0.5 object-cover"
|
|
|
- :src="v.url"
|
|
|
- :zoom-rate="1.2"
|
|
|
- :max-scale="7"
|
|
|
- :min-scale="0.2"
|
|
|
- :preview-src-list="[v.url]"
|
|
|
- :initial-index="4"
|
|
|
- fit="cover"
|
|
|
- />
|
|
|
+ <el-image v-loading="v.isUpload" class="w-full h-full p-0.5 object-cover" :src="v.url" :zoom-rate="1.2"
|
|
|
+ :max-scale="7" :min-scale="0.2" :preview-src-list="[v.url]" :initial-index="4" fit="cover" />
|
|
|
<el-icon class="closeIcon" size="16px" color="#909399" @click="deletePageInfo(i)">
|
|
|
<CircleClose />
|
|
|
</el-icon>
|
|
@@ -93,7 +90,7 @@
|
|
|
</div>
|
|
|
|
|
|
<el-input ref="textareaRef" v-model="inputMessage" type="textarea" :rows="3" placeholder="输入消息..."
|
|
|
- @keyup.enter="handleAsk" />
|
|
|
+ @keyup.enter="handleAsk" />
|
|
|
<div class="chat_area_op">
|
|
|
<el-button
|
|
|
:style="`background-color:${inputMessage.trim() ? '#4d6bfe' : '#d6dee8'};border-color:${inputMessage.trim() ? '#4d6bfe' : '#d6dee8'}`"
|
|
@@ -102,7 +99,7 @@
|
|
|
<svg-icon icon-class="send" color="#000000" />
|
|
|
</el-button>
|
|
|
<el-button style="background-color:#ffffff;border:2px solid rgb(134 143 153);" v-else circle
|
|
|
- @click="handleStopAsk">
|
|
|
+ @click="handleStopAsk">
|
|
|
<svg-icon icon-class="stop" color="red" />
|
|
|
</el-button>
|
|
|
|
|
@@ -113,7 +110,7 @@
|
|
|
|
|
|
|
|
|
<!-- 历史记录 -->
|
|
|
- <historyComponent :msgUuid="msgUuid" ref="historyComponentRef" @currentData="(e)=>handleCurrentData(e)" />
|
|
|
+ <historyComponent :msgUuid="msgUuid" ref="historyComponentRef" @currentData="(e) => handleCurrentData(e)" />
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
@@ -121,7 +118,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,
|
|
@@ -140,12 +136,20 @@ import document from '@/entrypoints/sidepanel/component/document.vue'
|
|
|
import pageMask from '@/entrypoints/sidepanel/component/pageMask.vue'
|
|
|
import ScrollToBottom from '@/entrypoints/sidepanel/component/ScrollToBottom.vue'
|
|
|
import formTable from '@/entrypoints/sidepanel/component/formTable.vue'
|
|
|
-
|
|
|
+import userAvatar from '@/assets/images/user.png'
|
|
|
+import avatar from '@/public/icon/32.png'
|
|
|
import { mockData, startMsg, mockData2, options, FunctionList } from '@/entrypoints/sidepanel/mock'
|
|
|
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)
|
|
@@ -154,9 +158,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 {
|
|
@@ -170,8 +173,6 @@ const {
|
|
|
} = useMsg(scrollbar)
|
|
|
const inputMessage = ref('')
|
|
|
const pageInfo = ref('')
|
|
|
-
|
|
|
-
|
|
|
function handleStopAsk() {
|
|
|
if (type.value === FunctionList.Intelligent_Form_filling) {
|
|
|
inputMessage.value = ''
|
|
@@ -185,46 +186,69 @@ 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
|
|
|
* @param {string} type 发送的类型 document 、text
|
|
|
* **/
|
|
|
async function addMessage(msg, raw, type) {
|
|
|
- // 添加indexDB Store配置
|
|
|
- if (msgUuid.value === '') {
|
|
|
- msgUuid.value = 'D' + Date.now().toString()
|
|
|
- await registerStore({
|
|
|
- name: msgUuid.value,
|
|
|
- keyPath: 'id'
|
|
|
- })
|
|
|
- }
|
|
|
+ console.log(msg);
|
|
|
const newMessage = reactive({
|
|
|
- id: messages.value.length + 1,
|
|
|
+ id: +new Date(),
|
|
|
type: type || '',
|
|
|
- username: '我',
|
|
|
rawContent: raw ?? msg,
|
|
|
+ senderId: userStore.userInfo.id,
|
|
|
+ receiverId: -1, //大模型
|
|
|
content: msg,
|
|
|
- timestamp: moment().format('YYYY-MM-DD HH:mm:ss'),
|
|
|
- isSelf: true,
|
|
|
- avatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
|
|
|
- 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)
|
|
|
})
|
|
@@ -253,17 +277,18 @@ const handleSummary = async () => {
|
|
|
})
|
|
|
params = [{
|
|
|
role: 'user',
|
|
|
- content: `请根据以下这几段内容综合总结出结果:
|
|
|
+ content: `请根据以下这几个内容综合总结出结果:
|
|
|
${tempStr}要求:
|
|
|
1. 用简洁清晰的语言提取所有的核心要点
|
|
|
2. 保持客观中立的语气
|
|
|
3. 按重要性排序
|
|
|
4. 返回内容做好换行,以及展示样式
|
|
|
5. 请以"以下是对该文件内容的总结:"开头,然后用要点的形式列出主要内容。
|
|
|
- 6. 对这几段内容进行综合分析及联想`
|
|
|
+ 6. 对这几个内容进行综合分析及联想`
|
|
|
}]
|
|
|
}
|
|
|
- await addMessage(pageInfoList.value, '总结', 'document')
|
|
|
+ const msg = await addMessage(JSON.stringify(pageInfoList.value), JSON.stringify(params.map(_ => _.content)), 'document')
|
|
|
+ putChat(msg)
|
|
|
if (requestFlowFn) {
|
|
|
isShowPage.value = false
|
|
|
taklToHtml.value = false
|
|
@@ -297,20 +322,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
|
|
|
- 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() {
|
|
@@ -344,29 +367,30 @@ function deletePageInfo(i) {
|
|
|
}
|
|
|
|
|
|
function addNewDialogue() {
|
|
|
- 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() {
|
|
|
const str = inputMessage.value.trim()
|
|
|
inputMessage.value = ''
|
|
|
if (sendLoading.value) return
|
|
|
- await addMessage(str)
|
|
|
+ const msg = await addMessage(str)
|
|
|
+ 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)
|
|
|
+ console.log(res, 55558)
|
|
|
} else {
|
|
|
type.value = FunctionList.File_Operation
|
|
|
isShowPage.value = false
|
|
@@ -374,7 +398,6 @@ async function handleAsk() {
|
|
|
pageInfoList.value = []
|
|
|
}
|
|
|
} else streamRes(taklToHtml.value)
|
|
|
- inputMessage.value = ''
|
|
|
}
|
|
|
|
|
|
function handleCapture() {
|
|
@@ -398,7 +421,6 @@ function handleCapture() {
|
|
|
function hisRecords() {
|
|
|
drawerRef.value.drawer = true
|
|
|
}
|
|
|
-
|
|
|
const handleUpload = async (file) => {
|
|
|
if (AIModel.value.file === true) {
|
|
|
const id = await modelFileUpload(file)
|
|
@@ -420,7 +442,8 @@ const handleUpload = async (file) => {
|
|
|
// if (!xlsxData.value[header]) xlsxData.value[header] = []
|
|
|
xlsxData.value[header] = readData[1][i]
|
|
|
})
|
|
|
- addMessage(`已上传文件:${file.name}`, buildExcelUnderstandingPrompt(readData[0], file?.name, formInfo.value))
|
|
|
+ const msg = addMessage(`已上传文件:${file.name}`, buildExcelUnderstandingPrompt(readData[0], file?.name, formInfo.value))
|
|
|
+ putChat(msg)
|
|
|
const { rawContent, status } = await streamRes()
|
|
|
if (status === 'ok') {
|
|
|
let form = []
|
|
@@ -429,8 +452,13 @@ const handleUpload = async (file) => {
|
|
|
handleInput(xlsxData.value, form)
|
|
|
}
|
|
|
} else {
|
|
|
+ const msg = await addMessage(`文件上传中`)
|
|
|
try {
|
|
|
- const { data, msg } = await getFileValue(file)
|
|
|
+ sendLoading.value = true
|
|
|
+ putChat(msg)
|
|
|
+ const data = await getFileValue(file)
|
|
|
+ msg.content = `已上传文件:${file.name}`
|
|
|
+ msg.rawContent = data
|
|
|
const res2 = await getFormKeyAndValue(data, formInfo.value)
|
|
|
xlsxData.value = res2.data
|
|
|
msg.rawContent = buildObjPrompt(res2.data, formInfo.value)
|
|
@@ -445,6 +473,10 @@ const handleUpload = async (file) => {
|
|
|
msg.content = '文件解析出错'
|
|
|
} finally {
|
|
|
sendLoading.value = false
|
|
|
+ // putChat({
|
|
|
+ // id: msgUuid.value,
|
|
|
+ // msg: msg
|
|
|
+ // })
|
|
|
}
|
|
|
}
|
|
|
type.value = FunctionList.File_Operation
|
|
@@ -468,25 +500,17 @@ const handleUpload = async (file) => {
|
|
|
isShowPage.value = true
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
async function getFileValue(file) {
|
|
|
- const msg = addMessage(`文件上传中`)
|
|
|
- sendLoading.value = true
|
|
|
let formData = new FormData()
|
|
|
formData.append('file', file)
|
|
|
const res = await getFileContent(formData)
|
|
|
- sendLoading.value = false
|
|
|
- msg.content = `已上传文件:${file.name}`
|
|
|
- msg.rawContent = res.data
|
|
|
- return {
|
|
|
- data: res.data,
|
|
|
- msg
|
|
|
- }
|
|
|
+ return res.data
|
|
|
}
|
|
|
-
|
|
|
+let a = null
|
|
|
// 组件挂载时滚动到底部
|
|
|
-onMounted(() => {
|
|
|
- msgStore.updateAIModel({ ...options[0].options[0], api_sk: options[0]['api_sk'], api_url: options[0]['api_url'] })
|
|
|
+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,7 +525,6 @@ onMounted(() => {
|
|
|
pageInfo.value = message.data
|
|
|
}
|
|
|
})
|
|
|
-
|
|
|
nextTick(() => {
|
|
|
scrollbar.value?.setScrollTop(99999)
|
|
|
})
|
|
@@ -510,4 +533,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>
|