<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">
|
|
<div v-for="(item,index) in chatHistoryList" :key="index"
|
:class="[item.id===activeHistoryIndex?'single-history-active':'']"
|
@click="switchToCurrentConversation(item,index)" @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">
|
删除后无法恢复,是否继续删除?
|
</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>
|
</template>
|
|
<template v-else>
|
<input id="edit-input" v-model="editedConversationTitle"
|
@keydown.enter="confirmEditConversationTitle(item)" maxlength="15"></input>
|
<div class="icon-container">
|
<a-icon type="check" @click.stop="confirmEditConversationTitle(item)"/>
|
<a-icon type="close" @click.stop="cancelEditConversationTitle(item)"/>
|
</div>
|
</template>
|
</a-popconfirm>
|
</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>
|
<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>
|
</div>
|
</div>
|
<div class="input-container" :class="[textareaFocused?'input-container-active':'']">
|
<textarea v-model="inputQuestion" placeholder="Enter发送,Shift+Enter换行"
|
@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">
|
<a-icon type="loading" class="loading-icon" v-else/>
|
</div>
|
</div>
|
</div>
|
</div>
|
</template>
|
|
<script>
|
import { getFileAccessHttpUrl } from '@/api/manage'
|
import { mapGetters } from 'vuex'
|
import { message } from 'ant-design-vue'
|
import { randomUUID } from '@/utils/util'
|
|
message.config({
|
maxCount: 1
|
})
|
|
export default {
|
name: 'LanguageModel',
|
components: {},
|
data() {
|
return {
|
chatHistoryList: [],
|
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
|
},
|
activeHistoryIndex: null,
|
editingHistoryIndex: null,
|
deletingHistoryIndex: null,
|
iconVisible: false,
|
inputVisible: false,
|
editedConversationTitle: '',
|
deletePopVisible: false,
|
conversationContainer: null,
|
inputQuestion: '',
|
textareaFocused: false,
|
isResponding: false
|
}
|
},
|
created() {
|
this.getConversationByApi()
|
|
},
|
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
|
},
|
|
/* 创建一个新会话 */
|
createNewConversation() {
|
this.currentConversation = {
|
id: randomUUID(),
|
title: '未命名对话',
|
messages: [],
|
stream: true,
|
max_tokens: 500,
|
iconVisible: false,
|
inputVisible: false,
|
deletePopVisible: false
|
}
|
// 退出其他功能
|
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))
|
|
this.chatHistoryList.unshift(this.currentConversation)
|
this.activeHistoryIndex = this.chatHistoryList[0].id
|
console.log(this.chatHistoryList)
|
},
|
|
/* 切换至当前点击会话 */
|
switchToCurrentConversation(record, index) {
|
if (record.inputVisible) return // 当条会话正在被修改时再次点击本条会话无反馈
|
this.activeHistoryIndex = record.id
|
this.currentConversation = this.chatHistoryList[index]
|
|
if (this.editingHistoryIndex !== null) {
|
this.chatHistoryList.find(item => item.id === this.editingHistoryIndex).inputVisible = false
|
this.editingHistoryIndex = null
|
}
|
},
|
|
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()
|
})
|
},
|
|
deleteConversation(record, index) {
|
// 点击删除按钮时关闭所有正在编辑的输入框
|
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
|
this.deletingHistoryIndex = record.id
|
},
|
|
confirmEditConversationTitle(record) {
|
record.title = this.editedConversationTitle
|
record.inputVisible = false
|
this.editingHistoryIndex = null
|
this.editedConversationTitle = ''
|
},
|
|
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
|
}
|
}
|
} else {
|
console.log('删除前只有一条')
|
this.currentConversation = {}
|
this.activeHistoryIndex = null
|
}
|
this.deletingHistoryIndex = null
|
this.$message.success('删除成功!')
|
},
|
|
cancelEditConversationTitle(record) {
|
record.inputVisible = false
|
this.editingHistoryIndex = null
|
},
|
|
cancelDeleteConversation(record) {
|
record.deletePopVisible = false
|
this.deletingHistoryIndex = null
|
},
|
|
/* 获取用户头像 */
|
getAvatar() {
|
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)
|
}
|
}
|
}
|
}
|
</script>
|
|
<style scoped lang="less">
|
@background: rgba(255, 255, 255, .8);
|
@container-border-radius: 12px;
|
@container-padding: 10px;
|
@single-history-border: 3px solid #ABC0CC;
|
@single-conversation-border: 1px solid #7295AB;
|
@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;
|
|
.page-container {
|
display: flex;
|
justify-content: center;
|
align-items: center;
|
font-size: 18px;
|
height: 100%;
|
|
.outer-container {
|
width: 100%;
|
height: 690px;
|
display: flex;
|
justify-content: center;
|
align-items: center;
|
|
.left-container {
|
width: 20%;
|
height: 100%;
|
background-color: @background;
|
border-radius: @container-border-radius;
|
padding: @container-padding;
|
margin-right: 25px;
|
display: flex;
|
flex-direction: column;
|
justify-content: space-between;
|
|
.chat-history-container {
|
height: 582px;
|
overflow: auto;
|
|
& > div {
|
border: 3px solid transparent;
|
border-radius: 10px;
|
padding: 10px 20px;
|
cursor: pointer;
|
|
& > span {
|
display: flex;
|
justify-content: space-between;
|
|
.conversation-title {
|
flex: 1;
|
white-space: nowrap;
|
text-overflow: ellipsis;
|
overflow: hidden;
|
}
|
|
.icon-container {
|
.anticon {
|
padding: 4px;
|
font-size: 16px;
|
border-radius: 8px;
|
margin-left: 5px;
|
|
&:hover {
|
background-color: rgba(0, 0, 0, .2);
|
}
|
}
|
}
|
|
input {
|
flex: 1;
|
height: 100%;
|
border: none;
|
outline: none;
|
padding: 1px 0;
|
letter-spacing: 1px;
|
background-color: transparent;
|
}
|
}
|
|
&:not(:last-child) {
|
margin-bottom: 8px;
|
}
|
|
&:hover {
|
background-color: #eee;
|
}
|
|
&.single-history-active {
|
// border: @single-history-border;
|
background-color: #e5ebed;
|
}
|
}
|
}
|
}
|
|
.right-container {
|
width: 60%;
|
height: 100%;
|
border-radius: @container-border-radius;
|
display: flex;
|
flex-direction: column;
|
justify-content: space-between;
|
|
.conversation-container {
|
height: 570px;
|
background-color: @background;
|
padding: @container-padding*2;
|
border-radius: @container-border-radius;
|
margin-bottom: 25px;
|
overflow: auto;
|
display: flex;
|
flex-direction: column;
|
|
.single-conversation {
|
& > div {
|
display: flex;
|
flex-direction: column;
|
|
&.user-question {
|
align-items: flex-end;
|
}
|
|
&.assistant-answer {
|
align-items: flex-start;
|
}
|
|
.avatar {
|
margin-bottom: 5px;
|
|
/deep/ .ant-avatar, img {
|
width: 35px;
|
height: 35px;
|
line-height: 35px;
|
}
|
}
|
|
.content {
|
max-width: 80%;
|
box-shadow: @conversation-content-container-box-shadow;
|
border-radius: @container-border-radius;
|
padding: @container-padding;
|
background-color: #e5ebed;
|
}
|
}
|
|
&:not(:first-child) {
|
padding-top: @container-padding*2;
|
}
|
}
|
}
|
|
.input-container {
|
flex: 1;
|
background-color: @background;
|
border-radius: @container-border-radius;
|
padding: @container-padding*2;
|
border: 3px solid transparent;
|
display: flex;
|
align-items: center;
|
box-shadow: none;
|
transition: all .2s linear;
|
|
textarea {
|
flex: 1;
|
outline: none;
|
border: none;
|
background-color: transparent;
|
width: 100%;
|
height: 100%;
|
resize: none;
|
margin-right: 10px;
|
}
|
|
img {
|
width: 35px;
|
height: 35px;
|
cursor: pointer;
|
}
|
|
.loading-icon {
|
font-size: 35px;
|
}
|
|
&:hover {
|
box-shadow: @input-container-box-shadow;
|
}
|
|
&.input-container-active {
|
box-shadow: @input-container-box-shadow;
|
}
|
}
|
}
|
}
|
|
}
|
</style>
|