diff --git a/packages/web-client/src/features/chat/presentation/components/ChatWindow.tsx b/packages/web-client/src/features/chat/presentation/components/ChatWindow.tsx index 23c52cb..c499405 100644 --- a/packages/web-client/src/features/chat/presentation/components/ChatWindow.tsx +++ b/packages/web-client/src/features/chat/presentation/components/ChatWindow.tsx @@ -8,7 +8,7 @@ import { MessageSquare, Menu } from 'lucide-react'; export function ChatWindow() { const messagesEndRef = useRef(null); - const { messages, currentConversationId, isStreaming, streamContent, sidebarOpen, toggleSidebar, activeAgents, coordinatorPhase } = useChatStore(); + const { messages, currentConversationId, isStreaming, streamContent, sidebarOpen, toggleSidebar, activeAgents, completedAgents, coordinatorPhase } = useChatStore(); const { sendMessage, pendingFiles, @@ -18,10 +18,10 @@ export function ChatWindow() { removeFile, } = useChat(); - // Auto-scroll to bottom (including when agent status changes) + // Auto-scroll to bottom on any content change useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - }, [messages, streamContent, activeAgents, coordinatorPhase]); + }, [messages, streamContent, isStreaming, activeAgents, completedAgents, coordinatorPhase, currentConversationId]); const currentMessages = currentConversationId ? messages[currentConversationId] || [] diff --git a/packages/web-client/src/features/chat/presentation/components/TypingIndicator.tsx b/packages/web-client/src/features/chat/presentation/components/TypingIndicator.tsx index 69a9888..40c8e6c 100644 --- a/packages/web-client/src/features/chat/presentation/components/TypingIndicator.tsx +++ b/packages/web-client/src/features/chat/presentation/components/TypingIndicator.tsx @@ -1,6 +1,7 @@ +import { useState, useEffect } from 'react'; import { Bot, Loader2, CheckCircle2, Brain } from 'lucide-react'; import { clsx } from 'clsx'; -import { useChatStore } from '../stores/chatStore'; +import { useChatStore, CompletedAgent } from '../stores/chatStore'; const AGENT_DISPLAY_NAMES: Record = { policy_expert: '政策专家', @@ -18,8 +19,65 @@ const PHASE_DISPLAY: Record = { evaluating: '正在进行质量检查...', }; +const BADGE_VISIBLE_MS = 2500; // Show completed badge for 2.5s +const BADGE_FADE_MS = 500; // Then fade out over 0.5s + export function TypingIndicator() { const { activeAgents, completedAgents, coordinatorPhase, coordinatorMessage } = useChatStore(); + const [visibleCompleted, setVisibleCompleted] = useState<(CompletedAgent & { fading?: boolean })[]>([]); + + // Track completed agents: show briefly then auto-fade + useEffect(() => { + if (completedAgents.length === 0) { + setVisibleCompleted([]); + return; + } + + // Add newly completed agents + setVisibleCompleted(prev => { + const prevKeys = new Set(prev.map(a => `${a.messageId}-${a.agentType}`)); + const newAgents = completedAgents.filter( + a => !prevKeys.has(`${a.messageId}-${a.agentType}`), + ); + if (newAgents.length === 0) return prev; + return [...prev, ...newAgents.map(a => ({ ...a, fading: false }))]; + }); + }, [completedAgents]); + + // Auto-fade completed agents after BADGE_VISIBLE_MS + useEffect(() => { + const nonFading = visibleCompleted.filter(a => !a.fading); + if (nonFading.length === 0) return; + + const oldest = Math.min(...nonFading.map(a => a.completedAt)); + const elapsed = Date.now() - oldest; + const delay = Math.max(0, BADGE_VISIBLE_MS - elapsed); + + const fadeTimer = setTimeout(() => { + setVisibleCompleted(prev => + prev.map(a => { + if (!a.fading && Date.now() - a.completedAt >= BADGE_VISIBLE_MS) { + return { ...a, fading: true }; + } + return a; + }), + ); + }, delay); + + return () => clearTimeout(fadeTimer); + }, [visibleCompleted]); + + // Remove fully faded agents after animation completes + useEffect(() => { + const fading = visibleCompleted.filter(a => a.fading); + if (fading.length === 0) return; + + const removeTimer = setTimeout(() => { + setVisibleCompleted(prev => prev.filter(a => !a.fading)); + }, BADGE_FADE_MS); + + return () => clearTimeout(removeTimer); + }, [visibleCompleted]); const hasAgentActivity = activeAgents.length > 0 || coordinatorPhase !== null; @@ -42,7 +100,7 @@ export function TypingIndicator() { {/* Agent status — shown below dots when there's activity */} - {hasAgentActivity && ( + {(hasAgentActivity || visibleCompleted.length > 0) && (
{/* Coordinator phase */} {coordinatorPhase && ( @@ -72,17 +130,18 @@ export function TypingIndicator() {
))} - {/* Completed agents (compact badges) */} - {completedAgents.length > 0 && activeAgents.length > 0 && ( + {/* Completed agents (auto-fading badges) */} + {visibleCompleted.length > 0 && (
- {completedAgents.map((agent) => ( + {visibleCompleted.map((agent) => ( diff --git a/packages/web-client/src/styles/globals.css b/packages/web-client/src/styles/globals.css index 5da56ef..4d65785 100644 --- a/packages/web-client/src/styles/globals.css +++ b/packages/web-client/src/styles/globals.css @@ -138,3 +138,19 @@ .agent-status-enter { animation: fadeIn 0.3s ease-out; } + +/* Completed agent badge fade-out */ +@keyframes badgeFadeOut { + from { + opacity: 1; + transform: scale(1); + } + to { + opacity: 0; + transform: scale(0.8); + } +} + +.agent-badge-fade { + animation: badgeFadeOut 0.5s ease-out forwards; +}