Files
trading_bot_v3/components/TradeFollowUpPanel.tsx
mindesbunister 7c4e9d0122 fix: TradeFollowUpPanel property mismatch causing position loading error
- 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
2025-07-17 16:28:17 +02:00

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>
)
}