import React, { useState, useCallback, useEffect } from 'react'; import { useDropzone } from 'react-dropzone'; import { Upload, X, ShieldCheck, Target, TrendingUp, AlertTriangle, Loader2, Zap, BrainCircuit, Camera, ScanLine, Bell, BellRing, Activity, LineChart, Crosshair } from 'lucide-react'; import { motion, AnimatePresence } from 'motion/react'; import { analyzeForexChart } from '../services/geminiService'; import { AnalysisResponse, SignalResult, SignalType } from '../types'; import { cn } from '../lib/utils'; import { toast } from 'sonner'; import { ref, uploadBytes, getDownloadURL } from 'firebase/storage'; import axios from 'axios'; import { compressImage, cleanupStorage } from '../lib/imageUtils'; import { storage, auth, db, handleFirestoreError, OperationType } from '../lib/firebase'; import { collection, addDoc, serverTimestamp, query, where, getDocs, Timestamp } from 'firebase/firestore'; import { fetchCurrentPrice } from '../services/marketData'; import { sendTelegramAlert } from '../services/notifications'; import { WatchlistPanel } from './WatchlistPanel'; export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void }> = ({ userData, onGoToHistory }) => { const [file, setFile] = useState(null); const [preview, setPreview] = useState(null); const [isAnalyzing, setIsAnalyzing] = useState(false); const [result, setResult] = useState(null); const [error, setError] = useState(null); const [mode, setMode] = useState<'Técnico' | 'Fundamental' | 'Híbrido'>('Híbrido'); const [tradingStyle, setTradingStyle] = useState<'Automático' | 'Scalping' | 'Day Trading' | 'Swing'>('Automático'); const [usageInfo, setUsageInfo] = useState<{ used: number, limit: number }>({ used: 0, limit: userData?.analysisLimit ?? 8 }); const [alerts, setAlerts] = useState<{type: string, price: string, targetValue: number}[]>([]); const [customAlertPrices, setCustomAlertPrices] = useState>({}); const [alertOptions, setAlertOptions] = useState({ execution: true, expiration: true }); const [marketPrice, setMarketPrice] = useState(null); const now = Date.now(); const isLifetime = userData?.plan === 'lifetime'; const endsAt = userData?.subscriptionEndsAt; const isExpired = !!(!isLifetime && endsAt && now > endsAt); const daysRemaining = !isLifetime && endsAt ? (endsAt - now) / (1000 * 60 * 60 * 24) : null; const isEndingSoon = daysRemaining !== null && Math.ceil(daysRemaining) <= 3 && Math.ceil(daysRemaining) > 0; useEffect(() => { if (result && result.entry) { if ('Notification' in window && Notification.permission !== 'granted') { Notification.requestPermission(); } } else { setMarketPrice(null); setAlerts([]); } }, [result]); useEffect(() => { if (!result || !result.pair) return; let isMounted = true; let currentPrice = marketPrice; const fetchPrice = async () => { try { const price = await fetchCurrentPrice(result.pair); if (price !== null && isMounted) { setMarketPrice(price); currentPrice = price; checkAlerts(price); } } catch (e: any) { console.error("Error fetching market price", e?.message || e); } }; const checkAlerts = (price: number) => { let triggeredIndex = -1; const triggeredAlert = alerts.find((alert, index) => { // Check if price crossed the target since last check // Fallback condition if currentPrice is null is just exact match but that's rare, // usually we just check within a small margin const isTriggered = Math.abs(price - alert.targetValue) < (alert.targetValue * 0.0002) || (currentPrice && currentPrice < alert.targetValue && price >= alert.targetValue) || (currentPrice && currentPrice > alert.targetValue && price <= alert.targetValue); if (isTriggered) triggeredIndex = index; return isTriggered; }); if (triggeredAlert && triggeredIndex !== -1) { const msg = `🚨 Alerta QuantScan: O ativo ${result.pair} atingiu seu alvo de ${triggeredAlert.type} (${triggeredAlert.price}).`; try { const audio = new Audio('https://assets.mixkit.co/active_storage/sfx/2869/2869-preview.mp3'); audio.play().catch(e => console.error("Audio play failed:", e)); } catch(e) { console.error(e) } if ('Notification' in window && Notification.permission === 'granted') { new Notification('🚨 Alerta QuantScan', { body: msg, icon: 'https://i.ibb.co/9BwbV3M/FXBROS-WORLD-3.png' }); } else { window.alert(msg); } setAlerts(prev => prev.filter((_, i) => i !== triggeredIndex)); } }; fetchPrice(); const interval = setInterval(fetchPrice, 60000); // Poll every minute return () => { isMounted = false; clearInterval(interval); }; }, [result, alerts]); useEffect(() => { const fetchUsage = async () => { if (!auth.currentUser) return; const today = new Date(); today.setHours(0, 0, 0, 0); try { const q = query( collection(db, 'signals'), where('userId', '==', auth.currentUser.uid) ); const snapshot = await getDocs(q).catch(err => { handleFirestoreError(err, OperationType.LIST, 'signals'); throw err; }); const todaySignals = snapshot.docs.filter((doc) => { const data = doc.data(); return data.timestamp >= today.getTime(); }); setUsageInfo({ used: todaySignals.length, limit: userData?.analysisLimit ?? 8 }); } catch (err) { console.error("Error fetching usage stats", err); } }; fetchUsage(); }, [result, userData]); const handleStartAnalysis = async () => { if (isExpired) { setError("Sua assinatura expirou. Por favor, contate o administrador para renovar o plano."); return; } if (!preview || !file) return; if (usageInfo.used >= usageInfo.limit) { setError(`Limite diário atingido (${usageInfo.used}/${usageInfo.limit}). Faça upgrade para análises ilimitadas.`); return; } setIsAnalyzing(true); setError(null); try { let downloadUrl = null; if (auth.currentUser) { const fileRef = ref(storage, `scans/${auth.currentUser.uid}/${Date.now()}_${file.name}`); await uploadBytes(fileRef, file); downloadUrl = await getDownloadURL(fileRef); // Clean up old scans for this user to avoid storage limits (keep max 100) cleanupStorage(`scans/${auth.currentUser.uid}`, 100); } const base64 = preview.split(',')[1]; const analysis = await analyzeForexChart(base64, undefined, mode, userData?.plan, tradingStyle); analysis.score = Math.round(analysis.score); setResult(analysis); if (auth.currentUser) { const sanitizedAnalysis = { ...analysis }; if (sanitizedAnalysis.pair) sanitizedAnalysis.pair = String(sanitizedAnalysis.pair).substring(0, 100); if (sanitizedAnalysis.timeframe) sanitizedAnalysis.timeframe = String(sanitizedAnalysis.timeframe).substring(0, 50); sanitizedAnalysis.score = Math.round(Number(sanitizedAnalysis.score) || 0); await addDoc(collection(db, 'signals'), { ...sanitizedAnalysis, screenshotUrl: downloadUrl, userId: auth.currentUser.uid, createdAt: serverTimestamp(), timestamp: Date.now(), result: SignalResult.PENDING, type: sanitizedAnalysis.decision || 'WAIT' }).catch(err => { handleFirestoreError(err, OperationType.CREATE, 'signals'); throw err; }); toast.success(`Scanner Concluído com ${sanitizedAnalysis.score}% de Confiança no ${sanitizedAnalysis.pair || 'Ativo'}!`, { description: `Decisão de ${sanitizedAnalysis.decision}` }); if (sanitizedAnalysis.decision !== 'WAIT') { axios.post('/api/onesignal/notify', { title: 'Novo Sinal QuantScan IA 🚨', message: `${sanitizedAnalysis.pair} - ${sanitizedAnalysis.decision} (${sanitizedAnalysis.signalType || 'Scalping'}). Confiança: ${sanitizedAnalysis.score}%` }).catch((err: any) => console.error("OneSignal error", err?.message || err)); sendTelegramAlert(sanitizedAnalysis).catch((err: any) => console.error("Telegram Error", err?.message || err)); } } } catch (err: any) { console.error("Gemini Error:", err?.message || err); let errorMessage = err.message || 'Falha ao analisar imagem. Verifique se o gráfico está claro.'; if (errorMessage.includes('429') || errorMessage.includes('RESOURCE_EXHAUSTED') || errorMessage.includes('prepayment credits')) { errorMessage = 'Sua cota de uso da API do Google Gemini (IA) foi excedida ou os créditos acabaram. Por favor, acesse o painel do Google AI Studio (https://ai.studio) para verificar seu faturamento e recarregar os créditos.'; } else { errorMessage = `Erro na API: ${errorMessage}`; } setError(errorMessage); toast.error('Erro na análise do Scanner', { description: errorMessage }); } finally { setIsAnalyzing(false); } }; const onDrop = useCallback(async (acceptedFiles: File[]) => { const selectedFile = acceptedFiles[0]; // Comprimir a imagem antes de setar state para reduzir MS const compressedFile = await compressImage(selectedFile, 1200, 0.7); setFile(compressedFile); const reader = new FileReader(); reader.onload = () => { setPreview(reader.result as string); }; reader.readAsDataURL(compressedFile); setError(null); }, []); const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop, accept: { 'image/*': [] }, multiple: false } as any); const reset = () => { setFile(null); setPreview(null); setResult(null); setError(null); setAlerts([]); }; const handleCustomAlertPriceChange = (type: string, value: string) => { setCustomAlertPrices(prev => ({ ...prev, [type]: value })); }; const toggleAlert = (type: string, priceStr: string) => { const numPrice = parseFloat(priceStr.replace(/[^0-9.]/g, '')); if (isNaN(numPrice)) return; setAlerts(prev => { const exists = prev.some(a => a.type === type); if (exists) { return prev.filter(a => a.type !== type); } else { return [...prev, { type, price: priceStr, targetValue: numPrice }]; } }); }; return (
{isExpired && (

Assinatura Expirada

Sua assinatura expirou. Renove seu plano para continuar usando o scanner analisador.

)} {isEndingSoon && (

Assinatura Expirando

Sua assinatura expira em {Math.ceil(daysRemaining)} dia(s). Renove agora para não perder o acesso ao scanner.

)}

Novo Scan

Envie um gráfico para análise de IA Pro.

= usageInfo.limit ? "bg-brand-red" : "bg-green-500" )} style={{ width: usageInfo.limit === 999999 ? '100%' : `${Math.min(100, (usageInfo.used / usageInfo.limit) * 100)}%` }} />
{usageInfo.limit === 999999 ? `${usageInfo.used}/∞ Scans` : `${usageInfo.used}/${usageInfo.limit} Scans`}
{!result ? (
{isAnalyzing && (
Logo

Processando Scan Institucional...

)} {!preview ? (
Tipo de Análise
{(['Técnico', 'Fundamental', 'Híbrido'] as const).map((m) => ( ))}
Estilo de Trading
{(['Automático', 'Scalping', 'Day Trading', 'Swing'] as const).map((s) => ( ))}

Fazer Upload ou Arrastar

TradingView, MT4/MT5, cTrader

) : (
Preview
{error &&

{error}

}
)}
) : (
{/* Score & Signal Info */}
IA SCORE {result.score}%

= 80 ? (result.decision === SignalType.BUY ? "text-green-500" : "text-brand-red") : result.score >= 60 ? "text-orange-500" : "text-zinc-500" )}> {result.score >= 80 ? '🔥 ALTA PROBABILIDADE' : result.score >= 60 ? '⚖️ MÉDIA PROBABILIDADE' : '❌ EVITAR'}

{result.justification}

⚠️ {result.alerta}

ATIVO IDENTIFICADO {result.pair}
TIMEFRAME {result.timeframe}
RISCO {result.riskLevel || 'MED'}

Padrões & Estrutura Detectada

{result.estrutura || result.tecnica || 'Padrões de liquidez identificados pelo modelo Quântico.'}

{/* Trading Decision Block */}
{result.signalType || 'EXECUÇÃO'} {result.pair}
{result.decision}
{marketPrice !== null && (
MARKET PRICE {marketPrice.toFixed(5)}
)}
ENTRY LEVEL
handleCustomAlertPriceChange('Entrada', e.target.value)} />
STOP LOSS
handleCustomAlertPriceChange('Stop Loss', e.target.value)} />
TAKE PROFIT 1
handleCustomAlertPriceChange('Take Profit 1', e.target.value)} />
{result.takeProfit2 && (
TAKE PROFIT 2
handleCustomAlertPriceChange('Take Profit 2', e.target.value)} />
)} {result.takeProfit3 && (
TAKE PROFIT 3
handleCustomAlertPriceChange('Take Profit 3', e.target.value)} />
)}

