AdvancedMode.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. <!-- Chat.vue -->
  2. <template>
  3. <div class="chat-container" style="padding-top: 3rem;">
  4. <!-- <div v-if="!messages.length && !msgLoading" class="message-list">
  5. <pageMask />
  6. </div> -->
  7. <!-- 消息列表 -->
  8. <div class="message-list">
  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. <StepsDisplay
  23. v-if="message.type === 'plan'"
  24. :content="message.rawContent"
  25. :initialStep="0"
  26. @step-change="handleStepChange" @complete="handleComplete"
  27. />
  28. <span v-else v-html="message.content"></span>
  29. <span class="loading-indicator" v-if="sendLoading && index === messages.length - 1">
  30. <span class="dot"></span>
  31. <span class="dot"></span>
  32. <span class="dot"></span>
  33. </span>
  34. </div>
  35. <document v-else-if="message.type === 'document' && message.role === 'user'" :content="message.content"
  36. :rawContent="message.rawContent" />
  37. <div v-else class="content">
  38. <span v-if="message.type === ''">{{ message.content }}</span>
  39. <span class="loading-indicator" v-if="sendLoading && index === messages.length - 1">
  40. <span class="dot"></span>
  41. <span class="dot"></span>
  42. <span class="dot"></span>
  43. </span>
  44. </div>
  45. <div class="timestamp ">{{ moment(message.sortKey).format('MM-DD HH:mm') }}
  46. <!-- <span v-if="message.add" style="cursor: pointer;" @click="handleInput">填充</span> -->
  47. </div>
  48. </div>
  49. </div>
  50. </div>
  51. </el-scrollbar>
  52. <ScrollToBottom :target="scrollbar" ref="scrollToBottomRef" />
  53. </div>
  54. <!-- <Tools v-if="selectModal" :disHistory="sendLoading" :upload="type === FunctionList.File_Operation || !!formInfo" @read-click="readClick" @upload-file="(file) => createFileObj(file)" @handle-capture="handleCapture" @his-records="hisRecords"
  55. @add-new-dialogue="addNewDialogue" @handle-current-change="handleCurrentChange"
  56. @handel-intelligent-filling-click="handelIntelligentFillingClick" /> -->
  57. <div>
  58. <!-- 输入区域 -->
  59. <div class="input-area">
  60. <el-icon class="closeShow" :style="{ display: isShowPage ? 'block' : 'none'}" size="16px" color="#909399"
  61. @click="closePageInfo">
  62. <CircleClose />
  63. </el-icon>
  64. <!-- <div v-show="isShowPage" style="border-bottom: 1px solid #F0F0F0;">
  65. <div class="card_list">
  66. <div v-for="(v, i) in pageInfoList" :key="i"
  67. :class="`card-content ${pageInfoList.length > 1 ? 'card_width' : ''}`">
  68. <div class="loading-more-indicator" v-if="v.loading">
  69. <div class="loading-spinner"></div>
  70. </div>
  71. <img v-else :src="v?.favIconUrl" style="width: 24px;display: block" />
  72. <div class="title-wrapper">
  73. <span class="els title-scroller">{{ v?.title }}</span>
  74. <span class="els url-scroller">{{ v.loading ? v?.state : v?.url }}</span>
  75. </div>
  76. <el-icon class="closeIcon" size="16px" color="#909399" @click="deletePageInfo(i)">
  77. <CircleClose />
  78. </el-icon>
  79. </div>
  80. <div v-show="!pageInfoList.length && type === FunctionList.Intelligent_Form_filling">智能填表</div>
  81. </div>
  82. <div class="card-btn">
  83. <el-tooltip content="总结" placement="top">
  84. <el-button :disabled="disabledBtn" v-if="type !== FunctionList.Intelligent_Form_filling" round @click="handleSummary">总结</el-button>
  85. </el-tooltip>
  86. <el-tooltip content="抽取表单信息" placement="top">
  87. <el-button :disabled="disabledBtn" v-if="type === FunctionList.Intelligent_Form_filling && pageInfoList.length" round @click="handleSummaryFile">抽取</el-button>
  88. </el-tooltip>
  89. </div>
  90. </div> -->
  91. <el-input ref="textareaRef" v-model="inputMessage" type="textarea" :rows="3" placeholder="输入消息..."
  92. @keyup.enter="() => handleAsk()" />
  93. <div class="chat_area_op">
  94. <el-button v-if="sendLoading" style="background-color:#ffffff;border:2px solid rgb(134 143 153);" circle
  95. @click="handleStopAsk">
  96. <svg-icon icon-class="stop" color="red" />
  97. </el-button>
  98. <el-button
  99. v-else
  100. :style="`background-color:${inputMessage.trim() ? '#4d6bfe' : '#d6dee8'};border-color:${inputMessage.trim() ? '#4d6bfe' : '#d6dee8'}`"
  101. type="primary" circle @click="() => handleAsk()"
  102. :disabled="disSend">
  103. <svg-icon icon-class="send" color="#000000" />
  104. </el-button>
  105. </div>
  106. </div>
  107. </div>
  108. <!-- <historyComponent :msgUuid="msgUuid" ref="historyComponentRef" @currentData="(e) => handleCurrentData(e)" /> -->
  109. </div>
  110. </template>
  111. <script setup lang="ts">
  112. import { ref, onMounted, nextTick, inject, useTemplateRef, reactive, onBeforeMount } from 'vue'
  113. import { ElScrollbar, ElAvatar, ElInput, ElButton } from 'element-plus'
  114. import moment from 'moment'
  115. import { sendMessage } from 'webext-bridge/side-panel'
  116. import {
  117. controllerList
  118. } from '@/entrypoints/sidepanel/utils/ai-service.js'
  119. import { storeToRefs } from 'pinia'
  120. import { useMsg } from '@/entrypoints/sidepanel/hook/useMsg.ts'
  121. import document from '@/entrypoints/sidepanel/component/document.vue'
  122. import ScrollToBottom from '@/entrypoints/sidepanel/component/ScrollToBottom.vue'
  123. import StepsDisplay from './component/StepsDisplay.vue'
  124. import userAvatar from '@/assets/images/user.png'
  125. import avatar from '@/public/icon/icon.png'
  126. import { FunctionList } from '@/entrypoints/sidepanel/mock'
  127. import { useAutoResizeTextarea } from '@/entrypoints/sidepanel/hook/useAutoResizeTextarea.ts'
  128. import { useMsgStore } from '@/store/modules/msg.ts'
  129. import { useUserStore } from '@/store/modules/user'
  130. import { debounce } from 'lodash'
  131. const userStore = useUserStore()
  132. // 在其他状态变量附近添加
  133. const isLoadingMore = ref(false)
  134. import { getPlan2 } from '@/api/advance.js'
  135. // 滚动条引用
  136. const scrollbar = ref(null)
  137. const scrollToBottomRef = ref(null)
  138. const isShowPage = ref(false)
  139. const tareRef = useTemplateRef('textareaRef')
  140. const disSend = computed(() => {
  141. if (pageInfoList.value.length && type.value === FunctionList.Intelligent_Form_filling) return true
  142. if (sendLoading.value) return true
  143. if (inputMessage.value.trim()) return false
  144. return true
  145. })
  146. // 获取父组件提供的 Hook 实例
  147. const msgStore = useMsgStore()
  148. const { pageInfoList, messages, msgUuid, hasNext, msgLoading } = storeToRefs(useMsgStore())
  149. const formInfo = ref('')
  150. const {
  151. taklToHtml,
  152. sendLoading,
  153. type,
  154. fetchRes
  155. } = useMsg(scrollbar)
  156. const inputMessage = ref('')
  157. const pageInfo = ref('')
  158. const summaryHtml = ref(false)
  159. function handleStopAsk() {
  160. inputMessage.value = ''
  161. pageInfoList.value = []
  162. isShowPage.value = false
  163. taklToHtml.value = false
  164. type.value = FunctionList.File_Operation
  165. controllerList.value[0]?.abort()
  166. controllerList.value = []
  167. sendLoading.value = false
  168. formInfo.value = null
  169. }
  170. function handleScroll(a) {
  171. const { wrapRef } = scrollbar.value
  172. const { scrollHeight, clientHeight } = wrapRef
  173. scrollToBottomRef.value.showButton = scrollHeight - a.scrollTop - clientHeight >= 350
  174. // 检测是否滚动到顶部
  175. if (a.scrollTop <= 10 && hasNext.value) {
  176. handleScrollToTop()
  177. }
  178. }
  179. const messagesContainer = ref(null)
  180. let innerText = ''
  181. // 处理滚动到顶部的事件
  182. const handleScrollToTop = debounce(async function() {
  183. console.log('滚动到顶部,可以加载更多历史消息')
  184. if (!sendLoading.value && !isLoadingMore.value) {
  185. isLoadingMore.value = true
  186. try {
  187. // 记录当前第一条消息的位置
  188. const firstMessage = messagesContainer.value?.firstElementChild.querySelector('.timestamp')
  189. innerText = firstMessage.innerText
  190. const oldHeight = firstMessage?.offsetTop || 0
  191. await msgStore.changePage()
  192. setTimeout(() => {
  193. if (firstMessage) {
  194. console.log([...messagesContainer.value.querySelectorAll('.timestamp')].find(_ => _.innerText === innerText).offsetTop, innerText, 778)
  195. const newHeight = [...messagesContainer.value.querySelectorAll('.timestamp')].find(_ => _.innerText === innerText).offsetTop
  196. const scrollOffset = newHeight - oldHeight
  197. scrollbar.value?.setScrollTop(scrollOffset)
  198. }
  199. }, 400)
  200. // 恢复滚动位置
  201. } finally {
  202. isLoadingMore.value = false
  203. }
  204. }
  205. }, 500, { leading: false, trailing: true })
  206. const handleStepChange = (step) => {
  207. console.log('当前步骤:', step)
  208. }
  209. const handleComplete = () => {
  210. console.log('所有步骤已完成')
  211. }
  212. /**
  213. * @param {string} msg
  214. * @param {string} raw
  215. * @param {string} type 发送的类型 document 、text
  216. * **/
  217. async function addMessage(msg, raw, type) {
  218. const newMessage = reactive({
  219. id: +new Date(),
  220. type: type || '',
  221. rawContent: raw ?? msg,
  222. senderId: userStore.userInfo.id,
  223. receiverId: -1, //大模型
  224. content: msg,
  225. sortKey: moment().valueOf(),
  226. role: 'user',
  227. conversationId: msgUuid.value,
  228. addToHistory: `${!taklToHtml.value}`,
  229. redisKey: undefined,
  230. fileId: undefined
  231. })
  232. if (type === 'document') {
  233. newMessage.content = msg
  234. newMessage.rawContent = raw
  235. }
  236. if (!msg) return
  237. messages.value.push(newMessage)
  238. nextTick(() => {
  239. if (scrollbar.value && scrollbar.value.wrapRef) {
  240. scrollbar.value.setScrollTop(scrollbar.value.wrapRef.scrollHeight)
  241. }
  242. })
  243. return newMessage
  244. }
  245. let SystemMsg = null
  246. watchEffect(() => {
  247. if (!isShowPage.value) {
  248. summaryHtml.value = false
  249. type.value = FunctionList.File_Operation
  250. SystemMsg = null
  251. inputMessage.value = ''
  252. }
  253. })
  254. function closePageInfo() {
  255. pageInfoList.value = []
  256. if (type.value === FunctionList.Intelligent_Form_filling && SystemMsg) {
  257. SystemMsg.content = '智能填表流程中断'
  258. SystemMsg.rawContent = '智能填表流程中断'
  259. putChat({
  260. ...SystemMsg,
  261. content: ''
  262. })
  263. }
  264. handleStopAsk()
  265. }
  266. async function handleAsk(value) {
  267. if (sendLoading.value) return
  268. const str = value ?? inputMessage.value.trim()
  269. inputMessage.value = ''
  270. if (type.value === FunctionList.Intelligent_Form_filling) {
  271. const msg = await addMessage(str)
  272. // await putChat(msg)
  273. const [res, obj] = await fetchRes(str)
  274. if (res.status === 'ok') {
  275. console.log(obj)
  276. formInfo.value = res.data
  277. SystemMsg = obj
  278. } else {
  279. type.value = FunctionList.File_Operation
  280. isShowPage.value = false
  281. }
  282. return
  283. }
  284. if (sendLoading.value) return
  285. let msg = null
  286. if (pageInfoList.value.length) {
  287. msg = await addMessage(JSON.stringify([...pageInfoList.value, { type: 'text', value: str }]), str, 'document')
  288. msg.fileId = pageInfoList.value[0].fileId
  289. msg.redisKey = pageInfoList.value[0].redisKey
  290. } else msg = await addMessage(str)
  291. pageInfoList.value = []
  292. isShowPage.value = false
  293. // await putChat(msg)
  294. createWS(msg)
  295. }
  296. /**
  297. *
  298. * @param msg 用户消息对象,调用askQues时需要
  299. */
  300. async function createWS(msg) {
  301. sendLoading.value = true
  302. const obj = reactive({
  303. type: '',
  304. rawContent: '',
  305. senderId: -1,
  306. receiverId: userStore.userInfo.id, //大模型
  307. content: '',
  308. sortKey: moment().valueOf(),
  309. role: 'system',
  310. conversationId: msgUuid.value
  311. })
  312. try {
  313. messages.value.push(obj)
  314. nextTick(() => {
  315. if (scrollbar.value && scrollbar.value.wrapRef) {
  316. scrollbar.value.setScrollTop(scrollbar.value.wrapRef.scrollHeight)
  317. }
  318. })
  319. let params = {
  320. conversationId: Math.floor(Math.random() * 1000) + 1,
  321. messageId: Math.floor(Math.random() * 1000) + 1,
  322. messageContent: msg.rawContent
  323. }
  324. getPlan2(params).then(res => {
  325. sendLoading.value = false
  326. obj.type = 'plan'
  327. console.log(res, 5558)
  328. obj.rawContent = res.data.plan
  329. sendMessage('FROM_PLAN', {
  330. params,
  331. payload: { ...res.data.plan }
  332. }, 'background')
  333. }).catch(res => {
  334. obj.rawContent = '接口出错,请重试。'
  335. obj.content = '接口出错,请重试。'
  336. })
  337. } finally {
  338. }
  339. }
  340. // 组件挂载时滚动到底部
  341. onBeforeMount(async () => {
  342. await msgStore.initMsg()
  343. await msgStore.initModal()
  344. })
  345. onMounted(async () => {
  346. // msgStore.updateAIModel(options[0].options[0])
  347. useAutoResizeTextarea(tareRef, inputMessage)
  348. chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  349. if (message.type === 'TO_SIDE_PANEL_PAGE_INFO') {
  350. pageInfo.value = message.data
  351. // 转发到 content.js
  352. // chrome.tabs.sendMessage(sender.tab.id, {
  353. // type: 'TO_CONTENT_SCRIPT',
  354. // data: message.data
  355. // })
  356. }
  357. if (message.type === 'TO_SIDE_PANEL_PAGE_CHANGE') {
  358. pageInfo.value = message.data
  359. }
  360. })
  361. nextTick(() => {
  362. if (scrollbar.value && scrollbar.value.wrapRef) {
  363. scrollbar.value.setScrollTop(scrollbar.value.wrapRef.scrollHeight)
  364. }
  365. })
  366. })
  367. </script>
  368. <style lang="scss" scoped>
  369. @use '@/entrypoints/sidepanel/css/chat.scss';
  370. @use '@/entrypoints/sidepanel/css/markdown.scss';
  371. .loading-more-indicator {
  372. display: flex;
  373. align-items: center;
  374. justify-content: center;
  375. padding: 10px 0;
  376. color: #909399;
  377. font-size: 14px;
  378. .loading-spinner {
  379. width: 20px;
  380. height: 20px;
  381. margin-right: 8px;
  382. border: 2px solid #e6e6e6;
  383. border-top-color: #4d6bfe;
  384. border-radius: 50%;
  385. animation: spin 1s linear infinite;
  386. }
  387. }
  388. @keyframes spin {
  389. to {
  390. transform: rotate(360deg);
  391. }
  392. }
  393. </style>