Files
QuantScan-Pro-/src/components/AnalysisView.tsx
T
desartstudio95 c95af0e973 feat: Enhance AI analysis capabilities and UI
Refactor AI analysis to support different modes (Technical, Fundamental, Hybrid) and introduce richer data points in the `Signal` and `AnalysisResponse` interfaces. This improves the granularity and detail of the AI's market insights.

Remove the `MarketTicker` component from the main layout and simplify the `Navbar` by removing the 'Plans' tab. These changes streamline the user interface.
2026-05-03 12:02:27 +02:00

453 lines
22 KiB
TypeScript

import React, { useState, useCallback, useEffect } from 'react';
import { useDropzone } from 'react-dropzone';
import { Upload, X, ShieldCheck, Target, TrendingUp, AlertTriangle, Loader2, Zap, BrainCircuit, Camera, ScanLine } 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 { ref, uploadBytes, getDownloadURL } from 'firebase/storage';
import { storage, auth, db, handleFirestoreError, OperationType } from '../lib/firebase';
import { collection, addDoc, serverTimestamp, query, where, getDocs, Timestamp } from 'firebase/firestore';
export const AnalysisView: React.FC<{ userData?: any }> = ({ userData }) => {
const [file, setFile] = useState<File | null>(null);
const [preview, setPreview] = useState<string | null>(null);
const [isAnalyzing, setIsAnalyzing] = useState(false);
const [result, setResult] = useState<AnalysisResponse | null>(null);
const [error, setError] = useState<string | null>(null);
const [mode, setMode] = useState<'Técnico' | 'Fundamental' | 'Híbrido'>('Híbrido');
const [usageInfo, setUsageInfo] = useState<{ used: number, limit: number }>({ used: 0, limit: userData?.analysisLimit ?? 8 });
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 (!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, `files/${Date.now()}_${file.name}`);
await uploadBytes(fileRef, file);
downloadUrl = await getDownloadURL(fileRef);
}
const base64 = preview.split(',')[1];
const analysis = await analyzeForexChart(base64, undefined, mode);
setResult(analysis);
if (auth.currentUser) {
await addDoc(collection(db, 'signals'), {
...analysis,
screenshotUrl: downloadUrl,
userId: auth.currentUser.uid,
createdAt: serverTimestamp(),
timestamp: Date.now(),
result: SignalResult.PENDING,
type: analysis.decision
}).catch(err => {
handleFirestoreError(err, OperationType.CREATE, 'signals');
throw err;
});
}
} catch (err: any) {
console.error(err);
setError(err.message || 'Falha ao analisar imagem. Verifique se o gráfico está claro.');
} finally {
setIsAnalyzing(false);
}
};
const onDrop = useCallback((acceptedFiles: File[]) => {
const selectedFile = acceptedFiles[0];
setFile(selectedFile);
const reader = new FileReader();
reader.onload = () => {
setPreview(reader.result as string);
};
reader.readAsDataURL(selectedFile);
setError(null);
}, []);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: { 'image/*': [] },
multiple: false
} as any);
const reset = () => {
setFile(null);
setPreview(null);
setResult(null);
setError(null);
};
return (
<div className="max-w-4xl mx-auto space-y-5">
<header className="space-y-1">
<h1 className="text-xl font-black italic tracking-tighter text-white flex items-center gap-2.5 uppercase">
<div className="w-7 h-7 rounded-lg border-2 border-brand-red flex items-center justify-center red-glow">
<TrendingUp size={14} className="text-brand-red" />
</div>
Novo Scan
</h1>
<div className="flex items-center justify-between">
<p className="text-zinc-500 text-[10px] font-medium leading-none">Envie um gráfico para análise de IA Pro.</p>
<div className="flex items-center gap-2">
<div className="h-1.5 w-24 bg-white/5 rounded-full overflow-hidden border border-white/5">
<div
className={cn(
"h-full transition-all duration-500",
usageInfo.used >= usageInfo.limit ? "bg-brand-red" : "bg-green-500"
)}
style={{ width: usageInfo.limit === 999999 ? '100%' : `${Math.min(100, (usageInfo.used / usageInfo.limit) * 100)}%` }}
/>
</div>
<span className="text-[10px] font-black uppercase text-zinc-500">
{usageInfo.limit === 999999 ? `${usageInfo.used}/∞ Scans` : `${usageInfo.used}/${usageInfo.limit} Scans`}
</span>
</div>
</div>
</header>
{!result ? (
<div className="glass-card p-6 border-zinc-900 min-h-[300px] flex flex-col justify-center items-center group relative">
{isAnalyzing && (
<div className="absolute inset-0 z-50 bg-brand-dark/95 backdrop-blur-sm flex flex-col items-center justify-center gap-6 rounded-xl">
<div className="relative w-40 h-40 flex items-center justify-center">
<div className="absolute inset-0 border-2 border-brand-red/30 animate-ping rounded-full" />
<div className="absolute inset-4 border-2 border-brand-red/20 animate-pulse rounded-full" />
<img src="https://i.ibb.co/9BwbV3M/FXBROS-WORLD-3.png" className="w-16 h-16 animate-pulse" alt="Logo" />
<motion.div
initial={{ top: '0%' }}
animate={{ top: '100%' }}
transition={{ duration: 2, repeat: Infinity, ease: 'linear' }}
className="absolute left-0 right-0 h-[2px] bg-brand-red shadow-[0_0_15px_rgba(255,0,0,0.8)]"
/>
</div>
<p className="text-brand-red font-black text-lg italic tracking-tighter animate-pulse uppercase">Processando Scan Institucional...</p>
</div>
)}
{!preview ? (
<div className="w-full flex flex-col gap-4 items-center justify-center">
<div className="flex gap-2 mb-4 w-full justify-center">
{(['Técnico', 'Fundamental', 'Híbrido'] as const).map((m) => (
<button
key={m}
onClick={() => setMode(m)}
className={cn(
"px-4 py-1.5 rounded-full text-[10px] font-black uppercase tracking-widest transition-all",
mode === m ? "bg-brand-red text-white" : "bg-white/5 text-white/50 hover:bg-white/10"
)}
>
{m}
</button>
))}
</div>
<div
{...getRootProps()}
className={cn(
"w-full flex flex-col items-center justify-center py-10 rounded-xl cursor-pointer transition-all duration-300 border border-transparent",
isDragActive ? "bg-brand-red/5 scale-[0.98] border-brand-red/30" : "hover:bg-zinc-800/20"
)}
>
<input {...getInputProps()} />
<div className="w-16 h-16 bg-brand-gray rounded-xl flex items-center justify-center mb-4 border border-white/5 group-hover:border-brand-red/30 transition-colors">
<Upload size={24} className="text-zinc-600 group-hover:scale-110 transition-transform" />
</div>
<p className="text-base font-black uppercase italic tracking-tighter text-white mb-1 text-center px-4">Fazer Upload ou Arrastar</p>
<p className="text-zinc-600 text-[10px] font-bold uppercase tracking-widest text-center px-4">TradingView, MT4/MT5, cTrader</p>
</div>
<div className="flex gap-3 w-full">
<label className="flex-1 bg-brand-dark border-2 border-white/5 text-white py-3.5 rounded-xl font-black text-xs hover:border-brand-red/50 transition-all active:scale-95 flex items-center justify-center gap-2 cursor-pointer relative overflow-hidden group">
<Camera size={16} className="text-zinc-400 group-hover:text-brand-red transition-colors" />
<span className="whitespace-nowrap pt-0.5">TIRAR FOTO</span>
<input type="file" accept="image/*" capture="environment" className="opacity-0 absolute inset-0 cursor-pointer w-full h-full" onChange={(e) => {
const f = e.target.files?.[0];
if (f) onDrop([f]);
}} />
</label>
<label className="flex-1 bg-brand-red/10 border-2 border-brand-red/30 text-brand-red py-3.5 rounded-xl font-black text-xs hover:bg-brand-red/20 transition-all active:scale-95 flex items-center justify-center gap-2 cursor-pointer relative overflow-hidden group">
<ScanLine size={16} className="group-hover:scale-110 transition-transform" />
<span className="whitespace-nowrap pt-0.5">SCAN AO VIVO</span>
<input type="file" accept="image/*" capture="environment" className="opacity-0 absolute inset-0 cursor-pointer w-full h-full" onChange={(e) => {
const f = e.target.files?.[0];
if (f) onDrop([f]);
}} />
</label>
</div>
</div>
) : (
<div className="w-full space-y-4">
<div className="relative rounded-lg overflow-hidden border border-white/5 max-h-[400px]">
<img src={preview} alt="Preview" className="w-full h-full object-contain" />
<button
onClick={reset}
className="absolute top-3 right-3 p-1.5 bg-black/60 rounded-full text-white hover:bg-brand-red transition-colors"
>
<X size={16} />
</button>
</div>
<div className="flex gap-3">
<button
onClick={handleStartAnalysis}
disabled={isAnalyzing}
className="flex-1 bg-brand-red text-white py-3.5 rounded-xl font-black text-base hover:bg-brand-red/90 transition-all active:scale-95 flex items-center justify-center gap-2 disabled:opacity-50"
>
{isAnalyzing ? (
<>
<Loader2 className="animate-spin" size={20} />
ANALISANDO...
</>
) : (
'ANALISAR GRÁFICO'
)}
</button>
</div>
{error && <p className="text-brand-red text-center text-xs font-bold">{error}</p>}
</div>
)}
</div>
) : (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="space-y-5"
>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Score & Signal Info */}
<div className="glass-card flex flex-col items-center justify-center text-center space-y-4">
<div className="relative w-36 h-36 flex items-center justify-center">
<svg className="w-full h-full" viewBox="0 0 100 100">
<circle
cx="50" cy="50" r="45"
fill="none"
stroke="rgba(255,255,255,0.03)"
strokeWidth="6"
/>
<circle
cx="50" cy="50" r="45"
fill="none"
stroke="currentColor"
strokeWidth="6"
strokeDasharray={283}
strokeDashoffset={283 - (283 * result.score) / 100}
className={cn(
"transition-all duration-1000 ease-out",
result.decision === SignalType.BUY ? "text-green-500" : "text-brand-red"
)}
strokeLinecap="round"
transform="rotate(-90 50 50)"
/>
</svg>
<div className="absolute inset-0 flex flex-col items-center justify-center">
<span className={cn(
"text-4xl font-black leading-none",
result.decision === SignalType.BUY ? "text-green-500" : "text-brand-red red-text-glow"
)}>
{result.score}%
</span>
</div>
</div>
<div>
<h3 className={cn(
"text-sm font-black uppercase tracking-wider italic",
result.score >= 80
? (result.decision === SignalType.BUY ? "text-green-500" : "text-brand-red red-text-glow")
: result.score >= 60 ? "text-orange-500" : "text-zinc-500"
)}>
{result.score >= 80 ? '🔥 ALTA PROBABILIDADE' : result.score >= 60 ? '⚖️ MÉDIA PROBABILIDADE' : '❌ EVITAR'}
</h3>
<p className="text-zinc-500 mt-1 text-[10px] font-medium max-w-[200px]">{result.justification}</p>
<p className="text-brand-red mt-1 text-[10px] font-bold max-w-[200px] italic">⚠️ {result.alerta}</p>
</div>
<div className="w-full pt-4 border-t border-white/5 flex gap-3">
<div className="flex-1 glass-card p-2">
<span className="block text-[8px] text-zinc-600 uppercase font-black mb-0.5">MODO</span>
<span className="text-xs font-black">{result.mode}</span>
</div>
<div className="flex-1 glass-card p-2">
<span className="block text-[8px] text-zinc-600 uppercase font-black mb-0.5">TF</span>
<span className="text-xs font-black">{result.timeframe}</span>
</div>
</div>
</div>
{/* Trading Decision */}
<div className="glass-card space-y-4">
<div className={cn(
"w-full py-5 rounded-lg flex flex-col items-center justify-center gap-1",
result.decision === SignalType.BUY ? "bg-green-500/10 text-green-500" :
result.decision === SignalType.SELL ? "bg-brand-red/10 text-brand-red" :
"bg-zinc-500/10 text-zinc-500"
)}>
<span className="text-4xl font-black uppercase italic tracking-tighter">
{result.decision === SignalType.BUY ? 'COMPRAR' :
result.decision === SignalType.SELL ? 'VENDER' :
'AGUARDAR'}
</span>
<span className="text-[8px] uppercase font-black tracking-widest opacity-60">Decisão QuantScan</span>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between p-3 glass-card bg-transparent border-white/5">
<div className="flex items-center gap-2">
<Target size={14} className="text-zinc-600" />
<span className="text-[9px] font-black text-zinc-500 uppercase">ENTRADA</span>
</div>
<span className="font-mono font-black text-sm">{result.entry}</span>
</div>
<div className="flex items-center justify-between p-3 glass-card bg-transparent border-white/5">
<div className="flex items-center gap-2">
<AlertTriangle size={14} className="text-brand-red/50" />
<span className="text-[9px] font-black text-zinc-500 uppercase">STOP LOSS</span>
</div>
<span className="font-mono font-black text-sm text-brand-red">{result.stopLoss}</span>
</div>
<div className="flex items-center justify-between p-3 glass-card bg-transparent border-white/5">
<div className="flex items-center gap-2">
<ShieldCheck size={14} className="text-green-500/50" />
<span className="text-[9px] font-black text-zinc-500 uppercase">TAKE PROFIT</span>
</div>
<span className="font-mono font-black text-sm text-green-500">{result.takeProfit}</span>
</div>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="glass-card space-y-4">
<h4 className="text-[10px] font-black uppercase tracking-widest text-brand-red flex items-center gap-2">
<BrainCircuit size={14} /> ANÁLISE TÉCNICA (SMC + LIQ)
</h4>
<p className="text-xs text-zinc-400 leading-relaxed font-medium">
{result.tecnica}
</p>
</div>
<div className="glass-card space-y-4">
<h4 className="text-[10px] font-black uppercase tracking-widest text-green-500 flex items-center gap-2">
<TrendingUp size={14} /> ANÁLISE FUNDAMENTAL
</h4>
<p className="text-xs text-zinc-400 leading-relaxed font-medium">
{result.fundamental}
</p>
</div>
<div className="glass-card space-y-4 col-span-full">
<h4 className="text-[10px] font-black uppercase tracking-widest text-zinc-400 flex items-center gap-2">
<Target size={14} /> ANALISE GERAL
</h4>
<p className="text-xs text-zinc-300 leading-relaxed font-medium">
{result.analiseGeral}
</p>
</div>
</div>
{/* Analysis Illustration */}
<div className="glass-card overflow-hidden !p-0 border-zinc-900/50 relative">
<div className="absolute top-4 left-4 z-20">
<h3 className="text-xs font-black flex items-center gap-2 uppercase italic tracking-wider text-white">
<ShieldCheck size={14} className={cn(result.decision === SignalType.BUY ? "text-green-500" : "text-brand-red")} />
Mapeamento Técnico da IA
</h3>
</div>
<div className="aspect-[21/9] relative bg-zinc-950 flex items-center justify-center">
<div className="absolute inset-0 z-10 bg-gradient-to-t from-brand-dark via-brand-dark/20 to-transparent" />
<img
src={preview || ''}
alt="Technical Analysis Map"
className="w-full h-full object-cover opacity-60 mix-blend-screen grayscale"
/>
{/* UI Overlays to simulate AI analysis */}
<div className="absolute inset-0 z-20 p-6 flex flex-col justify-end">
<div className="flex flex-wrap gap-2 mb-2">
<span className="px-2 py-0.5 bg-brand-red/20 border border-brand-red/30 rounded text-[8px] font-bold text-brand-red uppercase tracking-tighter backdrop-blur-sm">
Fluxo Institucional Detectado
</span>
<span className="px-2 py-0.5 bg-white/5 border border-white/10 rounded text-[8px] font-bold text-zinc-400 uppercase tracking-tighter backdrop-blur-sm">
HFT Signature
</span>
</div>
<div className="flex items-center justify-between">
<div>
<p className="text-xl font-black italic uppercase tracking-tighter text-white">
{result.pair} / {result.timeframe}
</p>
<p className="text-[10px] font-mono text-zinc-500 uppercase tracking-widest">
Ref: AIS-BETA-{Math.floor(Math.random() * 10000)}
</p>
</div>
<div className="text-right">
<p className="text-[10px] font-black text-brand-red uppercase">Confirmação IA</p>
<p className="text-2xl font-black italic text-white leading-none">{result.score}%</p>
</div>
</div>
</div>
{/* Decorative Scanning Line */}
<motion.div
initial={{ top: '0%' }}
animate={{ top: '100%' }}
transition={{ duration: 3, repeat: Infinity, ease: 'linear' }}
className="absolute left-0 right-0 h-px bg-brand-red/50 z-30 shadow-[0_0_10px_rgba(255,0,0,0.5)]"
/>
</div>
</div>
<button
onClick={reset}
className="w-full py-3 text-zinc-600 hover:text-white font-black text-xs uppercase tracking-widest transition-colors"
>
NOVO SCAN INSTITUCIONAL
</button>
</motion.div>
)}
</div>
);
};