- Remove Quick Actions section with individual timeframe buttons - Update coin selection to only show BTC, ETH, SOL with CoinGecko icons - Move Analysis Timeframes section between presets and coin selection - Change coin grid layout to 3 columns for better visual balance - Replace Unicode coin symbols with proper CoinGecko image URLs
1515 lines
67 KiB
TypeScript
1515 lines
67 KiB
TypeScript
"use client"
|
||
import React, { useState, useEffect } from 'react'
|
||
import TradeModal from './TradeModal'
|
||
import ScreenshotGallery from './ScreenshotGallery'
|
||
|
||
const layouts = (process.env.NEXT_PUBLIC_TRADINGVIEW_LAYOUTS || 'ai,Diy module').split(',').map(l => l.trim())
|
||
|
||
// Layout display names mapping
|
||
const layoutDisplayNames: { [key: string]: string } = {
|
||
'ai': 'ai',
|
||
'Diy module': 'Diy module'
|
||
}
|
||
const timeframes = [
|
||
{ label: '1m', value: '1' },
|
||
{ label: '5m', value: '5' },
|
||
{ label: '15m', value: '15' },
|
||
{ label: '30m', value: '30' },
|
||
{ label: '1h', value: '60' },
|
||
{ label: '2h', value: '120' },
|
||
{ label: '4h', value: '240' },
|
||
{ label: '1d', value: 'D' },
|
||
{ label: '1w', value: 'W' },
|
||
{ label: '1M', value: 'M' },
|
||
]
|
||
|
||
const popularCoins = [
|
||
{
|
||
name: 'Bitcoin',
|
||
symbol: 'BTCUSD',
|
||
icon: 'https://assets.coingecko.com/coins/images/1/large/bitcoin.png',
|
||
color: 'from-orange-400 to-orange-600'
|
||
},
|
||
{
|
||
name: 'Ethereum',
|
||
symbol: 'ETHUSD',
|
||
icon: 'https://assets.coingecko.com/coins/images/279/large/ethereum.png',
|
||
color: 'from-blue-400 to-blue-600'
|
||
},
|
||
{
|
||
name: 'Solana',
|
||
symbol: 'SOLUSD',
|
||
icon: 'https://assets.coingecko.com/coins/images/4128/large/solana.png',
|
||
color: 'from-purple-400 to-purple-600'
|
||
}
|
||
]
|
||
|
||
// Progress tracking interfaces
|
||
interface ProgressStep {
|
||
id: string
|
||
title: string
|
||
description: string
|
||
status: 'pending' | 'active' | 'completed' | 'error'
|
||
startTime?: number
|
||
endTime?: number
|
||
details?: string
|
||
}
|
||
|
||
interface AnalysisProgress {
|
||
sessionId: string
|
||
currentStep: number
|
||
totalSteps: number
|
||
steps: ProgressStep[]
|
||
timeframeProgress?: {
|
||
current: number
|
||
total: number
|
||
currentTimeframe?: string
|
||
}
|
||
}
|
||
|
||
interface AIAnalysisPanelProps {
|
||
onAnalysisComplete?: (analysis: any, symbol: string) => void
|
||
}
|
||
|
||
export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelProps = {}) {
|
||
const [symbol, setSymbol] = useState('BTCUSD')
|
||
const [selectedLayouts, setSelectedLayouts] = useState<string[]>(['ai', 'Diy module']) // Default to both layouts
|
||
const [selectedTimeframes, setSelectedTimeframes] = useState<string[]>(['60']) // Support multiple timeframes
|
||
const [loading, setLoading] = useState(false)
|
||
const [result, setResult] = useState<any>(null)
|
||
const [error, setError] = useState<string | null>(null)
|
||
const [progress, setProgress] = useState<AnalysisProgress | null>(null)
|
||
const [eventSource, setEventSource] = useState<EventSource | null>(null)
|
||
const [modalOpen, setModalOpen] = useState(false)
|
||
const [modalData, setModalData] = useState<any>(null)
|
||
const [enlargedScreenshot, setEnlargedScreenshot] = useState<string | null>(null)
|
||
const [tradeModalOpen, setTradeModalOpen] = useState(false)
|
||
const [tradeModalData, setTradeModalData] = useState<any>(null)
|
||
|
||
// Helper function to safely render any value
|
||
const safeRender = (value: any): string => {
|
||
if (typeof value === 'string') return value
|
||
if (typeof value === 'number') return value.toString()
|
||
if (Array.isArray(value)) return value.join(', ')
|
||
if (typeof value === 'object' && value !== null) {
|
||
return JSON.stringify(value)
|
||
}
|
||
return String(value)
|
||
}
|
||
|
||
// Real-time progress tracking
|
||
const startProgressTracking = (sessionId: string) => {
|
||
console.log(`🔍 Starting progress tracking for session: ${sessionId}`)
|
||
|
||
// Close existing connection
|
||
if (eventSource) {
|
||
eventSource.close()
|
||
}
|
||
|
||
const es = new EventSource(`/api/progress/${sessionId}/stream`)
|
||
|
||
es.onopen = () => {
|
||
console.log(`🔍 EventSource connection opened for ${sessionId}`)
|
||
}
|
||
|
||
es.onmessage = (event) => {
|
||
try {
|
||
const progressData = JSON.parse(event.data)
|
||
console.log(`🔍 Received progress update for ${sessionId}:`, progressData)
|
||
|
||
if (progressData.type === 'complete') {
|
||
console.log(`🔍 Analysis complete for ${sessionId}`)
|
||
es.close()
|
||
setEventSource(null)
|
||
} else if (progressData.type === 'connected') {
|
||
console.log(`🔍 EventSource connected for ${sessionId}`)
|
||
} else {
|
||
// Update progress state immediately
|
||
setProgress(progressData)
|
||
}
|
||
} catch (error) {
|
||
console.error('Error parsing progress data:', error)
|
||
}
|
||
}
|
||
|
||
es.onerror = (error) => {
|
||
console.error('EventSource error:', error)
|
||
es.close()
|
||
setEventSource(null)
|
||
}
|
||
|
||
setEventSource(es)
|
||
}
|
||
|
||
// Cleanup event source on unmount
|
||
React.useEffect(() => {
|
||
return () => {
|
||
if (eventSource) {
|
||
eventSource.close()
|
||
}
|
||
}
|
||
}, [eventSource])
|
||
|
||
// Normalize layout key to match backend expectations
|
||
const normalizeLayoutKey = (layout: string): string => {
|
||
// Keep the exact names as they appear in TradingView
|
||
if (layout === 'ai') return 'ai'
|
||
if (layout === 'Diy module') return 'Diy module'
|
||
return layout
|
||
}
|
||
|
||
// Get display name for layout (keep exact TradingView names)
|
||
const getLayoutDisplayName = (layout: string): string => {
|
||
return layoutDisplayNames[layout] || layout
|
||
}
|
||
|
||
const toggleLayout = (layout: string) => {
|
||
const normalizedLayout = normalizeLayoutKey(layout)
|
||
setSelectedLayouts(prev =>
|
||
prev.includes(normalizedLayout)
|
||
? prev.filter(l => l !== normalizedLayout)
|
||
: [...prev, normalizedLayout]
|
||
)
|
||
}
|
||
|
||
const toggleTimeframe = (timeframe: string) => {
|
||
setSelectedTimeframes(prev =>
|
||
prev.includes(timeframe)
|
||
? prev.filter(tf => tf !== timeframe)
|
||
: [...prev, timeframe]
|
||
)
|
||
}
|
||
|
||
// Helper function to create initial progress steps (no longer used - using real-time progress)
|
||
// const createProgressSteps = ...removed for real-time implementation
|
||
|
||
// Helper function to update progress (no longer used - using real-time progress)
|
||
// const updateProgress = ...removed for real-time implementation
|
||
|
||
const performAnalysis = async (analysisSymbol = symbol, analysisTimeframes = selectedTimeframes) => {
|
||
if (loading || selectedLayouts.length === 0 || analysisTimeframes.length === 0) return
|
||
|
||
setLoading(true)
|
||
setError(null)
|
||
setResult(null)
|
||
|
||
// Set initial progress state to show animation immediately
|
||
setProgress({
|
||
sessionId: 'initializing',
|
||
currentStep: 1,
|
||
totalSteps: 6,
|
||
steps: [
|
||
{
|
||
id: 'init',
|
||
title: 'Initializing Analysis',
|
||
description: 'Starting AI-powered trading analysis...',
|
||
status: 'active',
|
||
startTime: Date.now()
|
||
},
|
||
{
|
||
id: 'auth',
|
||
title: 'TradingView Authentication',
|
||
description: 'Logging into TradingView accounts',
|
||
status: 'pending'
|
||
},
|
||
{
|
||
id: 'navigation',
|
||
title: 'Chart Navigation',
|
||
description: 'Navigating to chart layouts',
|
||
status: 'pending'
|
||
},
|
||
{
|
||
id: 'loading',
|
||
title: 'Chart Data Loading',
|
||
description: 'Waiting for chart data and indicators',
|
||
status: 'pending'
|
||
},
|
||
{
|
||
id: 'capture',
|
||
title: 'Screenshot Capture',
|
||
description: 'Capturing high-quality screenshots',
|
||
status: 'pending'
|
||
},
|
||
{
|
||
id: 'analysis',
|
||
title: 'AI Analysis',
|
||
description: 'Analyzing screenshots with AI',
|
||
status: 'pending'
|
||
}
|
||
],
|
||
timeframeProgress: analysisTimeframes.length > 1 ? {
|
||
current: 0,
|
||
total: analysisTimeframes.length
|
||
} : undefined
|
||
})
|
||
|
||
try {
|
||
if (analysisTimeframes.length === 1) {
|
||
// Single timeframe analysis with real-time progress
|
||
|
||
// Pre-generate sessionId and start progress tracking BEFORE making the API call
|
||
const sessionId = `analysis_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||
startProgressTracking(sessionId)
|
||
|
||
const response = await fetch('/api/enhanced-screenshot', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
symbol: analysisSymbol,
|
||
timeframe: analysisTimeframes[0],
|
||
layouts: selectedLayouts,
|
||
analyze: true,
|
||
sessionId: sessionId // Pass pre-generated sessionId
|
||
})
|
||
})
|
||
|
||
const data = await response.json()
|
||
|
||
if (!response.ok) {
|
||
throw new Error(data.error || 'Analysis failed')
|
||
}
|
||
|
||
setResult(data)
|
||
|
||
// Call the callback with analysis result if provided
|
||
if (onAnalysisComplete && data.analysis) {
|
||
onAnalysisComplete(data.analysis, analysisSymbol)
|
||
}
|
||
} else {
|
||
// Multiple timeframe analysis
|
||
const results = []
|
||
|
||
for (let i = 0; i < analysisTimeframes.length; i++) {
|
||
const tf = analysisTimeframes[i]
|
||
const timeframeLabel = timeframes.find(t => t.value === tf)?.label || tf
|
||
|
||
console.log(`🧪 Analyzing timeframe: ${timeframeLabel}`)
|
||
|
||
const response = await fetch('/api/enhanced-screenshot', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
symbol: analysisSymbol,
|
||
timeframe: tf,
|
||
layouts: selectedLayouts,
|
||
analyze: true
|
||
})
|
||
})
|
||
|
||
const result = await response.json()
|
||
results.push({
|
||
timeframe: tf,
|
||
timeframeLabel,
|
||
success: response.ok,
|
||
result
|
||
})
|
||
|
||
// Start progress tracking for the first timeframe session
|
||
if (i === 0 && result.sessionId) {
|
||
startProgressTracking(result.sessionId)
|
||
}
|
||
|
||
// Update timeframe progress manually for multi-timeframe
|
||
setProgress(prev => prev ? {
|
||
...prev,
|
||
timeframeProgress: {
|
||
current: i + 1,
|
||
total: analysisTimeframes.length,
|
||
currentTimeframe: timeframeLabel
|
||
}
|
||
} : null)
|
||
|
||
// Small delay between requests
|
||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||
}
|
||
|
||
const multiResult = {
|
||
type: 'multi_timeframe',
|
||
symbol: analysisSymbol,
|
||
summary: `Analyzed ${results.length} timeframes for ${analysisSymbol}`,
|
||
results
|
||
}
|
||
|
||
setResult(multiResult)
|
||
|
||
// Call the callback with the first successful analysis result if provided
|
||
if (onAnalysisComplete) {
|
||
const firstSuccessfulResult = results.find(r => r.success && r.result?.analysis)
|
||
if (firstSuccessfulResult) {
|
||
onAnalysisComplete(firstSuccessfulResult.result.analysis, analysisSymbol)
|
||
}
|
||
}
|
||
}
|
||
} catch (err) {
|
||
const errorMessage = err instanceof Error ? err.message : 'Failed to perform analysis'
|
||
setError(errorMessage)
|
||
|
||
// Mark current active step as error
|
||
setProgress(prev => {
|
||
if (!prev) return null
|
||
const activeStepIndex = prev.steps.findIndex(step => step.status === 'active')
|
||
if (activeStepIndex >= 0) {
|
||
const updatedSteps = [...prev.steps]
|
||
updatedSteps[activeStepIndex] = {
|
||
...updatedSteps[activeStepIndex],
|
||
status: 'error',
|
||
details: errorMessage,
|
||
endTime: Date.now()
|
||
}
|
||
return { ...prev, steps: updatedSteps }
|
||
}
|
||
return prev
|
||
})
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
const quickAnalyze = async (coinSymbol: string, quickTimeframes = selectedTimeframes) => {
|
||
setSymbol(coinSymbol)
|
||
if (!loading) {
|
||
await performAnalysis(coinSymbol, quickTimeframes)
|
||
}
|
||
}
|
||
|
||
const quickTimeframeTest = async (testTimeframe: string) => {
|
||
// Toggle the timeframe in selection instead of replacing
|
||
const newTimeframes = selectedTimeframes.includes(testTimeframe)
|
||
? selectedTimeframes.filter(tf => tf !== testTimeframe)
|
||
: [...selectedTimeframes, testTimeframe]
|
||
|
||
setSelectedTimeframes(newTimeframes)
|
||
|
||
if (!loading && symbol && newTimeframes.length > 0) {
|
||
await performAnalysis(symbol, newTimeframes)
|
||
}
|
||
}
|
||
|
||
const testAllTimeframes = async () => {
|
||
if (loading) return
|
||
|
||
const allTimeframeValues = timeframes.map(tf => tf.value)
|
||
setSelectedTimeframes(allTimeframeValues)
|
||
|
||
if (!loading && symbol) {
|
||
await performAnalysis(symbol, allTimeframeValues)
|
||
}
|
||
}
|
||
|
||
async function handleAnalyze() {
|
||
await performAnalysis()
|
||
}
|
||
|
||
// Trade initiation handler
|
||
const handleTradeClick = (tfResult: any) => {
|
||
console.log('🔥 AIAnalysisPanel handleTradeClick called with:', tfResult)
|
||
const analysis = tfResult?.result?.analysis || tfResult?.analysis || {}
|
||
console.log('🔥 Extracted analysis:', analysis)
|
||
|
||
// Enhanced data extraction with better fallbacks
|
||
let entryPrice = ''
|
||
let takeProfit1 = ''
|
||
let takeProfit2 = ''
|
||
let stopLoss = ''
|
||
|
||
// Extract entry price with multiple fallback options
|
||
if (analysis.entry?.price) {
|
||
entryPrice = analysis.entry.price.toString()
|
||
} else if (analysis.entry && typeof analysis.entry === 'number') {
|
||
entryPrice = analysis.entry.toString()
|
||
} else if (analysis.entry && typeof analysis.entry === 'string') {
|
||
entryPrice = analysis.entry
|
||
}
|
||
|
||
// Extract take profit 1 with multiple fallback options
|
||
if (analysis.takeProfits?.tp1?.price) {
|
||
takeProfit1 = analysis.takeProfits.tp1.price.toString()
|
||
} else if (analysis.takeProfits?.tp1 && typeof analysis.takeProfits.tp1 === 'number') {
|
||
takeProfit1 = analysis.takeProfits.tp1.toString()
|
||
} else if (analysis.takeProfits && typeof analysis.takeProfits === 'number') {
|
||
takeProfit1 = analysis.takeProfits.toString()
|
||
} else if (analysis.takeProfit?.price) {
|
||
takeProfit1 = analysis.takeProfit.price.toString()
|
||
} else if (analysis.takeProfit && typeof analysis.takeProfit === 'number') {
|
||
takeProfit1 = analysis.takeProfit.toString()
|
||
}
|
||
|
||
// Extract take profit 2 if available
|
||
if (analysis.takeProfits?.tp2?.price) {
|
||
takeProfit2 = analysis.takeProfits.tp2.price.toString()
|
||
} else if (analysis.takeProfits?.tp2 && typeof analysis.takeProfits.tp2 === 'number') {
|
||
takeProfit2 = analysis.takeProfits.tp2.toString()
|
||
}
|
||
|
||
// Extract stop loss with multiple fallback options
|
||
if (analysis.stopLoss?.price) {
|
||
stopLoss = analysis.stopLoss.price.toString()
|
||
} else if (analysis.stopLoss && typeof analysis.stopLoss === 'number') {
|
||
stopLoss = analysis.stopLoss.toString()
|
||
} else if (analysis.stopLoss && typeof analysis.stopLoss === 'string') {
|
||
stopLoss = analysis.stopLoss
|
||
}
|
||
|
||
const tradeData = {
|
||
entry: entryPrice,
|
||
tp: takeProfit1, // This maps to tp1 in the modal
|
||
tp2: takeProfit2, // This will be handled in the modal
|
||
sl: stopLoss,
|
||
symbol: symbol,
|
||
timeframe: tfResult?.timeframeLabel || tfResult?.timeframe || '',
|
||
}
|
||
|
||
console.log('🔥 Enhanced trade data extraction:', {
|
||
originalAnalysis: analysis,
|
||
extractedData: tradeData
|
||
})
|
||
|
||
setTradeModalData(tradeData)
|
||
console.log('🔥 Opening trade modal...')
|
||
setTradeModalOpen(true)
|
||
}
|
||
|
||
// Trade execution API call
|
||
const executeTrade = async (tradeData: any) => {
|
||
try {
|
||
// Determine if this is a leveraged position or spot trade
|
||
const leverage = parseFloat(tradeData.leverage) || 1
|
||
const isLeveraged = leverage > 1
|
||
|
||
// Route to appropriate API based on leverage
|
||
const apiEndpoint = isLeveraged ? '/api/trading/execute-drift' : '/api/trading/execute-dex'
|
||
const tradingMode = isLeveraged ? 'PERP' : 'SPOT'
|
||
|
||
console.log(`🎯 Executing ${tradingMode} trade with ${leverage}x leverage via ${apiEndpoint}`)
|
||
|
||
const response = await fetch(apiEndpoint, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
symbol: tradeData.symbol || symbol,
|
||
side: 'BUY', // Could be derived from analysis
|
||
amount: parseFloat(tradeData.positionSize) || parseFloat(tradeData.size),
|
||
amountUSD: parseFloat(tradeData.amountUSD || tradeData.positionSize || tradeData.size),
|
||
leverage: leverage,
|
||
stopLoss: parseFloat(tradeData.sl),
|
||
takeProfit: parseFloat(tradeData.tp1), // Use TP1 as primary target
|
||
useRealDEX: true, // Enable real trading for manual execution
|
||
tradingMode: tradingMode,
|
||
tradingPair: `${tradeData.symbol || symbol}/USDC`,
|
||
quickSwap: false
|
||
})
|
||
})
|
||
|
||
const result = await response.json()
|
||
|
||
if (response.ok && result.success) {
|
||
// Show detailed success message based on trading type
|
||
const leverage = parseFloat(tradeData.leverage) || 1
|
||
const isLeveraged = leverage > 1
|
||
const tradeType = isLeveraged ? 'Leveraged Perpetual Position' : 'Spot Trade'
|
||
const platform = isLeveraged ? 'Drift Protocol' : 'Jupiter DEX'
|
||
|
||
let message = `✅ ${tradeType} executed successfully!\n\n`
|
||
message += `📊 Transaction ID: ${result.trade?.txId || result.txId}\n`
|
||
message += `💰 Symbol: ${tradeData.symbol || symbol}\n`
|
||
message += `📈 Size: ${tradeData.positionSize || tradeData.size} USDC\n`
|
||
if (isLeveraged) {
|
||
message += `⚡ Leverage: ${leverage}x (via increased position size)\n`
|
||
message += `<EFBFBD> Actual Trade Size: $${(parseFloat(tradeData.positionSize || tradeData.size) * leverage).toFixed(2)}\n`
|
||
}
|
||
message += `<EFBFBD>🏪 Platform: ${platform}\n`
|
||
|
||
if (tradeData.sl) message += `🛑 Stop Loss: $${tradeData.sl} (Jupiter Trigger Order)\n`
|
||
if (tradeData.tp1) message += `🎯 Take Profit: $${tradeData.tp1} (Jupiter Trigger Order)\n`
|
||
|
||
if (result.triggerOrders?.status === 'CREATED') {
|
||
message += `\n🔄 Trigger Orders: ACTIVE\n`
|
||
if (result.triggerOrders.stopLossOrderId) message += `🛑 SL Order: ${result.triggerOrders.stopLossOrderId.substring(0, 8)}...\n`
|
||
if (result.triggerOrders.takeProfitOrderId) message += `🎯 TP Order: ${result.triggerOrders.takeProfitOrderId.substring(0, 8)}...\n`
|
||
} else if (result.trade?.monitoring || result.position) {
|
||
message += `\n🔄 Position monitoring: ACTIVE`
|
||
}
|
||
|
||
alert(message)
|
||
} else {
|
||
// Show detailed error message
|
||
const errorMsg = result.error || 'Unknown error occurred'
|
||
|
||
if (errorMsg.includes('not configured') || errorMsg.includes('Wallet not initialized')) {
|
||
alert(`❌ Trade Failed: Jupiter DEX Not Configured\n\nPlease configure your Jupiter DEX wallet in the settings before executing real trades.\n\nError: ${errorMsg}`)
|
||
} else if (errorMsg.includes('insufficient') || errorMsg.includes('balance')) {
|
||
alert(`❌ Trade Failed: Insufficient Balance\n\nPlease ensure you have enough tokens in your wallet.\n\nError: ${errorMsg}`)
|
||
} else if (errorMsg.includes('Real Jupiter Perpetuals trading not yet implemented')) {
|
||
alert(`❌ Real Trading Not Available\n\nReal Jupiter Perpetuals trading is still in development. This trade will be simulated instead.\n\nTo use real spot trading, reduce the leverage to 1x.`)
|
||
} else if (errorMsg.includes('Trigger API error') || errorMsg.includes('trigger orders failed')) {
|
||
alert(`⚠️ Trade Executed, But Trigger Orders Failed\n\nYour main trade was successful, but stop loss/take profit orders could not be created.\n\nError: ${errorMsg}\n\nPlease monitor your position manually.`)
|
||
} else {
|
||
alert(`❌ Trade Failed\n\nError: ${errorMsg}`)
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Trade execution failed:', error)
|
||
alert('❌ Trade execution failed due to network error.\n\nPlease check your connection and try again.')
|
||
}
|
||
|
||
setTradeModalOpen(false)
|
||
}
|
||
|
||
// Screenshot gallery modal
|
||
const handleScreenshotClick = (src: string) => {
|
||
setEnlargedScreenshot(src)
|
||
}
|
||
|
||
return (
|
||
<div className="card card-gradient">
|
||
<div className="flex items-center justify-between mb-6">
|
||
<h2 className="text-xl font-bold text-white flex items-center">
|
||
<span className="w-8 h-8 bg-gradient-to-br from-cyan-400 to-blue-600 rounded-lg flex items-center justify-center mr-3">
|
||
🤖
|
||
</span>
|
||
AI Chart Analysis
|
||
</h2>
|
||
<div className="flex items-center space-x-2 text-sm text-gray-400">
|
||
<div className="w-2 h-2 bg-cyan-400 rounded-full animate-pulse"></div>
|
||
<span>AI Powered</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Quick Coin & Timeframe Analysis */}
|
||
<div className="mb-8">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<h3 className="text-sm font-semibold text-gray-300 flex items-center">
|
||
<span className="w-4 h-4 bg-yellow-500 rounded-full mr-2"></span>
|
||
Quick Analysis
|
||
</h3>
|
||
<span className="text-xs text-gray-500">Select coin + timeframe combo for instant analysis</span>
|
||
</div>
|
||
|
||
{/* Quick Timeframe Presets */}
|
||
<div className="mb-4 p-3 bg-gray-800/30 rounded-lg">
|
||
<label className="block text-xs font-medium text-gray-400 mb-2">Quick Timeframe Presets</label>
|
||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
|
||
<button
|
||
onClick={() => setSelectedTimeframes(['5', '15', '30'])}
|
||
className="py-2 px-3 rounded-lg text-xs font-medium bg-purple-600/20 text-purple-300 hover:bg-purple-600/30 transition-all"
|
||
>
|
||
🕒 Scalping (5m, 15m, 30m)
|
||
</button>
|
||
<button
|
||
onClick={() => setSelectedTimeframes(['60', '120', '240'])}
|
||
className="py-2 px-3 rounded-lg text-xs font-medium bg-blue-600/20 text-blue-300 hover:bg-blue-600/30 transition-all"
|
||
>
|
||
📊 Day Trading (1h, 2h, 4h)
|
||
</button>
|
||
<button
|
||
onClick={() => setSelectedTimeframes(['240', 'D', 'W'])}
|
||
className="py-2 px-3 rounded-lg text-xs font-medium bg-green-600/20 text-green-300 hover:bg-green-600/30 transition-all"
|
||
>
|
||
📈 Swing (4h, 1d, 1w)
|
||
</button>
|
||
<button
|
||
onClick={() => setSelectedTimeframes(['D', 'W', 'M'])}
|
||
className="py-2 px-3 rounded-lg text-xs font-medium bg-orange-600/20 text-orange-300 hover:bg-orange-600/30 transition-all"
|
||
>
|
||
🎯 Position (1d, 1w, 1m)
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Timeframe Selection */}
|
||
<div className="mb-4">
|
||
<label className="block text-xs font-medium text-gray-400 mb-3">
|
||
Analysis Timeframes
|
||
<span className="text-xs text-cyan-400 ml-2">({selectedTimeframes.length} selected)</span>
|
||
</label>
|
||
<div className="grid grid-cols-4 gap-2">
|
||
{timeframes.map(tf => (
|
||
<label key={tf.value} className="group relative cursor-pointer">
|
||
<input
|
||
type="checkbox"
|
||
checked={selectedTimeframes.includes(tf.value)}
|
||
onChange={() => toggleTimeframe(tf.value)}
|
||
className="sr-only"
|
||
/>
|
||
<div className={`flex items-center justify-center p-3 rounded-lg border transition-all ${
|
||
selectedTimeframes.includes(tf.value)
|
||
? 'border-cyan-500 bg-cyan-500/10 text-cyan-300 shadow-lg shadow-cyan-500/20'
|
||
: 'border-gray-700 bg-gray-800/30 text-gray-400 hover:border-gray-600 hover:bg-gray-800/50 hover:text-gray-300'
|
||
}`}>
|
||
<span className="text-sm font-medium">{tf.label}</span>
|
||
{selectedTimeframes.includes(tf.value) && (
|
||
<div className="absolute top-1 right-1 w-2 h-2 bg-cyan-400 rounded-full animate-pulse"></div>
|
||
)}
|
||
</div>
|
||
</label>
|
||
))}
|
||
</div>
|
||
{selectedTimeframes.length > 0 && (
|
||
<div className="mt-3 p-3 bg-gray-800/30 rounded-lg">
|
||
<div className="text-xs text-gray-400">
|
||
Selected timeframes: <span className="text-cyan-400">
|
||
{selectedTimeframes.map(tf => timeframes.find(t => t.value === tf)?.label).join(', ')}
|
||
</span>
|
||
</div>
|
||
<div className="text-xs text-gray-500 mt-1">
|
||
💡 Multiple timeframes provide broader market outlook
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Coin Selection */}
|
||
<div className="grid grid-cols-3 gap-3">
|
||
{popularCoins.map(coin => (
|
||
<button
|
||
key={coin.symbol}
|
||
onClick={() => quickAnalyze(coin.symbol)}
|
||
disabled={loading || selectedLayouts.length === 0}
|
||
className={`group relative p-3 rounded-lg border transition-all ${
|
||
loading || selectedLayouts.length === 0
|
||
? 'border-gray-700 bg-gray-800/30 cursor-not-allowed opacity-50'
|
||
: symbol === coin.symbol
|
||
? 'border-cyan-500 bg-cyan-500/10 text-cyan-300'
|
||
: 'border-gray-700 bg-gray-800/30 text-gray-300 hover:border-gray-600 hover:bg-gray-800/50 hover:text-white transform hover:scale-105'
|
||
}`}
|
||
>
|
||
<div className={`w-8 h-8 bg-gradient-to-br ${coin.color} rounded-lg flex items-center justify-center mx-auto mb-2 p-1`}>
|
||
<img
|
||
src={coin.icon}
|
||
alt={coin.name}
|
||
className="w-6 h-6 rounded-full"
|
||
/>
|
||
</div>
|
||
<div className="text-xs font-medium">{coin.name}</div>
|
||
<div className="text-xs text-gray-500">{coin.symbol}</div>
|
||
{symbol === coin.symbol && (
|
||
<div className="absolute top-1 right-1 w-2 h-2 bg-cyan-400 rounded-full animate-pulse"></div>
|
||
)}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Advanced Analysis Section */}
|
||
<div className="mb-8">
|
||
<h3 className="text-sm font-semibold text-gray-300 flex items-center mb-4">
|
||
<span className="w-4 h-4 bg-purple-500 rounded-full mr-2"></span>
|
||
Advanced Analysis
|
||
</h3>
|
||
|
||
{/* Symbol Input */}
|
||
<div className="grid grid-cols-1 gap-4 mb-6">
|
||
<div>
|
||
<label className="block text-xs font-medium text-gray-400 mb-2">Trading Pair</label>
|
||
<input
|
||
className="w-full px-4 py-3 bg-gray-800/50 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:border-cyan-500 focus:outline-none focus:ring-2 focus:ring-cyan-500/20 transition-all"
|
||
value={symbol}
|
||
onChange={e => setSymbol(e.target.value.toUpperCase())}
|
||
placeholder="e.g., BTCUSD, ETHUSD"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
{/* Layout Selection */}
|
||
<div className="mb-6">
|
||
<label className="block text-xs font-medium text-gray-400 mb-3">Analysis Layouts</label>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||
{layouts.map(layout => {
|
||
const normalizedLayout = normalizeLayoutKey(layout)
|
||
const displayName = getLayoutDisplayName(layout)
|
||
const isSelected = selectedLayouts.includes(normalizedLayout)
|
||
|
||
return (
|
||
<label key={layout} className="group relative">
|
||
<input
|
||
type="checkbox"
|
||
checked={isSelected}
|
||
onChange={() => toggleLayout(layout)}
|
||
className="sr-only"
|
||
/>
|
||
<div className={`flex items-center p-3 rounded-lg border cursor-pointer transition-all ${
|
||
isSelected
|
||
? 'border-cyan-500 bg-cyan-500/10 text-cyan-300'
|
||
: 'border-gray-700 bg-gray-800/30 text-gray-300 hover:border-gray-600 hover:bg-gray-800/50'
|
||
}`}>
|
||
<div className={`w-4 h-4 rounded border-2 mr-3 flex items-center justify-center ${
|
||
isSelected
|
||
? 'border-cyan-500 bg-cyan-500'
|
||
: 'border-gray-600'
|
||
}`}>
|
||
{isSelected && (
|
||
<svg className="w-2 h-2 text-white" fill="currentColor" viewBox="0 0 20 20">
|
||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||
</svg>
|
||
)}
|
||
</div>
|
||
<span className="text-sm font-medium">{displayName}</span>
|
||
</div>
|
||
</label>
|
||
)
|
||
})}
|
||
</div>
|
||
{selectedLayouts.length > 0 && (
|
||
<div className="mt-3 p-3 bg-gray-800/30 rounded-lg">
|
||
<div className="text-xs text-gray-400">
|
||
Selected layouts: <span className="text-cyan-400">
|
||
{selectedLayouts.map(layout => getLayoutDisplayName(layout)).join(', ')}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Analyze Button */}
|
||
<button
|
||
className={`w-full py-3 px-6 rounded-lg font-semibold transition-all duration-300 ${
|
||
loading
|
||
? 'bg-gray-700 text-gray-400 cursor-not-allowed'
|
||
: 'btn-primary transform hover:scale-[1.02] active:scale-[0.98]'
|
||
}`}
|
||
onClick={handleAnalyze}
|
||
disabled={loading}
|
||
>
|
||
{loading ? (
|
||
<div className="flex items-center justify-center space-x-2">
|
||
<div className="spinner"></div>
|
||
<span>Analyzing Chart...</span>
|
||
</div>
|
||
) : (
|
||
<div className="flex items-center justify-center space-x-2">
|
||
<span>🚀</span>
|
||
<span>Start AI Analysis</span>
|
||
</div>
|
||
)}
|
||
</button>
|
||
</div>
|
||
|
||
{/* Results Section */}
|
||
{error && (
|
||
<div className="mt-6 p-4 bg-red-500/10 border border-red-500/30 rounded-lg">
|
||
<div className="flex items-start space-x-3">
|
||
<div className="w-5 h-5 text-red-400 mt-0.5">⚠️</div>
|
||
<div>
|
||
<h4 className="text-red-400 font-medium text-sm">Analysis Error</h4>
|
||
<p className="text-red-300 text-xs mt-1 opacity-90">{error}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{loading && progress && (
|
||
<div className="mt-6 p-6 bg-gradient-to-br from-cyan-500/10 to-blue-500/10 border border-cyan-500/30 rounded-lg">
|
||
{/* Header */}
|
||
<div className="flex items-center justify-between mb-6">
|
||
<div className="flex items-center space-x-3">
|
||
<div className="spinner border-cyan-500"></div>
|
||
<div>
|
||
<h4 className="text-cyan-400 font-medium text-lg">AI Analysis in Progress</h4>
|
||
<p className="text-cyan-300 text-sm opacity-90">
|
||
Analyzing {symbol} • {selectedLayouts.map(layout => getLayoutDisplayName(layout)).join(', ')} layouts
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Overall Progress */}
|
||
<div className="text-right">
|
||
<div className="text-sm font-medium text-cyan-300">
|
||
Step {progress.currentStep} of {progress.totalSteps}
|
||
</div>
|
||
<div className="text-xs text-gray-400 mt-1">
|
||
{Math.round((progress.currentStep / progress.totalSteps) * 100)}% Complete
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Multi-timeframe progress */}
|
||
{progress.timeframeProgress && (
|
||
<div className="mb-6 p-4 bg-purple-800/20 rounded-lg border border-purple-500/30">
|
||
<div className="flex items-center justify-between mb-2">
|
||
<h5 className="text-purple-300 font-medium text-sm">Multi-Timeframe Analysis</h5>
|
||
<span className="text-xs text-purple-400">
|
||
{progress.timeframeProgress.current}/{progress.timeframeProgress.total} timeframes
|
||
</span>
|
||
</div>
|
||
<div className="w-full bg-purple-900/30 rounded-full h-2 mb-2">
|
||
<div
|
||
className="bg-gradient-to-r from-purple-500 to-purple-400 h-2 rounded-full transition-all duration-500"
|
||
style={{ width: `${(progress.timeframeProgress.current / progress.timeframeProgress.total) * 100}%` }}
|
||
/>
|
||
</div>
|
||
{progress.timeframeProgress.currentTimeframe && (
|
||
<p className="text-xs text-purple-300">
|
||
Current: {progress.timeframeProgress.currentTimeframe}
|
||
</p>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Progress Steps */}
|
||
<div className="space-y-3">
|
||
{progress.steps.map((step, index) => {
|
||
const isActive = step.status === 'active'
|
||
const isCompleted = step.status === 'completed'
|
||
const isError = step.status === 'error'
|
||
const isPending = step.status === 'pending'
|
||
|
||
return (
|
||
<div
|
||
key={step.id}
|
||
className={`flex items-center space-x-4 p-3 rounded-lg transition-all duration-300 ${
|
||
isActive ? 'bg-cyan-500/20 border border-cyan-500/50' :
|
||
isCompleted ? 'bg-green-500/10 border border-green-500/30' :
|
||
isError ? 'bg-red-500/10 border border-red-500/30' :
|
||
'bg-gray-800/30 border border-gray-700/50'
|
||
}`}
|
||
>
|
||
{/* Step Icon */}
|
||
<div className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium ${
|
||
isActive ? 'bg-cyan-500 text-white animate-pulse' :
|
||
isCompleted ? 'bg-green-500 text-white' :
|
||
isError ? 'bg-red-500 text-white' :
|
||
'bg-gray-600 text-gray-300'
|
||
}`}>
|
||
{isCompleted ? '✓' :
|
||
isError ? '✗' :
|
||
isActive ? '⟳' :
|
||
index + 1}
|
||
</div>
|
||
|
||
{/* Step Content */}
|
||
<div className="flex-1 min-w-0">
|
||
<div className="flex items-center justify-between">
|
||
<h6 className={`font-medium text-sm ${
|
||
isActive ? 'text-cyan-300' :
|
||
isCompleted ? 'text-green-300' :
|
||
isError ? 'text-red-300' :
|
||
'text-gray-400'
|
||
}`}>
|
||
{step.title}
|
||
</h6>
|
||
|
||
{/* Timing */}
|
||
{(step.startTime || step.endTime) && (
|
||
<span className="text-xs text-gray-500">
|
||
{step.endTime && step.startTime ?
|
||
`${((step.endTime - step.startTime) / 1000).toFixed(1)}s` :
|
||
isActive && step.startTime ?
|
||
`${((Date.now() - step.startTime) / 1000).toFixed(0)}s` :
|
||
''
|
||
}
|
||
</span>
|
||
)}
|
||
</div>
|
||
|
||
<p className={`text-xs mt-1 ${
|
||
isActive ? 'text-cyan-400' :
|
||
isCompleted ? 'text-green-400' :
|
||
isError ? 'text-red-400' :
|
||
'text-gray-500'
|
||
}`}>
|
||
{step.details || step.description}
|
||
</p>
|
||
</div>
|
||
|
||
{/* Active indicator */}
|
||
{isActive && (
|
||
<div className="flex-shrink-0">
|
||
<div className="w-3 h-3 bg-cyan-400 rounded-full animate-ping"></div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
|
||
{/* Overall Progress Bar */}
|
||
<div className="mt-6">
|
||
<div className="w-full bg-gray-700 rounded-full h-2">
|
||
<div
|
||
className="bg-gradient-to-r from-cyan-500 to-blue-500 h-2 rounded-full transition-all duration-500"
|
||
style={{ width: `${(progress.currentStep / progress.totalSteps) * 100}%` }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{result && result.type === 'multi_timeframe' && (
|
||
<div className="mt-6 space-y-4">
|
||
<div className="flex items-center justify-between">
|
||
<h3 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>
|
||
Multi-Timeframe Analysis
|
||
</h3>
|
||
<div className="text-xs text-gray-400">
|
||
{result.symbol} • {result.results.length} timeframes
|
||
</div>
|
||
</div>
|
||
|
||
<div className="p-4 bg-gradient-to-r from-purple-800/30 to-purple-700/30 rounded-lg border border-purple-500/30">
|
||
<h4 className="text-sm font-semibold text-purple-300 mb-2">Analysis Summary</h4>
|
||
<p className="text-white text-sm">{result.summary}</p>
|
||
</div>
|
||
|
||
<div className="grid gap-4">
|
||
{result.results
|
||
.sort((a: any, b: any) => {
|
||
// Sort by timeframe order: 5m, 15m, 30m, 1h, 2h, 4h, 1D
|
||
const timeframeOrder: {[key: string]: number} = {
|
||
'5': 1, '5m': 1,
|
||
'15': 2, '15m': 2,
|
||
'30': 3, '30m': 3,
|
||
'60': 4, '1h': 4,
|
||
'120': 5, '2h': 5,
|
||
'240': 6, '4h': 6,
|
||
'D': 7, '1D': 7
|
||
}
|
||
const orderA = timeframeOrder[a.timeframe] || timeframeOrder[a.timeframeLabel] || 999
|
||
const orderB = timeframeOrder[b.timeframe] || timeframeOrder[b.timeframeLabel] || 999
|
||
return orderA - orderB
|
||
})
|
||
.map((timeframeResult: any, index: number) => (
|
||
<div key={index} className={`p-4 rounded-lg border ${
|
||
timeframeResult.success
|
||
? 'bg-green-500/5 border-green-500/30'
|
||
: 'bg-red-500/5 border-red-500/30'
|
||
}`}>
|
||
<div className="flex items-center justify-between mb-3">
|
||
<h5 className={`font-semibold ${
|
||
timeframeResult.success ? 'text-green-400' : 'text-red-400'
|
||
}`}>
|
||
{timeframeResult.success ? '✅' : '❌'} {timeframeResult.timeframeLabel} Timeframe
|
||
</h5>
|
||
<div className="flex items-center space-x-2">
|
||
{timeframeResult.success && timeframeResult.result.analysis && (
|
||
<button
|
||
onClick={() => handleTradeClick(timeframeResult)}
|
||
className="px-3 py-1 bg-gradient-to-r from-green-500 to-green-600 text-white text-xs font-medium rounded hover:from-green-600 hover:to-green-700 transition-all transform hover:scale-105"
|
||
>
|
||
💰 Trade
|
||
</button>
|
||
)}
|
||
<span className="text-xs text-gray-400">
|
||
{timeframeResult.success ? 'Analysis Complete' : 'Failed'}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
{timeframeResult.success && timeframeResult.result.analysis && (
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||
<div className="text-center p-3 bg-gray-800/30 rounded">
|
||
<div className="text-xs text-gray-400">Sentiment</div>
|
||
<div className="text-sm font-medium text-white">
|
||
{safeRender(timeframeResult.result.analysis.marketSentiment)}
|
||
</div>
|
||
</div>
|
||
<div className="text-center p-3 bg-gray-800/30 rounded">
|
||
<div className="text-xs text-gray-400">Recommendation</div>
|
||
<div className="text-sm font-medium text-white">
|
||
{safeRender(timeframeResult.result.analysis.recommendation)}
|
||
</div>
|
||
</div>
|
||
<div className="text-center p-3 bg-gray-800/30 rounded">
|
||
<div className="text-xs text-gray-400">Confidence</div>
|
||
<div className="text-sm font-medium text-white">
|
||
{safeRender(timeframeResult.result.analysis.confidence)}%
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{timeframeResult.success && timeframeResult.result.analysis?.entry && (
|
||
<div className="mt-3 p-3 bg-yellow-500/5 border border-yellow-500/20 rounded">
|
||
<div className="text-xs text-yellow-400 font-semibold mb-1">Entry Setup</div>
|
||
<div className="text-sm text-white">
|
||
📍 ${safeRender(timeframeResult.result.analysis.entry.price)}
|
||
{timeframeResult.result.analysis.entry.buffer && (
|
||
<span className="text-yellow-400 ml-1">
|
||
{safeRender(timeframeResult.result.analysis.entry.buffer)}
|
||
</span>
|
||
)}
|
||
</div>
|
||
{timeframeResult.result.analysis.stopLoss && (
|
||
<div className="text-sm text-red-300 mt-1">
|
||
🛑 SL: ${safeRender(timeframeResult.result.analysis.stopLoss.price)}
|
||
</div>
|
||
)}
|
||
{timeframeResult.result.analysis.takeProfits && (
|
||
<div className="text-sm text-green-300 mt-1 space-y-1">
|
||
{timeframeResult.result.analysis.takeProfits.tp1 && (
|
||
<div>
|
||
🎯 TP1: ${typeof timeframeResult.result.analysis.takeProfits.tp1.price !== 'undefined'
|
||
? safeRender(timeframeResult.result.analysis.takeProfits.tp1.price)
|
||
: safeRender(timeframeResult.result.analysis.takeProfits.tp1)}
|
||
</div>
|
||
)}
|
||
{timeframeResult.result.analysis.takeProfits.tp2 && (
|
||
<div>
|
||
🎯 TP2: ${typeof timeframeResult.result.analysis.takeProfits.tp2.price !== 'undefined'
|
||
? safeRender(timeframeResult.result.analysis.takeProfits.tp2.price)
|
||
: safeRender(timeframeResult.result.analysis.takeProfits.tp2)}
|
||
</div>
|
||
)}
|
||
{/* Fallback for simple take profit format */}
|
||
{!timeframeResult.result.analysis.takeProfits.tp1 && !timeframeResult.result.analysis.takeProfits.tp2 && (
|
||
<div>
|
||
🎯 TP: {typeof timeframeResult.result.analysis.takeProfits === 'object'
|
||
? Object.values(timeframeResult.result.analysis.takeProfits).map((tp: any) => `$${safeRender(tp)}`).join(', ')
|
||
: `$${safeRender(timeframeResult.result.analysis.takeProfits)}`}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{!timeframeResult.success && (
|
||
<div className="text-red-300 text-sm">
|
||
Analysis failed for this timeframe
|
||
</div>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{result && result.analysis && (
|
||
<div className="mt-6 space-y-4">
|
||
<div className="flex items-center justify-between">
|
||
<h3 className="text-lg font-bold text-white flex items-center">
|
||
<span className="w-6 h-6 bg-gradient-to-br from-green-400 to-emerald-600 rounded-lg flex items-center justify-center mr-2 text-sm">
|
||
✅
|
||
</span>
|
||
Analysis Complete
|
||
</h3>
|
||
<div className="flex items-center space-x-3">
|
||
{result.analysis && (
|
||
<button
|
||
onClick={() => handleTradeClick({ result, timeframeLabel: selectedTimeframes.map(tf => timeframes.find(t => t.value === tf)?.label).join(', ') })}
|
||
className="px-4 py-2 bg-gradient-to-r from-green-500 to-green-600 text-white text-sm font-medium rounded-lg hover:from-green-600 hover:to-green-700 transition-all transform hover:scale-105"
|
||
>
|
||
💰 Execute Trade
|
||
</button>
|
||
)}
|
||
{result.screenshots && (
|
||
<div className="text-xs text-gray-400">
|
||
Screenshots: {result.screenshots.length} captured
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid gap-4">
|
||
{/* Summary */}
|
||
<div className="p-4 bg-gradient-to-r from-gray-800/50 to-gray-700/50 rounded-lg border border-gray-700">
|
||
<h4 className="text-sm font-semibold text-gray-300 mb-2 flex items-center">
|
||
<span className="w-4 h-4 bg-blue-500 rounded-full mr-2"></span>
|
||
Market Summary
|
||
</h4>
|
||
<p className="text-white text-sm leading-relaxed">{safeRender(result.analysis.summary)}</p>
|
||
</div>
|
||
|
||
{/* Key Metrics */}
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div className="p-4 bg-gradient-to-br from-green-500/10 to-emerald-500/10 border border-green-500/30 rounded-lg">
|
||
<h4 className="text-sm font-semibold text-green-400 mb-2">Market Sentiment</h4>
|
||
<p className="text-white font-medium">{safeRender(result.analysis.marketSentiment)}</p>
|
||
</div>
|
||
|
||
<div className="p-4 bg-gradient-to-br from-blue-500/10 to-cyan-500/10 border border-blue-500/30 rounded-lg">
|
||
<h4 className="text-sm font-semibold text-blue-400 mb-2">Recommendation</h4>
|
||
<p className="text-white font-medium">{safeRender(result.analysis.recommendation)}</p>
|
||
{result.analysis.confidence && (
|
||
<p className="text-cyan-300 text-xs mt-1">{safeRender(result.analysis.confidence)}% confidence</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Trading Levels */}
|
||
{result.analysis.keyLevels && (
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div className="p-4 bg-gradient-to-br from-red-500/10 to-rose-500/10 border border-red-500/30 rounded-lg">
|
||
<h4 className="text-sm font-semibold text-red-400 mb-2">Resistance Levels</h4>
|
||
<p className="text-red-300 font-mono text-sm">
|
||
{result.analysis.keyLevels.resistance?.join(', ') || 'None identified'}
|
||
</p>
|
||
</div>
|
||
|
||
<div className="p-4 bg-gradient-to-br from-green-500/10 to-emerald-500/10 border border-green-500/30 rounded-lg">
|
||
<h4 className="text-sm font-semibold text-green-400 mb-2">Support Levels</h4>
|
||
<p className="text-green-300 font-mono text-sm">
|
||
{result.analysis.keyLevels.support?.join(', ') || 'None identified'}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Trading Setup */}
|
||
{(result.analysis.entry || result.analysis.stopLoss || result.analysis.takeProfits) && (
|
||
<div className="p-4 bg-gradient-to-br from-purple-500/10 to-violet-500/10 border border-purple-500/30 rounded-lg">
|
||
<h4 className="text-sm font-semibold text-purple-400 mb-3">Trading Setup</h4>
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||
{result.analysis.entry && (
|
||
<div>
|
||
<span className="text-xs text-gray-400">Entry Point</span>
|
||
<p className="text-yellow-300 font-mono font-semibold">
|
||
📍 ${safeRender(result.analysis.entry.price || result.analysis.entry)}
|
||
{result.analysis.entry.buffer && (
|
||
<span className="text-yellow-400 text-xs ml-1">{safeRender(result.analysis.entry.buffer)}</span>
|
||
)}
|
||
</p>
|
||
{result.analysis.entry.rationale && (
|
||
<p className="text-xs text-gray-300 mt-1">💡 {safeRender(result.analysis.entry.rationale)}</p>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{result.analysis.stopLoss && (
|
||
<div>
|
||
<span className="text-xs text-gray-400">Stop Loss</span>
|
||
<p className="text-red-300 font-mono font-semibold">
|
||
🛑 ${safeRender(result.analysis.stopLoss.price || result.analysis.stopLoss)}
|
||
</p>
|
||
{result.analysis.stopLoss.rationale && (
|
||
<p className="text-xs text-gray-300 mt-1">💡 {safeRender(result.analysis.stopLoss.rationale)}</p>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{result.analysis.takeProfits && (
|
||
<div>
|
||
<span className="text-xs text-gray-400">Take Profit Targets</span>
|
||
<div className="space-y-2 mt-2">
|
||
{result.analysis.takeProfits.tp1 && (
|
||
<div className="p-2 bg-green-500/5 rounded border border-green-500/20">
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-yellow-400">🥉</span>
|
||
<span className="text-green-300 font-mono font-semibold">
|
||
TP1: ${typeof result.analysis.takeProfits.tp1.price !== 'undefined'
|
||
? result.analysis.takeProfits.tp1.price
|
||
: safeRender(result.analysis.takeProfits.tp1)}
|
||
</span>
|
||
</div>
|
||
{result.analysis.takeProfits.tp1.description && (
|
||
<p className="text-xs text-green-200 mt-1">
|
||
📋 {safeRender(result.analysis.takeProfits.tp1.description)}
|
||
</p>
|
||
)}
|
||
{result.analysis.takeProfits.tp1.rsiExpectation && (
|
||
<p className="text-xs text-blue-200 mt-1">
|
||
📊 RSI: {safeRender(result.analysis.takeProfits.tp1.rsiExpectation)}
|
||
</p>
|
||
)}
|
||
{result.analysis.takeProfits.tp1.obvExpectation && (
|
||
<p className="text-xs text-purple-200 mt-1">
|
||
📈 OBV: {safeRender(result.analysis.takeProfits.tp1.obvExpectation)}
|
||
</p>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{result.analysis.takeProfits.tp2 && (
|
||
<div className="p-2 bg-green-500/5 rounded border border-green-500/20">
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-gray-300">🥈</span>
|
||
<span className="text-green-300 font-mono font-semibold">
|
||
TP2: ${typeof result.analysis.takeProfits.tp2.price !== 'undefined'
|
||
? result.analysis.takeProfits.tp2.price
|
||
: safeRender(result.analysis.takeProfits.tp2)}
|
||
</span>
|
||
</div>
|
||
{result.analysis.takeProfits.tp2.description && (
|
||
<p className="text-xs text-green-200 mt-1">
|
||
📋 {safeRender(result.analysis.takeProfits.tp2.description)}
|
||
</p>
|
||
)}
|
||
{result.analysis.takeProfits.tp2.rsiExpectation && (
|
||
<p className="text-xs text-blue-200 mt-1">
|
||
📊 RSI: {safeRender(result.analysis.takeProfits.tp2.rsiExpectation)}
|
||
</p>
|
||
)}
|
||
{result.analysis.takeProfits.tp2.obvExpectation && (
|
||
<p className="text-xs text-purple-200 mt-1">
|
||
📈 OBV: {safeRender(result.analysis.takeProfits.tp2.obvExpectation)}
|
||
</p>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Fallback for simple take profit format */}
|
||
{!result.analysis.takeProfits.tp1 && !result.analysis.takeProfits.tp2 && (
|
||
<p className="text-green-300 font-mono font-semibold">
|
||
{typeof result.analysis.takeProfits === 'object'
|
||
? Object.values(result.analysis.takeProfits).map(tp => `$${safeRender(tp)}`).join(', ')
|
||
: `$${safeRender(result.analysis.takeProfits)}`}
|
||
</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Risk Management & Confirmation */}
|
||
{(result.analysis.riskToReward || result.analysis.confirmationTrigger) && (
|
||
<div className="p-4 bg-gradient-to-br from-amber-500/10 to-orange-500/10 border border-amber-500/30 rounded-lg">
|
||
<h4 className="text-sm font-semibold text-amber-400 mb-3">Risk Management</h4>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||
{result.analysis.riskToReward && (
|
||
<div>
|
||
<span className="text-xs text-amber-400">Risk/Reward Ratio</span>
|
||
<p className="text-amber-200 font-mono font-semibold">⚖️ {safeRender(result.analysis.riskToReward)}</p>
|
||
</div>
|
||
)}
|
||
|
||
{result.analysis.confirmationTrigger && (
|
||
<div>
|
||
<span className="text-xs text-amber-400">Confirmation Trigger</span>
|
||
<p className="text-amber-200 text-sm">🔔 {safeRender(result.analysis.confirmationTrigger)}</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Timeframe Risk Assessment */}
|
||
{result.analysis.timeframeRisk && (
|
||
<div className="p-4 bg-gradient-to-br from-red-500/10 to-pink-500/10 border border-red-500/30 rounded-lg">
|
||
<h4 className="text-sm font-semibold text-red-400 mb-3">⏰ Timeframe Risk Assessment</h4>
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||
{result.analysis.timeframeRisk.assessment && (
|
||
<div>
|
||
<span className="text-xs text-red-400">Risk Level</span>
|
||
<p className="text-red-200 font-semibold">📊 {safeRender(result.analysis.timeframeRisk.assessment)}</p>
|
||
</div>
|
||
)}
|
||
|
||
{result.analysis.timeframeRisk.positionSize && (
|
||
<div>
|
||
<span className="text-xs text-red-400">Position Size</span>
|
||
<p className="text-red-200 font-semibold">💼 {safeRender(result.analysis.timeframeRisk.positionSize)}</p>
|
||
</div>
|
||
)}
|
||
|
||
{result.analysis.timeframeRisk.leverageRecommendation && (
|
||
<div>
|
||
<span className="text-xs text-red-400">Leverage</span>
|
||
<p className="text-red-200 font-semibold">🎚️ {safeRender(result.analysis.timeframeRisk.leverageRecommendation)}</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Technical Indicators */}
|
||
{result.analysis.indicatorAnalysis && (
|
||
<div className="p-4 bg-gradient-to-br from-cyan-500/10 to-blue-500/10 border border-cyan-500/30 rounded-lg">
|
||
<h4 className="text-sm font-semibold text-cyan-400 mb-3">📈 Technical Indicators</h4>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||
{result.analysis.indicatorAnalysis.rsi && (
|
||
<div className="p-2 bg-cyan-500/5 rounded border border-cyan-500/20">
|
||
<span className="text-xs text-cyan-400 font-semibold">📊 RSI</span>
|
||
<p className="text-cyan-200 text-xs mt-1">{safeRender(result.analysis.indicatorAnalysis.rsi)}</p>
|
||
</div>
|
||
)}
|
||
|
||
{result.analysis.indicatorAnalysis.vwap && (
|
||
<div className="p-2 bg-blue-500/5 rounded border border-blue-500/20">
|
||
<span className="text-xs text-blue-400 font-semibold">📈 VWAP</span>
|
||
<p className="text-blue-200 text-xs mt-1">{safeRender(result.analysis.indicatorAnalysis.vwap)}</p>
|
||
</div>
|
||
)}
|
||
|
||
{result.analysis.indicatorAnalysis.obv && (
|
||
<div className="p-2 bg-purple-500/5 rounded border border-purple-500/20">
|
||
<span className="text-xs text-purple-400 font-semibold">📊 OBV</span>
|
||
<p className="text-purple-200 text-xs mt-1">{safeRender(result.analysis.indicatorAnalysis.obv)}</p>
|
||
</div>
|
||
)}
|
||
|
||
{result.analysis.indicatorAnalysis.macd && (
|
||
<div className="p-2 bg-indigo-500/5 rounded border border-indigo-500/20">
|
||
<span className="text-xs text-indigo-400 font-semibold">📉 MACD</span>
|
||
<p className="text-indigo-200 text-xs mt-1">{safeRender(result.analysis.indicatorAnalysis.macd)}</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Alternatives */}
|
||
{result.analysis.alternatives && (
|
||
<div className="p-4 bg-gradient-to-br from-violet-500/10 to-purple-500/10 border border-violet-500/30 rounded-lg">
|
||
<h4 className="text-sm font-semibold text-violet-400 mb-3">🔄 Alternative Strategies</h4>
|
||
<div className="space-y-2">
|
||
{result.analysis.alternatives.tigherStopOption && (
|
||
<div className="p-2 bg-violet-500/5 rounded border border-violet-500/20">
|
||
<span className="text-xs text-violet-400 font-semibold">🎯 Tighter Stop Option</span>
|
||
<p className="text-violet-200 text-xs mt-1">{safeRender(result.analysis.alternatives.tigherStopOption)}</p>
|
||
</div>
|
||
)}
|
||
|
||
{result.analysis.alternatives.scaledEntry && (
|
||
<div className="p-2 bg-purple-500/5 rounded border border-purple-500/20">
|
||
<span className="text-xs text-purple-400 font-semibold">📊 Scaled Entry</span>
|
||
<p className="text-purple-200 text-xs mt-1">{safeRender(result.analysis.alternatives.scaledEntry)}</p>
|
||
</div>
|
||
)}
|
||
|
||
{result.analysis.alternatives.invalidationScenario && (
|
||
<div className="p-2 bg-red-500/5 rounded border border-red-500/20">
|
||
<span className="text-xs text-red-400 font-semibold">❌ Invalidation Scenario</span>
|
||
<p className="text-red-200 text-xs mt-1">{safeRender(result.analysis.alternatives.invalidationScenario)}</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Layout Comparison Section */}
|
||
{result.analysis.layoutComparison && (
|
||
<div className="p-4 bg-gradient-to-br from-indigo-500/10 to-blue-500/10 border border-indigo-500/30 rounded-lg">
|
||
<h4 className="text-sm font-semibold text-indigo-400 mb-3">Multi-Layout Analysis</h4>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||
{result.analysis.layoutComparison.aiLayout && (
|
||
<div className="p-3 bg-blue-500/5 rounded border border-blue-500/20">
|
||
<span className="text-xs text-blue-400 font-semibold">AI Layout Insights</span>
|
||
<p className="text-xs text-blue-200 mt-1">{result.analysis.layoutComparison.aiLayout}</p>
|
||
</div>
|
||
)}
|
||
|
||
{result.analysis.layoutComparison.diyLayout && (
|
||
<div className="p-3 bg-green-500/5 rounded border border-green-500/20">
|
||
<span className="text-xs text-green-400 font-semibold">DIY Layout Insights</span>
|
||
<p className="text-xs text-green-200 mt-1">{result.analysis.layoutComparison.diyLayout}</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{result.analysis.layoutComparison.consensus && (
|
||
<div className="mt-3 p-3 bg-emerald-500/5 rounded border border-emerald-500/20">
|
||
<span className="text-xs text-emerald-400 font-semibold">Layout Consensus</span>
|
||
<p className="text-xs text-emerald-200 mt-1">{result.analysis.layoutComparison.consensus}</p>
|
||
</div>
|
||
)}
|
||
|
||
{result.analysis.layoutComparison.divergences && (
|
||
<div className="mt-3 p-3 bg-amber-500/5 rounded border border-amber-500/20">
|
||
<span className="text-xs text-amber-400 font-semibold">Layout Divergences</span>
|
||
<p className="text-xs text-amber-200 mt-1">{result.analysis.layoutComparison.divergences}</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Enhanced Indicator Analysis */}
|
||
{result.analysis.indicatorAnalysis?.crossLayoutConsensus && (
|
||
<div className="p-4 bg-gradient-to-br from-violet-500/10 to-purple-500/10 border border-violet-500/30 rounded-lg">
|
||
<h4 className="text-sm font-semibold text-violet-400 mb-2">Cross-Layout Consensus</h4>
|
||
<p className="text-violet-200 text-sm">{result.analysis.indicatorAnalysis.crossLayoutConsensus}</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{result && !result.analysis && result.screenshots && (
|
||
<div className="mt-6 p-4 bg-yellow-500/10 border border-yellow-500/30 rounded-lg">
|
||
<h3 className="text-lg font-bold text-yellow-400 mb-2">Screenshots Captured</h3>
|
||
<p className="text-yellow-300 text-sm mb-2">
|
||
Screenshots were captured successfully, but AI analysis failed or was not requested.
|
||
</p>
|
||
<div className="text-xs text-gray-400">
|
||
Screenshots: {result.screenshots.length} captured
|
||
</div>
|
||
<div className="mt-2">
|
||
{result.screenshots.map((screenshot: string, index: number) => (
|
||
<div key={index} className="text-xs text-gray-500 font-mono">
|
||
{screenshot.split('/').pop()}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Screenshot Gallery */}
|
||
{result && result.screenshots && (
|
||
<ScreenshotGallery
|
||
screenshots={result.screenshots}
|
||
symbol={symbol}
|
||
timeframes={selectedTimeframes.map(tf => timeframes.find(t => t.value === tf)?.label || tf)}
|
||
enlargedImage={enlargedScreenshot}
|
||
onImageClick={handleScreenshotClick}
|
||
onClose={() => setEnlargedScreenshot(null)}
|
||
/>
|
||
)}
|
||
|
||
{/* Multi-timeframe Screenshot Gallery */}
|
||
{result && result.type === 'multi_timeframe' && result.results && (
|
||
<ScreenshotGallery
|
||
screenshots={result.results
|
||
.filter((r: any) => r.success && r.result.screenshots)
|
||
.sort((a: any, b: any) => {
|
||
// Sort by timeframe order: 5m, 15m, 30m, 1h, 2h, 4h, 1D
|
||
const timeframeOrder: {[key: string]: number} = {
|
||
'5': 1, '5m': 1,
|
||
'15': 2, '15m': 2,
|
||
'30': 3, '30m': 3,
|
||
'60': 4, '1h': 4,
|
||
'120': 5, '2h': 5,
|
||
'240': 6, '4h': 6,
|
||
'D': 7, '1D': 7
|
||
}
|
||
const orderA = timeframeOrder[a.timeframe] || timeframeOrder[a.timeframeLabel] || 999
|
||
const orderB = timeframeOrder[b.timeframe] || timeframeOrder[b.timeframeLabel] || 999
|
||
return orderA - orderB
|
||
})
|
||
.flatMap((r: any) => r.result.screenshots)}
|
||
symbol={symbol}
|
||
timeframes={result.results
|
||
.filter((r: any) => r.success)
|
||
.sort((a: any, b: any) => {
|
||
// Sort by timeframe order: 5m, 15m, 30m, 1h, 2h, 4h, 1D
|
||
const timeframeOrder: {[key: string]: number} = {
|
||
'5': 1, '5m': 1,
|
||
'15': 2, '15m': 2,
|
||
'30': 3, '30m': 3,
|
||
'60': 4, '1h': 4,
|
||
'120': 5, '2h': 5,
|
||
'240': 6, '4h': 6,
|
||
'D': 7, '1D': 7
|
||
}
|
||
const orderA = timeframeOrder[a.timeframe] || timeframeOrder[a.timeframeLabel] || 999
|
||
const orderB = timeframeOrder[b.timeframe] || timeframeOrder[b.timeframeLabel] || 999
|
||
return orderA - orderB
|
||
})
|
||
.map((r: any) => r.timeframeLabel)}
|
||
enlargedImage={enlargedScreenshot}
|
||
onImageClick={handleScreenshotClick}
|
||
onClose={() => setEnlargedScreenshot(null)}
|
||
/>
|
||
)}
|
||
|
||
{/* Trade Modal */}
|
||
{console.log('🔥 About to render TradeModal with:', {
|
||
isOpen: tradeModalOpen,
|
||
tradeData: tradeModalData
|
||
})}
|
||
<TradeModal
|
||
isOpen={tradeModalOpen}
|
||
onClose={() => {
|
||
console.log('🔥 TradeModal onClose called')
|
||
setTradeModalOpen(false)
|
||
}}
|
||
tradeData={tradeModalData}
|
||
onExecute={executeTrade}
|
||
/>
|
||
</div>
|
||
)
|
||
}
|