From 1764ffedb0b0d974d362f850fd82988f0a51cb68 Mon Sep 17 00:00:00 2001 From: Wenjie Zhang Date: Mon, 16 Mar 2026 21:39:49 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=99=BA=E8=83=BD=E4=BD=93=E5=AF=B9?= =?UTF-8?q?=E8=AF=9D=E7=95=8C=E9=9D=A2=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 消息列表修改为所有的对话,不区分智能体 - 智能体会在新建对话的时候切换 - 配置选择从右上角移动到输入框 - 其他细节优化 --- server/routers/chat_router.py | 2 +- src/services/conversation_service.py | 5 +- web/src/apis/agent_api.js | 13 +- web/src/components/AgentChatComponent.vue | 174 ++++++++--- web/src/components/AgentInputArea.vue | 3 +- web/src/components/AgentPanel.vue | 16 +- web/src/components/MessageInputComponent.vue | 165 ++--------- web/src/views/AgentView.vue | 287 ++++--------------- 8 files changed, 236 insertions(+), 429 deletions(-) diff --git a/server/routers/chat_router.py b/server/routers/chat_router.py index 750defe7..dff71a3a 100644 --- a/server/routers/chat_router.py +++ b/server/routers/chat_router.py @@ -746,7 +746,7 @@ async def create_thread( @chat.get("/threads", response_model=list[ThreadResponse]) async def list_threads( - agent_id: str, + agent_id: str | None = Query(None), limit: int = Query(100, ge=1, le=500), offset: int = Query(0, ge=0), db: AsyncSession = Depends(get_db), diff --git a/src/services/conversation_service.py b/src/services/conversation_service.py index 6bd6aa55..11536571 100644 --- a/src/services/conversation_service.py +++ b/src/services/conversation_service.py @@ -153,15 +153,12 @@ async def create_thread_view( async def list_threads_view( *, - agent_id: str, + agent_id: str | None, db: AsyncSession, current_user_id: str, limit: int | None = None, offset: int = 0, ) -> list[dict]: - if not agent_id: - raise HTTPException(status_code=422, detail="agent_id 不能为空") - conv_repo = ConversationRepository(db) conversations = await conv_repo.list_conversations( user_id=str(current_user_id), diff --git a/web/src/apis/agent_api.js b/web/src/apis/agent_api.js index 6b1c2ce7..f1dba43e 100644 --- a/web/src/apis/agent_api.js +++ b/web/src/apis/agent_api.js @@ -285,13 +285,20 @@ export const multimodalApi = { export const threadApi = { /** * 获取对话线程列表 - * @param {string} agentId - 智能体ID + * @param {string | null | undefined} agentId - 智能体ID,可选;不传时返回全部智能体对话 * @param {number} limit - 返回数量限制,默认100 * @param {number} offset - 偏移量,默认0 * @returns {Promise} - 对话线程列表 */ - getThreads: (agentId, limit = 100, offset = 0) => { - const url = `/api/chat/threads?agent_id=${agentId}&limit=${limit}&offset=${offset}` + getThreads: (agentId = null, limit = 100, offset = 0) => { + const params = new URLSearchParams({ + limit: String(limit), + offset: String(offset) + }) + if (agentId) { + params.set('agent_id', agentId) + } + const url = `/api/chat/threads?${params.toString()}` return apiGet(url) }, diff --git a/web/src/components/AgentChatComponent.vue b/web/src/components/AgentChatComponent.vue index 2578aaa7..32be7e80 100644 --- a/web/src/components/AgentChatComponent.vue +++ b/web/src/components/AgentChatComponent.vue @@ -17,7 +17,6 @@ @rename-chat="renameChat" @toggle-pin="togglePinChat" @toggle-sidebar="toggleSidebar" - @open-agent-modal="openAgentModal" @load-more-chats="loadMoreChats" :class="{ 'sidebar-open': chatUIStore.isSidebarOpen, @@ -51,14 +50,6 @@ 新对话 -
- - - - {{ currentAgentName || '选择智能体' }} - - -
@@ -131,6 +122,14 @@

👋 您好,我是{{ currentAgentName }}!

+
+ +
+ + > + +
-
-

请注意辨别内容的可靠性

+
+

当前智能体:{{ currentThreadAgentName }};请注意辨别内容的可靠性

@@ -210,7 +213,7 @@ import AgentInputArea from '@/components/AgentInputArea.vue' import AgentMessageComponent from '@/components/AgentMessageComponent.vue' import ChatSidebarComponent from '@/components/ChatSidebarComponent.vue' import RefsComponent from '@/components/RefsComponent.vue' -import { PanelLeftOpen, MessageCirclePlus, LoaderCircle, ChevronDown, Bot } from 'lucide-vue-next' +import { PanelLeftOpen, MessageCirclePlus, LoaderCircle } from 'lucide-vue-next' import { handleChatError, handleValidationError } from '@/utils/errorHandler' import { ScrollController } from '@/utils/scrollController' import { AgentValidator } from '@/utils/agentValidator' @@ -229,7 +232,6 @@ const props = defineProps({ agentId: { type: String, default: '' }, singleMode: { type: Boolean, default: true } }) -const emit = defineEmits(['open-config', 'open-agent-modal']) // ==================== STORE MANAGEMENT ==================== const agentStore = useAgentStore() @@ -336,6 +338,17 @@ const currentThread = computed(() => { return threads.value.find((thread) => thread.id === currentChatId.value) || null }) +const currentThreadAgentName = computed(() => { + const threadAgentId = currentThread.value?.agent_id + if (threadAgentId && agents.value?.length) { + const threadAgent = agents.value.find((agent) => agent.id === threadAgentId) + if (threadAgent?.name) { + return threadAgent.name + } + } + return currentAgentName.value +}) + // 检查当前智能体是否支持文件上传 const supportsFileUpload = computed(() => { if (!currentAgent.value) return false @@ -510,6 +523,27 @@ const conversations = computed(() => { return historyConvs }) +const agentSegmentOptions = computed(() => { + return (agents.value || []).map((agent) => ({ + label: agent.name || 'Unknown', + value: agent.id + })) +}) + +const showStartAgentSegment = computed(() => { + return !props.singleMode && !conversations.value.length && agentSegmentOptions.value.length > 1 +}) + +const handleStartAgentChange = async (agentId) => { + if (!agentId || agentId === currentAgentId.value) return + if (conversations.value.length > 0) return + try { + await agentStore.selectAgent(agentId) + } catch (error) { + handleChatError(error, 'load') + } +} + const isLoadingMessages = computed(() => chatUIStore.isLoadingMessages) const isStreaming = computed(() => { const threadState = currentThreadState.value @@ -610,8 +644,8 @@ const resetOnGoingConv = (threadId = null) => { // ==================== 线程管理方法 ==================== // 获取当前智能体的线程列表 const fetchThreads = async (agentId = null) => { - const targetAgentId = agentId || currentAgentId.value - if (!targetAgentId) return + const targetAgentId = props.singleMode ? agentId || currentAgentId.value : agentId + if (props.singleMode && !targetAgentId) return chatUIStore.isLoadingThreads = true try { @@ -632,8 +666,8 @@ const fetchThreads = async (agentId = null) => { const loadMoreChats = async () => { if (isLoadingMoreChats.value || !hasMoreChats.value) return - const targetAgentId = currentAgentId.value - if (!targetAgentId) return + const targetAgentId = props.singleMode ? currentAgentId.value : null + if (props.singleMode && !targetAgentId) return isLoadingMoreChats.value = true try { @@ -1350,17 +1384,19 @@ const createNewChat = async () => { } const selectChat = async (chatId) => { - if ( - !AgentValidator.validateAgentIdWithError( - currentAgentId.value, - '选择对话', - handleValidationError - ) - ) + const targetChat = threads.value.find((chat) => chat.id === chatId) || null + const targetAgentId = targetChat?.agent_id || currentAgentId.value + const previousThreadId = chatState.currentThreadId + + if (!targetAgentId) { + handleValidationError('选择对话失败:缺少智能体信息') + return + } + + if (!AgentValidator.validateAgentIdWithError(targetAgentId, '选择对话', handleValidationError)) return // 中断之前线程的流式输出(如果存在) - const previousThreadId = chatState.currentThreadId if (previousThreadId && previousThreadId !== chatId) { const previousThreadState = getThreadState(previousThreadId) if (previousThreadState?.isStreaming && previousThreadState.streamAbortController) { @@ -1372,10 +1408,22 @@ const selectChat = async (chatId) => { stopRunStreamSubscription(previousThreadId) } + // 先更新当前线程,确保底部智能体名称与选中项即时同步 chatState.currentThreadId = chatId + + if (!props.singleMode && targetChat?.agent_id && targetChat.agent_id !== currentAgentId.value) { + try { + await agentStore.selectAgent(targetChat.agent_id) + } catch (error) { + chatState.currentThreadId = previousThreadId + handleChatError(error, 'load') + return + } + } + chatUIStore.isLoadingMessages = true try { - await fetchThreadMessages({ agentId: currentAgentId.value, threadId: chatId }) + await fetchThreadMessages({ agentId: targetAgentId, threadId: chatId }) } catch (error) { handleChatError(error, 'load') } finally { @@ -1384,7 +1432,7 @@ const selectChat = async (chatId) => { await nextTick() scrollController.scrollToBottomStaticForce() - await fetchAgentState(currentAgentId.value, chatId) + await fetchAgentState(targetAgentId, chatId) await resumeActiveRunForThread(chatId) } @@ -1657,7 +1705,6 @@ defineExpose({ const toggleSidebar = () => { chatUIStore.toggleSidebar() } -const openAgentModal = () => emit('open-agent-modal') const handleAgentStateRefresh = async (threadId = null) => { if (!currentAgentId.value) return @@ -1744,8 +1791,8 @@ const getConversationSources = (conv) => { // ==================== LIFECYCLE & WATCHERS ==================== const loadChatsList = async () => { - const agentId = currentAgentId.value - if (!agentId) { + const agentId = props.singleMode ? currentAgentId.value : null + if (props.singleMode && !agentId) { console.warn('No agent selected, cannot load chats list') threads.value = [] chatState.currentThreadId = null @@ -1754,7 +1801,7 @@ const loadChatsList = async () => { try { await fetchThreads(agentId) - if (currentAgentId.value !== agentId) return + if (props.singleMode && currentAgentId.value !== agentId) return // 如果当前线程不在线程列表中,清空当前线程 if ( @@ -1791,6 +1838,13 @@ onMounted(async () => { watch( currentAgentId, async (newAgentId, oldAgentId) => { + if (!props.singleMode) { + if (oldAgentId === undefined) { + await loadChatsList() + } + return + } + if (newAgentId !== oldAgentId) { // 清理当前线程状态 chatState.currentThreadId = null @@ -1895,19 +1949,26 @@ watch( .agent-panel-wrapper { flex: 0 0 auto; - height: calc(100% - 56px); + align-self: flex-end; + height: 70vh; overflow: hidden; z-index: 20; margin: 28px 8px; margin-left: 0; background: var(--gray-0); - border-radius: 12px; + border-radius: 16px; box-shadow: 0 4px 20px var(--shadow-1); border: 1px solid var(--gray-150); min-width: 0; will-change: flex-basis; } +@media (max-height: 700px) { + .agent-panel-wrapper { + height: calc(100% - 56px); + } +} + /* Workbench transition animations */ .agent-panel-wrapper { transition: flex-basis 0.3s cubic-bezier(0.4, 0, 0.2, 1); @@ -1937,6 +1998,43 @@ watch( } } +.agent-segment-wrapper { + width: fit-content; + max-width: 100%; + margin: 0 auto 10px; + overflow-x: auto; + scrollbar-width: none; + + &::-webkit-scrollbar { + display: none; + } + + :deep(.ant-segmented) { + width: auto; + max-width: 100%; + white-space: nowrap; + background: var(--gray-50); + border: 1px solid var(--gray-150); + border-radius: 10px; + } + + :deep(.ant-segmented-group) { + width: auto; + display: inline-flex; + } + + :deep(.ant-segmented-item) { + flex: 0 0 auto; + min-width: 0; + } + + :deep(.ant-segmented-item-label) { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +} + .example-questions { margin-top: 16px; text-align: center; @@ -2144,6 +2242,14 @@ watch( } @media (max-width: 768px) { + .agent-segment-wrapper { + margin-bottom: 8px; + + :deep(.ant-segmented-item-label) { + font-size: 12px; + } + } + .chat-header { .header__left { .text { diff --git a/web/src/components/AgentInputArea.vue b/web/src/components/AgentInputArea.vue index c564c590..fbbe22d1 100644 --- a/web/src/components/AgentInputArea.vue +++ b/web/src/components/AgentInputArea.vue @@ -8,7 +8,6 @@ :disabled="disabled" :send-button-disabled="sendButtonDisabled" :placeholder="placeholder" - :force-multi-line="hasStateContent" :mention="mention" @send="handleSend" @keydown="handleKeyDown" @@ -32,6 +31,7 @@