- Pre-generate sessionId on client side before API call to avoid race conditions - Add small delays in progress tracker to ensure EventSource connection is established - Improve logging and error handling in progress streaming - Add connection confirmation messages in EventSource stream - Fix TypeScript interface to include sessionId in AnalysisProgress This should resolve the lag between actual analysis progress and progress bar display.
1519 lines
68 KiB
TypeScript
1519 lines
68 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())
|
||
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: '₿', color: 'from-orange-400 to-orange-600' },
|
||
{ name: 'Ethereum', symbol: 'ETHUSD', icon: 'Ξ', color: 'from-blue-400 to-blue-600' },
|
||
{ name: 'Solana', symbol: 'SOLUSD', icon: '◎', color: 'from-purple-400 to-purple-600' },
|
||
{ name: 'Sui', symbol: 'SUIUSD', icon: '🔷', color: 'from-cyan-400 to-cyan-600' },
|
||
{ name: 'Avalanche', symbol: 'AVAXUSD', icon: '🔺', color: 'from-red-400 to-red-600' },
|
||
{ name: 'Cardano', symbol: 'ADAUSD', icon: '♠', color: 'from-indigo-400 to-indigo-600' },
|
||
{ name: 'Polygon', symbol: 'MATICUSD', icon: '🔷', color: 'from-violet-400 to-violet-600' },
|
||
{ name: 'Chainlink', symbol: 'LINKUSD', icon: '🔗', color: 'from-blue-400 to-blue-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']) // Default to both AI and DIY
|
||
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])
|
||
|
||
const toggleLayout = (layout: string) => {
|
||
setSelectedLayouts(prev =>
|
||
prev.includes(layout)
|
||
? prev.filter(l => l !== layout)
|
||
: [...prev, layout]
|
||
)
|
||
}
|
||
|
||
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>
|
||
|
||
{/* Coin Selection */}
|
||
<div className="grid grid-cols-2 md:grid-cols-4 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`}>
|
||
<span className="text-white font-bold text-sm">{coin.icon}</span>
|
||
</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>
|
||
|
||
{/* Timeframe Selection */}
|
||
<div className="mb-6">
|
||
<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>
|
||
|
||
{/* 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 => (
|
||
<label key={layout} className="group relative">
|
||
<input
|
||
type="checkbox"
|
||
checked={selectedLayouts.includes(layout)}
|
||
onChange={() => toggleLayout(layout)}
|
||
className="sr-only"
|
||
/>
|
||
<div className={`flex items-center p-3 rounded-lg border cursor-pointer transition-all ${
|
||
selectedLayouts.includes(layout)
|
||
? '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 ${
|
||
selectedLayouts.includes(layout)
|
||
? 'border-cyan-500 bg-cyan-500'
|
||
: 'border-gray-600'
|
||
}`}>
|
||
{selectedLayouts.includes(layout) && (
|
||
<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">{layout}</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.join(', ')}</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Quick Timeframe Actions */}
|
||
<div className="mb-6">
|
||
<label className="block text-xs font-medium text-gray-400 mb-3">Quick Actions</label>
|
||
<div className="grid grid-cols-4 gap-2 mb-3">
|
||
{timeframes.map(tf => (
|
||
<button
|
||
key={tf.value}
|
||
onClick={() => quickTimeframeTest(tf.value)}
|
||
disabled={loading || selectedLayouts.length === 0}
|
||
className={`py-2 px-3 rounded-lg text-xs font-medium transition-all ${
|
||
selectedTimeframes.includes(tf.value)
|
||
? 'bg-cyan-500 text-white shadow-lg'
|
||
: loading
|
||
? 'bg-gray-700 text-gray-500 cursor-not-allowed'
|
||
: 'bg-gray-700 text-gray-300 hover:bg-gray-600 hover:text-white transform hover:scale-105'
|
||
}`}
|
||
>
|
||
{tf.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||
<button
|
||
onClick={testAllTimeframes}
|
||
disabled={loading || selectedLayouts.length === 0 || !symbol}
|
||
className={`py-2 px-4 rounded-lg text-sm font-medium transition-all ${
|
||
loading
|
||
? 'bg-gray-700 text-gray-500 cursor-not-allowed'
|
||
: 'bg-gradient-to-r from-purple-500 to-pink-500 text-white hover:from-purple-600 hover:to-pink-600 transform hover:scale-[1.02] active:scale-[0.98] shadow-lg'
|
||
}`}
|
||
>
|
||
🔍 Analyze All Timeframes
|
||
</button>
|
||
<button
|
||
onClick={() => setSelectedTimeframes([])}
|
||
disabled={loading || selectedTimeframes.length === 0}
|
||
className={`py-2 px-4 rounded-lg text-sm font-medium transition-all ${
|
||
loading || selectedTimeframes.length === 0
|
||
? 'bg-gray-700 text-gray-500 cursor-not-allowed'
|
||
: 'bg-gray-700 text-gray-300 hover:bg-gray-600 hover:text-white transform hover:scale-[1.02] active:scale-[0.98]'
|
||
}`}
|
||
>
|
||
🗑️ Clear Selection
|
||
</button>
|
||
</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.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>
|
||
)
|
||
}
|