Browse Source

refactor(sidepanel): 重构高级模式为聊天界面

- 移除高级模式原有的布局和样式
- 添加聊天界面的布局和样式
- 实现消息列表的加载和滚动
- 添加消息发送和处理逻辑
- 集成智能填表功能
- 优化页面信息获取和显示
- 重构部分代码结构以适应新功能
chd 2 months ago
parent
commit
c96de40a73

+ 8 - 0
src/api/advance.js

@@ -0,0 +1,8 @@
+import request from '@/utils/request2'
+export function getPlan(data) {
+    return request({
+        url: '/plan-template/generate',
+        method: 'post',
+        data: data
+    })
+}

+ 665 - 76
src/entrypoints/sidepanel/AdvancedMode.vue

@@ -1,111 +1,700 @@
+<!-- Chat.vue -->
 <template>
   <div class="chat-container">
-    <div class="advanced-header">
-      <h2>高级模式</h2>
-      <p>在这里您可以使用更多高级功能</p>
-    </div>
-    
-    <div class="advanced-content">
-      <!-- 这里可以添加高级模式的具体内容 -->
-      <el-empty description="高级功能正在开发中..." />
-      
-      <!-- 示例功能区域 -->
-      <div class="feature-cards">
-        <el-card class="feature-card">
-          <template #header>
-            <div class="card-header">
-              <el-icon><Document /></el-icon>
-              <span>文档分析</span>
+    <!-- <div v-if="!messages.length && !msgLoading" class="message-list">
+      <pageMask />
+    </div> -->
+    <!-- 消息列表 -->
+    <div class="message-list">
+      <el-scrollbar ref="scrollbar" @scroll="handleScroll" v-loading="msgLoading">
+        <!-- 加载更多指示器 -->
+        <div v-if="isLoadingMore" class="loading-more-indicator">
+          <div class="loading-spinner"></div>
+          <span>加载更多消息...</span>
+        </div>
+        <div class="messages" ref="messagesContainer" >
+          <div v-for="(message, index) in messages" :key="index"
+            :class="['message-item', message.role === 'user'  ? 'self' : 'other']">
+            <el-avatar :size="32" :src="message.role === 'user' ? userAvatar : avatar" />
+            <div class="message-content">
+              <div class="content" v-if="message.role ==='system'"
+                :class="{ 'loading-content': message.content === '' }">
+                <StepsDisplay 
+                v-if="message.type === 'plan'"
+                 :content="message.rawContent" 
+                 :initialStep="0"
+                 @step-change="handleStepChange" @complete="handleComplete"
+                 />
+                <span v-else v-html="message.content"></span>
+                <span class="loading-indicator" v-if="sendLoading && index === messages.length - 1">
+                  <span class="dot"></span>
+                  <span class="dot"></span>
+                  <span class="dot"></span>
+                </span>
+              </div>
+              <document v-else-if="message.type === 'document' && message.role === 'user'" :content="message.content"
+                :rawContent="message.rawContent" />
+              <div v-else class="content">
+                <span v-if="message.type === ''">{{ message.content }}</span>
+                <span class="loading-indicator" v-if="sendLoading && index === messages.length - 1">
+                  <span class="dot"></span>
+                  <span class="dot"></span>
+                  <span class="dot"></span>
+                </span>
+              </div>
+              <div class="timestamp ">{{ moment(message.sortKey).format('MM-DD HH:mm') }}
+                <!-- <span v-if="message.add" style="cursor: pointer;" @click="handleInput">填充</span> -->
+              </div>
             </div>
-          </template>
-          <div class="card-content">
-            上传文档进行智能分析和处理
           </div>
-        </el-card>
-        
-        <el-card class="feature-card">
-          <template #header>
-            <div class="card-header">
-              <el-icon><DataAnalysis /></el-icon>
-              <span>数据可视化</span>
+        </div>
+      </el-scrollbar>
+      <ScrollToBottom :target="scrollbar" ref="scrollToBottomRef" />
+    </div>
+
+    <!-- <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"
+      @add-new-dialogue="addNewDialogue" @handle-current-change="handleCurrentChange"
+      @handel-intelligent-filling-click="handelIntelligentFillingClick" /> -->
+
+    <div>
+      <!-- 输入区域 -->
+      <div class="input-area">
+          <el-icon class="closeShow" :style="{ display: isShowPage ? 'block' : 'none'}" size="16px" color="#909399" @click="closePageInfo">
+                <CircleClose />
+              </el-icon>
+        <!-- <div v-show="isShowPage" style="border-bottom: 1px solid #F0F0F0;">
+          <div class="card_list">
+            <div v-for="(v, i) in pageInfoList" :key="i"
+              :class="`card-content ${pageInfoList.length > 1 ? 'card_width' : ''}`">
+              <div class="loading-more-indicator" v-if="v.loading">
+                <div class="loading-spinner"></div>
+              </div>
+              <img v-else :src="v?.favIconUrl" style="width: 24px;display: block" />
+
+              <div class="title-wrapper">
+                <span class="els title-scroller">{{ v?.title }}</span>
+                <span class="els url-scroller">{{ v.loading ? v?.state : v?.url }}</span>
+              </div>
+              <el-icon class="closeIcon" size="16px" color="#909399" @click="deletePageInfo(i)">
+                <CircleClose />
+              </el-icon>
             </div>