Opções de Notificação

Notificar Horário de Negociação
Sinal Inválido ou Expirado
{result.multiTimeFrameAnalysis && (

MULTI TIME FRAME ANALYSIS

{result.multiTimeFrameAnalysis}

)}

RECONHECIMENTO DE PADRÕES & ESTRUTURA

{result.estrutura || result.tecnica}

ANÁLISE FUNDAMENTAL & LIQUIDEZ

{result.fundamental}

{result.winrateLearning && (

WINRATE LEARNING AI

{result.winrateLearning}

)} {result.trailingStop && (

TRAILING STOP AI

{result.trailingStop}

)}

ANALISE GERAL

{result.analiseGeral}

{/* Analysis Illustration */}

Mapeamento Técnico da IA

Technical Analysis Map {/* UI Overlays to simulate AI analysis */} {/* Predictive Trend Overlay */}
{result.decision === SignalType.BUY && ( )} {result.decision === SignalType.SELL && ( )} {result.decision !== SignalType.WAIT && ( <> {result.score}% OVERLAY DE PREVISÃO )}
Fluxo Institucional Detectado HFT Signature

{result.pair} / {result.timeframe}

Ref: AIS-BETA-{Math.floor(Math.random() * 10000)}

Confirmação IA

{result.score}%

{/* Decorative Scanning Line */}
{onGoToHistory && ( )}
)}
); };