<template>
|
<div class="page-container">
|
<div class="outer-container">
|
<!--左侧历史会话区域-->
|
<div class="left-container">
|
<!--logo区域-->
|
<div class="logo-container"><img src="@/assets/page/languageModel/logo.png"></div>
|
|
<!--功能按键区域-->
|
<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">
|
<!--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.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,$event)"
|
@blur="cancelEditConversationTitle(item,$event)"
|
maxlength="15">
|
</input>
|
<div class="icon-container">
|
<a-icon type="check" @click.stop="confirmEditConversationTitle(item,$event)"/>
|
<a-icon type="close"/>
|
</div>
|
</template>
|
</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="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="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="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="!isModelResponding">
|
<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'
|
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,
|
duration: 2
|
})
|
|
export default {
|
name: 'LanguageModel',
|
components: {},
|
data() {
|
return {
|
chatHistoryList: [],
|
checkedConversationIdList: [],
|
deleteBatchPopVisible: false,
|
currentConversation: {
|
id: '',
|
messages: [],
|
stream: true,
|
max_tokens: 500
|
},
|
isAtNewConversation: false,
|
isDeletingBatch: false,
|
activeHistoryIndex: null,
|
editingHistoryIndex: null,
|
deletingHistoryIndex: null,
|
editedConversationTitle: '',
|
conversationContainer: null,
|
inputQuestion: '',
|
textareaPlaceholder: '',
|
textareaFocused: 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.getChatHistoryListByApi()
|
},
|
mounted() {
|
this.conversationContainer = document.querySelector('.conversation-container')
|
},
|
methods: {
|
...mapGetters(['avatar']),
|
|
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: '',
|
messages: [],
|
stream: true,
|
max_tokens: 500
|
}
|
// 新建会话时取消原先被选中的历史对话,更清晰告诉用户现在界面处于新建对话中
|
if (this.activeHistoryIndex !== null) this.activeHistoryIndex = null
|
},
|
|
/* 调用接口获取当前会话记录 */
|
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, event = {}) {
|
if (record.id === this.activeHistoryIndex) return // 避免重复点击
|
if (event.target && event.target.type === 'checkbox') return //点击多选按钮避免传递给此事件
|
if (record.inputVisible) return // 当条会话正在被修改时再次点击本条会话无反馈
|
// 关闭点击编辑按钮后的输入框
|
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 // 模型回答期间禁止切换会话
|
|
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
|
}
|
},
|
|
/* 点击批量删除会话图标时展开批量管理区域 */
|
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)
|
})
|
},
|
|
/* 取消批量删除会话功能 */
|
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.cancelDeleteConversation(this.chatHistoryList.find(item => item.id === this.deletingHistoryIndex))
|
if (this.isModelResponding) return // 模型回答期间禁止删除会话
|
record.deletePopVisible = !record.deletePopVisible
|
this.deletingHistoryIndex = record.id
|
},
|
|
/* 确认编辑会话标题 */
|
confirmEditConversationTitle(record, event) {
|
// TODO 调用编辑会话接口并重新获取历史会话数据
|
record.title = this.editedConversationTitle
|
this.cancelEditConversationTitle(record, event)
|
},
|
|
/* 确认删除会话时触发 */
|
confirmDeleteConversation(record, index) {
|
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)
|
}
|
})
|
.catch(err => {
|
this.$message.error(err.message)
|
})
|
},
|
|
/* 取消编辑会话标题时触发 */
|
cancelEditConversationTitle(record, event) {
|
// 失去焦点事件时若点击的元素是确认编辑按钮则不进行失去焦点事件,直接进入确认编辑事件
|
if (event.relatedTarget && event.relatedTarget.className === 'anticon anticon-check') return
|
record.inputVisible = false
|
this.editingHistoryIndex = null
|
},
|
|
/* 取消删除会话时触发 */
|
cancelDeleteConversation(record, event) {
|
// 失去焦点事件时若点击的元素是确认删除按钮则不进行失去焦点事件,直接进入确认删除事件
|
if (event && event.relatedTarget && event.relatedTarget.id === 'delete-conversation-button') return
|
record.deletePopVisible = false
|
this.deletingHistoryIndex = null
|
},
|
|
/* 获取用户头像 */
|
getAvatar() {
|
return getFileAccessHttpUrl(this.avatar())
|
},
|
|
scrollToConversationContainerBottom(scrollBehavior = 'auto') {
|
this.$nextTick(() => {
|
this.conversationContainer.scrollTo({
|
top: this.conversationContainer.scrollHeight,
|
behavior: scrollBehavior
|
})
|
})
|
}
|
}
|
}
|
</script>
|
|
<style scoped lang="less">
|
@main-container-background: rgba(255, 255, 255, .7);
|
@container-border-radius: 12px;
|
@container-padding: 10px;
|
@single-history-edit-border: 3px solid #ABC0CC;
|
@single-history-hover-background: #f1f1f1;
|
@single-history-active-background: #e5ebed;
|
@input-container-border: 3px solid #B8CAD5;
|
@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;
|
justify-content: center;
|
align-items: center;
|
font-size: 18px;
|
height: 100%;
|
font-family: ali_r_main;
|
|
.outer-container {
|
width: 100%;
|
height: 690px;
|
display: flex;
|
justify-content: center;
|
align-items: center;
|
|
.left-container {
|
width: 20%;
|
height: 100%;
|
background-color: @main-container-background;
|
border-radius: @container-border-radius;
|
padding: @container-padding;
|
margin-right: 25px;
|
display: flex;
|
flex-direction: column;
|
justify-content: space-between;
|
|
.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;
|
overflow: hidden;
|
white-space: nowrap;
|
}
|
|
.hover-icon-container {
|
// display:none的消失方式会让点击删除弹出的popover卡片无法点击空白处消失
|
opacity: 0;
|
position: absolute;
|
right: 0;
|
}
|
|
.icon-container {
|
.anticon {
|
padding: 4px;
|
font-size: 16px;
|
border-radius: 8px;
|
margin-left: 5px;
|
|
&:hover {
|
background-color: rgba(0, 0, 0, .1);
|
}
|
}
|
}
|
|
#edit-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: @single-history-hover-background;
|
.icon-container {
|
opacity: 1;
|
background-color: @single-history-hover-background;
|
}
|
}
|
|
&.single-history-active {
|
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;
|
}
|
}
|
}
|
}
|
|
.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: @main-container-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;
|
.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 {
|
margin-bottom: 5px;
|
|
/deep/ .ant-avatar, img {
|
width: 35px;
|
height: 35px;
|
line-height: 35px;
|
}
|
}
|
|
.conversation-content {
|
max-width: 80%;
|
box-shadow: @conversation-content-container-box-shadow;
|
border-radius: @container-border-radius;
|
padding: @container-padding;
|
}
|
}
|
|
&:not(:first-child) {
|
padding-top: @container-padding*2;
|
}
|
}
|
}
|
|
.input-container {
|
flex: 1;
|
background-color: @main-container-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>
|