feat: Add interactive Trade Follow-up Assistant with ChatGPT integration
- Interactive chat interface to ask questions about active trades - Automatic position detection and context-aware responses - Fresh screenshot capture with updated market analysis - Smart conversation flow with trade-specific insights - Quick action buttons for common trade management questions - TradeFollowUpPanel.tsx: Full-featured chat interface with position tracking - /api/trade-followup: GPT-4o mini integration with screenshot analysis - Enhanced AIAnalysisPanel with Follow-up button integration - 'Should I exit now?' - Real-time exit recommendations - 'Update my stop loss' - SL adjustment guidance based on current conditions - 'Move to break even' - Risk-free position management - 'Current market analysis' - Fresh chart analysis with updated screenshots - 'Risk assessment' - Position risk evaluation - 'Take profit strategy' - TP optimization recommendations - Enter trade based on AI analysis → Use Follow-up for ongoing management - Ask specific questions: 'Is this still a valid setup?' - Get updated analysis: 'What do the charts look like now?' - Risk management: 'Should I move my stop loss?' - Exit timing: 'Is this a good time to take profits?' The assistant provides context-aware guidance by: Tracking your current position details (entry, size, P&L) Capturing fresh screenshots when needed for updated analysis Combining position context with current market conditions Providing specific price levels and actionable advice Maintaining conversation history for continuity Perfect for traders who want ongoing AI guidance throughout their trades!
This commit is contained in:
180
app/api/trade-followup/route.js
Normal file
180
app/api/trade-followup/route.js
Normal file
@@ -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 })
|
||||
}
|
||||
}
|
||||
@@ -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<any>(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
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Trade Follow-up Button */}
|
||||
<button
|
||||
className="w-full py-3 px-6 rounded-lg font-semibold transition-all duration-300 bg-gradient-to-r from-green-600 to-emerald-600 hover:from-green-700 hover:to-emerald-700 text-white transform hover:scale-[1.02] active:scale-[0.98] border border-green-500/30"
|
||||
onClick={() => setFollowUpPanelOpen(true)}
|
||||
>
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
<span>💬</span>
|
||||
<span>Trade Follow-up Assistant</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Results Section */}
|
||||
@@ -1541,6 +1554,13 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
|
||||
tradeData={tradeModalData}
|
||||
onExecute={executeTrade}
|
||||
/>
|
||||
|
||||
{/* Trade Follow-up Panel */}
|
||||
{followUpPanelOpen && (
|
||||
<TradeFollowUpPanel
|
||||
onClose={() => setFollowUpPanelOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -157,84 +157,154 @@ export default function ScreenshotGallery({
|
||||
📸
|
||||
</span>
|
||||
Chart Screenshots
|
||||
{sortedData.length === 2 && (
|
||||
<span className="ml-2 text-xs bg-purple-500/20 text-purple-300 px-2 py-1 rounded-full border border-purple-500/30">
|
||||
Side-by-Side View
|
||||
</span>
|
||||
)}
|
||||
</h4>
|
||||
<div className="text-xs text-gray-400">
|
||||
{sortedData.length} captured • Click to enlarge
|
||||
{screenshots.length} captured • Click to enlarge
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`grid gap-4 ${
|
||||
sortedData.length === 1 ? 'grid-cols-1 max-w-md mx-auto' :
|
||||
sortedData.length === 2 ? 'grid-cols-1 md:grid-cols-2' :
|
||||
sortedData.length === 3 ? 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3' :
|
||||
sortedData.length === 4 ? 'grid-cols-1 md:grid-cols-2 xl:grid-cols-4' :
|
||||
'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4'
|
||||
}`}>
|
||||
{sortedData.map((item, displayIndex) => {
|
||||
const imageUrl = formatScreenshotUrl(item.screenshot)
|
||||
<div className="space-y-6">
|
||||
{/* AI Layout Screenshots */}
|
||||
{groupedData['ai'] && groupedData['ai'].length > 0 && (
|
||||
<div>
|
||||
<h5 className="text-sm font-medium text-purple-300 mb-3 flex items-center">
|
||||
<span className="w-4 h-4 bg-gradient-to-br from-blue-400 to-blue-600 rounded mr-2 flex items-center justify-center text-xs">🤖</span>
|
||||
AI Layout
|
||||
</h5>
|
||||
<div className={`grid gap-4 ${
|
||||
groupedData['ai'].length === 1 ? 'grid-cols-1' :
|
||||
'grid-cols-1 md:grid-cols-2'
|
||||
}`}>
|
||||
{groupedData['ai'].sort((a: any, b: any) => a.sortOrder - b.sortOrder).map((item: any, displayIndex: number) => {
|
||||
const imageUrl = formatScreenshotUrl(item.screenshot)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={displayIndex}
|
||||
className={`group relative bg-gray-800/30 rounded-lg overflow-hidden border border-gray-700 hover:border-purple-500/50 transition-all cursor-pointer transform hover:scale-[1.02] ${
|
||||
sortedData.length === 2 ? 'lg:aspect-[4/3]' : ''
|
||||
}`}
|
||||
onClick={() => onImageClick(imageUrl)}
|
||||
>
|
||||
{/* Preview Image */}
|
||||
<div className={`bg-gray-800 flex items-center justify-center relative ${
|
||||
sortedData.length === 2 ? 'aspect-[16/10] lg:aspect-[4/3]' : 'aspect-video'
|
||||
}`}>
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt={`${symbol} - ${item.displayLayout} - ${item.displayTimeframe} chart`}
|
||||
className="w-full h-full object-cover"
|
||||
onError={(e: any) => {
|
||||
const target = e.target as HTMLImageElement
|
||||
target.style.display = 'none'
|
||||
const fallback = target.nextElementSibling as HTMLElement
|
||||
if (fallback) fallback.classList.remove('hidden')
|
||||
}}
|
||||
/>
|
||||
<div className="hidden absolute inset-0 flex items-center justify-center text-gray-400">
|
||||
<div className="text-center">
|
||||
<div className="text-3xl mb-2">📊</div>
|
||||
<div className="text-sm">Chart Preview</div>
|
||||
<div className="text-xs text-gray-500">{item.filename}</div>
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<div
|
||||
key={`ai-${displayIndex}`}
|
||||
className="group relative bg-gray-800/30 rounded-lg overflow-hidden border border-gray-700 hover:border-purple-500/50 transition-all cursor-pointer transform hover:scale-[1.02]"
|
||||
onClick={() => onImageClick(imageUrl)}
|
||||
>
|
||||
{/* Preview Image */}
|
||||
<div className="aspect-video bg-gray-800 flex items-center justify-center relative">
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt={`${symbol} - ${item.displayLayout} - ${item.displayTimeframe} chart`}
|
||||
className="w-full h-full object-cover"
|
||||
onError={(e: any) => {
|
||||
const target = e.target as HTMLImageElement
|
||||
target.style.display = 'none'
|
||||
const fallback = target.nextElementSibling as HTMLElement
|
||||
if (fallback) fallback.classList.remove('hidden')
|
||||
}}
|
||||
/>
|
||||
<div className="hidden absolute inset-0 flex items-center justify-center text-gray-400">
|
||||
<div className="text-center">
|
||||
<div className="text-3xl mb-2">📊</div>
|
||||
<div className="text-sm">Chart Preview</div>
|
||||
<div className="text-xs text-gray-500">{item.filename}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Overlay */}
|
||||
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-all flex items-center justify-center">
|
||||
<div className="opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<div className="w-12 h-12 bg-purple-500/80 rounded-full flex items-center justify-center">
|
||||
<span className="text-white text-xl">🔍</span>
|
||||
{/* Overlay */}
|
||||
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-all flex items-center justify-center">
|
||||
<div className="opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<div className="w-12 h-12 bg-purple-500/80 rounded-full flex items-center justify-center">
|
||||
<span className="text-white text-xl">🔍</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Image Info */}
|
||||
<div className="p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-white">{symbol}</div>
|
||||
<div className="text-xs text-purple-300">{item.displayLayout}</div>
|
||||
<div className="text-xs text-gray-400">{item.displayTimeframe} Timeframe</div>
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
Click to view
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Image Info */}
|
||||
<div className="p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-white">{symbol}</div>
|
||||
<div className="text-xs text-purple-300">{item.displayLayout}</div>
|
||||
<div className="text-xs text-gray-400">{item.displayTimeframe} Timeframe</div>
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
Click to view
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* DIY Layout Screenshots */}
|
||||
{groupedData['Diy module'] && groupedData['Diy module'].length > 0 && (
|
||||
<div>
|
||||
<h5 className="text-sm font-medium text-green-300 mb-3 flex items-center">
|
||||
<span className="w-4 h-4 bg-gradient-to-br from-green-400 to-green-600 rounded mr-2 flex items-center justify-center text-xs">🔧</span>
|
||||
DIY Module
|
||||
</h5>
|
||||
<div className={`grid gap-4 ${
|
||||
groupedData['Diy module'].length === 1 ? 'grid-cols-1' :
|
||||
'grid-cols-1 md:grid-cols-2'
|
||||
}`}>
|
||||
{groupedData['Diy module'].sort((a: any, b: any) => a.sortOrder - b.sortOrder).map((item: any, displayIndex: number) => {
|
||||
const imageUrl = formatScreenshotUrl(item.screenshot)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`diy-${displayIndex}`}
|
||||
className="group relative bg-gray-800/30 rounded-lg overflow-hidden border border-gray-700 hover:border-green-500/50 transition-all cursor-pointer transform hover:scale-[1.02]"
|
||||
onClick={() => onImageClick(imageUrl)}
|
||||
>
|
||||
{/* Preview Image */}
|
||||
<div className="aspect-video bg-gray-800 flex items-center justify-center relative">
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt={`${symbol} - ${item.displayLayout} - ${item.displayTimeframe} chart`}
|
||||
className="w-full h-full object-cover"
|
||||
onError={(e: any) => {
|
||||
const target = e.target as HTMLImageElement
|
||||
target.style.display = 'none'
|
||||
const fallback = target.nextElementSibling as HTMLElement
|
||||
if (fallback) fallback.classList.remove('hidden')
|
||||
}}
|
||||
/>
|
||||
<div className="hidden absolute inset-0 flex items-center justify-center text-gray-400">
|
||||
<div className="text-center">
|
||||
<div className="text-3xl mb-2">📊</div>
|
||||
<div className="text-sm">Chart Preview</div>
|
||||
<div className="text-xs text-gray-500">{item.filename}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Overlay */}
|
||||
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-all flex items-center justify-center">
|
||||
<div className="opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<div className="w-12 h-12 bg-green-500/80 rounded-full flex items-center justify-center">
|
||||
<span className="text-white text-xl">🔍</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Image Info */}
|
||||
<div className="p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-white">{symbol}</div>
|
||||
<div className="text-xs text-green-300">{item.displayLayout}</div>
|
||||
<div className="text-xs text-gray-400">{item.displayTimeframe} Timeframe</div>
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
Click to view
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
346
components/TradeFollowUpPanel.tsx
Normal file
346
components/TradeFollowUpPanel.tsx
Normal file
@@ -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<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].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 <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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user