import { useState, useCallback, memo } from 'react'; import { MessageList } from './MessageList'; import { MessageInput } from './MessageInput'; import { ExpertSelector } from './ExpertSelector'; import { ModelSelector } from './ModelSelector'; import { Zap } from 'lucide-react'; import type { ChatMessage, Attachment } from '../../types/protocol'; interface ChatContainerProps { messages: ChatMessage[]; isLoading: boolean; isReadOnly?: boolean; channelName?: string; onSendMessage: (content: string, attachments: Attachment[]) => void; onNavigateToSubAgent?: (taskId: string, description: string, subagentType?: string) => void; onStop?: () => void; showThinking?: boolean; /** 视图标识,用于保存/恢复滚动位置 */ viewKey?: string; /** 高亮的消息 ID */ highlightedMessageId?: string | null; /** 当前 session ID,用于专家选择 */ sessionId?: string | null; /** 打开设置页(用于专家管理入口) */ onOpenSettings?: () => void; /** 设置弹窗关闭信号(每次关闭递增,用于触发 ExpertSelector 刷新) */ settingsClosedTick?: number; /** 当前话题 ID,用于切换话题时清空输入框草稿 */ topicId?: string | null; /** 历史分页:是否还有更早的消息可加载 */ hasMoreOlder?: boolean; /** 历史分页:是否正在加载更早一页 */ loadingOlder?: boolean; /** 触顶时请求加载更早的历史消息 */ onLoadOlder?: () => void; } // memo:props 除 messages 外全部稳定(useCallback/原始值), // App 因非消息类 state(侧栏折叠、主题等)重渲染时跳过整个聊天子树。 export const ChatContainer = memo(function ChatContainer({ messages, isLoading, isReadOnly = false, channelName, onSendMessage, onNavigateToSubAgent, onStop, showThinking = true, viewKey, highlightedMessageId, sessionId, onOpenSettings, settingsClosedTick, topicId, hasMoreOlder, loadingOlder, onLoadOlder, }: ChatContainerProps) { const [selectedExpert, setSelectedExpert] = useState<{ name: string; description: string; } | null>(null); // 当前生效模型(供 Task 卡片差异显示:子代理模型 ≠ 主代理模型时提示) const [effectiveModel, setEffectiveModel] = useState<{ provider: string; model: string; } | null>(null); // 稳定引用,避免内联箭头破坏下游 memo 化 const handleModelSelectionChange = useCallback( (effective: { provider: string; model: string }) => setEffectiveModel({ provider: effective.provider, model: effective.model }), [], ); const selectors = (
); const input = ( ); /* Hero 空状态:空话题 + 非加载 + 可写。 注意:composer 保持在组件树中的固定位置(不随分支 remount), 避免空→非空切换时丢失未发送草稿。 */ const isHero = messages.length === 0 && !isLoading && !isReadOnly; return (
{isHero && (
)} {isHero ? (

PicoBot

有什么可以帮你的?

) : (
)}
{selectors} {input}
); });