import { useState, useRef, useEffect, useCallback } from 'react' import { motion, AnimatePresence } from 'framer-motion' import { PanelRightClose, PanelRightOpen, TrendingUp, Wallet, Bot, ChevronDown, ChevronRight, Send, Sparkles, } from 'lucide-react' import { useLanguage } from '../contexts/LanguageContext' import { MarketTicker } from '../components/agent/MarketTicker' import { PositionsPanel } from '../components/agent/PositionsPanel' import { TraderStatusPanel } from '../components/agent/TraderStatusPanel' interface Message { id: string role: 'user' | 'bot' text: string time: string streaming?: boolean } // Simple markdown-ish renderer: bold, code, newlines function renderMessageContent(text: string) { // Split by code blocks first const parts = text.split(/(```[\s\S]*?```|`[^`]+`)/g) return parts.map((part, i) => { if (part.startsWith('```') && part.endsWith('```')) { const code = part.slice(3, -3).replace(/^\w+\n/, '') // strip language hint return (
          {code}
        
) } if (part.startsWith('`') && part.endsWith('`')) { return ( {part.slice(1, -1)} ) } // Handle bold **text** const boldParts = part.split(/(\*\*[^*]+\*\*)/g) return boldParts.map((bp, j) => { if (bp.startsWith('**') && bp.endsWith('**')) { return ( {bp.slice(2, -2)} ) } return {bp} }) }) } let msgIdCounter = 0 function nextId() { return `msg-${Date.now()}-${++msgIdCounter}` } export function AgentChatPage() { const { language } = useLanguage() const [sidebarOpen, setSidebarOpen] = useState(() => window.innerWidth > 1024) const [messages, setMessages] = useState([ { id: nextId(), role: 'bot', text: language === 'zh' ? '👋 你好!我是 NOFXi,你的 AI 交易 Agent。\n\n跟我说话就行,我能帮你分析市场、管理交易、配置策略。\n\n试试: "分析一下BTC" 或 /help' : "👋 Hi! I'm NOFXi, your AI trading agent.\n\nJust talk to me. I can analyze markets, manage trades, and configure strategies.\n\nTry: \"Analyze BTC\" or /help", time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', }), }, ]) const [input, setInput] = useState('') const [loading, setLoading] = useState(false) const [composing, setComposing] = useState(false) const messagesEndRef = useRef(null) const inputRef = useRef(null) // Sidebar section collapse state const [sections, setSections] = useState({ market: true, positions: true, traders: true, }) 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) }, []) // Auto-resize textarea const handleInputChange = useCallback( (e: React.ChangeEvent) => { setInput(e.target.value) const el = e.target el.style.height = 'auto' el.style.height = Math.min(el.scrollHeight, 120) + 'px' }, [] ) const send = async (text?: string) => { const msg = text || input.trim() if (!msg || loading) return setInput('') if (inputRef.current) { inputRef.current.style.height = 'auto' } const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', }) const userMsg: Message = { id: nextId(), role: 'user', text: msg, time } const botId = nextId() setMessages((prev) => [ ...prev, userMsg, { id: botId, role: 'bot', text: '', time: '', streaming: true, }, ]) setLoading(true) try { const res = await fetch('/api/agent/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: msg, user_id: 1, lang: language }), }) const data = await res.json() const responseText = data.response || data.error || 'No response' // Simulate streaming by revealing text progressively const words = responseText.split(/(\s+)/) let displayed = '' for (let i = 0; i < words.length; i++) { displayed += words[i] 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 < words.length - 1) { await new Promise((r) => setTimeout(r, 12)) } } // Mark streaming done setMessages((prev) => prev.map((m) => (m.id === botId ? { ...m, streaming: false } : m)) ) } catch (e: any) { 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) inputRef.current?.focus() } const quickActions = language === 'zh' ? [ { label: '📊 分析 BTC', cmd: '/analyze BTC' }, { label: '📊 分析 ETH', cmd: '/analyze ETH' }, { label: '💼 持仓', cmd: '/positions' }, { label: '💰 余额', cmd: '/balance' }, { label: '📋 Traders', cmd: '/traders' }, { label: '❓ 帮助', cmd: '/help' }, ] : [ { label: '📊 Analyze BTC', cmd: '/analyze BTC' }, { label: '📊 Analyze ETH', cmd: '/analyze ETH' }, { label: '💼 Positions', cmd: '/positions' }, { label: '💰 Balance', cmd: '/balance' }, { label: '📋 Traders', cmd: '/traders' }, { 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: language === 'zh' ? 'Traders' : 'Traders', component: , }, ] return (
{/* ==================== MAIN CHAT AREA ==================== */}
{/* Quick actions bar */}
{quickActions.map((a, i) => ( ))} {/* Sidebar toggle (desktop) */}
{/* Messages area */}
{messages.map((m) => ( {/* Avatar */}
{m.role === 'user' ? '👤' : '⚡'}
{/* Message bubble */}
{m.role === 'bot' ? renderMessageContent(m.text) : m.text} {m.streaming && ( )}
{m.time && (
{m.role === 'bot' && 'NOFXi · '} {m.time}
)}
))}
{/* Input area */}
{ ;(e.currentTarget as HTMLElement).style.borderColor = '#F0B90B40' }} onBlur={(e) => { ;(e.currentTarget as HTMLElement).style.borderColor = '#1f1f2c' }} >