Chat.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586
  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. <div v-html="message.content"></div>
  13. <div 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. </div>
  18. </div>
  19. <div v-else class="content">{{ message.content }}</div>
  20. <div class="timestamp ">{{ message.timestamp }}
  21. <span v-if="message.add" style="cursor: pointer;" @click="handleInput">填充</span>
  22. </div>
  23. </div>
  24. </div>
  25. </div>
  26. </el-scrollbar>
  27. <Tools @read-click="isShowPage = true, taklToHtml = true" @upload-file="handleUpload" />
  28. <div>
  29. <!-- 输入区域 -->
  30. <div class="input-area">
  31. <div v-show="isShowPage" style="border-bottom: 1px solid #F0F0F0;">
  32. <div class="card-content">
  33. <img :src="pageInfo?.favIconUrl" style="width: 24px;" />
  34. <div class="title-wrapper">
  35. <div class="title-scroller" :class="{ 'scroll': isHoveringTitle && titleScroll }">
  36. {{ pageInfo?.title }}
  37. </div>
  38. </div>
  39. <el-icon size="16px" @click="isShowPage = false; taklToHtml = false">
  40. <CircleClose />
  41. </el-icon>
  42. </div>
  43. <div class="card-btn">
  44. <el-tooltip content="总结当前页面" placement="top">
  45. <el-button round @click="handleCardButtonClick" :disabled="!taklToHtml">总结</el-button>
  46. </el-tooltip>
  47. <el-tooltip content="选择后,在输入框描述填表流程" placement="top">
  48. <el-button round @click="handelIntelligentFillingClick" :disabled="!taklToHtml">智能填表</el-button>
  49. </el-tooltip>
  50. </div>
  51. </div>
  52. <el-input ref="textareaRef" v-model="inputMessage" type="textarea" :rows="3" placeholder="输入消息..." @keyup.enter="() => {
  53. addMessage(inputMessage.trim(), true)
  54. inputMessage = ''
  55. }" />
  56. <div class="chat_area_op">
  57. <el-button type="primary" link @click="handleAsk" :disabled="!inputMessage.trim() || sendLoading">
  58. <el-icon size="18" :color="inputMessage.trim() ? 'black' : 'gray'">
  59. <Promotion />
  60. </el-icon>
  61. </el-button>
  62. </div>
  63. </div>
  64. </div>
  65. </div>
  66. </template>
  67. <script setup>
  68. import { ref, onMounted, nextTick, inject, useTemplateRef } from 'vue'
  69. import { ElScrollbar, ElAvatar, ElInput, ElButton } from 'element-plus'
  70. import avator from '@/public/icon/32.png'
  71. import moment from 'moment'
  72. import { buildExcelUnderstandingPrompt } from '@/utils/ai-service.js'
  73. import * as XLSX from "xlsx";
  74. import { ElMessage } from 'element-plus';
  75. import { useMsg } from '@/entrypoints/sidepanel/hook/useMsg.ts';
  76. import Tools from "@/entrypoints/sidepanel/component/tools.vue";
  77. import { useSummary } from '@/entrypoints/sidepanel/hook/useSummary.ts'
  78. import { mockData } from "@/entrypoints/sidepanel/mock"
  79. import {upload} from '@/utils/ai-service'
  80. import { useAutoResizeTextarea } from '@/entrypoints/sidepanel/hook/useAutoResizeTextarea.ts';
  81. // 滚动条引用
  82. const scrollbar = ref(null);
  83. const type = ref('');
  84. const xlsxData = ref({});
  85. const isShowPage = ref(false);
  86. const tareRef = useTemplateRef("textareaRef");
  87. const {
  88. messages,
  89. inputMessage,
  90. indexTemp,
  91. taklToHtml,
  92. pageInfo,
  93. sendLoading,
  94. addMessage,
  95. sendRequese,
  96. getPageInfo,
  97. streamRes,
  98. handleInput
  99. } = useMsg(scrollbar, type, xlsxData, fetchDataAndProcess);
  100. const { handleCardButtonClick } = useSummary(addMessage, sendRequese);
  101. function handelIntelligentFillingClick() {
  102. if (type.value !== '2') {
  103. inputMessage.value = '/智能填表 '
  104. type.value = '2'
  105. } else {
  106. type.value = ''
  107. }
  108. }
  109. function handleAsk() {
  110. addMessage(inputMessage.value.trim(), true);
  111. inputMessage.value = '';
  112. }
  113. // 计算标题是否需要滚动
  114. const titleScroll = computed(() => {
  115. return pageInfo.value?.title?.length > 20 // 当标题超过20个字符时触发滚动
  116. })
  117. // let formMap = []
  118. let formInfo = []
  119. const flag = ref(false) //true调用算法接口
  120. const handleUpload = (file) => {
  121. console.log(upload);
  122. chrome.runtime.sendMessage({
  123. type: 'FROM_SIDE_PANEL_TO_GET_PAGE_FORM',
  124. }, async (response) => {
  125. if (chrome.runtime.lastError) {
  126. console.error("消息发送错误:", chrome.runtime.lastError);
  127. } else {
  128. console.log(file);
  129. addMessage(`已上传文件:${file.name}`, false)
  130. const fileExtension = file.name.split('.').pop().toLowerCase();
  131. console.log('文件后缀:', fileExtension);
  132. if (fileExtension === 'xlsx') {
  133. formInfo = response.data
  134. const reader = new FileReader();
  135. reader.readAsArrayBuffer(file);
  136. reader.onload = async (e) => {
  137. const data = new Uint8Array(e.target.result);
  138. const workbook = XLSX.read(data, {
  139. type: "array",
  140. cellDates: false,
  141. cellNF: true,
  142. cellText: true,
  143. dateNF: 'yyyy-mm-dd'
  144. });
  145. // 修复日期处理
  146. const firstSheet = workbook.Sheets[workbook.SheetNames[0]];
  147. // 转换为JSON数据
  148. const readData = XLSX.utils.sheet_to_json(firstSheet, {
  149. header: 1,
  150. raw: false,
  151. defval: "",
  152. dateNF: 'yyyy-mm-dd'
  153. });
  154. console.log(readData, 58);
  155. readData[0].forEach((header, i) => {
  156. if (!xlsxData.value[header]) xlsxData.value[header] = []
  157. xlsxData.value[header].push(readData[1][i])
  158. })
  159. if (type.value === '2') {
  160. await streamRes(buildExcelUnderstandingPrompt(readData, file?.name, response), false)
  161. }
  162. };
  163. } else {
  164. let formData = new FormData();
  165. formData.append("file", file);
  166. const res = await upload(formData)
  167. console.log(res);
  168. }
  169. }
  170. return true
  171. });
  172. }
  173. async function fetchDataAndProcess(input, obj) {
  174. console.log(input);
  175. const pageInfo = await getPageInfo()
  176. console.log(pageInfo);
  177. // 发起请求获取数据
  178. // const res = await hepl({
  179. // input_data: input,
  180. // body:pageInfo.content.mainContent
  181. // })
  182. const res = await new Promise((resolve, reject) => {
  183. setTimeout(() => {
  184. resolve({ data: mockData[indexTemp.value] })
  185. }, 1000)
  186. })
  187. await handleClick(res.data, obj);
  188. }
  189. async function handleClick(res, msgObj) {
  190. await new Promise(resolve => setTimeout(resolve, 2000))
  191. if (res.action === 'click') {
  192. msgObj.content = `点击${res.tag}元素`
  193. chrome.runtime.sendMessage({
  194. type: 'FROM_SIDE_PANEL_TO_ACTION',
  195. data: res
  196. }, async (response) => {
  197. if (chrome.runtime.lastError) {
  198. console.error("消息发送错误:", chrome.runtime.lastError);
  199. rej(chrome.runtime.lastError)
  200. } else {
  201. if (res.next === '是') {
  202. indexTemp.value++
  203. fetchDataAndProcess('', msgObj)
  204. } else {
  205. ElMessage({ message: '操作执行完成', type: 'success', duration: 2 * 1000, grouping: true })
  206. index = 0
  207. }
  208. }
  209. return true
  210. });
  211. }
  212. if (res.action === 'show') {
  213. msgObj.content = `请上传数据`
  214. ElMessage({ message: '请上传数据', type: 'success', duration: 4 * 1000, grouping: true })
  215. }
  216. // chrome.runtime.sendMessage({
  217. // type: 'FROM_SIDE_PANEL_TO_ACTION',
  218. // data:obj
  219. // }, async (response) => {
  220. // if (chrome.runtime.lastError) {
  221. // console.error("消息发送错误:", chrome.runtime.lastError);
  222. // rej(chrome.runtime.lastError)
  223. // } else {
  224. // if (obj.next === '是') {
  225. // console.log(strArr[index]);
  226. // index++
  227. // fetchDataAndProcess(strArr[index])
  228. // } else {
  229. // ElMessage({ message: '操作执行完成', type: 'success', duration: 2 * 1000, grouping: true })
  230. // index = 0
  231. // }
  232. // }
  233. // return true
  234. // });
  235. }
  236. const isHoveringTitle = ref(false)
  237. // 组件挂载时滚动到底部
  238. onMounted(async () => {
  239. useAutoResizeTextarea(tareRef, inputMessage);
  240. chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  241. if (message.type === 'TO_SIDE_PANEL_PAGE_INFO') {
  242. pageInfo.value = message.data
  243. console.log(pageInfo.value);
  244. // 转发到 content.js
  245. chrome.tabs.sendMessage(sender.tab.id, {
  246. type: 'TO_CONTENT_SCRIPT',
  247. data: message.data
  248. });
  249. }
  250. if (message.type === 'TO_SIDE_PANEL_PAGE_CHANGE') {
  251. pageInfo.value = message.data
  252. }
  253. });
  254. await getPageInfo()
  255. nextTick(() => {
  256. scrollbar.value?.setScrollTop(99999)
  257. })
  258. // 添加标题悬停事件监听
  259. nextTick(() => {
  260. const titleWrapper = document.querySelector('.title-wrapper')
  261. if (titleWrapper) {
  262. titleWrapper.addEventListener('mouseenter', () => {
  263. isHoveringTitle.value = true
  264. })
  265. titleWrapper.addEventListener('mouseleave', () => {
  266. isHoveringTitle.value = false
  267. })
  268. }
  269. scrollbar.value?.setScrollTop(99999)
  270. })
  271. })
  272. </script>
  273. <style lang="scss" scoped>
  274. .chat-container {
  275. height: 100vh;
  276. display: flex;
  277. flex-direction: column;
  278. border: 1px solid #dcdfe6;
  279. border-radius: 4px;
  280. background-color: #F0F0F0;
  281. font-family: 'PingFang SC', 'Helvetica Neue', Arial, sans-serif;
  282. }
  283. .message-list {
  284. flex: 1;
  285. padding: 16px;
  286. overflow: auto;
  287. }
  288. .messages {
  289. min-height: 100%;
  290. display: flex;
  291. flex-direction: column;
  292. }
  293. .message-item {
  294. display: flex;
  295. margin-bottom: 20px;
  296. gap: 12px;
  297. color: #333;
  298. align-items: flex-start;
  299. animation: fadeIn 0.3s ease-in-out;
  300. }
  301. @keyframes fadeIn {
  302. from {
  303. opacity: 0;
  304. transform: translateY(10px);
  305. }
  306. to {
  307. opacity: 1;
  308. transform: translateY(0);
  309. }
  310. }
  311. .message-item.self {
  312. flex-direction: row-reverse;
  313. }
  314. .message-content {
  315. max-width: 80%;
  316. }
  317. .username {
  318. font-size: 14px;
  319. color: #606266;
  320. margin-bottom: 4px;
  321. }
  322. .content {
  323. padding: 12px;
  324. min-height: 40px;
  325. background-color: #F0F4F8;
  326. border-radius: 12px;
  327. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
  328. word-break: break-all;
  329. line-height: 1.5;
  330. font-size: 14px;
  331. }
  332. .content :deep(pre) {
  333. margin: 10px 0;
  334. border-radius: 8px;
  335. }
  336. .content :deep(code) {
  337. font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
  338. }
  339. .content :deep(p) {
  340. margin: 8px 0;
  341. }
  342. .content :deep(ul),
  343. .content :deep(ol) {
  344. padding-left: 20px;
  345. margin: 8px 0;
  346. }
  347. .content :deep(blockquote) {
  348. border-left: 4px solid #ddd;
  349. padding-left: 10px;
  350. color: #666;
  351. margin: 8px 0;
  352. }
  353. .self .content {
  354. background: #409eff;
  355. color: white;
  356. border-bottom-right-radius: 4px;
  357. }
  358. .other .content {
  359. border-bottom-left-radius: 4px;
  360. }
  361. .timestamp {
  362. display: flex;
  363. justify-content: space-between;
  364. font-size: 12px;
  365. color: #909399;
  366. margin-top: 6px;
  367. }
  368. .timestamp span {
  369. transition: color 0.2s;
  370. }
  371. .timestamp span:hover {
  372. color: #409eff;
  373. }
  374. .info-card {
  375. margin: 10px;
  376. }
  377. /* .card-content:hover {
  378. transform: translateY(-2px);
  379. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
  380. } */
  381. .card-icon {
  382. color: #409eff;
  383. }
  384. .title-wrapper {
  385. flex: 1;
  386. overflow: hidden;
  387. position: relative;
  388. }
  389. .title-scroller {
  390. display: inline-block;
  391. white-space: nowrap;
  392. color: #333;
  393. font-weight: 500;
  394. }
  395. .title-scroller.scroll {
  396. animation: scrollTitle 10s linear infinite;
  397. }
  398. @keyframes scrollTitle {
  399. 0% {
  400. transform: translateX(0);
  401. }
  402. 100% {
  403. transform: translateX(-100%);
  404. }
  405. }
  406. .upload :deep(.el-icon) {
  407. transition: color 0.3s;
  408. }
  409. .can-hover :deep(.el-icon:hover) {
  410. color: #409eff !important;
  411. }
  412. .el-check-tag {
  413. margin-right: 8px;
  414. transition: all 0.3s;
  415. }
  416. .el-check-tag:hover {
  417. transform: scale(1.05);
  418. }
  419. /* 加载中的消息样式 */
  420. .loading-content {
  421. min-height: 40px;
  422. display: flex;
  423. align-items: center;
  424. justify-content: flex-start;
  425. }
  426. .loading-indicator {
  427. display: flex;
  428. align-items: center;
  429. gap: 4px;
  430. }
  431. .dot {
  432. width: 8px;
  433. height: 8px;
  434. background-color: #409eff;
  435. border-radius: 50%;
  436. display: inline-block;
  437. animation: pulse 1.5s infinite ease-in-out;
  438. }
  439. .dot:nth-child(2) {
  440. animation-delay: 0.3s;
  441. }
  442. .dot:nth-child(3) {
  443. animation-delay: 0.6s;
  444. }
  445. @keyframes pulse {
  446. 0%,
  447. 100% {
  448. transform: scale(0.8);
  449. opacity: 0.6;
  450. }
  451. 50% {
  452. transform: scale(1.2);
  453. opacity: 1;
  454. }
  455. }
  456. .input-area {
  457. padding: 8px 10px;
  458. color: black;
  459. background-color: #fff;
  460. border: 1px solid rgba(102, 102, 102, 0.3);
  461. border-radius: 16px;
  462. margin: 0 12px 12px;
  463. .card-content {
  464. display: flex;
  465. align-items: center;
  466. gap: 12px;
  467. padding: 14px 12px;
  468. margin: 0 0 6px;
  469. background: #fff;
  470. border-radius: 10px;
  471. border: 1px solid rgba(0, 0, 0, 0.08);
  472. font-size: 14px;
  473. /* box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);*/
  474. transition: transform 0.4s;
  475. }
  476. .card-btn {
  477. padding: 0 0 4px;
  478. .op {
  479. margin-right: 3px;
  480. }
  481. }
  482. .chat_area_op {
  483. margin-top: 3px;
  484. display: flex;
  485. justify-content: end;
  486. }
  487. }
  488. .input-area:hover {
  489. border-color: rgba(102, 102, 102, 0.4);
  490. }
  491. .input-area :deep(.el-textarea__inner) {
  492. border-radius: 8px;
  493. transition: border-color 0.3s;
  494. resize: none;
  495. box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
  496. }
  497. .input-area :deep(.el-textarea__inner) {
  498. padding: 0 4px;
  499. margin-top: 3px;
  500. }
  501. .input-area :deep(.el-textarea__inner:focus) {
  502. border-color: #409eff;
  503. box-shadow: none;
  504. }
  505. </style>