feat(web): auto-scroll on all state changes + completed agent badges auto-fade
Fix auto-scroll by adding missing dependencies (currentConversationId, isStreaming, completedAgents). Completed agent badges now show for 2.5s then smoothly fade out instead of accumulating, keeping the status area clean. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
af8aea6b03
commit
636b10b733
|
|
@ -8,7 +8,7 @@ import { MessageSquare, Menu } from 'lucide-react';
|
|||
|
||||
export function ChatWindow() {
|
||||
const messagesEndRef = useRef<HTMLDivElement>(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] || []
|
||||
|
|
|
|||
|
|
@ -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<string, string> = {
|
||||
policy_expert: '政策专家',
|
||||
|
|
@ -18,8 +19,65 @@ const PHASE_DISPLAY: Record<string, string> = {
|
|||
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() {
|
|||
</div>
|
||||
|
||||
{/* Agent status — shown below dots when there's activity */}
|
||||
{hasAgentActivity && (
|
||||
{(hasAgentActivity || visibleCompleted.length > 0) && (
|
||||
<div className="agent-status-enter rounded-xl bg-secondary-50 border border-secondary-200 px-3 py-2.5 space-y-1.5">
|
||||
{/* Coordinator phase */}
|
||||
{coordinatorPhase && (
|
||||
|
|
@ -72,17 +130,18 @@ export function TypingIndicator() {
|
|||
</div>
|
||||
))}
|
||||
|
||||
{/* Completed agents (compact badges) */}
|
||||
{completedAgents.length > 0 && activeAgents.length > 0 && (
|
||||
{/* Completed agents (auto-fading badges) */}
|
||||
{visibleCompleted.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5 pt-1 border-t border-secondary-200">
|
||||
{completedAgents.map((agent) => (
|
||||
{visibleCompleted.map((agent) => (
|
||||
<span
|
||||
key={`${agent.messageId}-${agent.agentType}-done`}
|
||||
className={clsx(
|
||||
'inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded-full',
|
||||
'inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded-full transition-opacity',
|
||||
agent.success
|
||||
? 'bg-green-50 text-green-600'
|
||||
: 'bg-red-50 text-red-500',
|
||||
agent.fading ? 'agent-badge-fade' : 'opacity-100',
|
||||
)}
|
||||
>
|
||||
<CheckCircle2 className="w-3 h-3" />
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue