From 2d5f229828b0e9e8618246aa71eaadab9056fd6f Mon Sep 17 00:00:00 2001 From: mlogclub Date: Tue, 14 Apr 2026 20:19:16 +0800 Subject: [PATCH] feat: add realtime connection status handling and display in conversations page --- web/app/(console)/conversations/page.tsx | 4 +- web/components/realtime-connection-status.tsx | 64 +++++++++++++++++++ web/components/site-header.tsx | 12 +++- web/hooks/use-agent-conversation-realtime.ts | 9 ++- web/lib/stores/agent-conversations.ts | 8 +++ 5 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 web/components/realtime-connection-status.tsx diff --git a/web/app/(console)/conversations/page.tsx b/web/app/(console)/conversations/page.tsx index d791d9a..7cc81c4 100644 --- a/web/app/(console)/conversations/page.tsx +++ b/web/app/(console)/conversations/page.tsx @@ -267,10 +267,10 @@ export default function ConversationsPage() { <> - +
-

+

{conversation.subject}

diff --git a/web/components/realtime-connection-status.tsx b/web/components/realtime-connection-status.tsx new file mode 100644 index 0000000..205b7d6 --- /dev/null +++ b/web/components/realtime-connection-status.tsx @@ -0,0 +1,64 @@ +"use client"; + +import { cn } from "@/lib/utils"; + +export type RealtimeConnectionStatusValue = + | "connecting" + | "connected" + | "disconnected"; + +type RealtimeConnectionStatusProps = { + status: RealtimeConnectionStatusValue; + compact?: boolean; +}; + +const statusText: Record = { + connecting: "实时通道连接中", + connected: "实时通道已连接", + disconnected: "实时通道已断开", +}; + +const compactStatusText: Record = { + connecting: "平台实时:连接中", + connected: "平台实时:在线", + disconnected: "平台实时:已断开", +}; + +export function RealtimeConnectionStatus({ + status, + compact = false, +}: RealtimeConnectionStatusProps) { + const toneClass = + status === "connected" + ? "border-emerald-200/80 bg-emerald-50 text-emerald-700" + : status === "connecting" + ? "border-amber-200/80 bg-amber-50 text-amber-700" + : "border-slate-200/80 bg-slate-100 text-slate-600"; + + return ( +

+ + + {compact + ? compactStatusText[status] + : statusText[status]} + +
+ ); +} diff --git a/web/components/site-header.tsx b/web/components/site-header.tsx index b8c7169..02ed92d 100644 --- a/web/components/site-header.tsx +++ b/web/components/site-header.tsx @@ -3,7 +3,9 @@ import { useEffect, useRef } from "react" import { usePathname } from "next/navigation" +import { RealtimeConnectionStatus } from "@/components/realtime-connection-status" import { getPageTitle } from "@/lib/navigation" +import { useAgentConversationsStore } from "@/lib/stores/agent-conversations" import { ThemeToggle } from "@/components/theme-toggle" import { Breadcrumb, @@ -20,7 +22,10 @@ export function SiteHeader() { const pathname = usePathname() const { open, setOpen, isMobile } = useSidebar() const pageTitle = getPageTitle(pathname) + const realtimeStatus = useAgentConversationsStore((state) => state.realtimeStatus) const hasRestoredRef = useRef(false) + const showConversationRealtime = + pathname === "/conversations" || pathname.startsWith("/conversations/") useEffect(() => { if (hasRestoredRef.current || isMobile) { @@ -57,7 +62,12 @@ export function SiteHeader() { - {pageTitle} +
+ {pageTitle} + {showConversationRealtime ? ( + + ) : null} +
diff --git a/web/hooks/use-agent-conversation-realtime.ts b/web/hooks/use-agent-conversation-realtime.ts index 98efe5e..cc4dc74 100644 --- a/web/hooks/use-agent-conversation-realtime.ts +++ b/web/hooks/use-agent-conversation-realtime.ts @@ -17,6 +17,7 @@ export function useAgentConversationRealtime() { const syncLatestMessages = useAgentConversationsStore( (state) => state.syncLatestMessages, ) + const setRealtimeStatus = useAgentConversationsStore((state) => state.setRealtimeStatus) const websocketRef = useRef(null) const reconnectTimerRef = useRef(null) const pingTimerRef = useRef(null) @@ -67,8 +68,10 @@ export function useAgentConversationRealtime() { let socket: WebSocket try { + setRealtimeStatus("connecting") socket = new WebSocket(createAdminWebSocketUrl()) } catch (error) { + setRealtimeStatus("disconnected") toast.error(error instanceof Error ? error.message : "连接实时服务失败") scheduleReconnect() return @@ -80,6 +83,7 @@ export function useAgentConversationRealtime() { console.info("[agent-realtime] websocket connected", { url: socket.url, }) + setRealtimeStatus("connected") reconnectAttemptRef.current = 0 if (pingTimerRef.current) { window.clearInterval(pingTimerRef.current) @@ -191,6 +195,7 @@ export function useAgentConversationRealtime() { reason: event.reason, wasClean: event.wasClean, }) + setRealtimeStatus("disconnected") if (pingTimerRef.current) { window.clearInterval(pingTimerRef.current) pingTimerRef.current = null @@ -207,6 +212,7 @@ export function useAgentConversationRealtime() { url: socket.url, readyState: socket.readyState, }) + setRealtimeStatus("disconnected") scheduleReconnect() } } @@ -222,9 +228,10 @@ export function useAgentConversationRealtime() { if (socket) { socket.close() } + setRealtimeStatus("disconnected") subscribedConversationIdRef.current = null } - }, [loadConversations, syncLatestMessages]) + }, [loadConversations, setRealtimeStatus, syncLatestMessages]) useEffect(() => { const socket = websocketRef.current diff --git a/web/lib/stores/agent-conversations.ts b/web/lib/stores/agent-conversations.ts index 71a508b..bbd30ba 100644 --- a/web/lib/stores/agent-conversations.ts +++ b/web/lib/stores/agent-conversations.ts @@ -14,6 +14,7 @@ import { type AgentConversation, type AgentMessage, } from "@/lib/api/agent" +import type { RealtimeConnectionStatusValue } from "@/components/realtime-connection-status" import { summarizeIMMessage } from "@/lib/im-message" import { generateUUID } from "@/lib/utils" @@ -126,8 +127,10 @@ type AgentConversationsStore = { uploadingAsset: boolean recallingMessageId: number readingMessageId: number + realtimeStatus: RealtimeConnectionStatusValue setSearchKeyword: (keyword: string) => void setConversationFilter: (filter: AgentConversationFilterKey) => void + setRealtimeStatus: (status: RealtimeConnectionStatusValue) => void setConversationTags: ( conversationId: number, tags: AgentConversation["tags"] @@ -164,6 +167,7 @@ export const useAgentConversationsStore = create((set, uploadingAsset: false, recallingMessageId: 0, readingMessageId: 0, + realtimeStatus: "connecting", setSearchKeyword: (keyword) => { set({ searchKeyword: keyword }) @@ -173,6 +177,10 @@ export const useAgentConversationsStore = create((set, set({ conversationFilter: filter }) }, + setRealtimeStatus: (status) => { + set({ realtimeStatus: status }) + }, + setConversationTags: (conversationId, tags) => { set((state) => ({ conversations: state.conversations.map((item) =>