src/views/ai/LanguageModel.vue
@@ -1,64 +1,147 @@
<template>
  <div class="page-container">
    <div class="outer-container">
      <!--左侧历史会话区域-->
      <div class="left-container">
        <div><img src="@/assets/page/languageModel/logo.png" style="width: 100%"></div>
        <a-button style="margin: 20px 0 10px" @click="createNewConversation">新增会话</a-button>
        <div class="chat-history-container">
        <!--logo区域-->
        <div class="logo-container"><img src="@/assets/page/languageModel/logo.png"></div>
          <div v-for="(item,index) in chatHistoryList" :key="index"
               :class="[item.id===activeHistoryIndex?'single-history-active':'']"
               @click="switchToCurrentConversation(item,index)" @mouseenter="item.iconVisible=true"
        <!--功能按键区域-->
        <div class="manage-history-container">
          <div @click="createNewConversation"
               :class="[isAtNewConversation?'create-history-container-active':'',isDeletingBatch?'fold-create-history-container':'expand-create-history-container',isModelResponding?'disable-expand':'']"
               class="create-new-conversation">
            <a-icon type="plus"></a-icon>
            <span v-if="!isDeletingBatch">新增会话</span>
          </div>
          <div @click="expandDeleteBatchContainer"
               :class="[isDeletingBatch?'expand-delete-batch-container':'fold-delete-batch-container',chatHistoryList.length===0?'disable-expand':'']"
               class="delete-batch-container">
            <a-icon type="delete" v-if="!isDeletingBatch"/>
            <template v-else>
              <div class="expand-delete-batch-inner">
                <div class="select-all-button">
                  <label class="checkbox-custom">
                    <input type="checkbox" @change="allHistoryCheckedChange"
                           id="select-all-checkbox"></input>
                    <span class="check-mark"></span>
                    <div>全选</div>
                  </label>
                </div>
                <div class="split-line"></div>
                <a-popover placement="top" :visible="deleteBatchPopVisible" trigger="click"
                           :getPopupContainer="node=>node.parentNode" overlayClassName="delete-batch-popover">
                  <template slot="content">
                    <div class="popover-content">
                      <div>删除后无法恢复,是否继续删除?</div>
                      <div>
                        <button class="cancel-delete-button" @click="deleteBatchPopVisible=false">取消</button>
                        <button id="confirm-delete-batch-button" @click="confirmDeleteBatchConversation">删除
                        </button>
                      </div>
                    </div>
                  </template>
                  <div @click="deleteBatchConversation" id="delete-batch-button"
                       :class="[checkedConversationIdList.length&&!isModelResponding?'able-delete-button':'disable-delete-button']">
                    <a-icon type="delete"/>
                    <div>删除</div>
                  </div>
                </a-popover>
                <div class="split-line"></div>
                <div @click.stop="cancelDeleteBatchConversation">
                  <a-icon type="close"/>
                  <div>退出</div>
                </div>
              </div>
            </template>
          </div>
        </div>
        <!--历史会话列表区域-->
        <div class="chat-history-container">
          <template v-if="!chatHistoryList.length">
            <a-empty>
              <span slot="description">无历史会话</span>
            </a-empty
            >
          </template>
          <div v-for="(item,index) in chatHistoryList" :key="item.id"
               :class="[item.id===activeHistoryIndex?'single-history-active':'',item.inputVisible?'input-visible-class':'',isModelResponding?'disable-switch':'']"
               @click="switchToCurrentConversation(item,$event)"
               @mouseenter="item.iconVisible=true"
               @mouseleave="item.iconVisible=false">
            <a-popconfirm ok-text="删除" cancel-text="取消" @confirm="confirmDeleteConversation(item,index)"
                          @cancel="cancelDeleteConversation(item)"
                          :visible="item.deletePopVisible" :arrowPointAtCenter="true">
              <template slot="title">
                删除后无法恢复,是否继续删除?
            <!--input放置在popover中无法使用功能-->
            <label v-if="isDeletingBatch" class="checkbox-custom">
              <input type="checkbox" v-model="checkedConversationIdList" :value="item.id"
                     @change="singleHistoryCheckedChange"/>
              <span class="check-mark"></span>
            </label>
            <a-popover placement="top" :visible="item.deletePopVisible" trigger="click"
                       :getPopupContainer="node=>node.parentNode">
              <template slot="content">
                <div class="popover-content">
                  <div>删除后无法恢复,是否继续删除?</div>
                  <div>
                    <button class="cancel-delete-button" @click="cancelDeleteConversation(item,$event)">取消</button>
                    <button @click.stop="confirmDeleteConversation(item,index)" id="delete-conversation-button">删除
                    </button>
                  </div>
                </div>
              </template>
              <template v-if="!item.inputVisible">
                <div class="conversation-title">{{item.title}}</div>
                <div class="icon-container" v-show="item.iconVisible">
                  <a-icon type="edit" @click.stop="editConversationTitle(item,index)"/>
                  <a-icon type="delete" @click.stop="deleteConversation(item,index)"/>
                <div class="conversation-title">{{item.problem.slice(0,15)}}</div>
                <div class="icon-container hover-icon-container" v-show="!isDeletingBatch&&!isModelResponding">
                  <a-icon type="edit" @click.stop="editConversationTitle(item)"/>
                  <a-icon type="delete" @click.stop="deleteConversation(item)"
                          @blur="cancelDeleteConversation(item,$event)"/>
                </div>
              </template>
              <template v-else>
                <input id="edit-input" v-model="editedConversationTitle"
                       @keydown.enter="confirmEditConversationTitle(item)" maxlength="15"></input>
                       @keydown.enter="confirmEditConversationTitle(item,$event)"
                       @blur="cancelEditConversationTitle(item,$event)"
                       maxlength="15">
                </input>
                <div class="icon-container">
                  <a-icon type="check" @click.stop="confirmEditConversationTitle(item)"/>
                  <a-icon type="close" @click.stop="cancelEditConversationTitle(item)"/>
                  <a-icon type="check" @click.stop="confirmEditConversationTitle(item,$event)"/>
                  <a-icon type="close"/>
                </div>
              </template>
            </a-popconfirm>
            </a-popover>
          </div>
        </div>
      </div>
      <!--右侧会话内容区域-->
      <div class="right-container">
        <!--会话内容列表区域-->
        <div class="conversation-container">
          <div v-for="item in currentConversation.messages" class="single-conversation" :id="'id'+item.index">
            <div v-if="item.role==='user'" class="user-question">
              <div class="avatar">
                <a-avatar :src="getAvatar()"/>
              </div>
              <div class="content">{{item.content}}</div>
              <div class="conversation-content">{{item.content}}</div>
            </div>
            <div v-else class="assistant-answer">
              <div class="avatar">
                <img src="@/assets/page/languageModel/ai-avatar.png"/>
              </div>
              <div class="content">{{item.content}}</div>
              <div class="conversation-content" v-html="item.content.replace(/\n/g,'<br>')"></div>
            </div>
          </div>
        </div>
        <!--提问输入区域-->
        <div class="input-container" :class="[textareaFocused?'input-container-active':'']">
          <textarea v-model="inputQuestion" placeholder="Enter发送,Shift+Enter换行"
          <textarea v-model="inputQuestion" :placeholder="textareaPlaceholder"
                    @keydown.enter="sendQuestion($event)" @focus="textareaFocused=true"
                    @blur="textareaFocused=false"></textarea>
          <img src="@/assets/page/languageModel/send-message.png" @click="sendQuestion($event)" v-if="!isResponding">
          <img src="@/assets/page/languageModel/send-message.png" @click="sendQuestion($event)"
               v-if="!isModelResponding">
          <a-icon type="loading" class="loading-icon" v-else/>
        </div>
      </div>
