import { useState, useEffect, useRef, useCallback, useMemo, type CSSProperties, type RefObject, type MutableRefObject } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { List, type ListImperativeAPI } from 'react-window'; import { useChatStore, Message } from '../store/chatStore'; import { useConnectionStore } from '../store/connectionStore'; import { useAgentStore } from '../store/agentStore'; import { useConfigStore } from '../store/configStore'; import { Paperclip, ChevronDown, Terminal, SquarePen, ArrowUp, MessageSquare, Download, Copy, Check } from 'lucide-react'; import { Button, EmptyState } from './ui'; import { listItemVariants, defaultTransition, fadeInVariants } from '../lib/animations'; import { FirstConversationPrompt } from './FirstConversationPrompt'; import { MessageSearch } from './MessageSearch'; import { useVirtualizedMessages, type VirtualizedMessageItem, } from '../lib/message-virtualization'; // Default heights for virtualized messages const DEFAULT_MESSAGE_HEIGHTS: Record = { user: 80, assistant: 150, tool: 120, hand: 120, workflow: 100, system: 60, }; // Threshold for enabling virtualization (messages count) const VIRTUALIZATION_THRESHOLD = 100; export function ChatArea() { const { messages, currentAgent, isStreaming, currentModel, sendMessage: sendToGateway, setCurrentModel, initStreamListener, newConversation, } = useChatStore(); const connectionState = useConnectionStore((s) => s.connectionState); const clones = useAgentStore((s) => s.clones); const models = useConfigStore((s) => s.models); const [input, setInput] = useState(''); const [showModelPicker, setShowModelPicker] = useState(false); const scrollRef = useRef(null); const textareaRef = useRef(null); const messageRefs = useRef>(new Map()); // Convert messages to virtualization format const virtualizedMessages: VirtualizedMessageItem[] = useMemo( () => messages.map((msg) => ({ id: msg.id, height: DEFAULT_MESSAGE_HEIGHTS[msg.role] ?? 100, role: msg.role, })), [messages] ); // Use virtualization hook const { listRef, getHeight, setHeight, scrollToBottom, } = useVirtualizedMessages(virtualizedMessages, DEFAULT_MESSAGE_HEIGHTS); // Whether to use virtualization const useVirtualization = messages.length >= VIRTUALIZATION_THRESHOLD; // Get current clone for first conversation prompt const currentClone = useMemo(() => { if (!currentAgent) return null; return clones.find((c) => c.id === currentAgent.id) || null; }, [currentAgent, clones]); // Check if should show first conversation prompt const showFirstPrompt = messages.length === 0 && currentClone && !currentClone.onboardingCompleted; // Handle suggestion click from first conversation prompt const handleSelectSuggestion = (text: string) => { setInput(text); textareaRef.current?.focus(); }; // Auto-resize textarea const adjustTextarea = useCallback(() => { const el = textareaRef.current; if (el) { el.style.height = 'auto'; el.style.height = Math.min(el.scrollHeight, 160) + 'px'; } }, []); // Init agent stream listener on mount useEffect(() => { const unsub = initStreamListener(); return unsub; }, []); // Auto-scroll to bottom on new messages useEffect(() => { if (scrollRef.current && !useVirtualization) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } else if (useVirtualization && messages.length > 0) { scrollToBottom(); } }, [messages, useVirtualization, scrollToBottom]); const handleSend = () => { if (!input.trim() || isStreaming || !connected) return; sendToGateway(input); setInput(''); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); } }; const connected = connectionState === 'connected'; // Navigate to a specific message by ID const handleNavigateToMessage = useCallback((messageId: string) => { const messageEl = messageRefs.current.get(messageId); if (messageEl && scrollRef.current) { messageEl.scrollIntoView({ behavior: 'smooth', block: 'center' }); // Add highlight effect messageEl.classList.add('ring-2', 'ring-orange-400', 'ring-offset-2'); setTimeout(() => { messageEl.classList.remove('ring-2', 'ring-orange-400', 'ring-offset-2'); }, 2000); } }, []); return (
{/* Header */}

{currentAgent?.name || 'ZCLAW'}

{isStreaming ? ( 正在输入中 ) : ( {connected ? 'Gateway 已连接' : 'Gateway 未连接'} )}
{messages.length > 0 && ( )} {messages.length > 0 && ( )}
{/* Messages */}
{messages.length === 0 && ( {showFirstPrompt && currentClone ? ( ) : ( } title="欢迎使用 ZCLAW" description={connected ? '发送消息开始对话' : '请先在设置中连接 Gateway'} /> )} )} {/* Virtualized list for large message counts, smooth scroll for small counts */} {useVirtualization && messages.length > 0 ? ( ) : ( messages.map((message) => ( { if (el) messageRefs.current.set(message.id, el); }} variants={listItemVariants} initial="hidden" animate="visible" layout transition={defaultTransition} > )) )}
{/* Input */}