feat(web): move agent status inline with typing indicator for better UX
Instead of showing agent status in a separate panel below the chat,
display it inline beneath the typing dots ("...") in the message flow.
The dots remain the primary waiting indicator; agent status appears
below as supplementary context during specialist agent invocations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
40a0513b05
commit
af8aea6b03
|
|
@ -2,14 +2,13 @@ import { useRef, useEffect } from 'react';
|
|||
import { MessageBubble } from './MessageBubble';
|
||||
import { InputArea } from './InputArea';
|
||||
import { TypingIndicator } from './TypingIndicator';
|
||||
import { AgentStatusIndicator } from './AgentStatusIndicator';
|
||||
import { useChatStore } from '../stores/chatStore';
|
||||
import { useChat } from '../hooks/useChat';
|
||||
import { MessageSquare, Menu } from 'lucide-react';
|
||||
|
||||
export function ChatWindow() {
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const { messages, currentConversationId, isStreaming, streamContent, sidebarOpen, toggleSidebar } = useChatStore();
|
||||
const { messages, currentConversationId, isStreaming, streamContent, sidebarOpen, toggleSidebar, activeAgents, coordinatorPhase } = useChatStore();
|
||||
const {
|
||||
sendMessage,
|
||||
pendingFiles,
|
||||
|
|
@ -19,10 +18,10 @@ export function ChatWindow() {
|
|||
removeFile,
|
||||
} = useChat();
|
||||
|
||||
// Auto-scroll to bottom
|
||||
// Auto-scroll to bottom (including when agent status changes)
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}, [messages, streamContent]);
|
||||
}, [messages, streamContent, activeAgents, coordinatorPhase]);
|
||||
|
||||
const currentMessages = currentConversationId
|
||||
? messages[currentConversationId] || []
|
||||
|
|
@ -81,9 +80,8 @@ export function ChatWindow() {
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* Agent status + Input area */}
|
||||
{/* Input area */}
|
||||
<div className="border-t border-secondary-200 bg-white p-4">
|
||||
<AgentStatusIndicator />
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<InputArea
|
||||
onSend={sendMessage}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,28 @@
|
|||
import { Bot } from 'lucide-react';
|
||||
import { Bot, Loader2, CheckCircle2, Brain } from 'lucide-react';
|
||||
import { clsx } from 'clsx';
|
||||
import { useChatStore } from '../stores/chatStore';
|
||||
|
||||
const AGENT_DISPLAY_NAMES: Record<string, string> = {
|
||||
policy_expert: '政策专家',
|
||||
assessment_expert: '评估专家',
|
||||
strategist: '策略顾问',
|
||||
objection_handler: '异议处理专家',
|
||||
case_analyst: '案例分析师',
|
||||
memory_manager: '记忆管理',
|
||||
};
|
||||
|
||||
const PHASE_DISPLAY: Record<string, string> = {
|
||||
analyzing: '正在分析问题...',
|
||||
orchestrating: '正在协调专家团队...',
|
||||
synthesizing: '正在综合分析结果...',
|
||||
evaluating: '正在进行质量检查...',
|
||||
};
|
||||
|
||||
export function TypingIndicator() {
|
||||
const { activeAgents, completedAgents, coordinatorPhase, coordinatorMessage } = useChatStore();
|
||||
|
||||
const hasAgentActivity = activeAgents.length > 0 || coordinatorPhase !== null;
|
||||
|
||||
return (
|
||||
<div className="flex gap-3 message-enter">
|
||||
{/* Avatar */}
|
||||
|
|
@ -8,13 +30,74 @@ export function TypingIndicator() {
|
|||
<Bot className="w-4 h-4 text-secondary-600" />
|
||||
</div>
|
||||
|
||||
{/* Typing dots */}
|
||||
<div className="bg-secondary-100 rounded-2xl rounded-tl-sm px-4 py-3">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="w-2 h-2 bg-secondary-400 rounded-full typing-dot" />
|
||||
<span className="w-2 h-2 bg-secondary-400 rounded-full typing-dot" />
|
||||
<span className="w-2 h-2 bg-secondary-400 rounded-full typing-dot" />
|
||||
{/* Content area */}
|
||||
<div className="space-y-2 max-w-[80%]">
|
||||
{/* Typing dots — always shown as primary indicator */}
|
||||
<div className="bg-secondary-100 rounded-2xl rounded-tl-sm px-4 py-3">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="w-2 h-2 bg-secondary-400 rounded-full typing-dot" />
|
||||
<span className="w-2 h-2 bg-secondary-400 rounded-full typing-dot" />
|
||||
<span className="w-2 h-2 bg-secondary-400 rounded-full typing-dot" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Agent status — shown below dots when there's activity */}
|
||||
{hasAgentActivity && (
|
||||
<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 && (
|
||||
<div className="flex items-center gap-2 text-xs text-secondary-600">
|
||||
<Brain className="w-3.5 h-3.5 text-primary-500 agent-pulse flex-shrink-0" />
|
||||
<span className="text-primary-600 font-medium">
|
||||
{PHASE_DISPLAY[coordinatorPhase] || coordinatorMessage}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Active agents */}
|
||||
{activeAgents.map((agent) => (
|
||||
<div
|
||||
key={`${agent.messageId}-${agent.agentType}`}
|
||||
className="agent-enter flex items-center gap-2 text-xs"
|
||||
>
|
||||
<Loader2 className="w-3.5 h-3.5 text-primary-500 animate-spin flex-shrink-0" />
|
||||
<span className="text-secondary-700">
|
||||
<span className="font-medium">
|
||||
{AGENT_DISPLAY_NAMES[agent.agentType] || agent.agentName}
|
||||
</span>
|
||||
{agent.description && (
|
||||
<span className="text-secondary-500 ml-1">- {agent.description}</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Completed agents (compact badges) */}
|
||||
{completedAgents.length > 0 && activeAgents.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5 pt-1 border-t border-secondary-200">
|
||||
{completedAgents.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',
|
||||
agent.success
|
||||
? 'bg-green-50 text-green-600'
|
||||
: 'bg-red-50 text-red-500',
|
||||
)}
|
||||
>
|
||||
<CheckCircle2 className="w-3 h-3" />
|
||||
{AGENT_DISPLAY_NAMES[agent.agentType] || agent.agentName}
|
||||
{agent.durationMs > 0 && (
|
||||
<span className="text-secondary-400">
|
||||
{(agent.durationMs / 1000).toFixed(1)}s
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue