Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions ui/src/conversation-panel/components/header/portal/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ const handleNewConversation = () => (props.newConversation ? props.newConversati

<div class="flex-1 flex-align-center gap-2">
<h4 class="truncate">{{ list.currentConversation.value?.abstract || '新建对话' }}</h4>
<el-tag type="info">
<el-tag type="info" class="max-md:hidden!">
<div class="flex-align-center gap-1">
<ApplicationIcon :icon="list.appInfo.value?.icon" :size="18" class="shrink-0" />
<span v-if="list.appInfo.value?.name" class="text-sm">{{ list.appInfo.value.name }}</span>
<span v-if="list.appInfo.value?.name" :title="list.appInfo.value.name" class="truncate text-sm">{{ list.appInfo.value.name }}</span>
</div>
</el-tag>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const conversationTime = (conversation: Conversation) => relativeTimeFormat(conv
</div>

<!-- 历史列表独立滚动,应用信息和新建入口保持固定。 -->
<el-scrollbar class="min-h-0 flex-1 mt-1">
<el-scrollbar class="min-h-0 flex-1 mt-1 -mr-4" view-class="pr-4">
<div v-if="conversations.length === 0" class="text-N600 text-center mt-4">暂无历史记录</div>
<div v-else class="space-y-1">
<template v-for="conversation in conversations" :key="conversation.id">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<script setup lang="ts">
import { ref } from 'vue'
import ConversationApi from '@/api/chat/conversation'
import type { PortalConversationSummary } from '@/api/types'
import { relativeTimeFormat } from '@/utils/time'
import { useConversationListStore } from '../conversation-list/index'
import { usePortalConversationListStore } from './index'

defineOptions({ name: 'HistoryConversationDrawer' })
const props = defineProps<{ applicationId: string }>()
const visible = defineModel<boolean>({ required: true })
const portalList = usePortalConversationListStore()
const conversationList = useConversationListStore()
const selectedApplicationId = ref(props.applicationId)
const keyword = ref('')
const conversations = ref<PortalConversationSummary[]>([])
const scrollRef = ref<{ reset: () => Promise<void> }>()

// 搜索与切换智能体从第一页重新加载,分页由共享滚动组件维护。
const loadConversations = ({ currentPage, pageSize }: { currentPage: number; pageSize: number }) =>
ConversationApi.getPortalConversationPage(selectedApplicationId.value, currentPage, pageSize, keyword.value.trim())
const handleSearch = () => scrollRef.value?.reset()
const handleApplicationChange = () => {
keyword.value = ''
return scrollRef.value?.reset()
}

// 选择历史后关闭抽屉;当前智能体复用会话切换,其他智能体通过门户导航。
const handleSelectConversation = (conversation: PortalConversationSummary) => {
if (selectedApplicationId.value === portalList.currentApplicationId.value) conversationList.selectConversation(conversation.id)
else portalList.openConversation(selectedApplicationId.value, conversation.id)
visible.value = false
}
const isActiveConversation = (conversation: PortalConversationSummary) =>
selectedApplicationId.value === portalList.currentApplicationId.value && conversation.id === conversationList.currentChatId.value

// 删除成功后同步侧栏,并重新加载抽屉,补齐当前筛选下的历史。
const handleDeleteConversation = (conversation: PortalConversationSummary) => {
const applicationId = selectedApplicationId.value
const request =
applicationId === portalList.currentApplicationId.value
? conversationList.deleteChat(conversation.id)
: portalList.deleteGroupConversation(applicationId, conversation.id)
return request.then(() => Promise.all([portalList.loadApplicationGroups(), scrollRef.value?.reset()]))
}
</script>

<template>
<MkDrawer v-model="visible" title="历史对话" :size="600" :close-on-click-modal="true" :close-on-press-escape="true">
<div class="mb-4 flex-align-center gap-3">
<!-- 切换历史对话所属智能体 -->
<el-select v-model="selectedApplicationId" class="w-50! shrink-0" @change="handleApplicationChange">
<template #label="{ label }">
<span class="flex-align-center gap-2">
<ApplicationIcon :icon="portalList.applicationGroups.value.find((group) => group.id === selectedApplicationId)?.icon" :size="18" />
<span class="truncate">{{ label }}</span>
</span>
</template>
<el-option v-for="group in portalList.applicationGroups.value" :key="group.id" :label="group.name" :value="group.id">
<span class="flex-align-center gap-2">
<ApplicationIcon :icon="group.icon" :size="18" />
{{ group.name }}
</span>
</el-option>
</el-select>
<!-- 按历史对话摘要搜索 -->
<MkSearchInput v-model="keyword" class="min-w-0 flex-1" placeholder="搜索" @change="handleSearch" />
</div>
<MkInfiniteScroll ref="scrollRef" v-model="conversations" :load="loadConversations">
<div class="space-y-1">
<!-- 打开历史对话 -->
<MkListItem
v-for="conversation in conversations"
:key="conversation.id"
:active="isActiveConversation(conversation)"
@click="handleSelectConversation(conversation)"
class="px-4!"
>
<template #default="{ active }">
<span class="min-w-0 flex-1 truncate" :title="conversation.abstract || '新对话'">{{ conversation.abstract || '新对话' }}</span>
<span class="-mr-6 shrink-0 text-sm text-N500 group-hover:invisible group-focus-within:invisible" :class="{ invisible: active }">
{{ relativeTimeFormat(conversation.update_time || conversation.create_time) }}
</span>
</template>
<template #action-dropdown>
<!-- 删除历史对话 -->
<MkDropdownItem divided @click="handleDeleteConversation(conversation)">
<template #icon><MkIcon name="icon_delete-trash_outlined" /></template>
<span>删除</span>
</MkDropdownItem>
</template>
</MkListItem>
</div>
<template #empty><MkEmpty :type="keyword ? 'search' : 'default'" :description="keyword ? '没有匹配的历史对话' : '暂无历史记录'" /></template>
</MkInfiniteScroll>
</MkDrawer>
</template>
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<script setup lang="ts">
import { ref } from 'vue'
import { debounce } from 'lodash'
import type { ChatApplicationProfile } from '@/api/types'
import PortalApplicationCards from '../../components/portal-application-cards/index.vue'
import { usePortalConversationListStore } from './index'
Expand All @@ -26,9 +25,6 @@ const loadApplications = () => {
})
}

