From ff2d01588d4e69d3bf1ad856e8e225b7f6a3c0f2 Mon Sep 17 00:00:00 2001 From: zhaowei <zhaowei> Date: 星期一, 26 八月 2024 11:01:48 +0800 Subject: [PATCH] 1、基本实现电子说明书页面布局及功能 2、基本实现语言大模型页面与后端数据联动 --- src/views/ai/LanguageModel.vue | 1028 ++++++++++++++++++++++++++++++++++++++++++++++---------- 1 files changed, 836 insertions(+), 192 deletions(-) diff --git a/src/views/ai/LanguageModel.vue b/src/views/ai/LanguageModel.vue index 3a17391..36c4eb6 100644 --- a/src/views/ai/LanguageModel.vue +++ b/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鏀剧疆鍦╬opover涓棤娉曚娇鐢ㄥ姛鑳�--> + <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 鏍规嵁鐐瑰嚮鐨勫巻鍙蹭細璇滻D鑾峰彇瀵瑰簲鍘嗗彶浼氳瘽鐨勫璇濊褰曞垪琛� + 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 璋冪敤鍚庣鎺ュ彛淇濆瓨褰撳墠闂涓斿垱寤轰竴鏉″巻鍙蹭細璇濊褰曪紝鐒跺悗鍐嶉噸鏂拌皟鐢ㄨ幏鍙栧巻鍙茶褰曞垪琛ㄦ帴鍙e埛鏂板垪琛� + 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 + // 鍙戦�丳OST璇锋眰鍒版ā鍨嬶紝鑾峰彇鍝嶅簲娴� + 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) + // 姝ゅ涓哄睆钄借繑鍥炲甫鏈塸ing瀛楃涓� + 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 // 褰撴潯浼氳瘽姝e湪琚慨鏀规椂鍐嶆鐐瑰嚮鏈潯浼氳瘽鏃犲弽棣� - 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锛屾殏鏃朵娇鐢ㄥ巻鍙茶褰曢泦鍚堥暱搴︿綔涓篒D浣跨敤锛屽悗鏈熷繀椤昏皟鏁� - 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锛歯one鐨勬秷澶辨柟寮忎細璁╃偣鍑诲垹闄ゅ脊鍑虹殑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> \ No newline at end of file -- Gitblit v1.9.3