@@ -71,9 +154,20 @@
  import { mapGetters } from 'vuex'
  import { message } from 'ant-design-vue'
  import { randomUUID } from '@/utils/util'
  import {
    addNewConversationApi,
    getChatHistoryListApi,
    getCurrentConversationApi,
    deleteSingleChatHistoryApi,
    askToLanguageModelApi
  } from '@/api/ai'
  import { EventSourcePolyfill } from 'event-source-polyfill'
  import Vue from 'vue'
  import { ACCESS_TOKEN, TENANT_ID } from '@/store/mutation-types'
  message.config({
    maxCount: 1
    maxCount: 1,
    duration: 2
  })
  export default {
@@ -82,64 +176,51 @@
    data() {
      return {
        chatHistoryList: [],
        checkedConversationIdList: [],
        deleteBatchPopVisible: false,
        currentConversation: {
          'id': '683a65fd-8feb-4446-ad32-714c4785f667',
          'messages': [
            {
              'role': 'user',
              'content': '你是谁?你是谁你是谁你是谁你是谁你是谁你是谁你是谁你是谁你是谁你是谁你是谁你是谁你是谁你是谁你是谁你是谁你是谁'
            },
            {
              'role': 'assistant',
              'content': '我是26中生涯指导师小爱'
            },
            {
              'role': 'user',
              'content': '你都能做些什么?'
            },
            {
              'role': 'assistant',
              'content': '我能站在未来视角帮助同学们做好生涯规划指导。'
            },
            {
              'role': 'user',
              'content': '今天天气如何?'
            },
            {
              'role': 'assistant',
              'content': '我是26中生涯指导师小爱'
            },
            {
              'role': 'user',
              'content': '你都能做些什么?'
            },
            {
              'role': 'assistant',
              'content': '我能站在未来视角帮助同学们做好生涯规划指导。'
            }
          ],
          'stream': false,
          'max_tokens': 500,
          iconVisible: false,
          inputVisible: false,
          deletePopVisible: false
          id: '',
          messages: [],
          stream: true,
          max_tokens: 500
        },
        isAtNewConversation: false,
        isDeletingBatch: false,
        activeHistoryIndex: null,
        editingHistoryIndex: null,
        deletingHistoryIndex: null,
        iconVisible: false,
        inputVisible: false,
        editedConversationTitle: '',
        deletePopVisible: false,
        conversationContainer: null,
        inputQuestion: '',
        textareaPlaceholder: '',
        textareaFocused: false,
        isResponding: false
        isModelResponding: false
      }
    },
    watch: {
      deleteBatchPopVisible: {
        handler(val) {
          if (val) {
            document.addEventListener('click', this.handleDocumentClick)
          } else {
            document.removeEventListener('click', this.handleDocumentClick)
          }
        }
      },
      isAtNewConversation: {
        handler(val) {
          if (val) {
            this.textareaPlaceholder = '请尝试问我:你是谁?'
          } else {
            this.textareaPlaceholder = 'Enter发送,Shift+Enter换行'
          }
        },
        immediate: true
      }
    },
    created() {
      this.getConversationByApi()
      this.getChatHistoryListByApi()
    },
    mounted() {
      this.conversationContainer = document.querySelector('.conversation-container')
@@ -147,111 +228,397 @@
    methods: {
      ...mapGetters(['avatar']),
      /* 调用接口获取当前会话记录 */
      getConversationByApi() {
        this.currentConversation.title = this.currentConversation.messages[0].content.slice(0, 15)
        this.chatHistoryList.push(this.currentConversation)
        this.activeHistoryIndex = this.chatHistoryList[0].id
      getChatHistoryListByApi() {
        //  TODO 调用接口获取历史会话列表,如果历史会话不为空则跳转至第一条历史会话内容中
        getChatHistoryListApi()
          .then(res => {
            console.log('res', res)
            if (res.success && res.result && res.result.length > 0) {
              this.chatHistoryList = res.result.map(item => {
                return {
                  ...item,
                  iconVisible: false,
                  inputVisible: false,
                  deletePopVisible: false
                }
              })
              if (!this.activeHistoryIndex) this.switchToCurrentConversation(this.chatHistoryList[0])
              if (this.isDeletingBatch) this.singleHistoryCheckedChange()
            } else {
              this.chatHistoryList = []
              this.createNewConversation()
            }
          })
          .catch(err => {
            this.chatHistoryList = []
            this.createNewConversation()
            console.log('err', err)
          })
      },
      /* 创建一个新会话 */
      /* 点击新增会话后触发 */
      createNewConversation() {
        if (this.isAtNewConversation) this.$message.info('当前已是最新对话')
        if (this.isModelResponding) return // 模型回答期间禁止新增会话
        this.isAtNewConversation = true
        this.currentConversation = {
          id: randomUUID(),
          title: '未命名对话',
          id: '',
          messages: [],
          stream: true,
          max_tokens: 500,
          iconVisible: false,
          inputVisible: false,
          deletePopVisible: false
          max_tokens: 500
        }
        // 退出其他功能
        if (this.editingHistoryIndex !== null) this.cancelEditConversationTitle(this.chatHistoryList.find(item => item.id === this.editingHistoryIndex))
        if (this.deletingHistoryIndex !== null) this.cancelDeleteConversation(this.chatHistoryList.find(item => item.id === this.deletingHistoryIndex))
        // 新建会话时取消原先被选中的历史对话,更清晰告诉用户现在界面处于新建对话中
        if (this.activeHistoryIndex !== null) this.activeHistoryIndex = null
      },
        this.chatHistoryList.unshift(this.currentConversation)
        this.activeHistoryIndex = this.chatHistoryList[0].id
        console.log(this.chatHistoryList)
      /* 调用接口获取当前会话记录 */
      getConversationByApi(id) {
        // TODO 根据点击的历史会话ID获取对应历史会话的对话记录列表
        getCurrentConversationApi({ id })
          .then(res => {
            console.log('currentRes', res)
            if (res.success && res.result) {
              this.currentConversation.messages = res.result.map(item => {
                return {
                  id: item.id,
                  role: item.aiType === 2 ? 'user' : 'assistant',
                  content: item.aiType === 2 ? item.problem : item.answer
                }
              })
              this.scrollToConversationContainerBottom()
              this.currentConversation.id = res.result[0].parentId
              if (this.isModelResponding) this.isModelResponding = false
              if (res.result[res.result.length - 1].aiType === 2) {
                console.log('触发向模型提问', res.result)
                this.askToLanguageModel()
              }
            }
          })
      },
      addNewConversationByApi(params) {
        this.inputQuestion = ''
        // TODO 调用后端接口保存当前问题且创建一条历史会话记录,然后再重新调用获取历史记录列表接口刷新列表
        addNewConversationApi(params)
          .then(res => {
            if (res.success) {
              switch (+params.aiType) {
                case 1:
                  this.getChatHistoryListByApi()
                  this.$message.success('会话列表记录' + res.message)
                  break
                case 2:
                  this.getConversationByApi(this.activeHistoryIndex)
                  this.$message.success('新增会话内容问题记录' + res.message)
                  break
                case 3:
                  this.getConversationByApi(this.activeHistoryIndex)
                  // this.currentConversation.messages[this.currentConversation.messages.length - 1].content += '\n' + '对话结束'
                  this.$message.success('新增会话内容答案记录' + res.message)
                  break
              }
            } else {
              this.$message.error(res.message)
            }
          })
      },
      /* 向模型提问 */
      sendQuestion(event) {
        //监测是否按下shift键
        if (!event.shiftKey) {
          event.preventDefault()
          if (this.isModelResponding) {
            this.$message.error('请等待机器人回复后再发送哦~')
            return
          }
          if (!this.inputQuestion) {
            this.$message.error('你没有输入内容哦')
            return
          }
          const params = {
            problem: this.inputQuestion,
            aiType: ''
          }
          if (this.isAtNewConversation) {
            params.aiType = 1
            params.parentId = ''
          } else {
            params.aiType = 2
            params.parentId = this.activeHistoryIndex
          }
          this.addNewConversationByApi(params)
        }
      },
      askToLanguageModel() {
        const messages = JSON.parse(JSON.stringify(this.currentConversation))
        const answer = {
          id: '',
          role: 'assistant',
          content: ''
        }
        this.currentConversation.messages.push(answer)
        let lastElement
        this.$nextTick(() => {
          const elementArr = document.querySelectorAll('.single-conversation')
          lastElement = elementArr[elementArr.length - 1]
          console.log('elementArr', elementArr)
        })
        console.log('beforeAnswerConversation', messages)
        console.log('this.currentConversation', this.currentConversation)
        this.isModelResponding = true
        // 发送POST请求到模型,获取响应流
        askToLanguageModelApi(messages)
          .then(async (response) => {
            if (!response.body) return
            const reader = response.body.pipeThrough(new TextDecoderStream()).getReader()
            // const decoder = new TextDecoder()
            // console.log(reader)
            let discontinuousJsonArray = []
            let isContinuous = null
            while (true) {
              const { value, done } = await reader.read()
              if (done) {
                const params = {
                  parentId: this.activeHistoryIndex,
                  answer: this.currentConversation.messages.find(item => !item.id && item.role === 'assistant').content,
                  aiType: 3
                }
                this.addNewConversationByApi(params)
                break
              }
              this.scrollToConversationContainerBottom()
              const objectArray = parsePack(value)
              // console.log('objectArray', objectArray)
              if (Array.isArray(objectArray) && objectArray.length > 0) {
                if (discontinuousJsonArray.length === 2) discontinuousJsonArray = []
                objectArray.forEach(json => {
                  if (!json.choices || json.choices.length === 0) {
                    return
                  }
                  const text = json.choices[0].delta.content
                  this.currentConversation.messages.find(item => !item.id && item.role === 'assistant').content += text
                })
                if (isContinuous) {
                  discontinuousJsonArray = []
                  isContinuous = null
                }
              }
            }
            // 递归找到DOM下最后一个元素节点
            function parsePack(str) {
              const pattern = /data:\s*(?!\[DONE\])(\{.*?\})\s*\n/g
              const result = []
              let match
              while ((match = pattern.exec(str)) !== null) {
                const jsonStr = match[1]
                console.log('jsonStr', jsonStr)
                try {
                  const object = JSON.parse(jsonStr)
                  if (object) result.push(object)
                } catch (err) {
                  console.log('err', err)
                }
              }
              // 此判断处理返回的不完整的数组
              if (match = pattern.exec(str) === null) {
                isContinuous = false
                // console.log('str', str)
                // 此处为屏蔽返回带有ping字符串
                if (!str.includes('ping')) {
                  discontinuousJsonArray.push(str)
                  // 判断条件为2是由于不完整数组仅经过2次返回值就可以拼接完整,但还是不应该用数字作为判断条件,以防不止2次
                  if (discontinuousJsonArray.length === 2) {
                    // console.log('discontinuousJsonArray', discontinuousJsonArray[0], '---', discontinuousJsonArray[1])
                    const discontinuousMatch = pattern.exec(discontinuousJsonArray[0] + discontinuousJsonArray[1])
                    discontinuousJsonArray = [JSON.parse(discontinuousMatch[1])]
                    isContinuous = true
                    return discontinuousJsonArray
                  }
                }
              }
              return result
            }
          })
          .catch(error => {
            console.error('请求失败:', error)
          })
      },
      /* 切换至当前点击会话 */
      switchToCurrentConversation(record, index) {
      switchToCurrentConversation(record, event = {}) {
        if (record.id === this.activeHistoryIndex) return // 避免重复点击
        if (event.target && event.target.type === 'checkbox') return //点击多选按钮避免传递给此事件
        if (record.inputVisible) return // 当条会话正在被修改时再次点击本条会话无反馈
        this.activeHistoryIndex = record.id
        this.currentConversation = this.chatHistoryList[index]
        // 关闭点击编辑按钮后的输入框
        if (this.editingHistoryIndex !== null && this.editingHistoryIndex !== record.id) this.cancelEditConversationTitle(this.chatHistoryList.find(item => item.id === this.editingHistoryIndex))
        // 关闭所有确认删除弹窗
        if (this.deletingHistoryIndex !== null) this.cancelDeleteConversation(this.chatHistoryList.find(item => item.id === this.deletingHistoryIndex))
        if (this.isAtNewConversation) this.isAtNewConversation = false // 如果在创建新对话界面则将新对话界面关闭
        if (this.isModelResponding) return // 模型回答期间禁止切换会话
        if (this.editingHistoryIndex !== null) {
          this.chatHistoryList.find(item => item.id === this.editingHistoryIndex).inputVisible = false
          this.editingHistoryIndex = null
        this.activeHistoryIndex = record.id
        this.getConversationByApi(record.id)
      },
      /* 点击全选按钮后改变勾选后触发 */
      allHistoryCheckedChange(event) {
        // event.target.indeterminate = true
        if (event.target.checked) {
          this.checkedConversationIdList = this.chatHistoryList.map(item => item.id)
        } else {
          this.checkedConversationIdList = []
        }
        console.log('触发全选', this.checkedConversationIdList)
      },
      /* 点击历史记录中多选框改变勾选后触发 */
      singleHistoryCheckedChange() {
        // 仅在批量删除展开时获取到
        const selectAllCheckbox = document.getElementById('select-all-checkbox')
        if (this.checkedConversationIdList.length > 0) {
          if (this.checkedConversationIdList.length !== this.chatHistoryList.length) {
            console.log('触发未全选中', selectAllCheckbox.indeterminate)
            this.$nextTick(() => selectAllCheckbox.indeterminate = true)
          } else {
            console.log('触发全被选中', selectAllCheckbox)
            // document.getElementById('select-all-checkbox').indeterminate = false
            this.$nextTick(() => {
              selectAllCheckbox.indeterminate = false
              selectAllCheckbox.checked = true
            })
          }
        } else {
          selectAllCheckbox.indeterminate = false
          selectAllCheckbox.checked = false
        }
      },
      editConversationTitle(record, index) {
        // 仅开启最后一次点击编辑按钮后的输入框
        if (this.editingHistoryIndex !== null && this.editingHistoryIndex !== record.id) this.chatHistoryList.find(item => item.id === this.editingHistoryIndex).inputVisible = false
        // 进入编辑后关闭所有确认删除弹窗
        if (this.deletingHistoryIndex !== null) this.cancelDeleteConversation(this.chatHistoryList.find(item => item.id === this.deletingHistoryIndex))
        console.log('进入修改', record)
        this.editingHistoryIndex = record.id
        record.inputVisible = true
        this.editedConversationTitle = record.title
        this.$nextTick(() => {
          document.getElementById('edit-input').focus()
      /* 点击批量删除会话图标时展开批量管理区域 */
      expandDeleteBatchContainer() {
        if (this.chatHistoryList.length === 0) return
        this.isDeletingBatch = true
      },
      /* 点击红色图标批量删除按钮后触发 */
      deleteBatchConversation() {
        if (!this.checkedConversationIdList.length) return
        if (this.isModelResponding) return // 模型回答期间禁止删除会话
        this.deleteBatchPopVisible = !this.deleteBatchPopVisible
      },
      /* 点击文档空白关闭批量删除popover */
      handleDocumentClick(e) {
        const popover = document.querySelector('.delete-batch-popover')
        const button = document.getElementById('delete-batch-button')
        if (popover && !popover.contains(e.target) && !button.contains(e.target)) this.deleteBatchPopVisible = false
      },
      /* 确认批量删除对话 */
      confirmDeleteBatchConversation() {
        if (this.checkedConversationIdList.includes(this.activeHistoryIndex)) this.createNewConversation()
        this.chatHistoryList = this.chatHistoryList.filter(item => !this.checkedConversationIdList.includes(item.id))
        this.$message.success('删除成功')
        const timer = setTimeout(() => {
          this.cancelDeleteBatchConversation()
          clearTimeout(timer)
        })
      },
      deleteConversation(record, index) {
      /* 取消批量删除会话功能 */
      cancelDeleteBatchConversation() {
        this.deleteBatchPopVisible = false
        this.isDeletingBatch = false
        this.checkedConversationIdList = []
      },
      /* 点击编辑会话标题按钮时触发 */
      editConversationTitle(record) {
        // 仅开启最后一次点击编辑按钮后的输入框
        if (this.editingHistoryIndex !== null && this.editingHistoryIndex !== record.id) this.cancelEditConversationTitle(this.chatHistoryList.find(item => item.id === this.editingHistoryIndex))
        // 进入编辑后关闭所有确认删除弹窗
        if (this.deletingHistoryIndex !== null) this.cancelDeleteConversation(this.chatHistoryList.find(item => item.id === this.deletingHistoryIndex))
        this.editingHistoryIndex = record.id
        record.inputVisible = true
        this.editedConversationTitle = record.problem
        this.$nextTick(() => document.getElementById('edit-input').focus())
      },
      /* 点击单个删除会话按钮时触发 */
      deleteConversation(record) {
        // 点击删除按钮时关闭所有正在编辑的输入框
        if (this.editingHistoryIndex !== null) {
          this.cancelEditConversationTitle(this.chatHistoryList.find(item => item.id === this.editingHistoryIndex))
          return
        }
        // 仅可使用最后一次点击删除按钮的功能
        if (this.deletingHistoryIndex !== null && this.deletingHistoryIndex !== record.id) this.chatHistoryList.find(item => item.id === this.deletingHistoryIndex).deletePopVisible = false
        record.deletePopVisible = true
        if (this.deletingHistoryIndex !== null && this.deletingHistoryIndex !== record.id) this.cancelDeleteConversation(this.chatHistoryList.find(item => item.id === this.deletingHistoryIndex))
        if (this.isModelResponding) return // 模型回答期间禁止删除会话
        record.deletePopVisible = !record.deletePopVisible
        this.deletingHistoryIndex = record.id
      },
      confirmEditConversationTitle(record) {
      /* 确认编辑会话标题 */
      confirmEditConversationTitle(record, event) {
        // TODO 调用编辑会话接口并重新获取历史会话数据
        record.title = this.editedConversationTitle
        record.inputVisible = false
        this.editingHistoryIndex = null
        this.editedConversationTitle = ''
        this.cancelEditConversationTitle(record, event)
      },
      /* 确认删除会话时触发 */
      confirmDeleteConversation(record, index) {
        this.chatHistoryList = this.chatHistoryList.filter(item => item.id !== this.deletingHistoryIndex)
        if (this.chatHistoryList.length > 0) {
          // 判断当前会话是不是要删除的会话
          // TODO 由于目前没有唯一标识ID,暂时使用历史记录集合长度作为ID使用,后期必须调整
          if (this.activeHistoryIndex === record.id) {
            if (this.chatHistoryList[index]) {
              console.log('删除非最后一条')
              this.currentConversation = this.chatHistoryList[index]
              this.activeHistoryIndex = this.chatHistoryList[index].id
            } else {
              console.log('删除最后一条')
              this.currentConversation = this.chatHistoryList[this.chatHistoryList.length - 1]
              this.activeHistoryIndex = this.chatHistoryList[this.chatHistoryList.length - 1].id
        deleteSingleChatHistoryApi({ id: record.id })
          .then(res => {
            if (res.success) {
              if (this.chatHistoryList.length !== 1) {
                // 判断当前会话是不是要删除的会话
                console.log('record', record)
                console.log('activeHistoryIndex', this.activeHistoryIndex)
                if (this.activeHistoryIndex === record.id) {
                  if (index !== 0) {
                    console.log('删除非第一条记录')
                    this.switchToCurrentConversation(this.chatHistoryList[index - 1])
                  } else {
                    console.log('删除第一条记录')
                    this.switchToCurrentConversation(this.chatHistoryList[index + 1])
                  }
                }
              } else {
                console.log('删除最后一条记录')
                this.activeHistoryIndex = null
              }
              record.deletePopVisible = false
              this.deletingHistoryIndex = null
              this.getChatHistoryListByApi()
              this.$message.success(res.message)
            }
          }
        } else {
          console.log('删除前只有一条')
          this.currentConversation = {}
          this.activeHistoryIndex = null
        }
        this.deletingHistoryIndex = null
        this.$message.success('删除成功!')
          })
          .catch(err => {
            this.$message.error(err.message)
          })
      },
      cancelEditConversationTitle(record) {
      /* 取消编辑会话标题时触发 */
      cancelEditConversationTitle(record, event) {
        // 失去焦点事件时若点击的元素是确认编辑按钮则不进行失去焦点事件,直接进入确认编辑事件
        if (event.relatedTarget && event.relatedTarget.className === 'anticon anticon-check') return
        record.inputVisible = false
        this.editingHistoryIndex = null
      },
      cancelDeleteConversation(record) {
      /* 取消删除会话时触发 */
      cancelDeleteConversation(record, event) {
        // 失去焦点事件时若点击的元素是确认删除按钮则不进行失去焦点事件,直接进入确认删除事件
        if (event && event.relatedTarget && event.relatedTarget.id === 'delete-conversation-button') return
        record.deletePopVisible = false
        this.deletingHistoryIndex = null
      },
@@ -261,55 +628,30 @@
        return getFileAccessHttpUrl(this.avatar())
      },
      /* 向模型提问 */
      sendQuestion(e) {
        //监测是否按下shift键
        if (!e.shiftKey) {
          e.preventDefault()
          if (this.isResponding) {
            this.$message.error('请等待机器人回复后再发送哦~')
            return
          }
          if (!this.inputQuestion) {
            this.$message.error('你没有输入内容哦')
            return
          }
          const newQuestion = {
            role: 'user',
            content: this.inputQuestion
          }
          this.currentConversation.messages.push(newQuestion)
          this.isResponding = true
          const response = {
            role: 'assistant',
            content: '这个问题我也不太清楚'
          }
          setTimeout(() => {
            this.currentConversation.messages.push(response)
            this.inputQuestion = ''
            this.isResponding = false
            this.$nextTick(() => {
              this.conversationContainer.scrollTo({ top: 9999999999999999999999999999, behavior: 'smooth' })
            })
          }, 1000)
        }
      scrollToConversationContainerBottom(scrollBehavior = 'auto') {
        this.$nextTick(() => {
          this.conversationContainer.scrollTo({
            top: this.conversationContainer.scrollHeight,
            behavior: scrollBehavior
          })
        })
      }
    }
  }
</script>
<style scoped lang="less">
  @background: rgba(255, 255, 255, .8);
  @main-container-background: rgba(255, 255, 255, .7);
  @container-border-radius: 12px;
  @container-padding: 10px;
  @single-history-border: 3px solid #ABC0CC;
  @single-conversation-border: 1px solid #7295AB;
  @single-history-edit-border: 3px solid #ABC0CC;
  @single-history-hover-background: #f1f1f1;
  @single-history-active-background: #e5ebed;
  @input-container-border: 3px solid #B8CAD5;
  @conversation-content-container-box-shadow: 2px 2px 10px 0px #eeeeee;
  @input-container-box-shadow: 2px 2px 10px 0px #7295AB;
  @user-question-background: #e5ebed;
  @assistant-answer-background: #F0F5F5;
  @conversation-content-container-box-shadow: 2px 2px 10px 0px #ddd;
  @input-container-box-shadow: 2px 2px 10px 0px #ABC0CC;
  .page-container {
    display: flex;
@@ -317,6 +659,7 @@
    align-items: center;
    font-size: 18px;
    height: 100%;
    font-family: ali_r_main;
    .outer-container {
      width: 100%;
@@ -328,7 +671,7 @@
      .left-container {
        width: 20%;
        height: 100%;
        background-color: @background;
        background-color: @main-container-background;
        border-radius: @container-border-radius;
        padding: @container-padding;
        margin-right: 25px;
@@ -336,25 +679,168 @@
        flex-direction: column;
        justify-content: space-between;
        .chat-history-container {
          height: 582px;
          overflow: auto;
        .logo-container {
          height: 80px;
          img {
            width: 100%;
            height: 100%;
          }
        }
        .manage-history-container {
          margin: 20px 0 15px;
          display: flex;
          & > div {
            border: 1px solid #d9d9d9;
            display: flex;
            justify-content: center;
            align-items: center;
            cursor: pointer;
            color: #585258;
            transition: box-shadow .2s ease-in-out;
            &:hover {
              box-shadow: @conversation-content-container-box-shadow;
            }
          }
          .create-new-conversation {
            margin-right: 10px;
            &.create-history-container-active {
              border: 1px solid transparent;
              background-color: @single-history-active-background;
              &:hover {
                box-shadow: none;
              }
            }
            &.fold-create-history-container {
              flex: none;
              width: 40px;
              height: 40px;
              border-radius: 50%;
            }
            &.expand-create-history-container {
              flex: 1;
              height: 100%;
              border-radius: 20px;
              .anticon {
                margin-right: 10px;
              }
            }
            &.disable-expand {
              cursor: not-allowed;
              &:hover {
                box-shadow: none;
              }
            }
          }
          .delete-batch-container {
            &.expand-delete-batch-container {
              flex: 1;
              border-radius: 20px;
              cursor: default;
              box-shadow: none;
              .expand-delete-batch-inner {
                display: flex;
                justify-content: space-evenly;
                align-items: center;
                height: 100%;
                width: 100%;
                & > div {
                  display: flex;
                  align-items: center;
                  &.select-all-button {
                    height: 100%;
                  }
                  &.able-delete-button {
                    color: #D9737A;
                  }
                  &.disable-delete-button {
                    color: #bbb;
                    cursor: not-allowed !important;
                  }
                  &:not(.split-line) {
                    cursor: pointer;
                  }
                  .anticon {
                    margin-right: 10px;
                  }
                  &.split-line {
                    width: 1px;
                    background-color: #000;
                    height: 50%;
                  }
                }
              }
            }
            &.fold-delete-batch-container {
              width: 40px;
              height: 40px;
              border-radius: 50%;
            }
            &.disable-expand {
              cursor: not-allowed;
              &:hover {
                box-shadow: none;
              }
            }
          }
        }
        .chat-history-container {
          flex: 1;
          overflow: auto;
          .ant-empty {
            height: 100%;
            display: flex;
            flex-direction: column;
            justify-content: center;
          }
          & > div:not(.ant-empty) {
            border: 3px solid transparent;
            border-radius: 10px;
            padding: 10px 20px;
            cursor: pointer;
            display: flex;
            align-items: center;
            & > span {
              display: flex;
              justify-content: space-between;
              position: relative;
              flex: 1;
              .conversation-title {
                flex: 1;
                white-space: nowrap;
                text-overflow: ellipsis;
                overflow: hidden;
                white-space: nowrap;
              }
              .hover-icon-container {
                // display:none的消失方式会让点击删除弹出的popover卡片无法点击空白处消失
                opacity: 0;
                position: absolute;
                right: 0;
              }
              .icon-container {
@@ -365,12 +851,12 @@
                  margin-left: 5px;
                  &:hover {
                    background-color: rgba(0, 0, 0, .2);
                    background-color: rgba(0, 0, 0, .1);
                  }
                }
              }
              input {
              #edit-input {
                flex: 1;
                height: 100%;
                border: none;
@@ -386,12 +872,161 @@
            }
            &:hover {
              background-color: #eee;
              background-color: @single-history-hover-background;
              .icon-container {
                opacity: 1;
                background-color: @single-history-hover-background;
              }
            }
            &.single-history-active {
              // border: @single-history-border;
              background-color: #e5ebed;
              background-color: @single-history-active-background;
              &:hover {
                .icon-container {
                  background-color: @single-history-active-background;
                }
              }
            }
            &.input-visible-class {
              background-color: @main-container-background;
              border: @single-history-edit-border;
              &:hover {
                .icon-container {
                  background-color: transparent;
                }
              }
            }
            &.disable-switch {
              cursor: not-allowed;
              &:not(.single-history-active):hover {
                background-color: transparent;
              }
            }
          }
        }
        /deep/ .ant-popover {
          padding-bottom: 10px;
          .ant-popover-arrow {
            display: none;
          }
          .ant-popover-inner {
            border-radius: @container-border-radius;
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
            .ant-popover-inner-content {
              padding: 12px 16px;
              .popover-content {
                font-size: 16px;
                font-weight: bold;
                display: flex;
                flex-direction: column;
                align-items: center;
                font-family: ali_r_main;
                & > div {
                  width: 100%;
                  display: flex;
                  justify-content: space-evenly;
                  margin-top: 10px;
                  button {
                    border: none;
                    padding: 5px 30px;
                    border-radius: 20px;
                    font-weight: bold;
                    cursor: pointer;
                    outline: none;
                  }
                  .cancel-delete-button {
                    background-color: #D9D9D9;
                  }
                  #delete-conversation-button, #confirm-delete-batch-button {
                    background-color: #7295AB;
                    color: #fff;
                    font-weight: normal;
                  }
                }
              }
            }
          }
        }
        .checkbox-custom {
          height: 100%;
          position: relative;
          display: flex;
          justify-content: flex-end;
          align-items: center;
          cursor: pointer;
          -webkit-user-select: none;
          -moz-user-select: none;
          -ms-user-select: none;
          user-select: none;
          input {
            position: absolute;
            opacity: 0;
            cursor: pointer;
            height: 0;
            width: 0;
            &:checked ~ .check-mark {
              background-color: #7295AB;
              border: none;
              &:after {
                display: block;
              }
            }
            &:indeterminate ~ .check-mark {
              background-color: #fff;
              border: 1px solid #818181;
              &:after {
                display: block;
                width: 10px;
                height: 3px;
                background: #818181;
                border-radius: 12px;
              }
            }
          }
          .check-mark {
            position: relative;
            height: 16px;
            width: 16px;
            background-color: transparent;
            border: 1px solid #818181;
            border-radius: 50%;
            margin-right: 10px;
            &:after {
              content: "";
              position: absolute;
              display: none;
              top: 50%;
              left: 50%;
              width: 8px;
              height: 8px;
              border-radius: 50%;
              background: #fff;
              transform: translate(-50%, -50%);
            }
            &:hover {
              border: 3px solid #7295AB;
            }
          }
        }
@@ -407,7 +1042,7 @@
        .conversation-container {
          height: 570px;
          background-color: @background;
          background-color: @main-container-background;
          padding: @container-padding*2;
          border-radius: @container-border-radius;
          margin-bottom: 25px;
@@ -422,10 +1057,21 @@
              &.user-question {
                align-items: flex-end;
                .conversation-content {
                  background-color: @user-question-background;
                  text-align: justify;
                  text-align-last: left;
                }
              }
              &.assistant-answer {
                align-items: flex-start;
                .conversation-content {
                  background-color: @assistant-answer-background;
                  text-align: justify;
                  text-align-last: left;
                }
              }
              .avatar {
@@ -438,12 +1084,11 @@
                }
              }
              .content {
              .conversation-content {
                max-width: 80%;
                box-shadow: @conversation-content-container-box-shadow;
                border-radius: @container-border-radius;
                padding: @container-padding;
                background-color: #e5ebed;
              }
            }
@@ -455,7 +1100,7 @@
        .input-container {
          flex: 1;
          background-color: @background;
          background-color: @main-container-background;
          border-radius: @container-border-radius;
          padding: @container-padding*2;
          border: 3px solid transparent;
@@ -495,6 +1140,5 @@
        }
      }
    }
  }
</style>