-          </template>
-          <div class="card-content">
-            将数据转化为直观的图表展示
+           <div v-show="!pageInfoList.length && type === FunctionList.Intelligent_Form_filling">智能填表</div>
           </div>
-        </el-card>
+
+          <div class="card-btn">
+            <el-tooltip  content="总结" placement="top">
+              <el-button :disabled="disabledBtn" v-if="type !== FunctionList.Intelligent_Form_filling"  round @click="handleSummary">总结</el-button>
+            </el-tooltip>
+             <el-tooltip   content="抽取表单信息" placement="top">
+              <el-button :disabled="disabledBtn" v-if="type === FunctionList.Intelligent_Form_filling && pageInfoList.length" round @click="handleSummaryFile">抽取</el-button>
+            </el-tooltip>
+          </div>
+        </div> -->
+
+        <el-input ref="textareaRef" v-model="inputMessage" type="textarea" :rows="3" placeholder="输入消息..."
+          @keyup.enter="() => handleAsk()" />
+        <div class="chat_area_op">
+        
+          <el-button v-if="sendLoading" style="background-color:#ffffff;border:2px solid rgb(134 143 153);"  circle
+            @click="handleStopAsk">
+            <svg-icon icon-class="stop" color="red" />
+          </el-button>
+            <el-button
+            v-else
+            :style="`background-color:${inputMessage.trim() ? '#4d6bfe' : '#d6dee8'};border-color:${inputMessage.trim() ? '#4d6bfe' : '#d6dee8'}`"
+             type="primary" circle @click="() => handleAsk()"
+            :disabled="disSend">
+            <svg-icon icon-class="send" color="#000000" />
+          </el-button>
+        </div>
       </div>
+
     </div>
+
+    <!-- <historyComponent :msgUuid="msgUuid" ref="historyComponentRef" @currentData="(e) => handleCurrentData(e)" /> -->
   </div>
 </template>
 
