Chat.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804
  1. <!-- Chat.vue -->
  2. <template>
  3. <div class="chat-container items-center">
  4. <div v-if="!messages.length && !msgLoading" class="message-list">
  5. <pageMask />
  6. </div>
  7. <!-- 消息列表 -->
  8. <div class="message-list w-full" v-else>
  9. <el-scrollbar ref="scrollbar" @scroll="handleScroll" v-loading="msgLoading">
  10. <!-- 加载更多指示器 -->
  11. <div v-if="isLoadingMore" class="loading-more-indicator">
  12. <div class="loading-spinner"></div>
  13. <span>加载更多消息...</span>
  14. </div>
  15. <div class="messages" ref="messagesContainer">
  16. <div v-for="(message, index) in messages" :key="index"
  17. :class="['message-item', message.role === 'user' ? 'self' : 'other']">
  18. <el-avatar :size="32" :src="message.role === 'user' ? userAvatar : avatar" />
  19. <div class="message-content">
  20. <div class="content" v-if="message.role === 'system'"
  21. :class="{ 'loading-content': message.content === '' }">
  22. <formTable v-if="message.type === 'form'" :content="message.rawContent" />
  23. <div v-else>
  24. <div v-html="compiledMarkdown(message.rawContent)" class="markdown-body"
  25. style="font-size: small"></div>
  26. <!-- <link rel="stylesheet" href="/node_modules/github-markdown-css/github-markdown-dark.css"> -->
  27. </div>
  28. <span class="loading-indicator" v-if="sendLoading && index === messages.length - 1">
  29. <span class="dot"></span>
  30. <span class="dot"></span>
  31. <span class="dot"></span>
  32. </span>
  33. </div>
  34. <document v-else-if="message.type === 'document' && message.role === 'user'" :content="message.content"
  35. :rawContent="message.rawContent" />
  36. <div v-else class="content">
  37. <span v-if="message.type === ''">{{ message.content }}</span>
  38. <span class="loading-indicator" v-if="sendLoading && index === messages.length - 1">
  39. <span class="dot"></span>
  40. <span class="dot"></span>
  41. <span class="dot"></span>
  42. </span>
  43. </div>
  44. <div class="timestamp ">{{ moment(message.sortKey).format('MM-DD HH:mm') }}
  45. <!-- <span v-if="message.add" style="cursor: pointer;" @click="handleInput">填充</span> -->
  46. </div>
  47. </div>
  48. </div>
  49. </div>
  50. </el-scrollbar>
  51. <ScrollToBottom :target="scrollbar" ref="scrollToBottomRef" />
  52. </div>
  53. <Tools :disHistory="sendLoading" :upload="type === FunctionList.File_Operation || !!formInfo"
  54. @read-click="readClick" @upload-file="(file) => createFileObj(file)" @handle-capture="handleCapture"
  55. @his-records="hisRecords"
  56. @add-new-dialogue="addNewDialogue" @handle-current-change="handleCurrentChange"
  57. @handel-intelligent-filling-click="handelIntelligentFillingClick" />
  58. <div class="w-full max-w-[720px] p-[0_12px_12px]">
  59. <!-- 输入区域 -->
  60. <div class="input-area w-full">
  61. <el-icon class="closeShow" :style="{ display: isShowPage ? 'block' : 'none' }" size="16px" color="#909399"
  62. @click="closePageInfo">
  63. <CircleClose />
  64. </el-icon>
  65. <div v-show="isShowPage" style="border-bottom: 1px solid #F0F0F0;">
  66. <div class="card_list">
  67. <template v-for="(v, i) in pageInfoList" :key="i">
  68. <div class="card_image " v-if="v.type === 'image'">
  69. <el-image v-loading="v.isUpload" class="w-full h-full p-0.5 object-cover" :src="v.url" :zoom-rate="1.2"
  70. :max-scale="7" :min-scale="0.2" :preview-src-list="[v.url]" :initial-index="4" fit="cover" />
  71. <el-icon class="closeIcon" size="16px" color="#909399" @click="deletePageInfo(i)">
  72. <CircleClose />
  73. </el-icon>
  74. </div>
  75. <div v-else :class="`card-content ${pageInfoList.length > 1 ? 'card_width' : ''}`">
  76. <div class="loading-more-indicator" v-if="v.loading">
  77. <div class="loading-spinner"></div>
  78. </div>
  79. <img v-else :src="v?.favIconUrl" style="width: 24px;display: block" />
  80. <div class="title-wrapper">
  81. <span class="els title-scroller">{{ v?.title }}</span>
  82. <span class="els url-scroller">{{ v.loading ? v?.state : v?.url }}</span>
  83. </div>
  84. <el-icon class="closeIcon" size="16px" color="#909399" @click="deletePageInfo(i)">
  85. <CircleClose />
  86. </el-icon>
  87. </div>
  88. </template>
  89. <div v-show="!pageInfoList.length && type === FunctionList.Intelligent_Form_filling">智能填表</div>
  90. </div>
  91. <div class="card-btn">
  92. <el-tooltip content="总结" placement="top">
  93. <el-button :disabled="disabledBtn" v-if="type !== FunctionList.Intelligent_Form_filling" round
  94. @click="handleSummary">总结
  95. </el-button>
  96. </el-tooltip>
  97. <el-tooltip content="抽取表单信息" placement="top">
  98. <el-button :disabled="disabledBtn"
  99. v-if="type === FunctionList.Intelligent_Form_filling && pageInfoList.length" round
  100. @click="handleSummaryFile">抽取
  101. </el-button>
  102. </el-tooltip>
  103. </div>
  104. </div>
  105. <el-input ref="textareaRef" v-model="inputMessage" type="textarea" :rows="3" placeholder="输入消息..."
  106. @keyup.enter="() => handleAsk()" />
  107. <div class="chat_area_op">
  108. <el-button v-if="sendLoading" style="background-color:#ffffff;border:2px solid rgb(134 143 153);" circle
  109. @click="handleStopAsk">
  110. <svg-icon icon-class="stop" color="red" />
  111. </el-button>
  112. <el-button
  113. v-else
  114. :style="`background-color:${inputMessage.trim() ? '#4d6bfe' : '#d6dee8'};border-color:${inputMessage.trim() ? '#4d6bfe' : '#d6dee8'}`"
  115. type="primary" circle @click="() => handleAsk()"
  116. :disabled="disSend">
  117. <svg-icon icon-class="send" color="#000000" />
  118. </el-button>
  119. </div>
  120. </div>
  121. </div>
  122. <!-- 历史记录 -->
  123. <historyComponent :msgUuid="msgUuid" ref="historyComponentRef" @currentData="(e) => handleCurrentData(e)" />
  124. </div>
  125. </template>
  126. <script setup>
  127. import { ref, onMounted, nextTick, inject, useTemplateRef, reactive, onBeforeMount } from 'vue'
  128. import { ElScrollbar, ElAvatar, ElInput, ElButton } from 'element-plus'
  129. import moment from 'moment'
  130. import fileLogo from '@/assets/svg/file.svg'
  131. import {
  132. getFileContent,
  133. buildObjPrompt,
  134. controllerList,
  135. formatMessage
  136. } from '@/entrypoints/sidepanel/utils/ai-service.js'
  137. import { storeToRefs } from 'pinia'
  138. import { ElMessage } from 'element-plus'
  139. import { useMsg } from '@/entrypoints/sidepanel/hook/useMsg.ts'
  140. import Tools from '@/entrypoints/sidepanel/component/tools.vue'
  141. import historyComponent from '@/entrypoints/sidepanel/component/historyComponent.vue'
  142. import document from '@/entrypoints/sidepanel/component/document.vue'
  143. import pageMask from '@/entrypoints/sidepanel/component/pageMask.vue'
  144. import ScrollToBottom from '@/entrypoints/sidepanel/component/ScrollToBottom.vue'
  145. import formTable from '@/entrypoints/sidepanel/component/formTable.vue'
  146. import userAvatar from '@/assets/images/user.png'
  147. import avatar from '@/public/icon/icon.png'
  148. import { FunctionList } from '@/entrypoints/sidepanel/mock'
  149. import { useAutoResizeTextarea } from '@/entrypoints/sidepanel/hook/useAutoResizeTextarea.ts'
  150. import { getPageInfo, getPageInfoClean, handleInput, downloadFile } from './utils/index.js'
  151. import { useMsgStore } from '@/store/modules/msg.ts'
  152. import { useUserStore } from '@/store/modules/user'
  153. import { putChat, uploadFile } from "@/api/index.js";
  154. import {askQues,getFormKey} from '@/api/model.js'
  155. import { debounce } from 'lodash'
  156. import { renderMarkdown } from './utils/markdown.js'
  157. import 'github-markdown-css'
  158. // import "/node_modules/github-markdown-css/github-markdown-dark.css"
  159. const userStore = useUserStore()
  160. // 在其他状态变量附近添加
  161. const isLoadingMore = ref(false)
  162. import { v4 as uuidv4 } from 'uuid'
  163. import { browser } from 'wxt/browser'
  164. // 滚动条引用
  165. const scrollbar = ref(null)
  166. const scrollToBottomRef = ref(null)
  167. const xlsxData = ref({})
  168. const isShowPage = ref(false)
  169. const tareRef = useTemplateRef('textareaRef')
  170. const drawerRef = useTemplateRef('historyComponentRef')
  171. const disSend = computed(() => {
  172. if (pageInfoList.value.length && type.value === FunctionList.Intelligent_Form_filling) return true
  173. if (sendLoading.value) return true
  174. if (inputMessage.value.trim()) return false
  175. return true
  176. })
  177. // 获取父组件提供的 Hook 实例
  178. const msgStore = useMsgStore()
  179. const { pageInfoList, messages, msgUuid, AIModel, page, hasNext, msgLoading, selectModal } = storeToRefs(useMsgStore())
  180. const formInfo = ref('')
  181. const {
  182. taklToHtml,
  183. sendLoading,
  184. type,
  185. fetchRes
  186. } = useMsg(scrollbar)
  187. const inputMessage = ref('')
  188. const pageInfo = ref('')
  189. const summaryHtml = ref(false)
  190. const compiledMarkdown = computed(() => {
  191. return (val) => {
  192. return renderMarkdown(val)
  193. }
  194. })
  195. function handleStopAsk() {
  196. inputMessage.value = ''
  197. pageInfoList.value = []
  198. isShowPage.value = false
  199. taklToHtml.value = false
  200. type.value = FunctionList.File_Operation
  201. controllerList.value[0]?.abort()
  202. controllerList.value = []
  203. sendLoading.value = false
  204. formInfo.value = null
  205. }
  206. function handleScroll(a) {
  207. const { wrapRef } = scrollbar.value
  208. const { scrollHeight, clientHeight } = wrapRef
  209. scrollToBottomRef.value.showButton = scrollHeight - a.scrollTop - clientHeight >= 350
  210. // 检测是否滚动到顶部
  211. if (a.scrollTop <= 10 && hasNext.value) {
  212. handleScrollToTop()
  213. }
  214. }
  215. const messagesContainer = ref(null)
  216. let innerText = ''
  217. // 处理滚动到顶部的事件
  218. const handleScrollToTop = debounce(async function() {
  219. console.log('滚动到顶部,可以加载更多历史消息')
  220. if (!sendLoading.value && !isLoadingMore.value) {
  221. isLoadingMore.value = true
  222. try {
  223. // 记录当前第一条消息的位置
  224. const firstMessage = messagesContainer.value?.firstElementChild.querySelector('.timestamp')
  225. innerText = firstMessage.innerText
  226. const oldHeight = firstMessage?.offsetTop || 0
  227. await msgStore.changePage()
  228. setTimeout(() => {
  229. if (firstMessage) {
  230. console.log([...messagesContainer.value.querySelectorAll('.timestamp')].find(_ => _.innerText === innerText).offsetTop, innerText, 778)
  231. const newHeight = [...messagesContainer.value.querySelectorAll('.timestamp')].find(_ => _.innerText === innerText).offsetTop
  232. const scrollOffset = newHeight - oldHeight
  233. scrollbar.value?.setScrollTop(scrollOffset)
  234. }
  235. }, 400)
  236. // 恢复滚动位置
  237. } finally {
  238. isLoadingMore.value = false
  239. }
  240. }
  241. }, 500, { leading: false, trailing: true })
  242. /**
  243. * @param {string} msg
  244. * @param {string} raw
  245. * @param {string} type 发送的类型 document 、text
  246. * **/
  247. async function addMessage(msg, raw, type) {
  248. const newMessage = reactive({
  249. id: +new Date(),
  250. type: type || '',
  251. rawContent: raw ?? msg,
  252. senderId: userStore.userInfo.id,
  253. receiverId: -1, //大模型
  254. content: msg,
  255. sortKey: moment().valueOf(),
  256. role: 'user',
  257. conversationId: msgUuid.value,
  258. addToHistory: `${!taklToHtml.value}`,
  259. redisKey: undefined,
  260. fileId: undefined
  261. })
  262. if (type === 'document') {
  263. newMessage.content = msg
  264. newMessage.rawContent = raw
  265. }
  266. if (!msg) return
  267. messages.value.push(newMessage)
  268. nextTick(() => {
  269. if (scrollbar.value && scrollbar.value.wrapRef) {
  270. scrollbar.value.setScrollTop(scrollbar.value.wrapRef.scrollHeight)
  271. }
  272. })
  273. return newMessage
  274. }
  275. const handleSummary = async () => {
  276. if (sendLoading.value) return
  277. handleAsk(`请帮我总结当前${summaryHtml.value ? '页面' : '文件'}`)
  278. summaryHtml.value = false
  279. }
  280. const handleSummaryFile = async () => {
  281. if (sendLoading.value) return
  282. try {
  283. sendLoading.value = true
  284. const userMsg = await addMessage(JSON.stringify([...pageInfoList.value, {
  285. type: 'text',
  286. value: '抽取表单'
  287. }]), '抽取表单', 'document')
  288. userMsg.fileId = pageInfoList.value[0].fileId
  289. userMsg.redisKey = pageInfoList.value[0].redisKey
  290. await putChat(userMsg)
  291. createWS(userMsg)
  292. formInfo.value = ''
  293. // const msg = reactive({
  294. // type: '',
  295. // rawContent: '',
  296. // senderId: -1,
  297. // receiverId: userStore.userInfo.id, //大模型
  298. // content: '',
  299. // sortKey: moment().valueOf(),
  300. // role: 'system',
  301. // conversationId: msgUuid.value,
  302. // })
  303. // messages.value.push(msg)
  304. // isShowPage.value = false
  305. pageInfoList.value = []
  306. // const resForm = await askQues({
  307. // conversationId: msgUuid.value,
  308. // modelName: '通义千问-Max',
  309. // question: buildObjPrompt(xlsxData.value,formInfo.value),
  310. // id: '699637194561691650',
  311. // // redisKey:msg.redisKey
  312. // })
  313. // return console.log(resForm);
  314. } catch (error) {
  315. console.log(error)
  316. } finally {
  317. // sendLoading.value = false
  318. // type.value = FunctionList.File_Operation
  319. }
  320. }
  321. async function handelIntelligentFillingClick() {
  322. handleInput()
  323. isShowPage.value = true
  324. // taklToHtml.value = true
  325. // const tempPageInfo = await getPageInfo()
  326. // pageInfo.value = tempPageInfo
  327. // pageInfoList.value = [tempPageInfo]
  328. inputMessage.value = '/智能填表 '
  329. type.value = FunctionList.Intelligent_Form_filling
  330. }
  331. function handleCurrentChange(e) {
  332. msgStore.updateAIModel(e)
  333. }
  334. async function handleCurrentData(e) {
  335. drawerRef.value.drawer = false
  336. isShowPage.value = false
  337. if (!e) {
  338. addNewDialogue()
  339. return
  340. }
  341. messages.value = []
  342. page.value = 1
  343. msgUuid.value = e
  344. chrome.storage.local.set({ msgUuid: msgUuid.value })
  345. await msgStore.initMsg()
  346. nextTick(() => {
  347. if (scrollbar.value && scrollbar.value.wrapRef) {
  348. scrollbar.value.setScrollTop(scrollbar.value.wrapRef.scrollHeight)
  349. }
  350. })
  351. }
  352. let SystemMsg = null
  353. watchEffect(() => {
  354. console.log(isShowPage.value)
  355. if (!isShowPage.value) {
  356. summaryHtml.value = false
  357. type.value = FunctionList.File_Operation
  358. SystemMsg = null
  359. inputMessage.value = ''
  360. }
  361. })
  362. const htmlIcon = ref('')
  363. async function readClick() {
  364. if (type.value === FunctionList.Intelligent_Form_filling) {
  365. pageInfoList.value = []
  366. type.value = FunctionList.File_Operation
  367. }
  368. summaryHtml.value = true
  369. isShowPage.value = true
  370. taklToHtml.value = true
  371. if (pageInfoList.value.length >= Number(import.meta.env.VITE_MAX_FILE_NUMBER)) {
  372. ElMessage.warning(`最多添加${import.meta.env.VITE_MAX_FILE_NUMBER}个文件`)
  373. return
  374. }
  375. const tempPageInfo = await getPageInfo()
  376. htmlIcon.value = tempPageInfo.favIconUrl
  377. const blob = new Blob([tempPageInfo.content.mainContent], { type: 'text/plain' })
  378. const file = new File([blob], '页面' + '.txt', { type: 'text/plain' })
  379. // console.log(file);
  380. // 下载文件到本地
  381. // downloadFile(file);
  382. console.log(tempPageInfo)
  383. await createFileObj(file)
  384. // await handleUpload(file,tempPageInfo.title + '.txt')
  385. }
  386. // 添加下载文件的函数
  387. function deletePageInfo(i) {
  388. pageInfoList.value.splice(i, 1)
  389. if (pageInfoList.value.length === 0) {
  390. if (type.value === FunctionList.Intelligent_Form_filling) {
  391. inputMessage.value = ''
  392. SystemMsg.content = '智能填表流程中断'
  393. SystemMsg.rawContent = '智能填表流程中断'
  394. putChat({
  395. ...SystemMsg,
  396. content: ''
  397. })
  398. }
  399. handleStopAsk()
  400. }
  401. }
  402. function addNewDialogue() {
  403. if (messages.value.length === 0) {
  404. ElMessage.warning('已经是新对话')
  405. return
  406. }
  407. summaryHtml.value = false
  408. type.value = FunctionList.File_Operation
  409. isShowPage.value = false
  410. taklToHtml.value = false
  411. messages.value = []
  412. page.value = 1
  413. pageInfoList.value = []
  414. msgUuid.value = uuidv4()
  415. hasNext.value = false
  416. chrome.storage.local.set({ msgUuid: msgUuid.value })
  417. }
  418. const disabledBtn = computed(() => {
  419. return !!(pageInfoList.value.find(_ => _.loading))
  420. })
  421. function closePageInfo() {
  422. pageInfoList.value = []
  423. if (type.value === FunctionList.Intelligent_Form_filling && SystemMsg) {
  424. SystemMsg.content = '智能填表流程中断'
  425. SystemMsg.rawContent = '智能填表流程中断'
  426. putChat({
  427. ...SystemMsg,
  428. content: ''
  429. })
  430. }
  431. handleStopAsk()
  432. }
  433. async function handleAsk(value) {
  434. if (sendLoading.value) return
  435. const str = value ?? inputMessage.value.trim()
  436. inputMessage.value = ''
  437. if (type.value === FunctionList.Intelligent_Form_filling) {
  438. const msg = await addMessage(str)
  439. await putChat(msg)
  440. const [res, obj] = await fetchRes(str)
  441. if (res.status === 'ok') {
  442. console.log(obj)
  443. formInfo.value = res.data
  444. SystemMsg = obj
  445. } else {
  446. type.value = FunctionList.File_Operation
  447. isShowPage.value = false
  448. }
  449. return
  450. }
  451. if (sendLoading.value) return
  452. let msg = null
  453. if (pageInfoList.value.length) {
  454. msg = await addMessage(JSON.stringify([...pageInfoList.value, { type: 'text', value: str }]), str, 'document')
  455. msg.fileId = pageInfoList.value[0].fileId
  456. msg.redisKey = pageInfoList.value[0].redisKey
  457. } else msg = await addMessage(str)
  458. pageInfoList.value = []
  459. isShowPage.value = false
  460. await putChat(msg)
  461. createWS(msg)
  462. }
  463. /**
  464. *
  465. * @param msg 用户消息对象,调用askQues时需要
  466. */
  467. async function createWS(msg) {
  468. sendLoading.value = true
  469. const obj = reactive({
  470. type: type.value === FunctionList.Intelligent_Form_filling ? 'form' : '',
  471. rawContent: '',
  472. senderId: -1,
  473. receiverId: userStore.userInfo.id, //大模型
  474. content: '',
  475. sortKey: moment().valueOf(),
  476. role: 'system',
  477. conversationId: msgUuid.value
  478. })
  479. try {
  480. messages.value.push(obj)
  481. nextTick(() => {
  482. if (scrollbar.value && scrollbar.value.wrapRef) {
  483. scrollbar.value.setScrollTop(scrollbar.value.wrapRef.scrollHeight)
  484. }
  485. })
  486. const websocketId = uuidv4()
  487. const wsUrl = `${import.meta.env.VITE_API_WS_URL}/webSocket/clue/${websocketId}`
  488. const socket = new WebSocket(wsUrl)
  489. console.log(selectModal.value)
  490. askQues({
  491. conversationId: msgUuid.value,
  492. websocketId,
  493. modelName: selectModal.value.modelName,
  494. question: type.value === FunctionList.File_Operation ? msg.rawContent : buildObjPrompt(xlsxData.value, formInfo.value),
  495. id: selectModal.value.id,
  496. redisKey: msg.redisKey
  497. }).catch(res => {
  498. obj.rawContent = '接口出错,请重试。'
  499. obj.content = '接口出错,请重试。'
  500. socket.close()
  501. })
  502. socket.onmessage = (event) => {
  503. try {
  504. if (event.data === '') {
  505. console.log(23223)
  506. socket.close()
  507. if (type.value === FunctionList.Intelligent_Form_filling) {
  508. const formResult = obj.rawContent
  509. let form = null
  510. if (formResult.includes('json')) form = JSON.parse(formResult.split('json')[1].split('```')[0])
  511. else form = JSON.parse(formResult)
  512. sendLoading.value = false
  513. console.log(xlsxData.value, form)
  514. handleInput(xlsxData.value, form)
  515. }
  516. isShowPage.value = false
  517. } else {
  518. obj.rawContent += event.data
  519. obj.content = obj.type === 'form' ? obj.rawContent : formatMessage(obj.rawContent)
  520. // 滚动到底部
  521. nextTick(() => {
  522. if (scrollbar.value && scrollbar.value.wrapRef) {
  523. scrollbar.value.setScrollTop(scrollbar.value.wrapRef.scrollHeight)
  524. }
  525. })
  526. }
  527. } catch (error) {
  528. console.error('解析消息出错:', error)
  529. }
  530. }
  531. socket.onerror = (error) => {
  532. console.error('WebSocket 错误:', error)
  533. socket.close()
  534. }
  535. socket.onclose = () => {
  536. console.log('WebSocket 连接已关闭')
  537. sendLoading.value = false
  538. putChat({
  539. ...obj,
  540. content: ''
  541. })
  542. sendLoading.value = false
  543. controllerList.value = []
  544. type.value = FunctionList.File_Operation
  545. }
  546. // 添加到控制器列表,以便可以在需要时中断连接
  547. controllerList.value.push({
  548. abort: () => {
  549. socket.close()
  550. }
  551. })
  552. } catch (error) {
  553. console.error(error)
  554. sendLoading.value = false
  555. // 显示错误消息
  556. obj.content = '连接失败,请重试'
  557. putChat({
  558. ...obj,
  559. content: ''
  560. })
  561. } finally {
  562. }
  563. }
  564. function handleCapture() {
  565. browser.tabs.query({ active: true, currentWindow: true }).then(tabs => {
  566. if (tabs.length === 0) {
  567. ElMessage.warning('请先打开一个页面')
  568. return
  569. }
  570. // sendMessage('SCREENSHOT', { t: 'Start taking screenshots' }, `content-script@${tabs[0].id}`)
  571. })
  572. // chrome.runtime.sendMessage({
  573. // type: 'SCREENSHOT',
  574. // data: 'Start taking screenshots'
  575. // }, (response) => {
  576. // if (response.status === 'ok') {
  577. // isShowPage.value = true
  578. // pageInfoList.value.unshift({
  579. // isUpload: false,
  580. // fileId: '',
  581. // type: 'image',
  582. // url: response.data
  583. // })
  584. // // console.log(response);
  585. // // console.log(response, pageInfoList.value)
  586. // }
  587. // })
  588. }
  589. function hisRecords() {
  590. drawerRef.value.drawer = true
  591. console.log(123)
  592. }
  593. const createFileObj = async (file) => {
  594. handleUpload(file)
  595. }
  596. const handleUpload = async (file) => {
  597. const obj = reactive({
  598. title: summaryHtml.value ? file.name.split('.')[0] : file.name,
  599. state: '上传中...',
  600. favIconUrl: fileLogo,
  601. loading: true,
  602. url: '',
  603. fileId: '',
  604. redisKey: '',
  605. content: {
  606. mainContent: ''
  607. }
  608. })
  609. if (type.value === FunctionList.Intelligent_Form_filling) {
  610. pageInfoList.value = []
  611. }
  612. if (summaryHtml.value) obj.favIconUrl = htmlIcon.value
  613. isShowPage.value = true
  614. // const obj = reactive({
  615. // title: file.name,
  616. // state: '上传中...',
  617. // favIconUrl: fileLogo,
  618. // loading: true,
  619. // url:'',
  620. // fileId: '',
  621. // redisKey:'',
  622. // content: {
  623. // mainContent: ''
  624. // }
  625. // })
  626. pageInfoList.value = [obj]
  627. try {
  628. let formData = new FormData()
  629. formData.append('avatarFile', file)
  630. const res = await uploadFile(formData)
  631. if (type.value === FunctionList.Intelligent_Form_filling) {
  632. obj.state = '解析中...'
  633. obj.redisKey = res.data.redisKey
  634. obj.id = res.data.file.id
  635. obj.url = res.data.file.url
  636. // const fileExtension = file.name.split('.').pop().toLowerCase()
  637. const result = await getFormKey({
  638. body: formInfo.value,
  639. input_data: res.data.data
  640. })
  641. xlsxData.value = JSON.parse(result.data)
  642. // if (fileExtension === 'xlsx') {
  643. // const readData = await getXlsxValue(file)
  644. // readData[0].forEach((header, i) => {
  645. // // if (!xlsxData.value[header]) xlsxData.value[header] = []
  646. // xlsxData.value[header] = readData[1][i]
  647. // })
  648. // } else {
  649. // const result = await getFormKey({
  650. // body: formInfo.value,
  651. // input_data:res.data.data
  652. // })
  653. // xlsxData.value = JSON.parse(result.data)
  654. // }
  655. }
  656. nextTick(() => {
  657. if (scrollbar.value && scrollbar.value.wrapRef) {
  658. scrollbar.value.setScrollTop(scrollbar.value.wrapRef.scrollHeight)
  659. }
  660. })
  661. if (type.value === FunctionList.File_Operation) {
  662. obj.loading = false
  663. obj.url = res.data.url
  664. obj.redisKey = res.data.redisKey
  665. obj.fileId = res.data.file.id
  666. obj.url = res.data.file.url
  667. }
  668. obj.loading = false
  669. console.log(SystemMsg)
  670. } catch (error) {
  671. if (SystemMsg) {
  672. SystemMsg.content = '数据上传出错,请重试'
  673. SystemMsg.rawContent = '数据上传出错,请重试'
  674. }
  675. console.log(error)
  676. ElMessage.error('上传出错')
  677. pageInfoList.value = []
  678. isShowPage.value = false
  679. } finally {
  680. SystemMsg && putChat({
  681. ...SystemMsg,
  682. content: ''
  683. })
  684. SystemMsg = null
  685. }
  686. }
  687. async function getFileValue(file) {
  688. let formData = new FormData()
  689. formData.append('file', file)
  690. const res = await getFileContent(formData)
  691. return res.data
  692. }
  693. let a = null
  694. // 组件挂载时滚动到底部
  695. onBeforeMount(async () => {
  696. await msgStore.initMsg()
  697. await msgStore.initModal()
  698. })
  699. onMounted(async () => {
  700. useAutoResizeTextarea(tareRef, inputMessage)
  701. chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  702. if (message.type === 'TO_SIDE_PANEL_PAGE_INFO') {
  703. pageInfo.value = message.data
  704. // 转发到 content.js
  705. chrome.tabs.sendMessage(sender.tab.id, {
  706. type: 'TO_CONTENT_SCRIPT',
  707. data: message.data
  708. })
  709. }
  710. if (message.type === 'TO_SIDE_PANEL_PAGE_CHANGE') {
  711. pageInfo.value = message.data
  712. }
  713. })
  714. nextTick(() => {
  715. if (scrollbar.value && scrollbar.value.wrapRef) {
  716. scrollbar.value.setScrollTop(scrollbar.value.wrapRef.scrollHeight)
  717. }
  718. })
  719. })
  720. </script>
  721. <style lang="scss" scoped>
  722. @use '@/entrypoints/sidepanel/css/chat.scss';
  723. @use '@/entrypoints/sidepanel/css/markdown.scss';
  724. .loading-more-indicator {
  725. display: flex;
  726. align-items: center;
  727. justify-content: center;
  728. padding: 10px 0;
  729. color: #909399;
  730. font-size: 14px;
  731. .loading-spinner {
  732. width: 20px;
  733. height: 20px;
  734. margin-right: 8px;
  735. border: 2px solid #e6e6e6;
  736. border-top-color: #4d6bfe;
  737. border-radius: 50%;
  738. animation: spin 1s linear infinite;
  739. }
  740. }
  741. @keyframes spin {
  742. to {
  743. transform: rotate(360deg);
  744. }
  745. }
  746. </style>