import { useState, useRef, useEffect } from 'react' import { motion, AnimatePresence } from 'framer-motion' import { PanelRightClose, PanelRightOpen, TrendingUp, Wallet, Bot, ChevronDown, ChevronRight, } from 'lucide-react' import { useLanguage } from '../contexts/LanguageContext' import { useAuth } from '../contexts/AuthContext' import { MarketTicker } from '../components/agent/MarketTicker' import { PositionsPanel } from '../components/agent/PositionsPanel' import { TraderStatusPanel } from '../components/agent/TraderStatusPanel' import { WelcomeScreen } from '../components/agent/WelcomeScreen' import { ChatMessages } from '../components/agent/ChatMessages' import { ChatInput, type ChatInputHandle } from '../components/agent/ChatInput' interface Message { id: string role: 'user' | 'bot' text: string time: string streaming?: boolean } let msgIdCounter = 0 function nextId() { return `msg-${Date.now()}-${++msgIdCounter}` } export function AgentChatPage() { const { language } = useLanguage() const { token } = useAuth() const [sidebarOpen, setSidebarOpen] = useState(() => window.innerWidth > 1024) const [messages, setMessages] = useState([]) const [loading, setLoading] = useState(false) const messagesEndRef = useRef(null) const chatInputRef = useRef(null) const abortRef = useRef(null) // Sidebar section collapse state const [sections, setSections] = useState({ market: true, positions: true, traders: false, }) const toggleSection = (key: keyof typeof sections) => { setSections((prev) => ({ ...prev, [key]: !prev[key] })) } // Auto-scroll useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [messages]) // Responsive sidebar useEffect(() => { const handleResize = () => { if (window.innerWidth <= 768) setSidebarOpen(false) } window.addEventListener('resize', handleResize) return () => window.removeEventListener('resize', handleResize) }, []) // Escape to close sidebar on mobile useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape' && window.innerWidth <= 768) { setSidebarOpen(false) } } window.addEventListener('keydown', handleKeyDown) return () => window.removeEventListener('keydown', handleKeyDown) }, []) const send = async (text: string) => { if (!text || loading) return const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', }) const userMsg: Message = { id: nextId(), role: 'user', text, time } const botId = nextId() setMessages((prev) => [ ...prev, userMsg, { id: botId, role: 'bot', text: '', time: '', streaming: true, }, ]) setLoading(true) try { // Abort any in-flight request abortRef.current?.abort() const controller = new AbortController() abortRef.current = controller const res = await fetch('/api/agent/chat', { method: 'POST', headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}), }, body: JSON.stringify({ message: text, lang: language }), signal: controller.signal, }) if (!res.ok) { const errData = await res.json().catch(() => ({})) throw new Error(errData.error || `Server error (${res.status})`) } const data = await res.json() const responseText = data.response || data.error || 'No response' // Simulate streaming by revealing text in chunks (batched to reduce renders) const words = responseText.split(/(\s+)/) const chunkSize = Math.max(3, Math.ceil(words.length / 40)) // ~40 frames max let displayed = '' for (let i = 0; i < words.length; i += chunkSize) { const chunk = words.slice(i, i + chunkSize).join('') displayed += chunk const current = displayed setMessages((prev) => prev.map((m) => m.id === botId ? { ...m, text: current, time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', }), } : m ) ) if (i + chunkSize < words.length) { await new Promise((r) => setTimeout(r, 20)) } } // Mark streaming done setMessages((prev) => prev.map((m) => (m.id === botId ? { ...m, streaming: false } : m)) ) } catch (e: any) { if (e.name === 'AbortError') { // Request was cancelled (e.g. user sent a new message), clean up silently setMessages((prev) => prev.filter((m) => m.id !== botId)) } else { setMessages((prev) => prev.map((m) => m.id === botId ? { ...m, text: '⚠️ Error: ' + e.message, time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', }), streaming: false, } : m ) ) } } setLoading(false) chatInputRef.current?.focus() } const quickActions = language === 'zh' ? [ { label: '💼 持仓', cmd: '/positions' }, { label: '💰 余额', cmd: '/balance' }, { label: '📋 Traders', cmd: '/traders' }, { label: '🧹 清除记忆', cmd: '/clear' }, { label: '❓ 帮助', cmd: '/help' }, ] : [ { label: '💼 Positions', cmd: '/positions' }, { label: '💰 Balance', cmd: '/balance' }, { label: '📋 Traders', cmd: '/traders' }, { label: '🧹 Clear', cmd: '/clear' }, { label: '❓ Help', cmd: '/help' }, ] const sidebarSections = [ { key: 'market' as const, icon: , title: language === 'zh' ? '市场行情' : 'Market', component: , }, { key: 'positions' as const, icon: , title: language === 'zh' ? '持仓' : 'Positions', component: , }, { key: 'traders' as const, icon: , title: 'Traders', component: , }, ] const isWelcomeState = messages.length === 0 return (
{/* ==================== MAIN CHAT AREA ==================== */}
{/* Top bar with quick actions */}
{quickActions.map((a, i) => ( ))}
{/* Messages area or Welcome state */}
{isWelcomeState ? ( ) : ( )}
{/* Input area */}
{/* ==================== RIGHT SIDEBAR ==================== */} {sidebarOpen && (
{/* Sidebar header */}
{language === 'zh' ? '交易面板' : 'Trading Panel'}
{/* Sidebar sections */} {sidebarSections.map((section) => (
{sections[section.key] && (
{section.component}
)}
))}
)}
{/* Animations */}
) }