diff --git a/app/api/trade-followup/route.js b/app/api/trade-followup/route.js new file mode 100644 index 0000000..7d3a6e9 --- /dev/null +++ b/app/api/trade-followup/route.js @@ -0,0 +1,180 @@ +import { NextResponse } from 'next/server' +import OpenAI from 'openai' +import fs from 'fs' +import path from 'path' + +const openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY +}) + +// Helper function to convert image file to base64 +function imageToBase64(imagePath) { + try { + const fullPath = path.join(process.cwd(), 'screenshots', imagePath) + if (fs.existsSync(fullPath)) { + const imageBuffer = fs.readFileSync(fullPath) + return imageBuffer.toString('base64') + } + return null + } catch (error) { + console.error('Error converting image to base64:', error) + return null + } +} + +export async function POST(request) { + try { + const { message, position, screenshots, chatHistory } = await request.json() + + if (!message || !position) { + return NextResponse.json({ + success: false, + error: 'Message and position are required' + }, { status: 400 }) + } + + // Build context about the current position + const positionContext = ` +CURRENT POSITION DETAILS: +- Symbol: ${position.symbol} +- Side: ${position.side} +- Entry Price: $${position.entryPrice} +- Current Price: $${position.currentPrice || 'Unknown'} +- Position Size: ${position.size} +- Current P&L: ${position.pnl > 0 ? '+' : ''}$${position.pnl?.toFixed(2) || 'Unknown'} +- Stop Loss: ${position.stopLoss ? `$${position.stopLoss}` : 'Not set'} +- Take Profit: ${position.takeProfit ? `$${position.takeProfit}` : 'Not set'} +- Entry Time: ${position.entryTime} +- Entry Analysis: ${position.entryAnalysis || 'Not available'} +` + + // Build chat history context + const chatContext = chatHistory?.length > 0 + ? `\n\nRECENT CONVERSATION:\n${chatHistory.map((msg) => + `${msg.type === 'user' ? 'TRADER' : 'ASSISTANT'}: ${msg.content}` + ).join('\n')}` + : '' + + // Analyze screenshots if provided + let screenshotAnalysis = '' + if (screenshots && screenshots.length > 0) { + console.log('šŸ“ø Processing screenshots for analysis:', screenshots.length) + + const screenshotMessages = [] + + for (const screenshot of screenshots) { + // Extract filename from screenshot path/URL + const filename = screenshot.split('/').pop() || screenshot + console.log('šŸ” Processing screenshot:', filename) + + // Convert to base64 + const base64Image = imageToBase64(filename) + if (base64Image) { + screenshotMessages.push({ + type: "image_url", + image_url: { + url: `data:image/png;base64,${base64Image}`, + detail: "high" + } + }) + } else { + console.warn('āš ļø Failed to convert screenshot to base64:', filename) + } + } + + if (screenshotMessages.length > 0) { + console.log('šŸ¤– Sending screenshots to OpenAI for analysis...') + const analysisResponse = await openai.chat.completions.create({ + model: "gpt-4o-mini", + messages: [ + { + role: "system", + content: `You are an expert trading analyst providing real-time trade management advice. + +CURRENT POSITION: ${positionContext} + +Analyze the provided chart screenshots and provide specific guidance on: +1. Current market structure and price action +2. Whether to hold, exit, or adjust the position +3. Stop loss and take profit recommendations +4. Risk assessment based on current conditions +5. Key levels to watch + +Be specific with price levels and actionable advice. Focus on PRACTICAL trade management.` + }, + { + role: "user", + content: [ + { + type: "text", + text: `Analyze these current chart screenshots for my ${position.side} position in ${position.symbol}. What should I do now?` + }, + ...screenshotMessages + ] + } + ], + max_tokens: 1000, + temperature: 0.1 + }) + + screenshotAnalysis = analysisResponse.choices[0]?.message?.content || '' + console.log('āœ… Screenshot analysis completed') + } + } + + // Generate conversational response + const systemPrompt = `You are an expert trading coach helping a trader manage their active position. You have access to: + +${positionContext} +${chatContext} + +${screenshotAnalysis ? `\nLATEST CHART ANALYSIS:\n${screenshotAnalysis}` : ''} + +GUIDELINES: +- Be conversational and supportive +- Give specific, actionable advice +- Use exact price levels when possible +- Consider risk management principles +- Be honest about market uncertainty +- Use emojis appropriately +- Format important information clearly + +The trader is asking: "${message}" + +Provide helpful, specific guidance for their current position.` + + const response = await openai.chat.completions.create({ + model: "gpt-4o-mini", + messages: [ + { + role: "system", + content: systemPrompt + }, + { + role: "user", + content: message + } + ], + max_tokens: 800, + temperature: 0.3 + }) + + const assistantResponse = response.choices[0]?.message?.content + + return NextResponse.json({ + success: true, + response: assistantResponse, + analysis: screenshotAnalysis ? { + timestamp: new Date().toISOString(), + content: screenshotAnalysis + } : null + }) + + } catch (error) { + console.error('Trade follow-up error:', error) + return NextResponse.json({ + success: false, + error: 'Failed to process trade follow-up request' + }, { status: 500 }) + } +} diff --git a/components/AIAnalysisPanel.tsx b/components/AIAnalysisPanel.tsx index ff1f22c..50f379e 100644 --- a/components/AIAnalysisPanel.tsx +++ b/components/AIAnalysisPanel.tsx @@ -2,6 +2,7 @@ import React, { useState, useEffect, useRef } from 'react' import TradeModal from './TradeModal' import ScreenshotGallery from './ScreenshotGallery' +import TradeFollowUpPanel from './TradeFollowUpPanel' const layouts = (process.env.NEXT_PUBLIC_TRADINGVIEW_LAYOUTS || 'ai,Diy module').split(',').map(l => l.trim()) @@ -88,6 +89,7 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP const analysisInProgress = useRef(false) const [tradeModalOpen, setTradeModalOpen] = useState(false) const [tradeModalData, setTradeModalData] = useState(null) + const [followUpPanelOpen, setFollowUpPanelOpen] = useState(false) // Helper function to safely render any value const safeRender = (value: any): string => { @@ -807,6 +809,17 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP )} + + {/* Trade Follow-up Button */} + {/* Results Section */} @@ -1541,6 +1554,13 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP tradeData={tradeModalData} onExecute={executeTrade} /> + + {/* Trade Follow-up Panel */} + {followUpPanelOpen && ( + setFollowUpPanelOpen(false)} + /> + )} ) } diff --git a/components/ScreenshotGallery.tsx b/components/ScreenshotGallery.tsx index b85cc23..d47f9a0 100644 --- a/components/ScreenshotGallery.tsx +++ b/components/ScreenshotGallery.tsx @@ -157,84 +157,154 @@ export default function ScreenshotGallery({ šŸ“ø Chart Screenshots - {sortedData.length === 2 && ( - - Side-by-Side View - - )}
- {sortedData.length} captured • Click to enlarge + {screenshots.length} captured • Click to enlarge
-
- {sortedData.map((item, displayIndex) => { - const imageUrl = formatScreenshotUrl(item.screenshot) - - return ( -
onImageClick(imageUrl)} - > - {/* Preview Image */} -
- {`${symbol} { - const target = e.target as HTMLImageElement - target.style.display = 'none' - const fallback = target.nextElementSibling as HTMLElement - if (fallback) fallback.classList.remove('hidden') - }} - /> -
-
-
šŸ“Š
-
Chart Preview
-
{item.filename}
-
-
+
+ {/* AI Layout Screenshots */} + {groupedData['ai'] && groupedData['ai'].length > 0 && ( +
+
+ šŸ¤– + AI Layout +
+
+ {groupedData['ai'].sort((a: any, b: any) => a.sortOrder - b.sortOrder).map((item: any, displayIndex: number) => { + const imageUrl = formatScreenshotUrl(item.screenshot) - {/* Overlay */} -
-
-
- šŸ” + return ( +
onImageClick(imageUrl)} + > + {/* Preview Image */} +
+ {`${symbol} { + const target = e.target as HTMLImageElement + target.style.display = 'none' + const fallback = target.nextElementSibling as HTMLElement + if (fallback) fallback.classList.remove('hidden') + }} + /> +
+
+
šŸ“Š
+
Chart Preview
+
{item.filename}
+
+
+ + {/* Overlay */} +
+
+
+ šŸ” +
+
+
+
+ + {/* Image Info */} +
+
+
+
{symbol}
+
{item.displayLayout}
+
{item.displayTimeframe} Timeframe
+
+
+ Click to view +
+
-
-
- - {/* Image Info */} -
-
-
-
{symbol}
-
{item.displayLayout}
-
{item.displayTimeframe} Timeframe
-
-
- Click to view -
-
-
+ ) + })}
- ) - })} +
+ )} + + {/* DIY Layout Screenshots */} + {groupedData['Diy module'] && groupedData['Diy module'].length > 0 && ( +
+
+ šŸ”§ + DIY Module +
+
+ {groupedData['Diy module'].sort((a: any, b: any) => a.sortOrder - b.sortOrder).map((item: any, displayIndex: number) => { + const imageUrl = formatScreenshotUrl(item.screenshot) + + return ( +
onImageClick(imageUrl)} + > + {/* Preview Image */} +
+ {`${symbol} { + const target = e.target as HTMLImageElement + target.style.display = 'none' + const fallback = target.nextElementSibling as HTMLElement + if (fallback) fallback.classList.remove('hidden') + }} + /> +
+
+
šŸ“Š
+
Chart Preview
+
{item.filename}
+
+
+ + {/* Overlay */} +
+
+
+ šŸ” +
+
+
+
+ + {/* Image Info */} +
+
+
+
{symbol}
+
{item.displayLayout}
+
{item.displayTimeframe} Timeframe
+
+
+ Click to view +
+
+
+
+ ) + })} +
+
+ )}
diff --git a/components/TradeFollowUpPanel.tsx b/components/TradeFollowUpPanel.tsx new file mode 100644 index 0000000..efbbb08 --- /dev/null +++ b/components/TradeFollowUpPanel.tsx @@ -0,0 +1,346 @@ +"use client" +import React, { useState, useRef, useEffect } from 'react' + +interface TradePosition { + id: string + symbol: string + side: 'LONG' | 'SHORT' + entryPrice: number + currentPrice: number + size: number + pnl: number + stopLoss?: number + takeProfit?: number + entryTime: 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(null) + const [chatMessages, setChatMessages] = useState([]) + const [currentMessage, setCurrentMessage] = useState('') + const [isLoading, setIsLoading] = useState(false) + const [isAnalyzing, setIsAnalyzing] = useState(false) + const chatEndRef = useRef(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].size}\n• Current P&L: ${data.positions[0].pnl > 0 ? '+' : ''}$${data.positions[0].pnl.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
{line.slice(2, -2)}
+ } + if (line.startsWith('• ')) { + return
{line}
+ } + return
{line}
+ }) + } + + const quickActions = [ + "Should I exit now?", + "Update my stop loss", + "Move to break even", + "Current market analysis", + "Risk assessment", + "Take profit strategy" + ] + + return ( +
+
+ {/* Header */} +
+
+ + šŸ’¬ + +
+

Trade Follow-up Assistant

+ {activePosition && ( +

+ {activePosition.symbol} {activePosition.side} • Entry: ${activePosition.entryPrice} +

+ )} +
+
+ +
+ + {/* Chat Messages */} +
+ {chatMessages.map((message) => ( +
+
+ {formatMessage(message.content)} + + {/* Show screenshots if available */} + {message.screenshots && message.screenshots.length > 0 && ( +
+ {message.screenshots.map((screenshot, index) => ( + {`Analysis { + // TODO: Open enlarged view + }} + /> + ))} +
+ )} + +
+ {new Date(message.timestamp).toLocaleTimeString()} +
+
+
+ ))} + + {isAnalyzing && ( +
+
+
+
+ Analyzing market conditions... +
+
+
+ )} + +
+
+ + {/* Quick Actions */} + {activePosition && ( +
+
+
Quick Actions:
+
+ {quickActions.map((action, index) => ( + + ))} +
+
+
+ )} + + {/* Message Input */} +
+
+