- Fix property names: size → amount, pnl → unrealizedPnl - Update TradePosition interface to match actual API response - Add missing properties: pnlPercentage, totalValue, timestamp, status, leverage, txId - Resolves 'Error Loading Positions' issue when accessing marked trades - Follow-up assistant now properly displays position information
352 lines
13 KiB
TypeScript
352 lines
13 KiB
TypeScript
"use client"
|
|
import React, { useState, useRef, useEffect } from 'react'
|
|
|
|
interface TradePosition {
|
|
id: string
|
|
symbol: string
|
|
side: 'LONG' | 'SHORT'
|
|
entryPrice: number
|
|
currentPrice: number
|
|
amount: number
|
|
unrealizedPnl: number
|
|
pnlPercentage: number
|
|
totalValue: number
|
|
stopLoss?: number
|
|
takeProfit?: number
|
|
timestamp: number
|
|
status: string
|
|
leverage: number
|
|
txId: string
|
|
entryAnalysis?: string
|
|
}
|
|
|
|
interface ChatMessage {
|
|
id: string
|
|
type: 'user' | 'assistant' | 'system'
|
|
content: string
|
|
timestamp: string
|
|
analysis?: any
|
|
screenshots?: string[]
|
|
}
|
|
|
|
interface TradeFollowUpPanelProps {
|
|
onClose: () => void
|
|
}
|
|
|
|
export default function TradeFollowUpPanel({ onClose }: TradeFollowUpPanelProps) {
|
|
const [activePosition, setActivePosition] = useState<TradePosition | null>(null)
|
|
const [chatMessages, setChatMessages] = useState<ChatMessage[]>([])
|
|
const [currentMessage, setCurrentMessage] = useState('')
|
|
const [isLoading, setIsLoading] = useState(false)
|
|
const [isAnalyzing, setIsAnalyzing] = useState(false)
|
|
const chatEndRef = useRef<HTMLDivElement>(null)
|
|
|
|
// Auto-scroll to bottom of chat
|
|
useEffect(() => {
|
|
chatEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
|
}, [chatMessages])
|
|
|
|
// Load active positions on mount
|
|
useEffect(() => {
|
|
loadActivePositions()
|
|
}, [])
|
|
|
|
const loadActivePositions = async () => {
|
|
try {
|
|
const response = await fetch('/api/trading/positions')
|
|
const data = await response.json()
|
|
|
|
if (data.success && data.positions?.length > 0) {
|
|
// For now, take the first active position
|
|
// TODO: Add position selector if multiple positions
|
|
setActivePosition(data.positions[0])
|
|
|
|
// Add welcome message
|
|
setChatMessages([{
|
|
id: Date.now().toString(),
|
|
type: 'system',
|
|
content: `🎯 **Trade Follow-up Assistant**\n\nI'm here to help you manage your active ${data.positions[0].symbol} ${data.positions[0].side} position.\n\n**Current Position:**\n• Entry: $${data.positions[0].entryPrice}\n• Size: ${data.positions[0].amount}\n• Current P&L: ${data.positions[0].unrealizedPnl > 0 ? '+' : ''}$${data.positions[0].unrealizedPnl.toFixed(2)}\n\nAsk me anything about your trade!`,
|
|
timestamp: new Date().toISOString()
|
|
}])
|
|
} else {
|
|
setChatMessages([{
|
|
id: Date.now().toString(),
|
|
type: 'system',
|
|
content: '⚠️ **No Active Positions Found**\n\nI don\'t see any active positions to analyze. Please enter a trade first, then come back for follow-up analysis.',
|
|
timestamp: new Date().toISOString()
|
|
}])
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading positions:', error)
|
|
setChatMessages([{
|
|
id: Date.now().toString(),
|
|
type: 'system',
|
|
content: '❌ **Error Loading Positions**\n\nCouldn\'t load your active positions. Please try again.',
|
|
timestamp: new Date().toISOString()
|
|
}])
|
|
}
|
|
}
|
|
|
|
const handleSendMessage = async () => {
|
|
if (!currentMessage.trim() || isLoading) return
|
|
|
|
const userMessage: ChatMessage = {
|
|
id: Date.now().toString(),
|
|
type: 'user',
|
|
content: currentMessage,
|
|
timestamp: new Date().toISOString()
|
|
}
|
|
|
|
setChatMessages(prev => [...prev, userMessage])
|
|
setCurrentMessage('')
|
|
setIsLoading(true)
|
|
|
|
try {
|
|
// Check if user is asking for updated analysis
|
|
const needsScreenshot = currentMessage.toLowerCase().includes('analysis') ||
|
|
currentMessage.toLowerCase().includes('update') ||
|
|
currentMessage.toLowerCase().includes('current') ||
|
|
currentMessage.toLowerCase().includes('now')
|
|
|
|
let screenshots: string[] = []
|
|
|
|
if (needsScreenshot && activePosition) {
|
|
setIsAnalyzing(true)
|
|
|
|
// Add thinking message
|
|
const thinkingMessage: ChatMessage = {
|
|
id: (Date.now() + 1).toString(),
|
|
type: 'assistant',
|
|
content: '🔄 **Getting Updated Analysis...**\n\nCapturing fresh screenshots and analyzing current market conditions for your position...',
|
|
timestamp: new Date().toISOString()
|
|
}
|
|
setChatMessages(prev => [...prev, thinkingMessage])
|
|
|
|
// Get fresh screenshots
|
|
const screenshotResponse = await fetch('/api/enhanced-screenshot', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
symbol: activePosition.symbol,
|
|
timeframe: '240', // 4h default for trade follow-up
|
|
layouts: ['ai', 'diy'],
|
|
analyze: false // We'll analyze separately with trade context
|
|
})
|
|
})
|
|
|
|
const screenshotData = await screenshotResponse.json()
|
|
if (screenshotData.success && screenshotData.screenshots) {
|
|
screenshots = screenshotData.screenshots
|
|
}
|
|
|
|
setIsAnalyzing(false)
|
|
}
|
|
|
|
// Send to trade follow-up API
|
|
const response = await fetch('/api/trade-followup', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
message: currentMessage,
|
|
position: activePosition,
|
|
screenshots: screenshots,
|
|
chatHistory: chatMessages.slice(-5) // Last 5 messages for context
|
|
})
|
|
})
|
|
|
|
const data = await response.json()
|
|
|
|
if (data.success) {
|
|
const assistantMessage: ChatMessage = {
|
|
id: (Date.now() + 2).toString(),
|
|
type: 'assistant',
|
|
content: data.response,
|
|
timestamp: new Date().toISOString(),
|
|
analysis: data.analysis,
|
|
screenshots: screenshots
|
|
}
|
|
|
|
setChatMessages(prev => [...prev.filter(m => !m.content.includes('Getting Updated Analysis')), assistantMessage])
|
|
} else {
|
|
throw new Error(data.error || 'Failed to get response')
|
|
}
|
|
} catch (error) {
|
|
console.error('Error sending message:', error)
|
|
const errorMessage: ChatMessage = {
|
|
id: (Date.now() + 3).toString(),
|
|
type: 'assistant',
|
|
content: '❌ **Error**\n\nSorry, I encountered an error. Please try again.',
|
|
timestamp: new Date().toISOString()
|
|
}
|
|
setChatMessages(prev => [...prev.filter(m => !m.content.includes('Getting Updated Analysis')), errorMessage])
|
|
} finally {
|
|
setIsLoading(false)
|
|
setIsAnalyzing(false)
|
|
}
|
|
}
|
|
|
|
const handleKeyPress = (e: React.KeyboardEvent) => {
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
e.preventDefault()
|
|
handleSendMessage()
|
|
}
|
|
}
|
|
|
|
const formatMessage = (content: string) => {
|
|
// Convert markdown-style formatting to JSX
|
|
return content.split('\n').map((line, index) => {
|
|
if (line.startsWith('**') && line.endsWith('**')) {
|
|
return <div key={index} className="font-bold text-purple-300 mb-2">{line.slice(2, -2)}</div>
|
|
}
|
|
if (line.startsWith('• ')) {
|
|
return <div key={index} className="ml-4 text-gray-300">{line}</div>
|
|
}
|
|
return <div key={index} className="text-gray-300">{line}</div>
|
|
})
|
|
}
|
|
|
|
const quickActions = [
|
|
"Should I exit now?",
|
|
"Update my stop loss",
|
|
"Move to break even",
|
|
"Current market analysis",
|
|
"Risk assessment",
|
|
"Take profit strategy"
|
|
]
|
|
|
|
return (
|
|
<div className="fixed inset-0 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4 z-50">
|
|
<div className="bg-gradient-to-br from-gray-900 to-purple-900/20 border border-purple-500/30 rounded-xl w-full max-w-4xl h-[80vh] flex flex-col">
|
|
{/* Header */}
|
|
<div className="p-4 border-b border-purple-500/30 flex items-center justify-between">
|
|
<div className="flex items-center">
|
|
<span className="w-8 h-8 bg-gradient-to-br from-purple-400 to-purple-600 rounded-lg flex items-center justify-center mr-3 text-lg">
|
|
💬
|
|
</span>
|
|
<div>
|
|
<h3 className="text-lg font-bold text-white">Trade Follow-up Assistant</h3>
|
|
{activePosition && (
|
|
<p className="text-sm text-purple-300">
|
|
{activePosition.symbol} {activePosition.side} • Entry: ${activePosition.entryPrice}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={onClose}
|
|
className="w-8 h-8 bg-red-500/20 hover:bg-red-500/40 rounded-lg flex items-center justify-center text-red-400 transition-colors"
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
|
|
{/* Chat Messages */}
|
|
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
|
{chatMessages.map((message) => (
|
|
<div
|
|
key={message.id}
|
|
className={`flex ${message.type === 'user' ? 'justify-end' : 'justify-start'}`}
|
|
>
|
|
<div
|
|
className={`max-w-[80%] p-3 rounded-lg ${
|
|
message.type === 'user'
|
|
? 'bg-purple-600 text-white'
|
|
: message.type === 'system'
|
|
? 'bg-blue-600/20 border border-blue-500/30 text-blue-300'
|
|
: 'bg-gray-800 text-gray-300'
|
|
}`}
|
|
>
|
|
{formatMessage(message.content)}
|
|
|
|
{/* Show screenshots if available */}
|
|
{message.screenshots && message.screenshots.length > 0 && (
|
|
<div className="mt-3 grid grid-cols-2 gap-2">
|
|
{message.screenshots.map((screenshot, index) => (
|
|
<img
|
|
key={index}
|
|
src={`/api/image?file=${screenshot.split('/').pop()}`}
|
|
alt={`Analysis screenshot ${index + 1}`}
|
|
className="w-full rounded border border-gray-600 cursor-pointer hover:border-purple-500/50 transition-colors"
|
|
onClick={() => {
|
|
// TODO: Open enlarged view
|
|
}}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
<div className="text-xs opacity-60 mt-2">
|
|
{new Date(message.timestamp).toLocaleTimeString()}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{isAnalyzing && (
|
|
<div className="flex justify-start">
|
|
<div className="bg-purple-600/20 border border-purple-500/30 p-3 rounded-lg">
|
|
<div className="flex items-center space-x-2">
|
|
<div className="animate-spin w-4 h-4 border-2 border-purple-400 border-t-transparent rounded-full"></div>
|
|
<span className="text-purple-300">Analyzing market conditions...</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div ref={chatEndRef} />
|
|
</div>
|
|
|
|
{/* Quick Actions */}
|
|
{activePosition && (
|
|
<div className="p-4 border-t border-purple-500/30">
|
|
<div className="mb-3">
|
|
<div className="text-xs text-gray-400 mb-2">Quick Actions:</div>
|
|
<div className="flex flex-wrap gap-2">
|
|
{quickActions.map((action, index) => (
|
|
<button
|
|
key={index}
|
|
onClick={() => setCurrentMessage(action)}
|
|
className="px-3 py-1 bg-purple-600/20 hover:bg-purple-600/40 border border-purple-500/30 rounded-full text-xs text-purple-300 transition-colors"
|
|
>
|
|
{action}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Message Input */}
|
|
<div className="p-4 border-t border-purple-500/30">
|
|
<div className="flex items-center space-x-3">
|
|
<textarea
|
|
value={currentMessage}
|
|
onChange={(e) => setCurrentMessage(e.target.value)}
|
|
onKeyPress={handleKeyPress}
|
|
placeholder={
|
|
activePosition
|
|
? "Ask about your trade: 'Should I exit?', 'Update analysis', 'Move stop loss'..."
|
|
: "No active positions to analyze"
|
|
}
|
|
disabled={!activePosition || isLoading}
|
|
className="flex-1 bg-gray-800 border border-gray-600 rounded-lg px-4 py-2 text-white placeholder-gray-400 focus:border-purple-500 focus:outline-none resize-none"
|
|
rows={2}
|
|
/>
|
|
<button
|
|
onClick={handleSendMessage}
|
|
disabled={!currentMessage.trim() || isLoading || !activePosition}
|
|
className="w-10 h-10 bg-purple-600 hover:bg-purple-700 disabled:bg-gray-600 disabled:cursor-not-allowed rounded-lg flex items-center justify-center text-white transition-colors"
|
|
>
|
|
{isLoading ? (
|
|
<div className="animate-spin w-4 h-4 border-2 border-white border-t-transparent rounded-full"></div>
|
|
) : (
|
|
'📤'
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|