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 { MessageBubble } from './MessageBubble';
|
||||||
import { InputArea } from './InputArea';
|
import { InputArea } from './InputArea';
|
||||||
import { TypingIndicator } from './TypingIndicator';
|
import { TypingIndicator } from './TypingIndicator';
|
||||||
import { AgentStatusIndicator } from './AgentStatusIndicator';
|
|
||||||
import { useChatStore } from '../stores/chatStore';
|
import { useChatStore } from '../stores/chatStore';
|
||||||
import { useChat } from '../hooks/useChat';
|
import { useChat } from '../hooks/useChat';
|
||||||
import { MessageSquare, Menu } from 'lucide-react';
|
import { MessageSquare, Menu } from 'lucide-react';
|
||||||
|
|
||||||
export function ChatWindow() {
|
export function ChatWindow() {
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
const { messages, currentConversationId, isStreaming, streamContent, sidebarOpen, toggleSidebar } = useChatStore();
|
const { messages, currentConversationId, isStreaming, streamContent, sidebarOpen, toggleSidebar, activeAgents, coordinatorPhase } = useChatStore();
|
||||||
const {
|
const {
|
||||||
sendMessage,
|
sendMessage,
|
||||||
pendingFiles,
|
pendingFiles,
|
||||||
|
|
@ -19,10 +18,10 @@ export function ChatWindow() {
|
||||||
removeFile,
|
removeFile,
|
||||||
} = useChat();
|
} = useChat();
|
||||||
|
|
||||||
// Auto-scroll to bottom
|
// Auto-scroll to bottom (including when agent status changes)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
}, [messages, streamContent]);
|
}, [messages, streamContent, activeAgents, coordinatorPhase]);
|
||||||
|
|
||||||
const currentMessages = currentConversationId
|
const currentMessages = currentConversationId
|
||||||
? messages[currentConversationId] || []
|
? messages[currentConversationId] || []
|
||||||
|
|
@ -81,9 +80,8 @@ export function ChatWindow() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Agent status + Input area */}
|
{/* Input area */}
|
||||||
<div className="border-t border-secondary-200 bg-white p-4">
|
<div className="border-t border-secondary-200 bg-white p-4">
|
||||||
<AgentStatusIndicator />
|
|
||||||
<div className="max-w-3xl mx-auto">
|
<div className="max-w-3xl mx-auto">
|
||||||
<InputArea
|
<InputArea
|
||||||
onSend={sendMessage}
|
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() {
|
export function TypingIndicator() {
|
||||||
|
const { activeAgents, completedAgents, coordinatorPhase, coordinatorMessage } = useChatStore();
|
||||||
|
|
||||||
|
const hasAgentActivity = activeAgents.length > 0 || coordinatorPhase !== null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-3 message-enter">
|
<div className="flex gap-3 message-enter">
|
||||||
{/* Avatar */}
|
{/* Avatar */}
|
||||||
|
|
@ -8,13 +30,74 @@ export function TypingIndicator() {
|
||||||
<Bot className="w-4 h-4 text-secondary-600" />
|
<Bot className="w-4 h-4 text-secondary-600" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Typing dots */}
|
{/* Content area */}
|
||||||
<div className="bg-secondary-100 rounded-2xl rounded-tl-sm px-4 py-3">
|
<div className="space-y-2 max-w-[80%]">
|
||||||
<div className="flex items-center gap-1">
|
{/* Typing dots — always shown as primary indicator */}
|
||||||
<span className="w-2 h-2 bg-secondary-400 rounded-full typing-dot" />
|
<div className="bg-secondary-100 rounded-2xl rounded-tl-sm px-4 py-3">
|
||||||
<span className="w-2 h-2 bg-secondary-400 rounded-full typing-dot" />
|
<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" />
|
||||||
|
<span className="w-2 h-2 bg-secondary-400 rounded-full typing-dot" />
|
||||||
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue