2026-04-28 20:32:34 +02:00
import React , { useState , useCallback , useEffect } from 'react' ;
import { useDropzone } from 'react-dropzone' ;
2026-05-12 07:56:18 +02:00
import { Upload , X , ShieldCheck , Target , TrendingUp , AlertTriangle , Loader2 , Zap , BrainCircuit , Camera , ScanLine , Bell , BellRing } from 'lucide-react' ;
2026-04-28 20:32:34 +02:00
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' ;
2026-05-12 07:56:18 +02:00
import { compressImage , cleanupStorage } from '../lib/imageUtils' ;
2026-04-28 20:32:34 +02:00
import { storage , auth , db , handleFirestoreError , OperationType } from '../lib/firebase' ;
import { collection , addDoc , serverTimestamp , query , where , getDocs , Timestamp } from 'firebase/firestore' ;
2026-05-07 19:02:04 +02:00
export const AnalysisView : React.FC < { userData? : any , onGoToHistory ?: () => void } > = ({ userData , onGoToHistory }) => {
2026-04-28 20:32:34 +02:00
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 );
2026-05-03 12:02:27 +02:00
const [ mode , setMode ] = useState < 'Técnico' | 'Fundamental' | 'Híbrido' > ( 'Híbrido' );
2026-04-28 20:32:34 +02:00
const [ usageInfo , setUsageInfo ] = useState < { used : number , limit : number } > ({ used : 0 , limit : userData?.analysisLimit ?? 8 });
2026-05-12 07:56:18 +02:00
const [ alerts , setAlerts ] = useState < { type : string , price : string , targetValue : number }[] > ([]);
const [ mockPrice , setMockPrice ] = useState < number | null >( null );
2026-04-28 20:32:34 +02:00
2026-05-11 16:57:29 +02:00
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 ;
2026-05-12 07:56:18 +02:00
useEffect (() => {
if ( result && result . entry ) {
const entryNum = parseFloat ( result . entry . replace ( /[^0-9.]/g , '' ));
if ( ! isNaN ( entryNum )) setMockPrice ( entryNum );
if ( 'Notification' in window && Notification . permission !== 'granted' ) {
Notification . requestPermission ();
}
} else {
setMockPrice ( null );
setAlerts ([]);
}
}, [ result ]);
useEffect (() => {
if ( mockPrice === null || alerts . length === 0 || ! result ) return ;
const interval = setInterval (() => {
setMockPrice ( prevPrice => {
if ( prevPrice === null ) return null ;
// Simulando variação realista (0.01% - 0.05%)
const movePercent = ( Math . random () - 0.5 ) * 0.001 ;
let nextPrice = prevPrice * ( 1 + movePercent );
// Vamos forçar o preço a ir em direção a um dos alertas para que a feature possa ser testada
if ( Math . random () > 0.4 && alerts . length > 0 ) {
const target = alerts [ Math . floor ( Math . random () * alerts . length )]. targetValue ;
if ( target > nextPrice ) {
nextPrice += target * 0.0002 ;
} else {
nextPrice -= target * 0.0002 ;
}
}
let triggeredIndex = - 1 ;
const triggeredAlert = alerts . find (( alert , index ) => {
const isTriggered = Math . abs ( nextPrice - alert . targetValue ) < ( alert . targetValue * 0.0002 ) ||
( prevPrice < alert . targetValue && nextPrice >= alert . targetValue ) ||
( prevPrice > alert . targetValue && nextPrice <= alert . targetValue );
if ( isTriggered ) triggeredIndex = index ;
return isTriggered ;
});
if ( triggeredAlert && triggeredIndex !== - 1 ) {
if ( 'Notification' in window && Notification . permission === 'granted' ) {
new Notification ( '🚨 Alerta QuantScan' , {
body : `O preço do ativo ${ result . pair } atingiu seu alvo de ${ triggeredAlert . type } ( ${ triggeredAlert . price } ).` ,
icon : 'https://i.ibb.co/9BwbV3M/FXBROS-WORLD-3.png'
});
} else {
window . alert ( `🚨 Alerta QuantScan: O preço do ativo ${ result . pair } atingiu seu alvo de ${ triggeredAlert . type } ( ${ triggeredAlert . price } ).` );
}
setAlerts ( prev => prev . filter (( _ , i ) => i !== triggeredIndex ));
}
return nextPrice ;
});
}, 2000 );
return () => clearInterval ( interval );
}, [ alerts , mockPrice , result ]);
2026-04-28 20:32:34 +02:00
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 () => {
2026-05-11 16:57:29 +02:00
if ( isExpired ) {
setError ( "Sua assinatura expirou. Por favor, contate o administrador para renovar o plano." );
return ;
}
2026-04-28 20:32:34 +02:00
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 ) {
2026-05-06 23:48:03 +02:00
const fileRef = ref ( storage , `scans/ ${ auth . currentUser . uid } / ${ Date . now () } _ ${ file . name } ` );
2026-04-28 20:32:34 +02:00
await uploadBytes ( fileRef , file );
downloadUrl = await getDownloadURL ( fileRef );
2026-05-12 07:56:18 +02:00
// Clean up old scans for this user to avoid storage limits (keep max 100)
cleanupStorage ( `scans/ ${ auth . currentUser . uid } ` , 100 );
2026-04-28 20:32:34 +02:00
}
const base64 = preview . split ( ',' )[ 1 ];
2026-05-16 10:49:05 +02:00
const analysis = await analyzeForexChart ( base64 , undefined , mode , userData ? . plan );
2026-05-06 23:48:03 +02:00
analysis . score = Math . round ( analysis . score );
2026-04-28 20:32:34 +02:00
setResult ( analysis );
if ( auth . currentUser ) {
2026-05-16 10:49:05 +02:00
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 );
2026-04-28 20:32:34 +02:00
await addDoc ( collection ( db , 'signals' ), {
2026-05-16 10:49:05 +02:00
... sanitizedAnalysis ,
2026-04-28 20:32:34 +02:00
screenshotUrl : downloadUrl ,
userId : auth.currentUser.uid ,
createdAt : serverTimestamp (),
timestamp : Date.now (),
result : SignalResult.PENDING ,
2026-05-16 10:49:05 +02:00
type : sanitizedAnalysis . decision || 'WAIT'
2026-04-28 20:32:34 +02:00
}). catch ( err => {
handleFirestoreError ( err , OperationType . CREATE , 'signals' );
throw err ;
});
}
} catch ( err : any ) {
2026-05-12 07:56:18 +02:00
console . error ( "Gemini Error:" , err );
2026-05-07 13:24:31 +02:00
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.' ;
2026-05-12 07:56:18 +02:00
} else {
errorMessage = `Erro na API: ${ errorMessage } ` ;
2026-05-07 13:24:31 +02:00
}
setError ( errorMessage );
2026-04-28 20:32:34 +02:00
} finally {
setIsAnalyzing ( false );
}
};
2026-05-12 07:56:18 +02:00
const onDrop = useCallback ( async ( acceptedFiles : File []) => {
2026-04-28 20:32:34 +02:00
const selectedFile = acceptedFiles [ 0 ];
2026-05-12 07:56:18 +02:00
// Comprimir a imagem antes de setar state para reduzir MS
const compressedFile = await compressImage ( selectedFile , 1200 , 0.7 );
setFile ( compressedFile );
2026-04-28 20:32:34 +02:00
const reader = new FileReader ();
reader . onload = () => {
setPreview ( reader . result as string );
};
2026-05-12 07:56:18 +02:00
reader . readAsDataURL ( compressedFile );
2026-04-28 20:32:34 +02:00
setError ( null );
}, []);
const { getRootProps , getInputProps , isDragActive } = useDropzone ({
onDrop ,
accept : { 'image/*' : [] },
multiple : false
} as any );
const reset = () => {
setFile ( null );
setPreview ( null );
setResult ( null );
setError ( null );
2026-05-12 07:56:18 +02:00
setMockPrice ( null );
setAlerts ([]);
};
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 }];
}
});
2026-04-28 20:32:34 +02:00
};
return (
< div className = "max-w-4xl mx-auto space-y-5" >
2026-05-11 16:57:29 +02:00
{ isExpired && (
< div className = "bg-brand-red/10 border border-brand-red p-4 rounded-xl flex items-start gap-3" >
< AlertTriangle className = "text-brand-red shrink-0 mt-0.5" size = { 20 } />
< div >
< h3 className = "text-brand-red font-black text-sm uppercase tracking-widest" > Assinatura Expirada </ h3 >
< p className = "text-red-200/70 text-xs mt-1" >
Sua assinatura expirou . Renove seu plano para continuar usando o scanner analisador .
</ p >
</ div >
</ div >
)}
{ isEndingSoon && (
< div className = "bg-orange-500/10 border border-orange-500 p-4 rounded-xl flex items-start gap-3" >
< AlertTriangle className = "text-orange-500 shrink-0 mt-0.5" size = { 20 } />
< div >
< h3 className = "text-orange-500 font-black text-sm uppercase tracking-widest" > Assinatura Expirando </ h3 >
< p className = "text-orange-200/70 text-xs mt-1" >
Sua assinatura expira em { Math . ceil ( daysRemaining )} dia ( s ). Renove agora para não perder o acesso ao scanner .
</ p >
</ div >
</ div >
)}
2026-04-28 20:32:34 +02:00
< 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 ? (
2026-05-03 12:02:27 +02:00
< 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 >
)}
2026-04-28 20:32:34 +02:00
{ ! preview ? (
< div className = "w-full flex flex-col gap-4 items-center justify-center" >
2026-05-03 12:02:27 +02:00
< 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 >
2026-04-28 20:32:34 +02:00
< 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" ,
2026-05-11 16:57:29 +02:00
isDragActive ? "bg-brand-red/5 scale-[0.98] border-brand-red/30" : "hover:bg-zinc-800/20" ,
isExpired ? "opacity-50 pointer-events-none" : ""
2026-04-28 20:32:34 +02:00
)}
>
2026-05-11 16:57:29 +02:00
< input { ...getInputProps () } disabled = { isExpired } />
2026-04-28 20:32:34 +02:00
< 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" >
2026-05-11 16:57:29 +02:00
< label className = { cn (
"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" ,
isExpired ? "opacity-50 pointer-events-none" : ""
)}>
2026-04-28 20:32:34 +02:00
< Camera size = { 16 } className = "text-zinc-400 group-hover:text-brand-red transition-colors" />
< span className = "whitespace-nowrap pt-0.5" > TIRAR FOTO </ span >
2026-05-11 16:57:29 +02:00
< input type = "file" accept = "image/*" capture = "environment" disabled = { isExpired } className = "opacity-0 absolute inset-0 cursor-pointer w-full h-full" onChange = {( e ) => {
2026-04-28 20:32:34 +02:00
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" >
2026-05-07 13:38:56 +02:00
< button
onClick = { reset }
disabled = { isAnalyzing }
className = "w-1/3 bg-white/5 text-white py-3.5 rounded-xl font-black text-sm hover:bg-white/10 border border-white/10 transition-all active:scale-95 flex items-center justify-center gap-2 disabled:opacity-50 uppercase"
>
NOVO SCAN
</ button >
2026-04-28 20:32:34 +02:00
< 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" ,
2026-05-03 22:17:02 +02:00
result . decision === SignalType . BUY ? "text-green-500" : ( result . decision === SignalType . SELL ? "text-brand-red" : "text-zinc-500" )
2026-04-28 20:32:34 +02:00
)}
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" ,
2026-05-03 22:17:02 +02:00
result . decision === SignalType . BUY ? "text-green-500" : ( result . decision === SignalType . SELL ? "text-brand-red red-text-glow" : "text-zinc-500" )
2026-04-28 20:32:34 +02:00
)}>
{ 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 >
2026-05-03 12:02:27 +02:00
< p className = "text-brand-red mt-1 text-[10px] font-bold max-w-[200px] italic" > ⚠️ { result . alerta }</ p >
2026-04-28 20:32:34 +02:00
</ div >
< div className = "w-full pt-4 border-t border-white/5 flex gap-3" >
< div className = "flex-1 glass-card p-2" >
2026-05-03 12:02:27 +02:00
< 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 >
2026-04-28 20:32:34 +02:00
</ 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 >
2026-05-03 12:02:27 +02:00
< span className = "text-xs font-black" >{ result . timeframe }</ span >
2026-04-28 20:32:34 +02:00
</ div >
</ div >
2026-05-16 10:49:05 +02:00
{( result . riskReward || result . riskLevel || result . duration || result . signalType ) && (
< div className = "w-full pt-4 border-t border-white/5 grid grid-cols-2 gap-3" >
{ result . signalType && (
< div className = "glass-card p-2" >
< span className = "block text-[8px] text-zinc-600 uppercase font-black mb-0.5" > ESTILO </ span >
< span className = "text-[10px] font-black tracking-widest" >{ result . signalType }</ span >
</ div >
)}
{ result . riskLevel && (
< div className = "glass-card p-2" >
< span className = "block text-[8px] text-zinc-600 uppercase font-black mb-0.5" > RISCO </ span >
< span className = { cn (
"text-[10px] font-black tracking-widest" ,
result . riskLevel . toUpperCase () === 'LOW' ? "text-green-500" :
result . riskLevel . toUpperCase () === 'HIGH' ? "text-brand-red" : "text-orange-500"
)}>{ result . riskLevel }</ span >
</ div >
)}
{ result . riskReward && (
< div className = "glass-card p-2" >
< span className = "block text-[8px] text-zinc-600 uppercase font-black mb-0.5" > RISCO / RETORNO </ span >
< span className = "text-[10px] font-black font-mono" >{ result . riskReward }</ span >
</ div >
)}
{ result . duration && (
< div className = "glass-card p-2" >
< span className = "block text-[8px] text-zinc-600 uppercase font-black mb-0.5" > DURAÇÃO </ span >
< span className = "text-[10px] font-black tracking-widest uppercase" >{ result . duration }</ span >
</ div >
)}
</ div >
)}
2026-04-28 20:32:34 +02:00
</ div >
{ /* Trading Decision */ }
< div className = "glass-card space-y-4" >
2026-05-03 22:17:02 +02:00
< div className = "text-center" >
< span className = "text-[10px] font-black text-zinc-500 uppercase tracking-widest bg-white/5 px-2 py-0.5 rounded" >
{ result . pair }
</ span >
</ div >
2026-04-28 20:32:34 +02:00
< 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 >
2026-05-03 12:02:27 +02:00
< span className = "text-[8px] uppercase font-black tracking-widest opacity-60" > Decisão QuantScan </ span >
2026-04-28 20:32:34 +02:00
</ div >
2026-05-12 07:56:18 +02:00
{ mockPrice !== null && (
< div className = "flex flex-col items-center justify-center p-2 mb-2 bg-black/40 rounded border border-white/5" >
< span className = "text-[9px] uppercase tracking-widest text-zinc-500 font-bold" > Preço de Mercado ( Simulado )</ span >
< span className = "font-mono text-lg font-black text-white" >{ mockPrice . toFixed ( 5 )}</ span >
</ div >
)}
2026-04-28 20:32:34 +02:00
< 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 >
2026-05-12 07:56:18 +02:00
< div className = "flex items-center gap-2" >
< span className = "font-mono font-black text-sm" >{ result . entry }</ span >
< button
onClick = {() => toggleAlert ( 'Entrada' , result . entry )}
className = { cn (
"p-1.5 rounded transition-colors" ,
alerts . some ( a => a . type === 'Entrada' ) ? "text-yellow-500 bg-yellow-500/20" : "text-zinc-500 hover:text-yellow-500 hover:bg-white/5"
)}
>
{ alerts . some ( a => a . type === 'Entrada' ) ? < BellRing size = { 14 } /> : < Bell size = { 14 } />}
</ button >
</ div >
2026-04-28 20:32:34 +02:00
</ 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 >
2026-05-12 07:56:18 +02:00
< div className = "flex items-center gap-2" >
< span className = "font-mono font-black text-sm text-brand-red" >{ result . stopLoss }</ span >
< button
onClick = {() => toggleAlert ( 'Stop Loss' , result . stopLoss )}
className = { cn (
"p-1.5 rounded transition-colors" ,
alerts . some ( a => a . type === 'Stop Loss' ) ? "text-yellow-500 bg-yellow-500/20" : "text-zinc-500 hover:text-yellow-500 hover:bg-white/5"
)}
>
{ alerts . some ( a => a . type === 'Stop Loss' ) ? < BellRing size = { 14 } /> : < Bell size = { 14 } />}
</ button >
</ div >
2026-04-28 20:32:34 +02:00
</ 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" />
2026-05-16 10:49:05 +02:00
< span className = "text-[9px] font-black text-zinc-500 uppercase" > TAKE PROFIT 1 </ span >
2026-04-28 20:32:34 +02:00
</ div >
2026-05-12 07:56:18 +02:00
< div className = "flex items-center gap-2" >
< span className = "font-mono font-black text-sm text-green-500" >{ result . takeProfit }</ span >
< button
2026-05-16 10:49:05 +02:00
onClick = {() => toggleAlert ( 'Take Profit 1' , result . takeProfit )}
2026-05-12 07:56:18 +02:00
className = { cn (
"p-1.5 rounded transition-colors" ,
2026-05-16 10:49:05 +02:00
alerts . some ( a => a . type === 'Take Profit 1' ) ? "text-yellow-500 bg-yellow-500/20" : "text-zinc-500 hover:text-yellow-500 hover:bg-white/5"
2026-05-12 07:56:18 +02:00
)}
>
2026-05-16 10:49:05 +02:00
{ alerts . some ( a => a . type === 'Take Profit 1' ) ? < BellRing size = { 14 } /> : < Bell size = { 14 } />}
2026-05-12 07:56:18 +02:00
</ button >
</ div >
2026-04-28 20:32:34 +02:00
</ div >
2026-05-16 10:49:05 +02:00
{ result . takeProfit2 && (
< 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 2 </ span >
</ div >
< div className = "flex items-center gap-2" >
< span className = "font-mono font-black text-sm text-green-500" >{ result . takeProfit2 }</ span >
< button
onClick = {() => toggleAlert ( 'Take Profit 2' , result . takeProfit2 ! )}
className = { cn (
"p-1.5 rounded transition-colors" ,
alerts . some ( a => a . type === 'Take Profit 2' ) ? "text-yellow-500 bg-yellow-500/20" : "text-zinc-500 hover:text-yellow-500 hover:bg-white/5"
)}
>
{ alerts . some ( a => a . type === 'Take Profit 2' ) ? < BellRing size = { 14 } /> : < Bell size = { 14 } />}
</ button >
</ div >
</ div >
)}
{ result . takeProfit3 && (
< 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 3 </ span >
</ div >
< div className = "flex items-center gap-2" >
< span className = "font-mono font-black text-sm text-green-500" >{ result . takeProfit3 }</ span >
< button
onClick = {() => toggleAlert ( 'Take Profit 3' , result . takeProfit3 ! )}
className = { cn (
"p-1.5 rounded transition-colors" ,
alerts . some ( a => a . type === 'Take Profit 3' ) ? "text-yellow-500 bg-yellow-500/20" : "text-zinc-500 hover:text-yellow-500 hover:bg-white/5"
)}
>
{ alerts . some ( a => a . type === 'Take Profit 3' ) ? < BellRing size = { 14 } /> : < Bell size = { 14 } />}
</ button >
</ div >
</ div >
)}
2026-04-28 20:32:34 +02:00
</ 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" >
2026-05-03 12:02:27 +02:00
< BrainCircuit size = { 14 } /> ANÁLISE TÉCNICA ( SMC + LIQ )
2026-04-28 20:32:34 +02:00
</ h4 >
< p className = "text-xs text-zinc-400 leading-relaxed font-medium" >
2026-05-03 12:02:27 +02:00
{ result . tecnica }
2026-04-28 20:32:34 +02:00
</ 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" >
2026-05-03 12:02:27 +02:00
< TrendingUp size = { 14 } /> ANÁLISE FUNDAMENTAL
2026-04-28 20:32:34 +02:00
</ h4 >
< p className = "text-xs text-zinc-400 leading-relaxed font-medium" >
2026-05-03 12:02:27 +02:00
{ result . fundamental }
2026-04-28 20:32:34 +02:00
</ p >
</ div >
2026-05-03 12:02:27 +02:00
< div className = "glass-card space-y-4 col-span-full" >
2026-04-28 20:32:34 +02:00
< h4 className = "text-[10px] font-black uppercase tracking-widest text-zinc-400 flex items-center gap-2" >
2026-05-03 12:02:27 +02:00
< Target size = { 14 } /> ANALISE GERAL
2026-04-28 20:32:34 +02:00
</ h4 >
2026-05-03 12:02:27 +02:00
< p className = "text-xs text-zinc-300 leading-relaxed font-medium" >
{ result . analiseGeral }
</ p >
2026-04-28 20:32:34 +02:00
</ 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 >
2026-05-07 19:02:04 +02:00
< div className = "flex flex-col gap-2" >
< 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 >
{ onGoToHistory && (
< button
onClick = { onGoToHistory }
className = "w-full py-2 text-zinc-500 hover:text-white font-medium text-[10px] uppercase tracking-widest transition-colors"
>
Ver Histórico de Sinais
</ button >
)}
</ div >
2026-04-28 20:32:34 +02:00
</ motion.div >
)}
</ div >
);
};