Files
trading_bot_v3/components/ScreenshotGallery.tsx
mindesbunister 47d7b8b364 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!
2025-07-17 15:29:52 +02:00

356 lines
15 KiB
TypeScript

"use client"
import React, { useEffect } from 'react'
interface ScreenshotGalleryProps {
screenshots: string[]
symbol: string
timeframes: string[]
enlargedImage: string | null
onImageClick: (src: string) => void
onClose: () => void
}
export default function ScreenshotGallery({
screenshots,
symbol,
timeframes,
enlargedImage,
onImageClick,
onClose
}: ScreenshotGalleryProps) {
// Handle ESC key to close enlarged image
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape' && enlargedImage) {
onClose()
}
}
if (enlargedImage) {
document.addEventListener('keydown', handleKeyDown)
return () => document.removeEventListener('keydown', handleKeyDown)
}
}, [enlargedImage, onClose])
if (screenshots.length === 0) return null
// Utility function to convert timeframe to sortable number
const timeframeToMinutes = (timeframe: string): number => {
const tf = timeframe.toLowerCase()
if (tf.includes('5m') || tf === '5') return 5
if (tf.includes('15m') || tf === '15') return 15
if (tf.includes('30m') || tf === '30') return 30
if (tf.includes('1h') || tf === '60') return 60
if (tf.includes('2h') || tf === '120') return 120
if (tf.includes('4h') || tf === '240') return 240
if (tf.includes('1d') || tf === 'D') return 1440
// Default fallback
return parseInt(tf) || 999
}
// Extract layout and timeframe from filename
const extractInfoFromFilename = (filename: string) => {
// Pattern: SYMBOL_TIMEFRAME_LAYOUT_TIMESTAMP.png
// e.g., SOLUSD_5_ai_1752749431435.png or SOLUSD_15_Diy module_1752749479893.png
const parts = filename.replace('.png', '').split('_')
if (parts.length >= 4) {
const timeframe = parts[1]
const layout = parts.slice(2, -1).join('_') // Handle "Diy module" with space
return { timeframe, layout }
}
// Fallback: try to extract from anywhere in filename
const timeframeMatch = filename.match(/_(\d+|D)_/)
const layoutMatch = filename.match(/_(ai|Diy module|diy)_/)
return {
timeframe: timeframeMatch ? timeframeMatch[1] : 'Unknown',
layout: layoutMatch ? layoutMatch[1] : 'Unknown'
}
}
// Format timeframe for display
const formatTimeframe = (tf: string): string => {
if (tf === 'D') return '1D'
if (tf === '5') return '5m'
if (tf === '15') return '15m'
if (tf === '30') return '30m'
if (tf === '60') return '1h'
if (tf === '120') return '2h'
if (tf === '240') return '4h'
return `${tf}m`
}
// Format layout name for display
const formatLayoutName = (layout: string): string => {
if (layout === 'ai') return 'AI Layout'
if (layout === 'Diy module') return 'DIY Module'
return layout
}
// Create screenshot data with extracted info
const screenshotData = screenshots.map((screenshot, index) => {
const screenshotUrl = typeof screenshot === 'string'
? screenshot
: (screenshot as any)?.url || String(screenshot)
const filename = screenshotUrl.split('/').pop() || ''
const { timeframe, layout } = extractInfoFromFilename(filename)
return {
screenshot,
screenshotUrl,
filename,
timeframe,
layout,
displayTimeframe: formatTimeframe(timeframe),
displayLayout: formatLayoutName(layout),
index,
sortOrder: timeframeToMinutes(formatTimeframe(timeframe))
}
})
// Group by layout and sort within each group
const groupedData = screenshotData.reduce((acc: any, item) => {
if (!acc[item.layout]) {
acc[item.layout] = []
}
acc[item.layout].push(item)
return acc
}, {})
// Sort each layout group by timeframe and combine
// First layout (ai), then second layout (Diy module)
const layoutOrder = ['ai', 'Diy module']
const sortedData = layoutOrder.reduce((result: any[], layoutKey) => {
if (groupedData[layoutKey]) {
const sortedGroup = groupedData[layoutKey].sort((a: any, b: any) => a.sortOrder - b.sortOrder)
result.push(...sortedGroup)
}
return result
}, [])
// Add any remaining layouts not in the predefined order
Object.keys(groupedData).forEach(layoutKey => {
if (!layoutOrder.includes(layoutKey)) {
const sortedGroup = groupedData[layoutKey].sort((a: any, b: any) => a.sortOrder - b.sortOrder)
sortedData.push(...sortedGroup)
}
})
// Helper function to format screenshot URL
const formatScreenshotUrl = (screenshot: string | any) => {
// Handle both string URLs and screenshot objects
const screenshotUrl = typeof screenshot === 'string' ? screenshot : screenshot.url || screenshot
// Extract just the filename from the full path
const filename = screenshotUrl.split('/').pop() || screenshotUrl
// Use the new API route with query parameter
return `/api/image?file=${filename}`
}
return (
<>
{/* Gallery Grid */}
<div className="mt-6 p-4 bg-gradient-to-br from-purple-500/10 to-indigo-500/10 border border-purple-500/30 rounded-lg">
<div className="flex items-center justify-between mb-4">
<h4 className="text-lg font-bold text-white flex items-center">
<span className="w-6 h-6 bg-gradient-to-br from-purple-400 to-purple-600 rounded-lg flex items-center justify-center mr-2 text-sm">
📸
</span>
Chart Screenshots
</h4>
<div className="text-xs text-gray-400">
{screenshots.length} captured Click to enlarge
</div>
</div>
<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={`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>
</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>
)}
{/* 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>
{/* Enlarged Image Modal */}
{enlargedImage && (
<div
className="fixed inset-0 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4 z-50"
onClick={onClose}
>
<div className="relative max-w-6xl max-h-[90vh] w-full" onClick={(e: any) => e.stopPropagation()}>
{/* Close Button */}
<button
onClick={onClose}
className="absolute top-4 right-4 w-10 h-10 bg-black/50 hover:bg-black/70 rounded-full flex items-center justify-center text-white z-10 transition-colors"
>
</button>
{/* Image */}
<img
src={enlargedImage}
alt="Enlarged chart"
className="w-full h-full object-contain rounded-lg border border-gray-600"
onError={(e: any) => {
console.error('Failed to load enlarged image:', enlargedImage)
const target = e.target as HTMLImageElement
target.alt = 'Failed to load image'
}}
/>
{/* Image Info Overlay */}
<div className="absolute bottom-4 left-4 right-4 bg-black/70 backdrop-blur-sm rounded-lg p-4">
<div className="flex items-center justify-between">
<div>
<div className="text-white font-medium">{symbol} Chart Analysis</div>
<div className="text-gray-300 text-sm">AI analyzed screenshot High resolution view</div>
</div>
<div className="text-xs text-gray-400">
ESC to close Click outside to close
</div>
</div>
</div>
</div>
</div>
)}
</>
)
}