Chat.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. <!-- Chat.vue -->
  2. <template>
  3. <div class="chat-container">
  4. <!-- 消息列表 -->
  5. <el-scrollbar class="message-list" ref="scrollbar">
  6. <div class="messages">
  7. <div v-for="(message, index) in messages" :key="index"
  8. :class="['message-item', message.isSelf ? 'self' : 'other']">
  9. <el-avatar :size="32" :src="message.avatar" />
  10. <div class="message-content">
  11. <div class="content" v-if="!message.isSelf" :class="{ 'loading-content': message.content === '' }">
  12. <span v-html="message.content"></span>
  13. <span class="loading-indicator" v-if="sendLoading && index === messages.length - 1">
  14. <span class="dot"></span>
  15. <span class="dot"></span>
  16. <span class="dot"></span>
  17. </span>
  18. </div>
  19. <div v-else class="content">{{ message.content }}
  20. <span class="loading-indicator" v-if="sendLoading && index === messages.length - 1">
  21. <span class="dot"></span>
  22. <span class="dot"></span>
  23. <span class="dot"></span>
  24. </span>
  25. </div>
  26. <div class="timestamp ">{{ message.timestamp }}
  27. <span v-if="message.add" style="cursor: pointer;" @click="handleInput">填充</span>
  28. </div>
  29. </div>
  30. </div>
  31. </div>
  32. </el-scrollbar>
  33. <Tools @read-click="readClick"
  34. @upload-file="handleUpload"
  35. @handle-capture="handleCapture"
  36. @his-records="hisRecords"
  37. @add-new-dialogue="addNewDialogue" />
  38. <div>
  39. <!-- 输入区域 -->
  40. <div class="input-area">
  41. <div v-show="isShowPage" style="border-bottom: 1px solid #F0F0F0;">
  42. <div class="card_list">
  43. <div v-for="(v,i) in pageInfoList" :key="i"
  44. :class="`card-content ${pageInfoList.length > 1 ? 'card_width' : ''}`">
  45. <img :src="v?.favIconUrl" style="width: 24px;display: block" />
  46. <div class="title-wrapper">
  47. <span class="els title-scroller">{{ v?.title }}</span>
  48. <span class="els url-scroller">{{ v?.url }}</span>
  49. </div>
  50. <el-icon class="closeIcon" size="16px" color="#909399" @click="deletePageInfo(i)">
  51. <CircleClose />
  52. </el-icon>
  53. </div>
  54. </div>
  55. <div class="card-btn">
  56. <el-tooltip content="总结当前页面" placement="top">
  57. <el-button round @click="handleSummary" :disabled="!taklToHtml">总结</el-button>
  58. </el-tooltip>
  59. <el-tooltip content="选择后,在输入框描述填表流程" placement="top">
  60. <el-button round @click="handelIntelligentFillingClick" :disabled="!taklToHtml">智能填表</el-button>
  61. </el-tooltip>
  62. </div>
  63. </div>
  64. <el-input ref="textareaRef" v-model="inputMessage" type="textarea" :rows="3" placeholder="输入消息..."
  65. @keyup.enter="handleAsk" />
  66. <div class="chat_area_op">
  67. <el-button type="primary" link @click="handleAsk" :disabled="!inputMessage.trim() || sendLoading">
  68. <el-icon size="18" :color="inputMessage.trim() ? 'black' : 'gray'">
  69. <Promotion />
  70. </el-icon>
  71. </el-button>
  72. </div>
  73. </div>
  74. </div>
  75. <!-- 历史记录 -->
  76. <historyComponent :msgUuid="msgUuid" ref="historyComponentRef" @currentData="(e)=>handleCurrentData(e)" />
  77. </div>
  78. </template>
  79. <script setup>
  80. import { ref, onMounted, nextTick, inject, useTemplateRef } from 'vue'
  81. import { ElScrollbar, ElAvatar, ElInput, ElButton } from 'element-plus'
  82. import moment from 'moment'
  83. import {
  84. buildExcelUnderstandingPrompt,
  85. getFileSummaryPrompt,
  86. getSummaryPrompt,
  87. getFileContent,
  88. buildObjPrompt
  89. } from '@/utils/ai-service.js'
  90. import { storeToRefs } from 'pinia'
  91. import { ElMessage } from 'element-plus'
  92. import { useMsg } from '@/entrypoints/sidepanel/hook/useMsg.ts'
  93. import Tools from '@/entrypoints/sidepanel/component/tools.vue'
  94. import historyComponent from '@/entrypoints/sidepanel/component/historyComponent.vue'
  95. import { mockData, startMsg, mockData2 } from '@/entrypoints/sidepanel/mock'
  96. import { useAutoResizeTextarea } from '@/entrypoints/sidepanel/hook/useAutoResizeTextarea.ts'
  97. import { getPageInfo, getXlsxValue, handleInput } from './utils/index.js'
  98. import { useMsgStore } from '@/store/modules/msg.ts'
  99. // 滚动条引用
  100. const scrollbar = ref(null)
  101. const xlsxData = ref({})
  102. const isShowPage = ref(false)
  103. const tareRef = useTemplateRef('textareaRef')
  104. const drawerRef = useTemplateRef('historyComponentRef')
  105. // 获取父组件提供的 Hook 实例
  106. const { registerStore, useStore } = inject('indexedDBHook')
  107. const { pageInfoList, messages, msgUuid } = storeToRefs(useMsgStore())
  108. const {
  109. taklToHtml,
  110. sendLoading,
  111. type,
  112. streamRes,
  113. getFormKeyAndValue,
  114. handleSend
  115. } = useMsg(scrollbar)
  116. const inputMessage = ref('')
  117. const pageInfo = ref('')
  118. const addMessage = (msg, raw) => {
  119. if (!msg) return
  120. const newMessage = reactive({
  121. id: messages.value.length + 1,
  122. username: '我',
  123. rawContent: raw ?? msg,
  124. content: msg,
  125. timestamp: moment().format('YYYY-MM-DD HH:mm:ss'),
  126. isSelf: true,
  127. avatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
  128. addToHistory: !taklToHtml.value
  129. })
  130. messages.value.push(newMessage)
  131. useStore(msgUuid.value).add(newMessage)
  132. nextTick(() => {
  133. scrollbar.value?.setScrollTop(99999)
  134. })
  135. return newMessage
  136. }
  137. const handleSummary = async () => {
  138. const res = await getPageInfo()
  139. addMessage('总结页面', getSummaryPrompt(res.content))
  140. handleSend()
  141. }
  142. function handelIntelligentFillingClick() {
  143. inputMessage.value = '/智能填表 '
  144. type.value = '2'
  145. }
  146. function handleCurrentData(e) {
  147. drawerRef.value.drawer = false
  148. if (!e) {
  149. addNewDialogue()
  150. return
  151. }
  152. // 添加indexDB Store配置
  153. msgUuid.value = e
  154. useStore(e).getAll().then((res) => {
  155. messages.value = res
  156. nextTick(() => {
  157. scrollbar.value?.setScrollTop(99999)
  158. })
  159. })
  160. }
  161. async function readClick() {
  162. isShowPage.value = true
  163. taklToHtml.value = true
  164. const tempPageInfo = await getPageInfo()
  165. pageInfo.value = tempPageInfo
  166. pageInfoList.value.push(tempPageInfo)
  167. }
  168. function deletePageInfo(i) {
  169. pageInfoList.value.splice(i, 1)
  170. if (pageInfoList.value.length === 0) {
  171. isShowPage.value = false
  172. taklToHtml.value = false
  173. }
  174. }
  175. function addNewDialogue() {
  176. messages.value = []
  177. msgUuid.value = 'D' + Date.now().toString()
  178. registerStore({
  179. name: msgUuid.value,
  180. keyPath: 'id'
  181. })
  182. messages.value.push(startMsg)
  183. useStore(msgUuid.value).add(startMsg)
  184. }
  185. async function handleAsk() {
  186. if (sendLoading.value) return
  187. addMessage(inputMessage.value.trim())
  188. handleSend(inputMessage.value.trim())
  189. inputMessage.value = ''
  190. // streamRes(taklToHtml)
  191. }
  192. function handleCapture() {
  193. ElMessage({
  194. message: '开发中...',
  195. grouping: true,
  196. showClose: true
  197. })
  198. }
  199. function hisRecords() {
  200. drawerRef.value.drawer = true
  201. }
  202. const handleUpload = async (file) => {
  203. if (type.value === '2') {
  204. chrome.runtime.sendMessage({
  205. type: 'FROM_SIDE_PANEL_TO_GET_PAGE_FORM'
  206. }, async (response) => {
  207. if (chrome.runtime.lastError) {
  208. console.error('消息发送错误:', chrome.runtime.lastError)
  209. } else {
  210. const fileExtension = file.name.split('.').pop().toLowerCase()
  211. if (response.status === 'error') return ElMessage({
  212. message: response.message,
  213. type: 'error',
  214. duration: 4 * 1000,
  215. grouping: true
  216. })
  217. if (fileExtension === 'xlsx') {
  218. const readData = await getXlsxValue(file)
  219. readData[0].forEach((header, i) => {
  220. // if (!xlsxData.value[header]) xlsxData.value[header] = []
  221. xlsxData.value[header] = readData[1][i]
  222. })
  223. addMessage(`已上传文件:${file.name}`, buildExcelUnderstandingPrompt(readData[0], file?.name, response.data))
  224. const a = await streamRes()
  225. handleInput(xlsxData.value, JSON.parse(a.split('json')[1].split('```')[0]))
  226. } else {
  227. const { data, msg } = await getFileValue(file)
  228. const res2 = await getFormKeyAndValue(data, response.data)
  229. xlsxData.value = res2.data
  230. msg.rawContent = buildObjPrompt(res2.data, response.data)
  231. console.log()
  232. const a = await streamRes()
  233. handleInput(xlsxData.value, JSON.parse(a.split('json')[1].split('```')[0]))
  234. console.log(xlsxData.value)
  235. console.log(type.value)
  236. }
  237. }
  238. return true
  239. })
  240. }
  241. if (type.value === '') {
  242. const fileVaue = await getFileValue(file)
  243. // streamRes()
  244. }
  245. }
  246. let str = ''
  247. async function getFileValue(file) {
  248. const msg = addMessage(`文件上传中`)
  249. sendLoading.value = true
  250. let formData = new FormData()
  251. formData.append('file', file)
  252. const res = await getFileContent(formData)
  253. sendLoading.value = false
  254. msg.content = `已上传文件:${file.name}`
  255. msg.rawContent = res.data
  256. return {
  257. data: res.data,
  258. msg
  259. }
  260. }
  261. }
  262. const isHoveringTitle = ref(false)
  263. // 组件挂载时滚动到底部
  264. onMounted(() => {
  265. useAutoResizeTextarea(tareRef, inputMessage)
  266. chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  267. if (message.type === 'TO_SIDE_PANEL_PAGE_INFO') {
  268. pageInfo.value = message.data
  269. // 转发到 content.js
  270. chrome.tabs.sendMessage(sender.tab.id, {
  271. type: 'TO_CONTENT_SCRIPT',
  272. data: message.data
  273. })
  274. }
  275. if (message.type === 'TO_SIDE_PANEL_PAGE_CHANGE') {
  276. pageInfo.value = message.data
  277. }
  278. })
  279. // 添加标题悬停事件监听
  280. nextTick(() => {
  281. const titleWrapper = document.querySelector('.title-wrapper')
  282. if (titleWrapper) {
  283. titleWrapper.addEventListener('mouseenter', () => {
  284. isHoveringTitle.value = true
  285. })
  286. titleWrapper.addEventListener('mouseleave', () => {
  287. isHoveringTitle.value = false
  288. })
  289. }
  290. scrollbar.value?.setScrollTop(99999)
  291. })
  292. // 添加indexDB Store配置
  293. msgUuid.value = 'D' + Date.now().toString()
  294. registerStore({
  295. name: msgUuid.value,
  296. keyPath: 'id'
  297. })
  298. messages.value.push(startMsg)
  299. useStore(msgUuid.value).add(startMsg)
  300. })
  301. </script>
  302. <style lang="scss" scoped>
  303. @import '@/entrypoints/sidepanel/css/chat.scss';
  304. </style>