import { useState, useRef, useEffect, useMemo } from 'react'; import { createPortal } from 'react-dom'; import { tradingRuntime } from '../lib/runtime'; import { getPlatformAccessToken } from '../lib/authSession'; import { createRequestId } from '../../../shared/request-id.js'; import { Send, X, Bot, User, Check, Loader2, Zap, Copy } from 'lucide-react'; import { Button } from './ui/button'; import { cn } from '../lib/utils'; interface ChatMessage { id: number; role: 'user' | 'assistant'; content: string; profileData?: any; action?: string; timestamp: Date; } interface ChatControlProps { profiles: any[]; onApplyProfile: (action: string, profile: any) => Promise<{ success: boolean; error?: string }>; } export interface QuickAction { label: string; prompt: string; } export const BASE_QUICK_ACTIONS: QuickAction[] = [ { label: 'Conservative Trader', prompt: 'Create a conservative BTC/ETH swing trader with $2000 capital and 1% risk' }, { label: 'Aggressive Scalper', prompt: 'Build an aggressive scalper for SOL/DOGE with $500 capital, 3% risk and all rules enabled' }, { label: 'Low Risk Profile', prompt: 'Create a low-risk profile that only trades BTC during London and NY sessions with $5000 capital' }, { label: 'AI Momentum', prompt: 'Create a momentum strategy with AI analysis enabled, focusing on ETH/SOL with 2% risk' }, { label: 'What rules?', prompt: 'What rules should I use for a day trading strategy?' }, { label: 'Modify existing', prompt: 'Show me my existing profiles and suggest improvements' }, ]; export const cloneProfileDraft = (profileData: any) => ({ ...profileData, strategy_config: profileData?.strategy_config ? JSON.parse(JSON.stringify(profileData.strategy_config)) : undefined, }); export const normalizeName = (value: string) => value.toLowerCase().replace(/[^a-z0-9]/g, ''); export const buildQuickActions = (profiles: any[]): QuickAction[] => { const profileActions: QuickAction[] = []; const highScalper = profiles.find((profileOption) => { const normalized = normalizeName(String(profileOption?.name || '')); return normalized.includes('highriskscalper') || normalized.includes('highscal'); }); const conservativeBag = profiles.find((profileOption) => { const normalized = normalizeName(String(profileOption?.name || '')); return normalized.includes('conservativebag'); }); if (highScalper?.name) { profileActions.push({ label: `Tune ${highScalper.name}`, prompt: `Review my current profile "${highScalper.name}" and recommend improved parameters before applying an update.`, }); } if (conservativeBag?.name) { profileActions.push({ label: `Tune ${conservativeBag.name}`, prompt: `Review my current profile "${conservativeBag.name}" and recommend improved parameters before applying an update.`, }); } return [...profileActions, ...BASE_QUICK_ACTIONS]; }; export const normalizeProfileForApply = (profileData: any) => ({ ...profileData, name: String(profileData?.name || 'AI Profile').trim(), allocated_capital: Number(profileData?.allocated_capital || 0), risk_per_trade_percent: Number(profileData?.risk_per_trade_percent || 0), symbols: String(profileData?.symbols || '').trim(), is_active: profileData?.is_active !== false, }); // 3D Robot SVG Icon const RobotIcon = ({ size = 32 }: { size?: number }) => ( {/* Antenna */} {/* Head */} {/* Eyes */} {/* Mouth */} {/* Body */} {/* Body detail */} {/* Arms */} ); export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => { const [isOpen, setIsOpen] = useState(false); const [messages, setMessages] = useState([ { id: 0, role: 'assistant', content: "Hi! I'm your trading assistant. Tell me what kind of strategy profile you'd like to create or modify, and I'll generate the configuration for you.\n\nTry: \"Create a conservative BTC scalper with $1000 capital\"", timestamp: new Date(), } ]); const [input, setInput] = useState(''); const [isLoading, setIsLoading] = useState(false); const [appliedIds, setAppliedIds] = useState>(new Set()); const [cancelledIds, setCancelledIds] = useState>(new Set()); const [editingIds, setEditingIds] = useState>(new Set()); const [draftProfiles, setDraftProfiles] = useState>({}); const messagesEndRef = useRef(null); const inputRef = useRef(null); const quickActions = useMemo(() => buildQuickActions(profiles), [profiles]); const openDraftEditor = (msg: ChatMessage) => { if (!msg.profileData) return; setDraftProfiles((prev) => ({ ...prev, [msg.id]: cloneProfileDraft(msg.profileData) })); setEditingIds((prev) => new Set(prev).add(msg.id)); }; const closeDraftEditor = (msgId: number) => { setEditingIds((prev) => { const next = new Set(prev); next.delete(msgId); return next; }); }; const resetDraft = (msg: ChatMessage) => { if (!msg.profileData) return; setDraftProfiles((prev) => ({ ...prev, [msg.id]: cloneProfileDraft(msg.profileData) })); }; const updateDraftField = (msgId: number, field: string, value: any) => { setDraftProfiles((prev) => ({ ...prev, [msgId]: { ...(prev[msgId] || {}), [field]: value, }, })); }; useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]); useEffect(() => { if (isOpen) { setTimeout(() => inputRef.current?.focus(), 300); } }, [isOpen]); const sendMessage = async (text?: string) => { const msg = text || input.trim(); if (!msg || isLoading) return; const userMsg: ChatMessage = { id: Date.now(), role: 'user', content: msg, timestamp: new Date(), }; setMessages(prev => [...prev, userMsg]); setInput(''); setIsLoading(true); try { const apiUrl = tradingRuntime.tradingApiUrl; const accessToken = await getPlatformAccessToken(); const res = await fetch(`${apiUrl}/api/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}`, 'x-request-id': createRequestId('web-chat') }, body: JSON.stringify({ message: msg, context: profiles.map(p => ({ id: p.id, name: p.name, allocated_capital: p.allocated_capital, risk_per_trade_percent: p.risk_per_trade_percent, symbols: p.symbols, is_active: p.is_active, strategy_config: p.strategy_config, })), }), }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || 'Chat request failed'); } const data = await res.json(); const assistantMsg: ChatMessage = { id: Date.now() + 1, role: 'assistant', content: data.summary || data.reasoning || 'Profile configuration generated.', profileData: data.profile || null, action: data.action, timestamp: new Date(), }; if (data.reasoning && data.summary) { assistantMsg.content = `${data.summary}\n\n${data.reasoning}`; } setMessages(prev => [...prev, assistantMsg]); } catch (err: any) { setMessages(prev => [...prev, { id: Date.now() + 1, role: 'assistant', content: `Error: ${err.message}`, timestamp: new Date(), }]); } setIsLoading(false); }; const handleApply = async (msg: ChatMessage) => { if (msg.profileData && msg.action) { const activeDraft = draftProfiles[msg.id] || msg.profileData; const payload = normalizeProfileForApply(activeDraft); const result = await onApplyProfile(msg.action, payload); if (result.success) { setAppliedIds(prev => new Set(prev).add(msg.id)); closeDraftEditor(msg.id); setMessages(prev => [...prev, { id: Date.now(), role: 'assistant', content: msg.action === 'create_profile' ? `Profile "${payload.name}" has been created successfully! It's now visible in Strategy Clusters and the bot will pick it up on next sync (~60s).` : `Profile "${payload.name}" has been updated successfully!`, timestamp: new Date(), }]); } else { setMessages(prev => [...prev, { id: Date.now(), role: 'assistant', content: `Failed to ${msg.action === 'create_profile' ? 'create' : 'update'} profile: ${result.error || 'Unknown error'}. Please try again or check your permissions.`, timestamp: new Date(), }]); } } }; const handleCancel = (msgId: number) => { setCancelledIds(prev => new Set(prev).add(msgId)); closeDraftEditor(msgId); setMessages(prev => [...prev, { id: Date.now(), role: 'assistant', content: 'Profile creation cancelled. You can ask me to create a different one or modify the parameters.', timestamp: new Date(), }]); }; const copyJson = (data: any) => { navigator.clipboard.writeText(JSON.stringify(data, null, 2)); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }; const assistantTint = 'var(--accent-soft)'; const panelStyle: React.CSSProperties = { background: 'var(--card)', border: '1px solid var(--border)', boxShadow: '0 25px 80px rgba(0,0,0,0.22)', }; const inputStyle: React.CSSProperties = { background: 'var(--input)', border: '1px solid var(--border)', color: 'var(--foreground)', boxShadow: 'none', caretColor: 'var(--ring)', }; // Floating robot button - bottom right corner (portaled to body to avoid parent CSS issues) if (!isOpen) { return createPortal( , document.body ); } return createPortal( <> {/* Backdrop */}
setIsOpen(false)} style={{ position: 'fixed', inset: 0, zIndex: 999998, background: 'rgba(15, 23, 42, 0.45)', backdropFilter: 'blur(6px)', animation: 'fadeIn 0.15s ease-out', }} />
{/* Header */}