// 输入名称时延迟搜索,避免每个字符都发起请求
const handleKeywordInput = debounce(loadApplications, 300)

// 每次打开清空搜索条件并重新查询
const open = () => {
applicationKeyword.value = ''
Expand All @@ -54,20 +50,15 @@ defineExpose({ open })
:close-on-press-escape="true"
>
<template #header>
<div class="flex w-full">
<div class="flex w-full flex-col gap-4 md:flex-row">
<h4>选择智能体</h4>
<div class="absolute-center hidden w-85 md:block">
<div class="w-full md:absolute-center md:w-85">
<!-- 搜索智能体 -->
<MkSearchInput v-model="applicationKeyword" @input="handleKeywordInput" />
<MkSearchInput v-model="applicationKeyword" @change="loadApplications" />
</div>
</div>
</template>

<div class="mb-4 md:hidden">
<!-- 移动端搜索智能体 -->
<MkSearchInput v-model="applicationKeyword" @input="handleKeywordInput" />
</div>

<div v-loading="isLoading">
<MkEmpty
v-if="!isLoading && applications.length === 0"
Expand All @@ -84,6 +75,7 @@ defineExpose({ open })
/* 移动端选择智能体标题栏 */
@media (width < 768px) {
:global(.select-application-drawer .el-drawer__header) {
align-items: flex-start;
border-bottom: none;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface PortalApplicationGroup {
name: string
icon: string
conversations: Conversation[]
count: number
expanded: boolean
}

Expand Down Expand Up @@ -78,12 +79,14 @@ export function createPortalConversationListStore(deps: PortalConversationListDe
const deleteGroupConversation = async (applicationId: string, chatId: string) => {
await removeConversation(applicationId, chatId)
const group = applicationGroups.value.find((applicationGroup) => applicationGroup.id === applicationId)
if (group) group.conversations = group.conversations.filter((conversation) => conversation.id !== chatId)
if (group) {
group.conversations = group.conversations.filter((conversation) => conversation.id !== chatId)
group.count = Math.max(0, group.count - 1)
}
}

// 按名称查询可访问的智能体,全部智能体页与选择智能体抽屉各自维护搜索条件与结果
const fetchApplications = (keyword: string) =>
pageApplications(1, APPLICATION_PAGE_SIZE, keyword.trim()).then((response) => response.records ?? [])
const fetchApplications = (keyword: string) => pageApplications(1, APPLICATION_PAGE_SIZE, keyword.trim()).then((response) => response.records ?? [])

// 全部智能体页
const applications = ref<ChatApplicationProfile[]>([])
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { relativeTimeFormat } from '@/utils/time'
import type { Conversation } from '../../core/types'
import { useConversationListStore } from '../conversation-list/index'
import HistoryConversationDrawer from './HistoryConversationDrawer.vue'
import AccountMenu from '../account-menu/index.vue'
import { usePortalConversationListStore, type PortalApplicationGroup } from './index'

Expand All @@ -11,6 +13,13 @@ const portalList = usePortalConversationListStore()
const { applicationGroups, currentApplicationId, isApplicationListActive, toggleGroup } = portalList
const { conversations, currentChatId, selectConversation, deleteChat, toggleLeftSide } = useConversationListStore()

const historyApplicationId = ref('')
const historyVisible = ref(false)
const handleOpenHistory = (group: PortalApplicationGroup) => {
historyApplicationId.value = group.id
historyVisible.value = true
}

// 每个智能体最多展示最近 5 条会话
const RECENT_CONVERSATION_LIMIT = 5

Expand All @@ -20,14 +29,11 @@ const isCurrentApplication = (group: PortalApplicationGroup) => group.id === cur
const groupConversations = (group: PortalApplicationGroup) =>
(isCurrentApplication(group) ? conversations.value : group.conversations).slice(0, RECENT_CONVERSATION_LIMIT)

// 当前智能体的新建对话(没有选中会话)时,选中智能体本身
const isActiveApplication = (group: PortalApplicationGroup) => isCurrentApplication(group) && !currentChatId.value
const groupConversationCount = (group: PortalApplicationGroup) =>
Math.max(group.count, isCurrentApplication(group) ? conversations.value.length : group.conversations.length)

// 选中智能体进入其新建对话,并展开其会话;进入后会话以会话列表 Store 为准,无需查询分组会话
const handleSelectApplication = (group: PortalApplicationGroup) => {
group.expanded = true
portalList.openApplication(group.id)
}
// 仅展示有历史记录的智能体,随当前会话的新建、删除同步更新。
const historyApplicationGroups = computed(() => applicationGroups.value.filter((group) => groupConversations(group).length > 0))

const isActiveConversation = (group: PortalApplicationGroup, conversation: Conversation) =>
isCurrentApplication(group) && currentChatId.value === conversation.id
Expand All @@ -40,7 +46,7 @@ const handleSelectConversation = (group: PortalApplicationGroup, conversation: C
}

const handleDeleteConversation = (group: PortalApplicationGroup, conversation: Conversation) => {
if (isCurrentApplication(group)) deleteChat(conversation.id)
if (isCurrentApplication(group)) return deleteChat(conversation.id).then(() => portalList.loadApplicationGroups())
else portalList.deleteGroupConversation(group.id, conversation.id)
}
</script>
Expand Down Expand Up @@ -83,14 +89,14 @@ const handleDeleteConversation = (group: PortalApplicationGroup, conversation: C
</div>

<!-- 历史列表独立滚动,门户入口和新建入口保持固定。 -->
<el-scrollbar class="min-h-0 flex-1 mt-1">
<div v-if="applicationGroups.length === 0" class="text-N600 text-center mt-4">暂无历史记录</div>
<el-scrollbar class="min-h-0 flex-1 mt-1 -mr-4" view-class="pr-4">
<div v-if="historyApplicationGroups.length === 0" class="text-N600 text-center mt-4">暂无历史记录</div>
<div v-else class="space-y-1">
<template v-for="group in applicationGroups" :key="group.id">
<template v-for="group in historyApplicationGroups" :key="group.id">
<MkCollapse
:expanded="group.expanded"
trigger="header"
trigger-class="group rounded-md p-2 hover:bg-N900/10"
trigger-class="group rounded-md p-2 hover:bg-N900/10 mb-1"
indicator-position="after"
@update:expanded="toggleGroup(group.id)"
>
Expand Down Expand Up @@ -127,7 +133,7 @@ const handleDeleteConversation = (group: PortalApplicationGroup, conversation: C
</div>
</template>

<template v-if="group.expanded">
<div class="space-y-1" v-if="group.expanded">
<MkListItem
v-for="conversation in groupConversations(group)"
:key="conversation.id"
Expand Down Expand Up @@ -156,11 +162,30 @@ const handleDeleteConversation = (group: PortalApplicationGroup, conversation: C
</MkDropdownItem>
</template>
</MkListItem>
</template>
<!-- 超过5条 查看更多 -->
<MkListItem class="pl-8!" v-if="groupConversationCount(group) > RECENT_CONVERSATION_LIMIT" @click="handleOpenHistory(group)">
<template #default>
<span>查看更多</span>
<span>
({{
groupConversationCount(group) - RECENT_CONVERSATION_LIMIT > 99
? '99+'
: groupConversationCount(group) - RECENT_CONVERSATION_LIMIT
}})
</span>
</template>
</MkListItem>
</div>
</MkCollapse>
</template>
</div>
</el-scrollbar>
<HistoryConversationDrawer
v-if="historyApplicationId"
v-model="historyVisible"
:application-id="historyApplicationId"
@closed="historyApplicationId = ''"
/>
<!-- 账户入口固定在侧栏底部,不随历史列表滚动。 -->
<div class="mt-3 shrink-0">
<AccountMenu />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<script setup lang="ts">
import { onMounted } from 'vue'
import { debounce } from 'lodash'
import { useConversationListStore } from '../../left-sidebar/conversation-list/index'
import { usePortalConversationListStore } from '../../left-sidebar/portal-conversation-list/index'
import PortalApplicationCards from '../../components/portal-application-cards/index.vue'
Expand All @@ -9,10 +8,7 @@ defineOptions({ name: 'PortalApplicationPanel' })

const { leftSideOpen, toggleLeftSide } = useConversationListStore()
const portalList = usePortalConversationListStore()
const { applications, applicationKeyword, isApplicationsLoading, loadApplications, openApplication, newConversation } = portalList

// 输入名称时延迟搜索,避免每个字符都发起请求
const handleKeywordInput = debounce(() => loadApplications(), 300)
const { applications, applicationKeyword, isApplicationsLoading, loadApplications, openApplication } = portalList

onMounted(() => {
loadApplications()
Expand All @@ -22,7 +18,7 @@ onMounted(() => {
<template>
<!-- 全部智能体主面板:头部 + 智能体卡片列表 -->
<main class="mk-conversation-main">
<header class="flex-align-center shrink-0 gap-2 px-4 py-3">
<header class="flex-align-center shrink-0 gap-2 px-4 py-3 max-md:flex-wrap max-md:gap-y-4">
<!-- 左侧收起时才在顶部显示:展开左侧 + 新建对话(展开时它们在侧栏内) -->
<template v-if="!leftSideOpen">
<!-- 展开左侧 -->
Expand All @@ -35,7 +31,8 @@ onMounted(() => {
<el-divider direction="vertical" />
</template>
<h4 class="min-w-0 flex-1 truncate">智能体</h4>
<MkSearchInput v-model="applicationKeyword" class="w-50!" placeholder="搜索" @input="handleKeywordInput" />
<!-- 搜索智能体 -->
<MkSearchInput v-model="applicationKeyword" class="w-full! md:w-50!" placeholder="搜索" @change="loadApplications" />
</header>

<el-scrollbar v-loading="isApplicationsLoading" class="min-h-0 flex-1 p-4 pt-0!">
Expand Down
2 changes: 2 additions & 0 deletions ui/src/views/VIEW_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -488,3 +488,5 @@ View 负责路由、资源与模式、图加载、保存/发布/调试编排
同目录的 `UserPwdDialog.vue` 修改当前对话用户密码,`ApiKeyDialog.vue` 展示门户地址及用户唯一 API Key,支持复制完整密钥和重新生成;密钥在界面中仅显示末四位。
门户首页默认进入全部智能体列表,不自动选择智能体;没有当前智能体时不查询其信息/会话、禁用输入,无可用智能体显示空态。
全部智能体与选择抽屉复用卡片,抽屉每次打开重新查询,搜索状态独立。
门户历史侧栏仅展示有历史的智能体及最近 5 条会话,剩余数量取分组总数;“查看更多”打开历史对话抽屉,
支持切换智能体、摘要搜索、滚动分页及打开/删除会话,删除后同步刷新侧栏。
Loading