| | |
| | | <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> |
| | |
| | | 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 { |
| | |
| | | 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') |
| | |
| | | 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 |
| | | }, |
| | |
| | | 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; |
| | |
| | | align-items: center; |
| | | font-size: 18px; |
| | | height: 100%; |
| | | font-family: ali_r_main; |
| | | |
| | | .outer-container { |
| | | width: 100%; |
| | |
| | | .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; |
| | |
| | | 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 { |
| | |
| | | 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; |
| | |
| | | } |
| | | |
| | | &: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; |
| | | } |
| | | } |
| | | } |
| | |
| | | |
| | | .conversation-container { |
| | | height: 570px; |
| | | background-color: @background; |
| | | background-color: @main-container-background; |
| | | padding: @container-padding*2; |
| | | border-radius: @container-border-radius; |
| | | margin-bottom: 25px; |
| | |
| | | |
| | | &.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 { |
| | |
| | | } |
| | | } |
| | | |
| | | .content { |
| | | .conversation-content { |
| | | max-width: 80%; |
| | | box-shadow: @conversation-content-container-box-shadow; |
| | | border-radius: @container-border-radius; |
| | | padding: @container-padding; |
| | | background-color: #e5ebed; |
| | | } |
| | | } |
| | | |
| | |
| | | |
| | | .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; |
| | |
| | | } |
| | | } |
| | | } |
| | | |
| | | } |
| | | </style> |