AI Strategy Assistant

Create & manage profiles with natural language

{/* Messages */}
{messages.map(msg => (
{/* Avatar */}
{msg.role === 'user' ? : }
{/* Bubble */}
{msg.content}
{/* Profile preview card */} {msg.profileData && msg.action !== 'explain' && (() => { const activeProfileData = draftProfiles[msg.id] || msg.profileData; const isEditing = editingIds.has(msg.id); const activeRules = Array.isArray(activeProfileData?.strategy_config?.rules) ? activeProfileData.strategy_config.rules.filter((r: any) => r?.enabled).length : 0; return (
{msg.action === 'create_profile' ? 'New Profile' : 'Update Profile'}
Name {activeProfileData?.name}
{activeProfileData?.allocated_capital ? (
Capital ${activeProfileData.allocated_capital}
) : null} {activeProfileData?.risk_per_trade_percent ? (
Risk / Trade {activeProfileData.risk_per_trade_percent}%
) : null} {activeProfileData?.symbols ? (
Symbols {activeProfileData.symbols}
) : null} {activeRules > 0 ? (
Rules {activeRules} active
) : null}
{isEditing ? (
Edit Parameters Before Apply
updateDraftField(msg.id, 'name', e.target.value)} placeholder="Profile Name" className="w-full rounded-lg px-2.5 py-1.5 text-[11px] outline-none" style={inputStyle} />
updateDraftField(msg.id, 'allocated_capital', e.target.value)} placeholder="Capital" className="w-full rounded-lg px-2.5 py-1.5 text-[11px] outline-none" style={inputStyle} /> updateDraftField(msg.id, 'risk_per_trade_percent', e.target.value)} placeholder="Risk %" className="w-full rounded-lg px-2.5 py-1.5 text-[11px] outline-none" style={inputStyle} />
updateDraftField(msg.id, 'symbols', e.target.value)} placeholder="Symbols (e.g. BTC/USDT,ETH/USDT)" className="w-full rounded-lg px-2.5 py-1.5 text-[11px] outline-none" style={inputStyle} />
Auto Trading
) : null} {!appliedIds.has(msg.id) && !cancelledIds.has(msg.id) ? (
) : cancelledIds.has(msg.id) ? (
Cancelled
) : (
Applied
)}
); })()} {msg.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
))} {/* Suggested quick actions - shown when only welcome message exists */} {messages.length <= 1 && !isLoading && (

Quick Actions

{quickActions.map((action, i) => ( ))}
)} {isLoading && (
Generating configuration...
)}
{/* Input area */}