-<script lang="ts" setup>
-import { Document, DataAnalysis } from '@element-plus/icons-vue'
-</script>
+<script setup>
+import { ref, onMounted, nextTick, inject, useTemplateRef, reactive,onBeforeMount } from 'vue'
+import { ElScrollbar, ElAvatar, ElInput, ElButton } from 'element-plus'
+import moment from 'moment'
+import fileLogo from '@/assets/svg/file.svg'
+import {
+  getFileContent,
+  buildObjPrompt,
+  controllerList,
+  formatMessage
+} from '@/entrypoints/sidepanel/utils/ai-service.js'
+import { storeToRefs } from 'pinia'
+import { ElMessage } from 'element-plus'
+import { useMsg } from '@/entrypoints/sidepanel/hook/useMsg.ts'
+import Tools from '@/entrypoints/sidepanel/component/tools.vue'
+import historyComponent from '@/entrypoints/sidepanel/component/historyComponent.vue'
+import document from '@/entrypoints/sidepanel/component/document.vue'
+import pageMask from '@/entrypoints/sidepanel/component/pageMask.vue'
+import ScrollToBottom from '@/entrypoints/sidepanel/component/ScrollToBottom.vue'
+import formTable from '@/entrypoints/sidepanel/component/formTable.vue'
+import StepsDisplay from './component/StepsDisplay.vue'
+import userAvatar from '@/assets/images/user.png'
+import avatar from '@/public/icon/icon.png'
+import {  options, FunctionList } from '@/entrypoints/sidepanel/mock'
+import { useAutoResizeTextarea } from '@/entrypoints/sidepanel/hook/useAutoResizeTextarea.ts'
+import { getPageInfo, getPageInfoClean, handleInput,downloadFile } from './utils/index.js'
+import { useMsgStore } from '@/store/modules/msg.ts'
+import { useUserStore } from '@/store/modules/user'
+// import { putChat, uploadFile } from "@/api/index.js";
+// import {askQues,getFormKey} from '@/api/modal.js'
+import { debounce } from 'lodash'
+const userStore = useUserStore()
+// 在其他状态变量附近添加
+const isLoadingMore = ref(false)
+import { v4 as uuidv4 } from 'uuid';
+import { getPlan } from '@/api/advance.js'
+// 滚动条引用
+const scrollbar = ref(null)
+const scrollToBottomRef = ref(null)
+const xlsxData = ref({})
+const isShowPage = ref(false)
+const tareRef = useTemplateRef('textareaRef')
+const drawerRef = useTemplateRef('historyComponentRef')
+const disSend = computed(() => {
+  if (pageInfoList.value.length && type.value === FunctionList.Intelligent_Form_filling) return true
+  if (sendLoading.value) return true
+  if (inputMessage.value.trim()) return false
+  return true
+})
+// 获取父组件提供的 Hook 实例
+const msgStore = useMsgStore()
+const { pageInfoList, messages, msgUuid, AIModel,page,hasNext,msgLoading,selectModal } = storeToRefs(useMsgStore())
+const formInfo = ref('')
+const {
+  taklToHtml,
+  sendLoading,
+  type,
+  fetchRes
 
-<style scoped>
-.chat-container {
-  border-radius: 14px;
-  height: 100%;
-  display: flex;
-  flex-direction: column;
-  border: 1px solid #dcdfe6;
-  background-color: #ffffff;
+} = useMsg(scrollbar)
+const inputMessage = ref('')
+const pageInfo = ref('')
+const summaryHtml = ref(false)
+function handleStopAsk() {
+    inputMessage.value = ''
+    pageInfoList.value = []
+    isShowPage.value = false
+    taklToHtml.value = false
+    type.value = FunctionList.File_Operation
+  controllerList.value[0]?.abort()
+  controllerList.value = []
+  sendLoading.value = false
+  formInfo.value = null
 }
-.advanced-mode-container {
-  height: 100%;
-  padding: 20px;
-  overflow-y: auto;
+
+function handleScroll(a) {
+  const { wrapRef } = scrollbar.value
+  const { scrollHeight, clientHeight } = wrapRef
+  scrollToBottomRef.value.showButton = scrollHeight - a.scrollTop - clientHeight >= 350
+  
+  // 检测是否滚动到顶部
+  if (a.scrollTop <= 10 && hasNext.value) {
+    handleScrollToTop()
+  }
 }
+const messagesContainer = ref(null)
+let innerText = ''
+// 处理滚动到顶部的事件
+const handleScrollToTop = debounce(async function () {
+  console.log('滚动到顶部,可以加载更多历史消息')
+  if (!sendLoading.value && !isLoadingMore.value) {
+    isLoadingMore.value = true
+    try {
+      // 记录当前第一条消息的位置
+      const firstMessage = messagesContainer.value?.firstElementChild.querySelector('.timestamp')
+      innerText = firstMessage.innerText
+      const oldHeight = firstMessage?.offsetTop || 0
+      await msgStore.changePage()
+      setTimeout(() => {
+        if (firstMessage) {
+          console.log([...messagesContainer.value.querySelectorAll('.timestamp')].find(_ => _.innerText === innerText).offsetTop, innerText, 778);
+          const newHeight = [...messagesContainer.value.querySelectorAll('.timestamp')].find(_ => _.innerText === innerText).offsetTop
+          const scrollOffset = newHeight - oldHeight
+          scrollbar.value?.setScrollTop(scrollOffset)
+        }
+      }, 400);
+      // 恢复滚动位置
+     
+    } finally {
+      isLoadingMore.value = false
+    }
+  }
+}, 500, { leading: false, trailing: true })
 
-.advanced-header {
-  margin-bottom: 24px;
-  text-align: center;
+const handleStepChange = (step) => {
+  console.log('当前步骤:', step)
 }
 
-.advanced-header h2 {
-  font-size: 20px;
-  color: #303133;
-  margin-bottom: 8px;
+const handleComplete = () => {
+  console.log('所有步骤已完成')
+}
+/**
+ * @param {string} msg
+ * @param {string} raw
+ * @param {string} type 发送的类型 document 、text
+ * **/
+async function addMessage(msg, raw, type) {
+  const newMessage = reactive({
+    id:+new Date(),
+    type: type || '',
+    rawContent: raw ?? msg,
+    senderId: userStore.userInfo.id,
+    receiverId:-1, //大模型
+    content: msg,
+    sortKey: moment().valueOf(),
+    role: 'user',
+    conversationId:msgUuid.value,
+    addToHistory: `${!taklToHtml.value}`,
+    redisKey: undefined,
+    fileId:undefined
+  })
+  if (type === 'document') {
+    newMessage.content = msg
+    newMessage.rawContent = raw
+  }
+  if (!msg) return
+  messages.value.push(newMessage)
+  nextTick(() => {
+    if (scrollbar.value && scrollbar.value.wrapRef) {
+      scrollbar.value.setScrollTop(scrollbar.value.wrapRef.scrollHeight)
+    }
+  })
+  return newMessage
 }
 
-.advanced-header p {
-  font-size: 14px;
-  color: #909399;
+const handleSummary = async () => {
+  if(sendLoading.value) return
+  handleAsk(`请帮我总结当前${summaryHtml.value ? '页面' : '文件'}`)
+  summaryHtml.value = false
+}
+const handleSummaryFile = async () => {
+  if (sendLoading.value) return
+ try {
+   sendLoading.value = true
+   const  userMsg = await addMessage(JSON.stringify([...pageInfoList.value, { type: 'text', value: '抽取表单' }]), '抽取表单', 'document')
+    userMsg.fileId = pageInfoList.value[0].fileId
+    userMsg.redisKey = pageInfoList.value[0].redisKey
+   await putChat(userMsg)
+   createWS(userMsg)
+    formInfo.value = ''
+  //   const msg = reactive({
+  //   type: '',
+  //   rawContent: '',
+  //   senderId: -1,
+  //   receiverId: userStore.userInfo.id, //大模型
+  //   content: '',
+  //   sortKey: moment().valueOf(),
+  //   role: 'system',
+  //   conversationId: msgUuid.value,
+  //   })
+  // messages.value.push(msg)
+    //  isShowPage.value = false
+      pageInfoList.value = []
+  //   const resForm = await askQues({
+  //   conversationId: msgUuid.value,
+  //   modelName: '通义千问-Max',
+  //   question: buildObjPrompt(xlsxData.value,formInfo.value),
+  //   id: '699637194561691650',
+  //   // redisKey:msg.redisKey
+  //   })
+  // return console.log(resForm);
+  
+ 
+ } catch (error) {
+  console.log(error);
+  
+ } finally {
+  //  sendLoading.value = false
+  // type.value = FunctionList.File_Operation
+ }
+}
+async function handelIntelligentFillingClick() {
+  handleInput()
+  isShowPage.value = true
+  // taklToHtml.value = true
+  // const tempPageInfo = await getPageInfo()
+  // pageInfo.value = tempPageInfo
+  // pageInfoList.value = [tempPageInfo]
+  inputMessage.value = '/智能填表 '
+  type.value = FunctionList.Intelligent_Form_filling
 }
 
-.feature-cards {
-  display: flex;
-  flex-wrap: wrap;
-  gap: 16px;
-  margin-top: 24px;
+function handleCurrentChange(e) {
+  msgStore.updateAIModel(e)
+  // if (AIModel.value.file === true) {
+  //   isShowPage.value = false
+  //   taklToHtml.value = false
+  //   pageInfoList.value = []
+  // }
 }
 
-.feature-card {
-  width: calc(50% - 8px);
-  cursor: pointer;
-  transition: transform 0.3s;
+async function handleCurrentData(e) {
+  drawerRef.value.drawer = false
+  isShowPage.value = false
+  if (!e) {
+    addNewDialogue()
+    return
+  }
+  messages.value = []
+  page.value = 1
+  msgUuid.value = e
+  chrome.storage.local.set({ msgUuid: msgUuid.value })
+  await msgStore.initMsg()
+  nextTick(() => {
+  if (scrollbar.value && scrollbar.value.wrapRef) {
+    scrollbar.value.setScrollTop(scrollbar.value.wrapRef.scrollHeight)
+  }
+})
 }
+let SystemMsg = null
 
-.feature-card:hover {
-  transform: translateY(-5px);
+watchEffect(() => {
+  console.log(isShowPage.value);
+  
+  if (!isShowPage.value) {
+    summaryHtml.value = false
+   
+    type.value = FunctionList.File_Operation
+    SystemMsg = null
+    inputMessage.value = ''
+  }
+})
+const htmlIcon = ref('')
+async function readClick() {
+  if (type.value === FunctionList.Intelligent_Form_filling) {
+    pageInfoList.value = []
+    type.value = FunctionList.File_Operation
+  }
+  summaryHtml.value = true
+  isShowPage.value = true
+  taklToHtml.value = true
+  if (pageInfoList.value.length >= Number(import.meta.env.VITE_MAX_FILE_NUMBER)) {
+    ElMessage.warning(`最多添加${import.meta.env.VITE_MAX_FILE_NUMBER}个文件`)
+    return
+  }
+  const tempPageInfo = await getPageInfo()
+  htmlIcon.value = tempPageInfo.favIconUrl
+  const blob = new Blob([tempPageInfo.content.mainContent], { type: 'text/plain' })
+  const file = new File([blob], '页面' + '.txt', { type: 'text/plain' })
+  // console.log(file);
+  // 下载文件到本地
+  // downloadFile(file);
+  console.log(tempPageInfo);
+  
+  await createFileObj(file)
+  // await handleUpload(file,tempPageInfo.title + '.txt')
 }
 
-.card-header {
-  display: flex;
-  align-items: center;
-  gap: 8px;
+// 添加下载文件的函数
+
+function deletePageInfo(i) {
+  pageInfoList.value.splice(i, 1)
+  if (pageInfoList.value.length === 0) {
+    if (type.value === FunctionList.Intelligent_Form_filling) {
+      inputMessage.value = ''
+      SystemMsg.content = '智能填表流程中断'
+      SystemMsg.rawContent = '智能填表流程中断'
+      putChat({
+        ...SystemMsg,
+        content: '',
+      })
+    }
+    handleStopAsk()
+  }
 }
 
-.card-content {
+function addNewDialogue() {
+  if (messages.value.length === 0) {
+    ElMessage.warning('已经是新对话')
+    return
+  }
+  summaryHtml.value = false
+  type.value = FunctionList.File_Operation
+  isShowPage.value = false
+  taklToHtml.value = false
+  messages.value = []
+  page.value = 1
+  pageInfoList.value = []
+  msgUuid.value = uuidv4()
+  hasNext.value = false
+  chrome.storage.local.set({ msgUuid: msgUuid.value })
+}
+const disabledBtn = computed(() => {
+ return  !!(pageInfoList.value.find(_ => _.loading ))
+})
+function closePageInfo() {
+  pageInfoList.value = []
+   if (type.value === FunctionList.Intelligent_Form_filling && SystemMsg ) {
+      SystemMsg.content = '智能填表流程中断'
+      SystemMsg.rawContent = '智能填表流程中断'
+      putChat({
+        ...SystemMsg,
+        content: '',
+      })
+    }
+    handleStopAsk()
+  
+}
+async function handleAsk(value) {
+  if (sendLoading.value) return
+  const str = value ?? inputMessage.value.trim()
+  inputMessage.value = ''
+  if (type.value === FunctionList.Intelligent_Form_filling) {
+    const msg = await addMessage(str)
+    // await putChat(msg)
+     const [res,obj] = await fetchRes(str)
+    if (res.status === 'ok') {
+      console.log(obj);
+      formInfo.value = res.data
+      SystemMsg = obj
+    } else {
+      type.value = FunctionList.File_Operation
+      isShowPage.value = false
+    }
+    return 
+  }
+  if (sendLoading.value) return
+  let msg = null
+  if (pageInfoList.value.length) {
+    msg = await addMessage(JSON.stringify([...pageInfoList.value, { type: 'text', value: str }]), str, 'document')
+    msg.fileId = pageInfoList.value[0].fileId
+    msg.redisKey = pageInfoList.value[0].redisKey
+  } else msg = await addMessage(str)
+   pageInfoList.value = []
+  isShowPage.value = false
+  // await putChat(msg)
+  createWS(msg)
+}
+/**
+ * 
+ * @param msg 用户消息对象,调用askQues时需要
+ */
+async function createWS(msg) {
+     sendLoading.value = true
+  const obj = reactive({
+    type:  '',
+    rawContent: '',
+    senderId: -1,
+    receiverId: userStore.userInfo.id, //大模型
+    content: '',
+    sortKey: moment().valueOf(),
+    role: 'system',
+    conversationId: msgUuid.value,
+  })
+  try {
+  messages.value.push(obj)
+  nextTick(() => {
+    if (scrollbar.value && scrollbar.value.wrapRef) {
+      scrollbar.value.setScrollTop(scrollbar.value.wrapRef.scrollHeight)
+    }
+  })
+     getPlan({
+    query: msg.rawContent,
+     }).then(res => {
+      sendLoading.value = false
+        obj.type = 'plan'
+      console.log(res);
+      console.log(JSON.parse(res.planJson));
+      
+      obj.rawContent = res.planJson
+      // handlePlan(res)
+     }).catch(res => {
+       obj.rawContent = '接口出错,请重试。'
+       obj.content = '接口出错,请重试。'
+     })
+  } finally {
+  }
+}
+function handlePlan(res) {
+   const obj = reactive({
+    type:  'plan',
+    rawContent: res,
+    senderId: -1,
+    receiverId: userStore.userInfo.id, //大模型
+    content: '',
+    sortKey: moment().valueOf(),
+    role: 'system',
+    conversationId: msgUuid.value,
+   })
+  messages.value.push(obj)
+}
+function handleCapture() {
+  ElMessage({
+    message: '开发中...',
+    grouping: true,
+    showClose: true
+  })
+}
+function hisRecords() {
+  drawerRef.value.drawer = true
+}
+const createFileObj = async (file) => {
+  handleUpload(file)
+}
+const handleUpload = async (file) => {
+    const obj = reactive({
+    title: summaryHtml.value ?  file.name.split('.')[0] :  file.name,
+    state: '上传中...',
+    favIconUrl: fileLogo,
+    loading: true,
+    url:'',
+    fileId: '',
+    redisKey:'',
+    content: {
+      mainContent: ''
+    }
+  })
+  if (type.value === FunctionList.Intelligent_Form_filling) {
+    pageInfoList.value = []
+  }
+  if (summaryHtml.value) obj.favIconUrl = htmlIcon.value
+  isShowPage.value = true
+  // const obj = reactive({
+  //   title: file.name,
+  //   state: '上传中...',    
+  //   favIconUrl: fileLogo,
+  //   loading: true,
+  //   url:'',
+  //   fileId: '',
+  //   redisKey:'',
+  //   content: {
+  //     mainContent: ''
+  //   }
+  // })
+  pageInfoList.value = [obj]
+  try {
+    let formData = new FormData()
+    formData.append('avatarFile', file)
+    const res = await uploadFile(formData)
+  if (type.value === FunctionList.Intelligent_Form_filling) {
+    obj.state = '解析中...'
+    obj.redisKey = res.data.redisKey
+    obj.id = res.data.file.id
+    obj.url = res.data.file.url
+    // const fileExtension = file.name.split('.').pop().toLowerCase()
+     const result = await getFormKey({
+      body: formInfo.value,
+      input_data:res.data.data
+    })
+    xlsxData.value = JSON.parse(result.data)
+    // if (fileExtension === 'xlsx') {
+    //       const readData = await getXlsxValue(file)
+    //   readData[0].forEach((header, i) => {
+    //     // if (!xlsxData.value[header]) xlsxData.value[header] = []
+    //     xlsxData.value[header] = readData[1][i]
+    //   })
+    // } else {
+    //      const result = await getFormKey({
+    //   body: formInfo.value,
+    //   input_data:res.data.data
+    // })
+    // xlsxData.value = JSON.parse(result.data)
+    //   }
+    }
+ 
+   nextTick(() => {
+  if (scrollbar.value && scrollbar.value.wrapRef) {
+    scrollbar.value.setScrollTop(scrollbar.value.wrapRef.scrollHeight)
+  }
+})
+  if (type.value === FunctionList.File_Operation) {
+    obj.loading = false
+    obj.url = res.data.url
+    obj.redisKey = res.data.redisKey
+    obj.fileId = res.data.file.id
+    obj.url = res.data.file.url
+    }
+    obj.loading = false
+  console.log(SystemMsg);
+  
+  } catch (error) {
+    if (SystemMsg) {
+      SystemMsg.content = '数据上传出错,请重试'
+      SystemMsg.rawContent = '数据上传出错,请重试'
+    }
+    console.log(error);
+    ElMessage.error('上传出错')
+    pageInfoList.value = []
+    isShowPage.value = false
+  } finally {
+      SystemMsg && putChat({
+        ...SystemMsg,
+        content: '',
+      })
+    SystemMsg = null
+  }
+}
+async function getFileValue(file) {
+  let formData = new FormData()
+  formData.append('file', file)
+  const res = await getFileContent(formData)
+  return res.data
+}
+let a = null
+// 组件挂载时滚动到底部
+onBeforeMount(async () => {
+  await msgStore.initMsg()
+  await msgStore.initModal()
+})
+onMounted(async () => {
+  // msgStore.updateAIModel(options[0].options[0])
+  useAutoResizeTextarea(tareRef, inputMessage)
+  chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
+    if (message.type === 'TO_SIDE_PANEL_PAGE_INFO') {
+      pageInfo.value = message.data
+      // 转发到 content.js
+      chrome.tabs.sendMessage(sender.tab.id, {
+        type: 'TO_CONTENT_SCRIPT',
+        data: message.data
+      })
+    }
+    if (message.type === 'TO_SIDE_PANEL_PAGE_CHANGE') {
+      pageInfo.value = message.data
+    }
+  })
+ nextTick(() => {
+  if (scrollbar.value && scrollbar.value.wrapRef) {
+    scrollbar.value.setScrollTop(scrollbar.value.wrapRef.scrollHeight)
+  }
+})
+})
+</script>
+
+<style lang="scss" scoped>
+@use '@/entrypoints/sidepanel/css/chat.scss';
+@use '@/entrypoints/sidepanel/css/markdown.scss';
+
+.loading-more-indicator {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 10px 0;
+  color: #909399;
   font-size: 14px;
-  color: #606266;
-  line-height: 1.5;
+  
+  .loading-spinner {
+    width: 20px;
+    height: 20px;
+    margin-right: 8px;
+    border: 2px solid #e6e6e6;
+    border-top-color: #4d6bfe;
+    border-radius: 50%;
+    animation: spin 1s linear infinite;
+  }
 }
 
-@media (max-width: 768px) {
-  .feature-card {
-    width: 100%;
+@keyframes spin {
+  to {
+    transform: rotate(360deg);
   }
 }
 </style>

+ 348 - 0
src/entrypoints/sidepanel/component/StepsDisplay.vue

@@ -0,0 +1,348 @@
+<template>
+  <div class="steps-display-container">
+    <div v-if="parsedContent" class="steps-content">
+      <h2 class="steps-title">{{ parsedContent.title }}</h2>
+      <p class="steps-id">计划ID: {{ parsedContent.planId }}</p>
+      <el-steps 
+        :active="activeStep" 
+        finish-status="success"
+        class="custom-steps"
+        :space="80"
+        direction="vertical"
+      >
+        <el-step 
+          v-for="(step, index) in parsedContent.steps" 
+          :key="index"
+          :title="'步骤 ' + (index + 1)"
+          :description="step.stepRequirement"
+        >
+          <template #icon>
+            <div class="step-icon-container">
+              <div v-if="index < activeStep" class="step-icon completed">
+                <el-icon><Check /></el-icon>
+              </div>
+              <div v-else-if="index === activeStep" class="step-icon current">
+                <el-icon><Loading /></el-icon>
+              </div>
+              <div v-else style="border-radius: 50%;">
+                {{ index + 1 }}
+              </div>
+            </div>
+          </template>
+          <template #description>
+            <div>
+              {{ step.stepRequirement }}
+              <!-- <div v-if="index === activeStep" class="step-progress">
+                <el-progress :percentage="stepProgress" :show-text="false" :stroke-width="8" />
+                <span class="progress-text">{{ stepProgress }}%</span>
+              </div> -->
+            </div>
+          </template>
+        </el-step>
+      </el-steps>
+      
+      <div class="steps-controls">
+        <el-button 
+          type="primary" 
+          size="small" 
+          @click="prevStep" 
+          :disabled="activeStep <= 0"
+        >
+          上一步
+        </el-button>
+        <el-button 
+          type="primary" 
+          size="small" 
+          @click="nextStep" 
+          :disabled="activeStep >= parsedContent.steps.length"
+        >
+          下一步
+        </el-button>
+        <el-button 
+          type="success" 
+          size="small" 
+          @click="completeAll" 
+          :disabled="activeStep >= parsedContent.steps.length"
+        >
+          完成所有
+        </el-button>
+      </div>
+    </div>
+    <div v-else class="error-message">
+      <el-alert
+        title="内容格式错误"
+        type="error"
+        description="无法解析传入的内容,请检查格式是否正确"
+        show-icon
+      />
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, computed, onMounted, watch } from 'vue'
+import { ChatDotRound, Check, Loading } from '@element-plus/icons-vue'
+
+// 定义props
+const props = defineProps({
+  content: {
+    type: [String, Object],
+    required: true
+  },
+  initialStep: {
+    type: Number,
+    default: 0
+  }
+})
+
+// 定义事件
+const emit = defineEmits(['step-change', 'complete'])
+
+// 当前激活的步骤
+const activeStep = ref(props.initialStep)
+
+// 步骤进度(0-100)
+const stepProgress = ref(0)
+
+// 进度条更新定时器
+let progressTimer = null
+
+// 解析内容
+const parsedContent = computed(() => {
+  try {
+      if (typeof props.content === 'string') {
+          const obj = JSON.parse(props.content)
+        console.log(obj);
+        obj.steps = obj.steps.map(_ => ({..._,stepRequirement:_.stepRequirement.split(' ')[1]}))
+      return obj
+    } else {
+      return props.content
+    }
+  } catch (error) {
+    console.error('解析内容失败:', error)
+    return null
+  }
+})
+
+// 获取步骤类型
+const getStepType = (requirement) => {
+  if (requirement.includes('[BROWSER_AGENT]')) {
+    return 'BROWSER_AGENT'
+  } else if (requirement.includes('[DEFAULT_AGENT]')) {
+    return 'DEFAULT_AGENT'
+  } else {
+    return 'UNKNOWN'
+  }
+}
+
+// 开始进度条动画
+const startProgressAnimation = () => {
+  // 重置进度
+  stepProgress.value = 0
+  
+  // 清除之前的定时器
+  if (progressTimer) {
+    clearInterval(progressTimer)
+  }
+  
+  // 创建新的定时器,模拟进度增长
+  progressTimer = setInterval(() => {
+    if (stepProgress.value < 100) {
+      // 进度增长速度可以根据需要调整
+      stepProgress.value += 1
+    } else {
+      clearInterval(progressTimer)
+    }
+  }, 300) // 每300毫秒更新一次进度
+}
+
+// 上一步
+const prevStep = () => {
+  if (activeStep.value > 0) {
+    activeStep.value--
+    emit('step-change', activeStep.value)
+    startProgressAnimation()
+  }
+}
+
+// 下一步
+const nextStep = () => {
+  if (parsedContent.value && activeStep.value < parsedContent.value.steps.length) {
+    activeStep.value++
+    emit('step-change', activeStep.value)
+    if (activeStep.value < parsedContent.value.steps.length) {
+      startProgressAnimation()
+    }
+  }
+}
+
+// 完成所有步骤
+const completeAll = () => {
+  if (parsedContent.value) {
+    activeStep.value = parsedContent.value.steps.length
+    emit('complete')
+    // 清除进度条定时器
+    if (progressTimer) {
+      clearInterval(progressTimer)
+    }
+  }
+}
+
+// 监听content变化,重置步骤
+watch(() => props.content, () => {
+  activeStep.value = props.initialStep
+  // 重置进度条
+  stepProgress.value = 0
+  if (progressTimer) {
+    clearInterval(progressTimer)
+  }
+  // 如果有初始步骤,开始进度动画
+  if (activeStep.value > 0 && activeStep.value < parsedContent.value?.steps.length) {
+    startProgressAnimation()
+  }
+}, { deep: true })
+
+// 组件挂载时初始化
+onMounted(() => {
+  if (activeStep.value > 0 && parsedContent.value) {
+    emit('step-change', activeStep.value)
+    startProgressAnimation()
+  }
+})
+
+// 组件卸载时清理定时器
+onBeforeUnmount(() => {
+  if (progressTimer) {
+    clearInterval(progressTimer)
+  }
+})
+</script>
+
+<style scoped>
+.steps-display-container {
+  padding: 20px;
+  background-color: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+}
+
+.steps-title {
+  font-size: 18px;
+  font-weight: bold;
+  margin-bottom: 8px;
+  color: #303133;
+}
+
+.steps-id {
+  font-size: 14px;
+  color: #909399;
+  margin-bottom: 20px;
+}
+
+.custom-steps {
+  margin: 20px 0;
+}
+
+.steps-controls {
+  display: flex;
+  gap: 10px;
+  margin-top: 20px;
+  justify-content: flex-end;
+}
+
+.error-message {
+  margin: 20px 0;
+}
+
+:deep(.el-step__title) {
+  font-weight: bold;
+}
+
+:deep(.el-step__description) {
+  font-size: 14px;
+  white-space: pre-wrap;
+  word-break: break-word;
+}
+
+:deep(.el-step__icon) {
+  background-color: #f5f7fa;
+}
+
+:deep(.el-step.is-success .el-step__icon) {
+  background-color: #67c23a;
+  color: white;
+}
+
+:deep(.el-step.is-process .el-step__icon) {
+  background-color: #409eff;
+  color: white;
+}
+
+/* 步骤进度条样式 */
+.step-progress {
+  margin-top: 10px;
+  display: flex;
+  align-items: center;
+}
+
+.progress-text {
+  margin-left: 8px;
+  font-size: 12px;
+  color: #409eff;
+}
+
+/* 自定义步骤图标 */
+.step-icon-container {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  width: 100%;
+  height: 100%;
+}
+
+.step-icon {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  width: 24px;
+  height: 24px;
+  border-radius: 50%;
+  background-color: #f5f7fa;
+  color: #909399;
+}
+
+.step-icon.current {
+  background-color: #409eff;
+  color: white;
+  /* animation:  ; */
+  animation: rotate 2s linear infinite, pulse 1.5s infinite;
+}
+
+.step-icon.completed {
+  background-color: #67c23a;
+  color: white;
+}
+
+@keyframes pulse {
+  0% {
+    box-shadow: 0 0 0 0 rgba(64, 158, 255, 0.4);
+  }
+  70% {
+    box-shadow: 0 0 0 10px rgba(64, 158, 255, 0);
+  }
+  100% {
+    box-shadow: 0 0 0 0 rgba(64, 158, 255, 0);
+  }
+}
+@keyframes rotate {
+  0% {
+   transform: rotate(0);
+  }
+  100% {
+    transform: rotate(360deg);
+  }
+}
+:deep(.el-step__icon ){
+    border-radius: 50%;
+}
+</style>

+ 131 - 0
src/utils/request2.js

@@ -0,0 +1,131 @@
+import axios, { CancelToken, isCancel } from 'axios'
+import { ElMessage, ElNotification } from 'element-plus'
+import errorCode from './errorCode.js'
+import { useUserStore } from '@/store/modules/user'
+
+axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
+
+// 创建axios实例
+const service = axios.create({
+  baseURL: 'http://192.168.1.118:18080/api',
+})
+
+let cancel
+// request拦截器
+service.interceptors.request.use(
+ async (config) => {
+    if (cancel) cancel('取消了')
+    if (config.cancel) {
+      config.cancelToken = new CancelToken((c) => {
+        cancel = c
+      })
+    }
+
+    // 处理GET请求,将data参数拼接到URL
+    if (config.method === 'get' && config.data) {
+      // 将data对象转换为URL参数
+      let params = Object.keys(config.data)
+        .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(config.data[key])}`)
+        .join('&');
+      
+      // 拼接到URL
+      if (params) {
+        config.url += (config.url.includes('?') ? '&' : '?') + params;
+      }
+      // 清空data,避免重复发送
+      config.data = undefined;
+    }
+    console.log(config.url);
+
+    const {token} = await new Promise((resolve) => {
+      chrome.storage.local.get(['token'], (result) => {
+        resolve(result)
+      })
+    })
+    
+    // 当token不存在时,执行退出登录
+    // if (!token && !config.url.includes('/login')) {
+    //   return Promise.reject('请先登录')
+    // }
+
+    if (token) {
+      config.headers['Authorization'] = 'Bearer ' + token
+    }
+    return config
+  },
+  (error) => {
+    Promise.reject(error)
+  }
+)
+
+// 响应拦截器
+service.interceptors.response.use(
+  (res) => {
+    console.log(res,85888);
+
+    // 未设置状态码则默认成功状态
+    const code = res.data.code || 200
+    // 获取错误信息
+    const msg = errorCode[code] || res.data.msg || errorCode['default']
+    // 二进制数据则直接返回
+    if (
+      res.request.responseType === 'blob' ||
+      res.request.responseType === 'arraybuffer'
+    ) {
+      let filename = res.headers['content-disposition'].split('filename=')[1]
+      return { fileName: filename, data: res.data }
+    }
+    
+    if (code === '401') {
+      // if (!isRelogin.show) {
+      //   isRelogin.show = true;
+      //   ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => {
+      //     isRelogin.show = false;
+      //     useUserStore().logOut().then(() => {
+      //       location.href = '/index';
+      //     })
+      //   }).catch(() => {
+      //     isRelogin.show = false;
+      //   });
+      // }
+      ElMessage.error('您的登录状态已过期,请重新登录。')
+      const userStore = useUserStore()
+      userStore.logout(false)
+      return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
+    } else if (code === 500) {
+      ElMessage({ message: msg, type: 'error', grouping: true })
+      return Promise.reject(msg)
+    } if (code === '1') {
+      ElMessage({ message: '接口异常', type: 'error', grouping: true })
+      return Promise.reject(msg)
+    } else if (code === 601) {
+      ElMessage({ message: msg, type: 'warning', grouping: true })
+      return Promise.reject(new Error(msg))
+    }  else {
+      return Promise.resolve(res.data)
+    }
+  },
+  (error) => {
+    if (isCancel(error)) {
+      // console.log('用户取消')
+    } else {
+      let { message } = error
+      if (message === 'Network Error') {
+        message = '后端接口连接异常'
+      } else if (message.includes('timeout')) {
+        message = '系统接口请求超时'
+      } else if (message.includes('Request failed with status code')) {
+        message = '系统接口' + message.substr(message.length - 3) + '异常'
+      }
+      ElMessage({
+        message: message,
+        type: 'error',
+        duration: 5 * 1000,
+        grouping: true
+      })
+      return Promise.reject(error)
+    }
+  }
+)
+
+export default service