|
@@ -1,81 +1,19 @@
|
|
<template>
|
|
<template>
|
|
<el-container class="ai-layout">
|
|
<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">
|
|
<el-container class="detail-container">
|
|
<!-- 右顶部 TODO 芋艿:右对齐 -->
|
|
<!-- 右顶部 TODO 芋艿:右对齐 -->
|
|
<el-header class="header">
|
|
<el-header class="header">
|
|
<div class="title">
|
|
<div class="title">
|
|
- {{ useConversation?.title }}
|
|
|
|
|
|
+ {{ activeConversation?.title }}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div>
|
|
<!-- TODO @fan:样式改下;这里我已经改成点击后,弹出了 -->
|
|
<!-- TODO @fan:样式改下;这里我已经改成点击后,弹出了 -->
|
|
<el-button type="primary" @click="openChatConversationUpdateForm">
|
|
<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"/>
|
|
<Icon icon="ep:setting" style="margin-left: 10px"/>
|
|
</el-button>
|
|
</el-button>
|
|
<el-button>
|
|
<el-button>
|
|
@@ -107,8 +45,6 @@
|
|
<el-text class="time">{{ formatDate(item.createTime) }}</el-text>
|
|
<el-text class="time">{{ formatDate(item.createTime) }}</el-text>
|
|
</div>
|
|
</div>
|
|
<div class="left-text-container" ref="markdownViewRef">
|
|
<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" />
|
|
<MarkdownView class="left-text" :content="item.content" />
|
|
</div>
|
|
</div>
|
|
<div class="left-btns">
|
|
<div class="left-btns">
|
|
@@ -136,7 +72,6 @@
|
|
</div>
|
|
</div>
|
|
<div class="right-text-container">
|
|
<div class="right-text-container">
|
|
<div class="right-text">{{ item.content }}</div>
|
|
<div class="right-text">{{ item.content }}</div>
|
|
-<!-- <MarkdownView class="right-text" :content="item.content" />-->
|
|
|
|
</div>
|
|
</div>
|
|
<div class="right-btns">
|
|
<div class="right-btns">
|
|
<div class="btn-cus" @click="noCopy(item.content)">
|
|
<div class="btn-cus" @click="noCopy(item.content)">
|
|
@@ -152,10 +87,6 @@
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
- <!-- 角色仓库抽屉 -->
|
|
|
|
- <el-drawer v-model="drawer" title="角色仓库" size="50%">
|
|
|
|
- <Role/>
|
|
|
|
- </el-drawer>
|
|
|
|
</el-main>
|
|
</el-main>
|
|
<el-footer class="footer-container">
|
|
<el-footer class="footer-container">
|
|
<form @submit.prevent="onSend" class="prompt-from">
|
|
<form @submit.prevent="onSend" class="prompt-from">
|
|
@@ -191,38 +122,35 @@
|
|
</form>
|
|
</form>
|
|
</el-footer>
|
|
</el-footer>
|
|
</el-container>
|
|
</el-container>
|
|
- </el-container>
|
|
|
|
|
|
|
|
- <ChatConversationUpdateForm
|
|
|
|
- ref="chatConversationUpdateFormRef"
|
|
|
|
- @success="getChatConversationList"
|
|
|
|
- />
|
|
|
|
|
|
+ <!-- ========= 额外组件 ========== -->
|
|
|
|
+ <!-- 更新对话 form -->
|
|
|
|
+ <ChatConversationUpdateForm
|
|
|
|
+ ref="chatConversationUpdateFormRef"
|
|
|
|
+ @success="handlerTitleSuccess"
|
|
|
|
+ />
|
|
|
|
+ </el-container>
|
|
</template>
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
<script setup lang="ts">
|
|
import MarkdownView from '@/components/MarkdownView/index.vue'
|
|
import MarkdownView from '@/components/MarkdownView/index.vue'
|
|
|
|
+import Conversation from './Conversation.vue'
|
|
import {ChatMessageApi, ChatMessageVO} from '@/api/ai/chat/message'
|
|
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 {formatDate} from '@/utils/formatTime'
|
|
import {useClipboard} from '@vueuse/core'
|
|
import {useClipboard} from '@vueuse/core'
|
|
|
|
+import ChatConversationUpdateForm from "@/views/ai/chat/components/ChatConversationUpdateForm.vue";
|
|
|
|
|
|
const route = useRoute() // 路由
|
|
const route = useRoute() // 路由
|
|
const message = useMessage() // 消息弹窗
|
|
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 conversationInProgress = ref(false) // 对话进行中
|
|
const conversationInAbortController = ref<any>() // 对话进行中 abort 控制器(控制 stream 对话)
|
|
const conversationInAbortController = ref<any>() // 对话进行中 abort 控制器(控制 stream 对话)
|
|
-
|
|
|
|
|
|
+const inputTimeout = ref<any>() // 处理输入中回车的定时器
|
|
const prompt = ref<string>() // prompt
|
|
const prompt = ref<string>() // prompt
|
|
|
|
|
|
// 判断 消息列表 滚动的位置(用于判断是否需要滚动到消息最下方)
|
|
// 判断 消息列表 滚动的位置(用于判断是否需要滚动到消息最下方)
|
|
@@ -231,66 +159,73 @@ const isScrolling = ref(false) //用于判断用户是否在滚动
|
|
const isComposing = ref(false) // 判断用户是否在输入
|
|
const isComposing = ref(false) // 判断用户是否在输入
|
|
|
|
|
|
/** chat message 列表 */
|
|
/** chat message 列表 */
|
|
-// defineOptions({ name: 'chatMessageList' })
|
|
|
|
const list = ref<ChatMessageVO[]>([]) // 列表的数据
|
|
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 () => {
|
|
const onSend = async () => {
|
|
// 判断用户是否在输入
|
|
// 判断用户是否在输入
|
|
if (isComposing.value) {
|
|
if (isComposing.value) {
|
|
@@ -311,21 +246,15 @@ const onSend = async () => {
|
|
// TODO 芋艿:这块交互要在优化;应该是先插入到 UI 界面,里面会有当前的消息,和正在思考中;之后发起请求;
|
|
// TODO 芋艿:这块交互要在优化;应该是先插入到 UI 界面,里面会有当前的消息,和正在思考中;之后发起请求;
|
|
// 清空输入框
|
|
// 清空输入框
|
|
prompt.value = ''
|
|
prompt.value = ''
|
|
- // const requestParams = {
|
|
|
|
- // conversationId: conversationId.value,
|
|
|
|
- // content: content
|
|
|
|
- // } as unknown as ChatMessageSendVO
|
|
|
|
- // // 添加 message
|
|
|
|
const userMessage = {
|
|
const userMessage = {
|
|
- conversationId: conversationId.value,
|
|
|
|
|
|
+ conversationId: activeConversationId.value,
|
|
content: content
|
|
content: content
|
|
} as ChatMessageVO
|
|
} as ChatMessageVO
|
|
// list.value.push(userMessage)
|
|
// list.value.push(userMessage)
|
|
- // // 滚动到住下面
|
|
|
|
- // scrollToBottom()
|
|
|
|
|
|
+ // 滚动到住下面
|
|
|
|
+ scrollToBottom()
|
|
// stream
|
|
// stream
|
|
await doSendStream(userMessage)
|
|
await doSendStream(userMessage)
|
|
- //
|
|
|
|
}
|
|
}
|
|
|
|
|
|
const doSendStream = async (userMessage: ChatMessageVO) => {
|
|
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 {
|
|
try {
|
|
- if (conversationId.value === null) {
|
|
|
|
|
|
+ if (activeConversationId.value === null) {
|
|
return
|
|
return
|
|
}
|
|
}
|
|
// 获取列表数据
|
|
// 获取列表数据
|
|
- const res = await ChatMessageApi.messageList(conversationId.value)
|
|
|
|
- list.value = res
|
|
|
|
-
|
|
|
|
|
|
+ list.value = await ChatMessageApi.messageList(activeConversationId.value)
|
|
// 滚动到最下面
|
|
// 滚动到最下面
|
|
- scrollToBottom()
|
|
|
|
|
|
+ await nextTick(() => {
|
|
|
|
+ scrollToBottom()
|
|
|
|
+ })
|
|
} finally {
|
|
} 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) {
|
|
function noCopy(content) {
|
|
copy(content)
|
|
copy(content)
|
|
ElMessage({
|
|
ElMessage({
|
|
@@ -445,186 +361,57 @@ const onDelete = async (id) => {
|
|
type: 'success'
|
|
type: 'success'
|
|
})
|
|
})
|
|
// tip:如果 stream 进行中的 message,就需要调用 controller 结束
|
|
// tip:如果 stream 进行中的 message,就需要调用 controller 结束
|
|
- stopStream()
|
|
|
|
|
|
+ await stopStream()
|
|
// 重新获取 message 列表
|
|
// 重新获取 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 chatConversationUpdateFormRef = ref()
|
|
const openChatConversationUpdateForm = async () => {
|
|
const openChatConversationUpdateForm = async () => {
|
|
- chatConversationUpdateFormRef.value.open(conversationId.value)
|
|
|
|
|
|
+ chatConversationUpdateFormRef.value.open(activeConversationId.value)
|
|
}
|
|
}
|
|
|
|
|
|
-// 输入
|
|
|
|
-const onCompositionstart = () => {
|
|
|
|
- console.log('onCompositionstart。。。.')
|
|
|
|
- isComposing.value = true
|
|
|
|
-}
|
|
|
|
|
|
|
|
-const onCompositionend = () => {
|
|
|
|
- // console.log('输入结束...')
|
|
|
|
- setTimeout(() => {
|
|
|
|
- console.log('输入结束...')
|
|
|
|
- isComposing.value = false
|
|
|
|
- }, 200)
|
|
|
|
|
|
+/**
|
|
|
|
+ * 对话 - 标题修改成功
|
|
|
|
+ */
|
|
|
|
+const handlerTitleSuccess = async () => {
|
|
|
|
+ // TODO 需要刷新 对话列表
|
|
}
|
|
}
|
|
|
|
|
|
-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
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-
|
|
|
|
-// 对话点击
|
|
|
|
-const handleConversationClick = async (id: number) => {
|
|
|
|
- // 切换对话
|
|
|
|
- conversationId.value = id
|
|
|
|
- console.log('conversationId.value', conversationId.value)
|
|
|
|
- // 获取列表数据
|
|
|
|
- await messageList()
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-// 角色仓库
|
|
|
|
-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 () => {
|
|
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();
|
|
// scrollToBottom();
|
|
// await nextTick
|
|
// await nextTick
|
|
// 监听滚动事件,判断用户滚动状态
|
|
// 监听滚动事件,判断用户滚动状态
|
|
@@ -642,6 +429,7 @@ onMounted(async () => {
|
|
})
|
|
})
|
|
})
|
|
})
|
|
</script>
|
|
</script>
|
|
|
|
+
|
|
<style lang="scss" scoped>
|
|
<style lang="scss" scoped>
|
|
.ai-layout {
|
|
.ai-layout {
|
|
// TODO @范 这里height不能 100% 先这样临时处理
|
|
// TODO @范 这里height不能 100% 先这样临时处理
|