【优化】Ai 对话解耦
parent
91593d5d40
commit
482ce62370
|
@ -0,0 +1,393 @@
|
|||
<!-- AI 对话 -->
|
||||
<template>
|
||||
<el-aside width="260px" class="conversation-container">
|
||||
|
||||
<!-- 左顶部:对话 -->
|
||||
<div>
|
||||
<el-button class="w-1/1 btn-new-conversation" type="primary" @click="createConversation">
|
||||
<Icon icon="ep:plus" class="mr-5px"/>
|
||||
新建对话
|
||||
</el-button>
|
||||
|
||||
<!-- 左顶部:搜索对话 -->
|
||||
<el-input
|
||||
v-model="searchName"
|
||||
size="large"
|
||||
class="mt-10px search-input"
|
||||
placeholder="搜索历史记录"
|
||||
@keyup="searchConversation"
|
||||
>
|
||||
<template #prefix>
|
||||
<Icon icon="ep:search"/>
|
||||
</template>
|
||||
</el-input>
|
||||
|
||||
<!-- 左中间:对话列表 -->
|
||||
<div class="conversation-list">
|
||||
<!-- TODO @fain:置顶、聊天记录、一星期钱、30天前,前端对数据重新做一下分组,或者后端接口改一下 -->
|
||||
<div v-for="conversationKey in Object.keys(conversationMap)" :key="conversationKey">
|
||||
<div v-if="conversationMap[conversationKey].length">
|
||||
<el-text class="mx-1" size="small" tag="b">{{ conversationKey }}</el-text>
|
||||
</div>
|
||||
<el-row
|
||||
v-for="conversation in conversationMap[conversationKey]"
|
||||
:key="conversation.id"
|
||||
@click="handleConversationClick(conversation.id)">
|
||||
<div
|
||||
:class="conversation.id === activeConversationId ? 'conversation active' : 'conversation'"
|
||||
>
|
||||
<div class="title-wrapper">
|
||||
<img class="avatar" :src="conversation.roleAvatar"/>
|
||||
<span class="title">{{ conversation.title }}</span>
|
||||
</div>
|
||||
<!-- TODO @fan:缺一个【置顶】按钮,效果改成 hover 上去展示 -->
|
||||
<div class="button-wrapper">
|
||||
<el-icon title="编辑" @click="updateConversationTitle(conversation)">
|
||||
<Icon icon="ep:edit"/>
|
||||
</el-icon>
|
||||
<el-icon title="删除会话" @click="deleteChatConversation(conversation)">
|
||||
<Icon icon="ep:delete"/>
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 左底部:工具栏 -->
|
||||
<div class="tool-box">
|
||||
<div @click="handleRoleRepository">
|
||||
<Icon icon="ep:user"/>
|
||||
<el-text size="small">角色仓库</el-text>
|
||||
</div>
|
||||
<div @click="handleClearConversation">
|
||||
<Icon icon="ep:delete"/>
|
||||
<el-text size="small">清空未置顶对话</el-text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============= 额外组件 ============= -->
|
||||
|
||||
<!-- 角色仓库抽屉 -->
|
||||
<el-drawer v-model="drawer" title="角色仓库" size="50%">
|
||||
<Role/>
|
||||
</el-drawer>
|
||||
|
||||
</el-aside>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ChatConversationApi, ChatConversationVO} from '@/api/ai/chat/conversation'
|
||||
import {ref} from "vue";
|
||||
import ChatConversationUpdateForm from "@/views/ai/chat/components/ChatConversationUpdateForm.vue";
|
||||
import Role from "@/views/ai/chat/role/index.vue";
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
// 定义属性
|
||||
const searchName = ref<string>('') // 对话搜索
|
||||
const activeConversationId = ref<number | null>(null) // 选中的对话,默认为 null
|
||||
const conversationList = ref([] as ChatConversationVO[]) // 对话列表
|
||||
const conversationMap = ref<any>({}) // 对话分组 (置顶、今天、三天前、一星期前、一个月前)
|
||||
const drawer = ref<boolean>(false) // 角色仓库抽屉
|
||||
|
||||
// 定义组件 props
|
||||
const props = defineProps({
|
||||
activeId: {
|
||||
type: Number || null,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
// 定义钩子
|
||||
const emits = defineEmits(['onConversationClick', 'onConversationClear'])
|
||||
|
||||
/**
|
||||
* 对话 - 搜索
|
||||
*/
|
||||
const searchConversation = () => {
|
||||
// TODO fan:待实现
|
||||
}
|
||||
|
||||
/**
|
||||
* 对话 - 点击
|
||||
*/
|
||||
const handleConversationClick = async (id: number) => {
|
||||
// 切换对话
|
||||
activeConversationId.value = id
|
||||
|
||||
const filterConversation = conversationList.value.filter(item => {
|
||||
return item.id !== id
|
||||
})
|
||||
// 回调 onConversationClick
|
||||
emits('onConversationClick', filterConversation[0])
|
||||
}
|
||||
|
||||
/**
|
||||
* 对话 - 获取列表
|
||||
*/
|
||||
const getChatConversationList = async () => {
|
||||
// 1、获取 对话数据
|
||||
conversationList.value = await ChatConversationApi.getChatConversationMyList()
|
||||
// 2、没有 任何对话情况
|
||||
if (conversationList.value.length === 0) {
|
||||
activeConversationId.value = null
|
||||
conversationMap.value = {}
|
||||
return
|
||||
}
|
||||
// 3、对话根据时间分组(置顶、今天、一天前、三天前、七天前、30天前)
|
||||
conversationMap.value = await conversationTimeGroup(conversationList.value)
|
||||
}
|
||||
|
||||
const conversationTimeGroup = async (list: ChatConversationVO[]) => {
|
||||
// 排序、指定、时间分组(今天、一天前、三天前、七天前、30天前)
|
||||
const groupMap = {
|
||||
'置顶': [],
|
||||
'今天': [],
|
||||
'一天前': [],
|
||||
'三天前': [],
|
||||
'七天前': [],
|
||||
'三十天前': []
|
||||
}
|
||||
// 当前时间的时间戳
|
||||
const now = Date.now();
|
||||
// 定义时间间隔常量(单位:毫秒)
|
||||
const oneDay = 24 * 60 * 60 * 1000;
|
||||
const threeDays = 3 * oneDay;
|
||||
const sevenDays = 7 * oneDay;
|
||||
const thirtyDays = 30 * oneDay;
|
||||
console.log('listlistlist', list)
|
||||
for (const conversation: ChatConversationVO of list) {
|
||||
// 置顶
|
||||
if (conversation.pinned) {
|
||||
groupMap['置顶'].push(conversation)
|
||||
continue
|
||||
}
|
||||
// 计算时间差(单位:毫秒)
|
||||
const diff = now - conversation.updateTime;
|
||||
// 根据时间间隔判断
|
||||
if (diff < oneDay) {
|
||||
groupMap['今天'].push(conversation)
|
||||
} else if (diff < threeDays) {
|
||||
groupMap['一天前'].push(conversation)
|
||||
} else if (diff < sevenDays) {
|
||||
groupMap['三天前'].push(conversation)
|
||||
} else if (diff < thirtyDays) {
|
||||
groupMap['七天前'].push(conversation)
|
||||
} else {
|
||||
groupMap['三十天前'].push(conversation)
|
||||
}
|
||||
}
|
||||
return groupMap
|
||||
}
|
||||
|
||||
/**
|
||||
* 对话 - 新建
|
||||
*/
|
||||
const createConversation = async () => {
|
||||
// 1、新建对话
|
||||
const conversationId = await ChatConversationApi.createChatConversationMy(
|
||||
{} as unknown as ChatConversationVO
|
||||
)
|
||||
// 2、选中对话
|
||||
await handleConversationClick(conversationId)
|
||||
// 3、获取对话内容
|
||||
await getChatConversationList()
|
||||
}
|
||||
|
||||
/**
|
||||
* 对话 - 更新标题
|
||||
*/
|
||||
const updateConversationTitle = async (conversation: ChatConversationVO) => {
|
||||
// 1、二次确认
|
||||
const {value} = await ElMessageBox.prompt('修改标题', {
|
||||
inputPattern: /^[\s\S]*.*\S[\s\S]*$/, // 判断非空,且非空格
|
||||
inputErrorMessage: '标题不能为空',
|
||||
inputValue: conversation.title
|
||||
})
|
||||
// 2、发起修改
|
||||
await ChatConversationApi.updateChatConversationMy({
|
||||
id: conversation.id,
|
||||
title: value
|
||||
} as ChatConversationVO)
|
||||
message.success('重命名成功')
|
||||
// 刷新列表
|
||||
await getChatConversationList()
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除聊天会话
|
||||
*/
|
||||
const deleteChatConversation = async (conversation: ChatConversationVO) => {
|
||||
try {
|
||||
// 删除的二次确认
|
||||
await message.delConfirm(`是否确认删除会话 - ${conversation.title}?`)
|
||||
// 发起删除
|
||||
await ChatConversationApi.deleteChatConversationMy(conversation.id)
|
||||
message.success('会话已删除')
|
||||
// 刷新列表
|
||||
await getChatConversationList()
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============ 角色仓库
|
||||
|
||||
|
||||
/**
|
||||
* 角色仓库抽屉
|
||||
*/
|
||||
const handleRoleRepository = async () => {
|
||||
drawer.value = !drawer.value
|
||||
}
|
||||
|
||||
// ============= 清空对话
|
||||
|
||||
|
||||
/**
|
||||
* 清空对话
|
||||
*/
|
||||
const handleClearConversation = async () => {
|
||||
ElMessageBox.confirm(
|
||||
'确认后对话会全部清空,置顶的对话除外。',
|
||||
'确认提示',
|
||||
{
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
.then(async () => {
|
||||
await ChatConversationApi.deleteMyAllExceptPinned()
|
||||
ElMessage({
|
||||
message: '操作成功!',
|
||||
type: 'success'
|
||||
})
|
||||
// 清空 对话 和 对话内容
|
||||
activeConversationId.value = null
|
||||
// 获取 对话列表
|
||||
await getChatConversationList()
|
||||
// 回调 方法
|
||||
emits('onConversationClear')
|
||||
})
|
||||
.catch(() => {
|
||||
})
|
||||
}
|
||||
|
||||
// ============ 组件 onMounted
|
||||
|
||||
onMounted(async () => {
|
||||
//
|
||||
if (props.activeId != null) {
|
||||
|
||||
}
|
||||
// 获取 对话列表
|
||||
await getChatConversationList()
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.conversation-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 0 10px;
|
||||
padding-top: 10px;
|
||||
|
||||
.btn-new-conversation {
|
||||
padding: 18px 0;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.conversation-list {
|
||||
margin-top: 20px;
|
||||
|
||||
.conversation {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
flex: 1;
|
||||
padding: 0 5px;
|
||||
margin-top: 10px;
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
align-items: center;
|
||||
line-height: 30px;
|
||||
|
||||
&.active {
|
||||
background-color: #e6e6e6;
|
||||
|
||||
.button {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.title-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
padding: 5px 10px;
|
||||
max-width: 220px;
|
||||
font-size: 14px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
// 对话编辑、删除
|
||||
.button-wrapper {
|
||||
right: 2px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-items: center;
|
||||
color: #606266;
|
||||
|
||||
.el-icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 角色仓库、清空未设置对话
|
||||
.tool-box {
|
||||
line-height: 35px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: var(--el-text-color);
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #606266;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
|
||||
> span {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,81 +1,19 @@
|
|||
<template>
|
||||
<el-container class="ai-layout">
|
||||
<!-- 左侧:会话列表 -->
|
||||
<el-aside width="260px" class="conversation-container">
|
||||
<div>
|
||||
<!-- 左顶部:新建对话 -->
|
||||
<el-button class="w-1/1 btn-new-conversation" type="primary" @click="createConversation">
|
||||
<Icon icon="ep:plus" class="mr-5px"/>
|
||||
新建对话
|
||||
</el-button>
|
||||
<!-- 左顶部:搜索对话 -->
|
||||
<el-input
|
||||
v-model="searchName"
|
||||
size="large"
|
||||
class="mt-10px search-input"
|
||||
placeholder="搜索历史记录"
|
||||
@keyup="searchConversation"
|
||||
>
|
||||
<template #prefix>
|
||||
<Icon icon="ep:search"/>
|
||||
</template>
|
||||
</el-input>
|
||||
<!-- 左中间:对话列表 -->
|
||||
<div class="conversation-list">
|
||||
<!-- TODO @fain:置顶、聊天记录、一星期钱、30天前,前端对数据重新做一下分组,或者后端接口改一下 -->
|
||||
<div v-for="conversationKey in Object.keys(conversationMap)" :key="conversationKey" >
|
||||
<div v-if="conversationMap[conversationKey].length">
|
||||
<el-text class="mx-1" size="small" tag="b">{{conversationKey}}</el-text>
|
||||
</div>
|
||||
<el-row
|
||||
v-for="conversation in conversationMap[conversationKey]"
|
||||
:key="conversation.id"
|
||||
@click="handleConversationClick(conversation.id)">
|
||||
<div
|
||||
:class="conversation.id === conversationId ? 'conversation active' : 'conversation'"
|
||||
@click="changeConversation(conversation.id)"
|
||||
>
|
||||
<div class="title-wrapper">
|
||||
<img class="avatar" :src="conversation.roleAvatar"/>
|
||||
<span class="title">{{ conversation.title }}</span>
|
||||
</div>
|
||||
<!-- TODO @fan:缺一个【置顶】按钮,效果改成 hover 上去展示 -->
|
||||
<div class="button-wrapper">
|
||||
<el-icon title="编辑" @click="updateConversationTitle(conversation)">
|
||||
<Icon icon="ep:edit"/>
|
||||
</el-icon>
|
||||
<el-icon title="删除会话" @click="deleteChatConversation(conversation)">
|
||||
<Icon icon="ep:delete"/>
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 左底部:工具栏 -->
|
||||
<div class="tool-box">
|
||||
<div @click="handleRoleRepository">
|
||||
<Icon icon="ep:user"/>
|
||||
<el-text size="small">角色仓库</el-text>
|
||||
</div>
|
||||
<div @click="handleClearConversation">
|
||||
<Icon icon="ep:delete"/>
|
||||
<el-text size="small">清空未置顶对话</el-text>
|
||||
</div>
|
||||
</div>
|
||||
</el-aside>
|
||||
<Conversation @onConversationClick="handleConversationClick"
|
||||
@onConversationClear="handlerConversationClear" />
|
||||
<!-- 右侧:会话详情 -->
|
||||
<el-container class="detail-container">
|
||||
<!-- 右顶部 TODO 芋艿:右对齐 -->
|
||||
<el-header class="header">
|
||||
<div class="title">
|
||||
{{ useConversation?.title }}
|
||||
{{ activeConversation?.title }}
|
||||
</div>
|
||||
<div>
|
||||
<!-- TODO @fan:样式改下;这里我已经改成点击后,弹出了 -->
|
||||
<el-button type="primary" @click="openChatConversationUpdateForm">
|
||||
<span v-html="useConversation?.modelName"></span>
|
||||
<span v-html="activeConversation?.modelName"></span>
|
||||
<Icon icon="ep:setting" style="margin-left: 10px"/>
|
||||
</el-button>
|
||||
<el-button>
|
||||
|
@ -107,8 +45,6 @@
|
|||
<el-text class="time">{{ formatDate(item.createTime) }}</el-text>
|
||||
</div>
|
||||
<div class="left-text-container" ref="markdownViewRef">
|
||||
<!-- <div class="left-text markdown-view" v-html="item.content"></div>-->
|
||||
<!-- <mdPreview :content="item.content" :delay="false" />-->
|
||||
<MarkdownView class="left-text" :content="item.content" />
|
||||
</div>
|
||||
<div class="left-btns">
|
||||
|
@ -136,7 +72,6 @@
|
|||
</div>
|
||||
<div class="right-text-container">
|
||||
<div class="right-text">{{ item.content }}</div>
|
||||
<!-- <MarkdownView class="right-text" :content="item.content" />-->
|
||||
</div>
|
||||
<div class="right-btns">
|
||||
<div class="btn-cus" @click="noCopy(item.content)">
|
||||
|
@ -152,10 +87,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 角色仓库抽屉 -->
|
||||
<el-drawer v-model="drawer" title="角色仓库" size="50%">
|
||||
<Role/>
|
||||
</el-drawer>
|
||||
</el-main>
|
||||
<el-footer class="footer-container">
|
||||
<form @submit.prevent="onSend" class="prompt-from">
|
||||
|
@ -191,38 +122,35 @@
|
|||
</form>
|
||||
</el-footer>
|
||||
</el-container>
|
||||
</el-container>
|
||||
|
||||
<ChatConversationUpdateForm
|
||||
ref="chatConversationUpdateFormRef"
|
||||
@success="getChatConversationList"
|
||||
/>
|
||||
<!-- ========= 额外组件 ========== -->
|
||||
<!-- 更新对话 form -->
|
||||
<ChatConversationUpdateForm
|
||||
ref="chatConversationUpdateFormRef"
|
||||
@success="handlerTitleSuccess"
|
||||
/>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import MarkdownView from '@/components/MarkdownView/index.vue'
|
||||
import Conversation from './Conversation.vue'
|
||||
import {ChatMessageApi, ChatMessageVO} from '@/api/ai/chat/message'
|
||||
import {ChatConversationApi, ChatConversationVO} from '@/api/ai/chat/conversation'
|
||||
import ChatConversationUpdateForm from './components/ChatConversationUpdateForm.vue'
|
||||
import Role from '@/views/ai/chat/role/index.vue'
|
||||
import {ChatConversationVO} from '@/api/ai/chat/conversation'
|
||||
import {formatDate} from '@/utils/formatTime'
|
||||
import {useClipboard} from '@vueuse/core'
|
||||
import ChatConversationUpdateForm from "@/views/ai/chat/components/ChatConversationUpdateForm.vue";
|
||||
|
||||
const route = useRoute() // 路由
|
||||
const message = useMessage() // 消息弹窗
|
||||
const {copy} = useClipboard() // 初始化 copy 到粘贴板
|
||||
|
||||
const conversationList = ref([] as ChatConversationVO[])
|
||||
const conversationMap = ref<any>({})
|
||||
// 初始化 copy 到粘贴板
|
||||
const {copy} = useClipboard()
|
||||
|
||||
const drawer = ref<boolean>(false) // 角色仓库抽屉
|
||||
const searchName = ref('') // 查询的内容
|
||||
const inputTimeout = ref<any>() // 处理输入中回车的定时器
|
||||
const conversationId = ref<number | null>(null) // 选中的对话编号
|
||||
// ref 属性定义
|
||||
const activeConversationId = ref<number | null>(null) // 选中的对话编号
|
||||
const activeConversation = ref<ChatConversationVO | null>(null) // 选中的 Conversation
|
||||
const conversationInProgress = ref(false) // 对话进行中
|
||||
const conversationInAbortController = ref<any>() // 对话进行中 abort 控制器(控制 stream 对话)
|
||||
|
||||
const inputTimeout = ref<any>() // 处理输入中回车的定时器
|
||||
const prompt = ref<string>() // prompt
|
||||
|
||||
// 判断 消息列表 滚动的位置(用于判断是否需要滚动到消息最下方)
|
||||
|
@ -231,66 +159,73 @@ const isScrolling = ref(false) //用于判断用户是否在滚动
|
|||
const isComposing = ref(false) // 判断用户是否在输入
|
||||
|
||||
/** chat message 列表 */
|
||||
// defineOptions({ name: 'chatMessageList' })
|
||||
const list = ref<ChatMessageVO[]>([]) // 列表的数据
|
||||
const useConversation = ref<ChatConversationVO | null>(null) // 使用的 Conversation
|
||||
|
||||
/** 新建对话 */
|
||||
const createConversation = async () => {
|
||||
// 新建对话
|
||||
const conversationId = await ChatConversationApi.createChatConversationMy(
|
||||
{} as unknown as ChatConversationVO
|
||||
)
|
||||
changeConversation(conversationId)
|
||||
// 刷新对话列表
|
||||
await getChatConversationList()
|
||||
}
|
||||
// ============ 处理对话滚动 ==============
|
||||
|
||||
const changeConversation = (id: number) => {
|
||||
// 切换对话
|
||||
conversationId.value = id
|
||||
// TODO 芋艿:待实现
|
||||
// 刷新 message 列表
|
||||
messageList()
|
||||
}
|
||||
|
||||
/** 更新聊天会话的标题 */
|
||||
const updateConversationTitle = async (conversation: ChatConversationVO) => {
|
||||
// 二次确认
|
||||
const {value} = await ElMessageBox.prompt('修改标题', {
|
||||
inputPattern: /^[\s\S]*.*\S[\s\S]*$/, // 判断非空,且非空格
|
||||
inputErrorMessage: '标题不能为空',
|
||||
inputValue: conversation.title
|
||||
function scrollToBottom() {
|
||||
nextTick(() => {
|
||||
//注意要使用nexttick以免获取不到dom
|
||||
console.log('isScrolling.value', isScrolling.value)
|
||||
if (!isScrolling.value) {
|
||||
messageContainer.value.scrollTop =
|
||||
messageContainer.value.scrollHeight - messageContainer.value.offsetHeight
|
||||
}
|
||||
})
|
||||
// 发起修改
|
||||
await ChatConversationApi.updateChatConversationMy({
|
||||
id: conversation.id,
|
||||
title: value
|
||||
} as ChatConversationVO)
|
||||
message.success('重命名成功')
|
||||
// 刷新列表
|
||||
await getChatConversationList()
|
||||
}
|
||||
|
||||
/** 删除聊天会话 */
|
||||
const deleteChatConversation = async (conversation: ChatConversationVO) => {
|
||||
try {
|
||||
// 删除的二次确认
|
||||
await message.delConfirm(`是否确认删除会话 - ${conversation.title}?`)
|
||||
// 发起删除
|
||||
await ChatConversationApi.deleteChatConversationMy(conversation.id)
|
||||
message.success('会话已删除')
|
||||
// 刷新列表
|
||||
await getChatConversationList()
|
||||
} catch {
|
||||
function handleScroll() {
|
||||
const scrollContainer = messageContainer.value
|
||||
const scrollTop = scrollContainer.scrollTop
|
||||
const scrollHeight = scrollContainer.scrollHeight
|
||||
const offsetHeight = scrollContainer.offsetHeight
|
||||
|
||||
if (scrollTop + offsetHeight < scrollHeight) {
|
||||
// 用户开始滚动并在最底部之上,取消保持在最底部的效果
|
||||
isScrolling.value = true
|
||||
} else {
|
||||
// 用户停止滚动并滚动到最底部,开启保持到最底部的效果
|
||||
isScrolling.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const searchConversation = () => {
|
||||
// TODO fan:待实现
|
||||
// ============= 处理聊天输入回车发送 =============
|
||||
|
||||
const onCompositionstart = () => {
|
||||
isComposing.value = true
|
||||
}
|
||||
|
||||
/** send */
|
||||
const onCompositionend = () => {
|
||||
// console.log('输入结束...')
|
||||
setTimeout(() => {
|
||||
isComposing.value = false
|
||||
}, 200)
|
||||
}
|
||||
|
||||
const onPromptInput = (event) => {
|
||||
// 非输入法 输入设置为 true
|
||||
if (!isComposing.value) {
|
||||
// 回车 event data 是 null
|
||||
if (event.data == null) {
|
||||
return
|
||||
}
|
||||
isComposing.value = true
|
||||
}
|
||||
// 清理定时器
|
||||
if (inputTimeout.value) {
|
||||
clearTimeout(inputTimeout.value)
|
||||
}
|
||||
// 重置定时器
|
||||
inputTimeout.value = setTimeout(() => {
|
||||
isComposing.value = false
|
||||
}, 400)
|
||||
}
|
||||
|
||||
// ============== 对话消息相关 =================
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
*/
|
||||
const onSend = async () => {
|
||||
// 判断用户是否在输入
|
||||
if (isComposing.value) {
|
||||
|
@ -311,21 +246,15 @@ const onSend = async () => {
|
|||
// TODO 芋艿:这块交互要在优化;应该是先插入到 UI 界面,里面会有当前的消息,和正在思考中;之后发起请求;
|
||||
// 清空输入框
|
||||
prompt.value = ''
|
||||
// const requestParams = {
|
||||
// conversationId: conversationId.value,
|
||||
// content: content
|
||||
// } as unknown as ChatMessageSendVO
|
||||
// // 添加 message
|
||||
const userMessage = {
|
||||
conversationId: conversationId.value,
|
||||
conversationId: activeConversationId.value,
|
||||
content: content
|
||||
} as ChatMessageVO
|
||||
// list.value.push(userMessage)
|
||||
// // 滚动到住下面
|
||||
// scrollToBottom()
|
||||
// 滚动到住下面
|
||||
scrollToBottom()
|
||||
// stream
|
||||
await doSendStream(userMessage)
|
||||
//
|
||||
}
|
||||
|
||||
const doSendStream = async (userMessage: ChatMessageVO) => {
|
||||
|
@ -387,48 +316,35 @@ const doSendStream = async (userMessage: ChatMessageVO) => {
|
|||
}
|
||||
}
|
||||
|
||||
/** 查询列表 */
|
||||
const messageList = async () => {
|
||||
const stopStream = async () => {
|
||||
// tip:如果 stream 进行中的 message,就需要调用 controller 结束
|
||||
if (conversationInAbortController.value) {
|
||||
conversationInAbortController.value.abort()
|
||||
}
|
||||
// 设置为 false
|
||||
conversationInProgress.value = false
|
||||
}
|
||||
|
||||
// ============== message 数据 =================
|
||||
|
||||
/**
|
||||
* 获取 - message 列表
|
||||
*/
|
||||
const getMessageList = async () => {
|
||||
try {
|
||||
if (conversationId.value === null) {
|
||||
if (activeConversationId.value === null) {
|
||||
return
|
||||
}
|
||||
// 获取列表数据
|
||||
const res = await ChatMessageApi.messageList(conversationId.value)
|
||||
list.value = res
|
||||
|
||||
list.value = await ChatMessageApi.messageList(activeConversationId.value)
|
||||
// 滚动到最下面
|
||||
scrollToBottom()
|
||||
await nextTick(() => {
|
||||
scrollToBottom()
|
||||
})
|
||||
} finally {
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
nextTick(() => {
|
||||
//注意要使用nexttick以免获取不到dom
|
||||
console.log('isScrolling.value', isScrolling.value)
|
||||
if (!isScrolling.value) {
|
||||
messageContainer.value.scrollTop =
|
||||
messageContainer.value.scrollHeight - messageContainer.value.offsetHeight
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleScroll() {
|
||||
const scrollContainer = messageContainer.value
|
||||
const scrollTop = scrollContainer.scrollTop
|
||||
const scrollHeight = scrollContainer.scrollHeight
|
||||
const offsetHeight = scrollContainer.offsetHeight
|
||||
|
||||
if (scrollTop + offsetHeight < scrollHeight) {
|
||||
// 用户开始滚动并在最底部之上,取消保持在最底部的效果
|
||||
isScrolling.value = true
|
||||
} else {
|
||||
// 用户停止滚动并滚动到最底部,开启保持到最底部的效果
|
||||
isScrolling.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function noCopy(content) {
|
||||
copy(content)
|
||||
ElMessage({
|
||||
|
@ -445,186 +361,57 @@ const onDelete = async (id) => {
|
|||
type: 'success'
|
||||
})
|
||||
// tip:如果 stream 进行中的 message,就需要调用 controller 结束
|
||||
stopStream()
|
||||
await stopStream()
|
||||
// 重新获取 message 列表
|
||||
await messageList()
|
||||
await getMessageList()
|
||||
}
|
||||
|
||||
const stopStream = async () => {
|
||||
// tip:如果 stream 进行中的 message,就需要调用 controller 结束
|
||||
if (conversationInAbortController.value) {
|
||||
conversationInAbortController.value.abort()
|
||||
}
|
||||
// 设置为 false
|
||||
conversationInProgress.value = false
|
||||
}
|
||||
|
||||
/** 修改聊天会话 */
|
||||
const chatConversationUpdateFormRef = ref()
|
||||
const openChatConversationUpdateForm = async () => {
|
||||
chatConversationUpdateFormRef.value.open(conversationId.value)
|
||||
}
|
||||
|
||||
// 输入
|
||||
const onCompositionstart = () => {
|
||||
console.log('onCompositionstart。。。.')
|
||||
isComposing.value = true
|
||||
}
|
||||
|
||||
const onCompositionend = () => {
|
||||
// console.log('输入结束...')
|
||||
setTimeout(() => {
|
||||
console.log('输入结束...')
|
||||
isComposing.value = false
|
||||
}, 200)
|
||||
}
|
||||
|
||||
const onPromptInput = (event) => {
|
||||
// 非输入法 输入设置为 true
|
||||
if (!isComposing.value) {
|
||||
// 回车 event data 是 null
|
||||
if (event.data == null) {
|
||||
return
|
||||
}
|
||||
console.log('setTimeout 输入开始...')
|
||||
isComposing.value = true
|
||||
}
|
||||
// 清理定时器
|
||||
if (inputTimeout.value) {
|
||||
clearTimeout(inputTimeout.value)
|
||||
}
|
||||
// 重置定时器
|
||||
inputTimeout.value = setTimeout(() => {
|
||||
console.log('setTimeout 输入结束...')
|
||||
isComposing.value = false
|
||||
}, 400)
|
||||
}
|
||||
|
||||
const getConversation = async (conversationId: number | null) => {
|
||||
if (!conversationId) {
|
||||
return
|
||||
}
|
||||
// 获取对话信息
|
||||
useConversation.value = await ChatConversationApi.getChatConversationMy(conversationId)
|
||||
console.log('useConversation.value', useConversation.value)
|
||||
}
|
||||
|
||||
/** 获得聊天会话列表 */
|
||||
const getChatConversationList = async () => {
|
||||
conversationList.value = await ChatConversationApi.getChatConversationMyList()
|
||||
// 默认选中第一条
|
||||
if (conversationList.value.length === 0) {
|
||||
conversationId.value = null
|
||||
list.value = []
|
||||
} else {
|
||||
if (conversationId.value === null) {
|
||||
conversationId.value = conversationList.value[0].id
|
||||
changeConversation(conversationList.value[0].id)
|
||||
}
|
||||
}
|
||||
// map
|
||||
const groupRes = await conversationTimeGroup(conversationList.value)
|
||||
conversationMap.value = groupRes
|
||||
}
|
||||
|
||||
const conversationTimeGroup = async (list: ChatConversationVO[]) => {
|
||||
// 排序、指定、时间分组(今天、一天前、三天前、七天前、30天前)
|
||||
const groupMap = {
|
||||
'置顶': [],
|
||||
'今天': [],
|
||||
'一天前': [],
|
||||
'三天前': [],
|
||||
'七天前': [],
|
||||
'三十天前': []
|
||||
}
|
||||
// 当前时间的时间戳
|
||||
const now = Date.now();
|
||||
// 定义时间间隔常量(单位:毫秒)
|
||||
const oneDay = 24 * 60 * 60 * 1000;
|
||||
const threeDays = 3 * oneDay;
|
||||
const sevenDays = 7 * oneDay;
|
||||
const thirtyDays = 30 * oneDay;
|
||||
console.log('listlistlist', list)
|
||||
for (const conversation: ChatConversationVO of list) {
|
||||
// 置顶
|
||||
if (conversation.pinned) {
|
||||
groupMap['置顶'].push(conversation)
|
||||
continue
|
||||
}
|
||||
// 计算时间差(单位:毫秒)
|
||||
const diff = now - conversation.updateTime;
|
||||
// 根据时间间隔判断
|
||||
if (diff < oneDay) {
|
||||
groupMap['今天'].push(conversation)
|
||||
} else if (diff < threeDays) {
|
||||
groupMap['一天前'].push(conversation)
|
||||
} else if (diff < sevenDays) {
|
||||
groupMap['三天前'].push(conversation)
|
||||
} else if (diff < thirtyDays) {
|
||||
groupMap['七天前'].push(conversation)
|
||||
} else {
|
||||
groupMap['三十天前'].push(conversation)
|
||||
}
|
||||
}
|
||||
return groupMap
|
||||
chatConversationUpdateFormRef.value.open(activeConversationId.value)
|
||||
}
|
||||
|
||||
|
||||
// 对话点击
|
||||
const handleConversationClick = async (id: number) => {
|
||||
// 切换对话
|
||||
conversationId.value = id
|
||||
console.log('conversationId.value', conversationId.value)
|
||||
// 获取列表数据
|
||||
await messageList()
|
||||
/**
|
||||
* 对话 - 标题修改成功
|
||||
*/
|
||||
const handlerTitleSuccess = async () => {
|
||||
// TODO 需要刷新 对话列表
|
||||
}
|
||||
|
||||
// 角色仓库
|
||||
const handleRoleRepository = async () => {
|
||||
drawer.value = !drawer.value
|
||||
/**
|
||||
* 对话 - 点击
|
||||
*/
|
||||
const handleConversationClick = async (conversation: ChatConversationVO) => {
|
||||
// 更新选中的对话 id
|
||||
activeConversationId.value = conversation.id
|
||||
// 刷新 message 列表
|
||||
await getMessageList()
|
||||
}
|
||||
|
||||
// 清空对话
|
||||
const handleClearConversation = async () => {
|
||||
ElMessageBox.confirm(
|
||||
'确认后对话会全部清空,置顶的对话除外。',
|
||||
'确认提示',
|
||||
{
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
)
|
||||
.then(async () => {
|
||||
await ChatConversationApi.deleteMyAllExceptPinned()
|
||||
ElMessage({
|
||||
message: '操作成功!',
|
||||
type: 'success'
|
||||
})
|
||||
// 清空选中的对话
|
||||
useConversation.value = null
|
||||
conversationId.value = null
|
||||
list.value = []
|
||||
// 获得聊天会话列表
|
||||
await getChatConversationList()
|
||||
})
|
||||
.catch(() => {
|
||||
})
|
||||
/**
|
||||
* 对话 - 清理全部对话
|
||||
*/
|
||||
const handlerConversationClear = async ()=> {
|
||||
activeConversationId.value = null
|
||||
activeConversation.value = null
|
||||
list.value = []
|
||||
}
|
||||
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(async () => {
|
||||
// 设置当前对话
|
||||
if (route.query.conversationId) {
|
||||
conversationId.value = route.query.conversationId as number
|
||||
}
|
||||
// 设置当前对话 TODO 角色仓库过来的,自带 conversationId 需要选中
|
||||
// if (route.query.conversationId) {
|
||||
// conversationId.value = route.query.conversationId as number
|
||||
// }
|
||||
// 获得聊天会话列表
|
||||
await getChatConversationList()
|
||||
// await getChatConversationList()
|
||||
// 获取对话信息
|
||||
await getConversation(conversationId.value)
|
||||
// await getConversation(conversationId.value)
|
||||
// 获取列表数据
|
||||
await messageList()
|
||||
await getMessageList()
|
||||
// scrollToBottom();
|
||||
// await nextTick
|
||||
// 监听滚动事件,判断用户滚动状态
|
||||
|
@ -642,6 +429,7 @@ onMounted(async () => {
|
|||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ai-layout {
|
||||
// TODO @范 这里height不能 100% 先这样临时处理
|
||||
|
|
Loading…
Reference in New Issue