Эх сурвалжийг харах

【优化】Ai 对话解耦

cherishsince 1 жил өмнө
parent
commit
482ce62370

+ 393 - 0
src/views/ai/chat/Conversation.vue

@@ -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>

+ 129 - 341
src/views/ai/chat/index.vue

@@ -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% 先这样临时处理