123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498 |
- <!-- Chat.vue -->
- <template>
- <div class="chat-container">
- <div v-if="!messages.length" class="message-list">
- <pageMask />
- </div>
- <!-- 消息列表 -->
- <div class="message-list" v-else>
- <el-scrollbar ref="scrollbar" @scroll="handleScroll">
- <div class="messages">
- <div v-for="(message, index) in messages" :key="index"
- :class="['message-item', message.isSelf ? 'self' : 'other']">
- <el-avatar :size="32" :src="message.avatar" />
- <div class="message-content">
- <div class="content" v-if="!message.isSelf" :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">
- <span class="dot"></span>
- <span class="dot"></span>
- <span class="dot"></span>
- </span>
- </div>
- <document v-else-if="message.type === 'document' && message.isSelf" :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>
- </div>
- <div class="timestamp ">{{ moment(message.timestamp).format('MM-DD HH:mm') }}
- <span v-if="message.add" style="cursor: pointer;" @click="handleInput">填充</span>
- </div>
- </div>
- </div>
- </div>
- </el-scrollbar>
- <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" />
- <div>
- <!-- 输入区域 -->
- <div class="input-area">
- <div v-show="isShowPage" style="border-bottom: 1px solid #F0F0F0;">
- <div class="card_list">
- <div v-for="(v,i) in pageInfoList" :key="i"
- :class="`card-content ${pageInfoList.length > 1 ? 'card_width' : ''}`">
- <img :src="v?.favIconUrl" style="width: 24px;display: block" />
- <div class="title-wrapper">
- <span class="els title-scroller">{{ v?.title }}</span>
- <span class="els url-scroller">{{ v?.url }}</span>
- </div>
- <el-icon class="closeIcon" size="16px" color="#909399" @click="deletePageInfo(i)">
- <CircleClose />
- </el-icon>
- </div>
- </div>
- <div v-show="type !== FunctionList.Intelligent_Form_filling" class="card-btn">
- <el-tooltip content="总结当前页面" placement="top">
- <el-button round @click="handleSummary">总结</el-button>
- </el-tooltip>
- </div>
- </div>
- <el-input ref="textareaRef" v-model="inputMessage" type="textarea" :rows="3" placeholder="输入消息..."
- @keyup.enter="handleAsk" />
- <div class="chat_area_op">
- <el-button
- :style="`background-color:${inputMessage.trim() ? '#4d6bfe' : '#d6dee8'};border-color:${inputMessage.trim() ? '#4d6bfe' : '#d6dee8'}`"
- v-if="inputMessage.trim() || !sendLoading" type="primary" circle @click="handleAsk"
- :disabled="!inputMessage.trim() || sendLoading">
- <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">
- <svg-icon icon-class="stop" color="red" />
- </el-button>
- </div>
- </div>
- </div>
- <!-- 历史记录 -->
- <historyComponent :msgUuid="msgUuid" ref="historyComponentRef" @currentData="(e)=>handleCurrentData(e)" />
- </div>
- </template>
- <script setup>
- 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,
- getSummaryPrompt,
- getFileContent,
- buildObjPrompt,
- modelFileUpload,
- controllerList
- } from '@/entrypoints/sidepanel/utils/ai-service.js'
- import { storeToRefs } from 'pinia'
- import { ElMessage } from 'element-plus'
- import { useMsg } from '@/entrypoints/sidepanel/hook/useMsg.ts'
- import Tools from '@/entrypoints/sidepanel/component/tools.vue'
- import historyComponent from '@/entrypoints/sidepanel/component/historyComponent.vue'
- 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 { 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'
- // 滚动条引用
- const scrollbar = ref(null)
- const scrollToBottomRef = ref(null)
- const xlsxData = ref({})
- 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 formInfo = ref('')
- const {
- taklToHtml,
- sendLoading,
- type,
- streamRes,
- getFormKeyAndValue,
- requestFlowFn,
- fetchRes
- } = useMsg(scrollbar)
- const inputMessage = ref('')
- const pageInfo = ref('')
- function handleStopAsk() {
- if (type.value === FunctionList.Intelligent_Form_filling) {
- inputMessage.value = ''
- pageInfoList.value = []
- isShowPage.value = false
- taklToHtml.value = false
- type.value = FunctionList.File_Operation
- }
- controllerList.value[0].abort()
- controllerList.value = []
- sendLoading.value = false
- }
- function handleScroll({ scrollTop }) {
- // scrollTop 滚动条的位置
- const { wrapRef } = scrollbar.value
- const { scrollHeight, clientHeight } = wrapRef
- scrollToBottomRef.value.showButton = scrollHeight - scrollTop - clientHeight >= 350
- }
- /**
- * @param {string} msg
- * @param {string} raw
- * @param {string} type 发送的类型 document 、text
- * **/
- 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: messages.value.length + 1,
- type: type || '',
- username: '我',
- rawContent: raw ?? msg,
- 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
- })
- 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)
- })
- return newMessage
- }
- const handleSummary = async () => {
- let params = []
- if (AIModel.value.file === true) {
- pageInfoList.value.forEach((pageInfo) => {
- params.push({ role: 'system', content: `fileid://${pageInfo.fileId}` })
- })
- params.push({
- role: 'user', content: `
- 请根据这几个文件的文本内容综合分析总结
- 要求:
- 1.抽取文件的文本内容
- 2.对每个文件中的文本内容进行分别总结
- 3.对步骤二总结的内容进行综合总结`
- })
- } else {
- let tempStr = ''
- pageInfoList.value.forEach((pageInfo, index) => {
- pageInfo.handleText = getSummaryPrompt(pageInfo.content)
- tempStr += `第${index + 1}段:${pageInfo.handleText}\n`
- })
- params = [{
- role: 'user',
- content: `请根据以下这几段内容综合总结出结果:
- ${tempStr}要求:
- 1. 用简洁清晰的语言提取所有的核心要点
- 2. 保持客观中立的语气
- 3. 按重要性排序
- 4. 返回内容做好换行,以及展示样式
- 5. 请以"以下是对该文件内容的总结:"开头,然后用要点的形式列出主要内容。
- 6. 对这几段内容进行综合分析及联想`
- }]
- }
- await addMessage(pageInfoList.value, '总结', 'document')
- if (requestFlowFn) {
- isShowPage.value = false
- taklToHtml.value = false
- pageInfoList.value = []
- const res = await requestFlowFn(params)
- }
- }
- async function handelIntelligentFillingClick() {
- isShowPage.value = true
- taklToHtml.value = true
- const tempPageInfo = await getPageInfo()
- pageInfo.value = tempPageInfo
- pageInfoList.value = [tempPageInfo]
- inputMessage.value = '/智能填表 '
- type.value = FunctionList.Intelligent_Form_filling
- }
- function handleCurrentChange(e) {
- options.forEach((item) => {
- item.options.forEach((item2) => {
- if (item2.value === e) {
- msgStore.updateAIModel(item2)
- }
- })
- })
- if (AIModel.value.file === true) {
- isShowPage.value = false
- taklToHtml.value = false
- pageInfoList.value = []
- }
- }
- function handleCurrentData(e) {
- drawerRef.value.drawer = false
- if (!e) {
- addNewDialogue()
- return
- }
- // 添加indexDB Store配置
- msgUuid.value = e
- useStore(e).getAll().then((res) => {
- messages.value = res
- nextTick(() => {
- scrollbar.value?.setScrollTop(99999)
- })
- })
- }
- async function readClick() {
- if (type.value === FunctionList.Intelligent_Form_filling) {
- pageInfoList.value = []
- type.value = FunctionList.File_Operation
- }
- isShowPage.value = true
- taklToHtml.value = true
- if (pageInfoList.value.length >= Number(import.meta.env.VITE_MAX_FILE_NUMBER)) {
- ElMessage.warning(`最多添加${import.meta.env.VITE_MAX_FILE_NUMBER}个文件`)
- return
- }
- const tempPageInfo = await getPageInfo()
- 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' })
- tempPageInfo.fileId = await modelFileUpload(file)
- }
- pageInfoList.value.unshift(tempPageInfo)
- }
- function deletePageInfo(i) {
- pageInfoList.value.splice(i, 1)
- if (pageInfoList.value.length === 0) {
- isShowPage.value = false
- taklToHtml.value = false
- if (type.value === FunctionList.Intelligent_Form_filling) inputMessage.value = ''
- type.value = FunctionList.File_Operation
- }
- }
- function addNewDialogue() {
- if (msgUuid.value === '') {
- ElMessage.warning('已经是新对话')
- return
- }
- isShowPage.value = false
- taklToHtml.value = false
- messages.value = []
- pageInfoList.value = []
- msgUuid.value = ''
- }
- async function handleAsk() {
- const str = inputMessage.value.trim()
- inputMessage.value = ''
- if (sendLoading.value) return
- addMessage(str)
- 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)
- } else {
- type.value = FunctionList.File_Operation
- isShowPage.value = false
- taklToHtml.value = false
- pageInfoList.value = []
- }
- } else streamRes(taklToHtml.value)
- inputMessage.value = ''
- }
- function handleCapture() {
- ElMessage({
- message: '开发中...',
- grouping: true,
- showClose: true
- })
- }
- function hisRecords() {
- drawerRef.value.drawer = true
- }
- const handleUpload = async (file) => {
- if (AIModel.value.file === true) {
- const id = await modelFileUpload(file)
- pageInfoList.value.unshift({
- fileId: id,
- title: file.name,
- url: 'File',
- favIconUrl: fileLogo
- })
- isShowPage.value = true
- taklToHtml.value = true
- return
- }
- if (type.value === FunctionList.Intelligent_Form_filling) {
- const fileExtension = file.name.split('.').pop().toLowerCase()
- if (fileExtension === 'xlsx') {
- const readData = await getXlsxValue(file)
- readData[0].forEach((header, i) => {
- // if (!xlsxData.value[header]) xlsxData.value[header] = []
- xlsxData.value[header] = readData[1][i]
- })
- addMessage(`已上传文件:${file.name}`, buildExcelUnderstandingPrompt(readData[0], file?.name, formInfo.value))
- const {rawContent,status} = await streamRes()
- if (status === 'ok') {
- let form = []
- if (rawContent.includes('json')) form = JSON.parse(rawContent.split('json')[1].split('```')[0])
- else form = JSON.parse(rawContent)
- handleInput(xlsxData.value, form)
- }
- } else {
- try {
- const { data, msg } = await getFileValue(file)
- const res2 = await getFormKeyAndValue(data, formInfo.value)
- xlsxData.value = res2.data
- msg.rawContent = buildObjPrompt(res2.data, formInfo.value)
- console.log(msg.rawContent);
- console.log(messages);
-
- const { rawContent, status } = await streamRes()
- console.log(22555);
-
- if (status === 'ok') {
- let form = []
- if (rawContent.includes('json')) form = JSON.parse(rawContent.split('json')[1].split('```')[0])
- else form = JSON.parse(rawContent)
- handleInput(xlsxData.value, form)
- }
- } catch (error) {
- msg.content = '文件解析出错'
- } finally {
- sendLoading.value = false
- }
- }
- type.value = FunctionList.File_Operation
- isShowPage.value = false
- taklToHtml.value = false
- pageInfoList.value = []
- return
- }
- if (type.value === FunctionList.File_Operation) {
- let formData = new FormData()
- formData.append('file', file)
- const res = await getFileContent(formData)
- pageInfoList.value.unshift({
- title: file.name,
- url: 'File',
- favIconUrl: fileLogo,
- content: {
- mainContent: res.data
- }
- })
- isShowPage.value = true
- }
- }
- async function getFileValue(file) {
- const msg = await addMessage(`文件上传中`)
- console.log(msg,messages);
-
- sendLoading.value = true
- let formData = new FormData()
- formData.append('file', file)
- const res = await getFileContent(formData)
- console.log(res,5555);
-
- sendLoading.value = false
- msg.content = `已上传文件:${file.name}`
- console.log(msg.content);
-
- msg.rawContent = res.data
- return {
- data: res.data,
- msg
- }
- }
- // 组件挂载时滚动到底部
- onMounted(() => {
- msgStore.updateAIModel(options[0].options[0])
- useAutoResizeTextarea(tareRef, inputMessage)
- chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
- if (message.type === 'TO_SIDE_PANEL_PAGE_INFO') {
- pageInfo.value = message.data
- // 转发到 content.js
- chrome.tabs.sendMessage(sender.tab.id, {
- type: 'TO_CONTENT_SCRIPT',
- data: message.data
- })
- }
- if (message.type === 'TO_SIDE_PANEL_PAGE_CHANGE') {
- pageInfo.value = message.data
- }
- })
- nextTick(() => {
- scrollbar.value?.setScrollTop(99999)
- })
- })
- </script>
- <style lang="scss" scoped>
- @use '@/entrypoints/sidepanel/css/chat.scss';
- </style>
|