Chat.vue 25 KB

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