feat: Update Gemini model and improve error handling

Switches to the `gemini-2.5-flash` model for potentially faster and more cost-effective analysis.

Enhances error handling in chart analysis to provide more specific user feedback for API rate limits and credit exhaustion.

Adds UI elements for signal history expansion and introduces pair selection for dashboard statistics, improving user interaction and data visualization.
This commit is contained in:
desartstudio95
2026-05-07 13:24:31 +02:00
parent adc6402b04
commit e225cad4b9
4 changed files with 221 additions and 84 deletions
+5 -1
View File
@@ -93,7 +93,11 @@ export const AnalysisView: React.FC<{ userData?: any }> = ({ userData }) => {
} catch (err: any) {
console.error(err);
setError(err.message || 'Falha ao analisar imagem. Verifique se o gráfico está claro.');
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.';
}
setError(errorMessage);
} finally {
setIsAnalyzing(false);
}
+100 -19
View File
@@ -3,7 +3,8 @@ import { Signal, SignalResult, SignalType } from '../types';
import { motion } from 'motion/react';
import {
AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
BarChart, Bar, Cell
BarChart, Bar, Cell,
LineChart, Line, Legend
} from 'recharts';
import { TrendingUp, Award, Target, Activity } from 'lucide-react';
import { collection, query, where, orderBy, onSnapshot } from 'firebase/firestore';
@@ -12,6 +13,7 @@ import { auth, db, handleFirestoreError, OperationType } from '../lib/firebase';
export const DashboardStats: React.FC = () => {
const [signals, setSignals] = useState<Signal[]>([]);
const [loading, setLoading] = useState(true);
const [selectedPair, setSelectedPair] = useState<string>('');
useEffect(() => {
if (!auth.currentUser) return;
@@ -36,11 +38,37 @@ export const DashboardStats: React.FC = () => {
return () => unsubscribe();
}, []);
useEffect(() => {
if (!selectedPair && signals.length > 0) {
const pairs = Array.from(new Set(signals.filter(s => s.pair).map(s => s.pair)));
if (pairs.length > 0) {
setSelectedPair(pairs[0]);
}
}
}, [signals, selectedPair]);
const totalSignals = signals.length;
// const gains = signals.filter(s => s.result === SignalResult.GAIN).length;
// const winRate = totalSignals > 0 ? (gains / totalSignals) * 100 : 0;
// User requested 90% precision
const gains = signals.filter(s => s.result === SignalResult.GAIN).length;
const losses = signals.filter(s => s.result === SignalResult.LOSS).length;
const completedSignals = gains + losses;
const winRate = completedSignals > 0 ? (gains / completedSignals) * 100 : 0;
const profitLossRatio = losses > 0 ? (gains / losses).toFixed(2) : (gains > 0 ? '∞' : '0');
const pairStats = signals.reduce((acc: any[], signal) => {
const existing = acc.find(i => i.name === signal.pair);
if (existing) {
existing.total += 1;
if (signal.result === SignalResult.GAIN) existing.gains += 1;
} else {
acc.push({ name: signal.pair, total: 1, gains: signal.result === SignalResult.GAIN ? 1 : 0 });
}
return acc;
}, []).map(p => ({ ...p, winRate: Math.round((p.gains / p.total) * 100) }));
const bestAssetEntry = [...pairStats].sort((a,b) => b.winRate - a.winRate)[0];
const bestAsset = bestAssetEntry ? `${bestAssetEntry.name} (${bestAssetEntry.winRate}%)` : 'N/A';
const chartData = signals
.sort((a, b) => a.timestamp - b.timestamp)
.reduce((acc: any[], signal, index) => {
@@ -57,22 +85,27 @@ export const DashboardStats: React.FC = () => {
const existing = acc.find(i => i.name === signal.timeframe);
if (existing) {
existing.count += 1;
} else {
acc.push({ name: signal.timeframe, count: 1 });
}
return acc;
}, []);
const pairStats = signals.reduce((acc: any[], signal) => {
const existing = acc.find(i => i.name === signal.pair);
if (existing) {
existing.total += 1;
if (signal.result === SignalResult.GAIN) existing.gains += 1;
} else {
acc.push({ name: signal.pair, total: 1, gains: signal.result === SignalResult.GAIN ? 1 : 0 });
acc.push({ name: signal.timeframe, count: 1, gains: signal.result === SignalResult.GAIN ? 1 : 0 });
}
return acc;
}, []).map(p => ({ ...p, winRate: Math.round((p.gains / p.total) * 100) }));
}, []).map((t: any) => ({ ...t, winRate: Math.round((t.gains / t.count) * 100) }));
const bestTimeframeEntry = [...timeframeStats].sort((a,b) => b.winRate - a.winRate)[0];
const bestTimeframe = bestTimeframeEntry ? `${bestTimeframeEntry.name} (${bestTimeframeEntry.winRate}%)` : 'N/A';
const priceHistoryData = signals
.filter(s => s.type !== SignalType.WAIT && parseFloat(s.entry) && parseFloat(s.takeProfit) && parseFloat(s.stopLoss) && s.pair === selectedPair)
.sort((a, b) => a.timestamp - b.timestamp)
.map(s => ({
name: new Date(s.timestamp).toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' }),
entry: parseFloat(s.entry.replace(/[^0-9.-]/g, '')),
stopLoss: parseFloat(s.stopLoss.replace(/[^0-9.-]/g, '')),
takeProfit: parseFloat(s.takeProfit.replace(/[^0-9.-]/g, '')),
}));
const availablePairs = Array.from(new Set(signals.filter(s => s.pair).map(s => s.pair)));
if (loading) return null;
@@ -96,9 +129,9 @@ export const DashboardStats: React.FC = () => {
>
{[
{ label: 'Total Sinais', value: totalSignals, icon: Activity },
{ label: 'Taxa de Acerto', value: `90.0%`, icon: Award },
{ label: 'Melhor Ativo', value: [...pairStats].sort((a,b) => b.winRate - a.winRate)[0]?.name || 'N/A', icon: Target },
{ label: 'IA Learning', value: '+14%', icon: TrendingUp, positive: true },
{ label: 'Taxa de Acerto', value: `${winRate.toFixed(1)}%`, icon: Award },
{ label: 'Ratio Win/Loss', value: profitLossRatio, icon: Target },
{ label: 'Melhor Ativo', value: bestAsset, icon: TrendingUp, positive: true },
].map((stat, i) => (
<motion.div
key={i}
@@ -115,6 +148,53 @@ export const DashboardStats: React.FC = () => {
))}
</motion.div>
<div className="glass-card p-5 min-h-[350px] flex flex-col">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-6 gap-4 border-b border-white/5 pb-4">
<h3 className="font-black italic uppercase tracking-wider text-xs text-zinc-400">Histórico de Preços dos Sinais</h3>
{availablePairs.length > 0 && (
<select
value={selectedPair}
onChange={(e) => setSelectedPair(e.target.value)}
className="bg-black/50 border border-white/10 rounded-lg px-3 py-1.5 text-sm text-white focus:outline-none focus:border-brand-red"
>
{availablePairs.map(p => (
<option key={p} value={p}>{p}</option>
))}
</select>
)}
</div>
<div className="flex-1 w-full">
<ResponsiveContainer width="100%" height={300}>
<LineChart data={priceHistoryData}>
<CartesianGrid strokeDasharray="3 3" stroke="#ffffff05" vertical={false} />
<XAxis
dataKey="name"
stroke="#52525b"
fontSize={10}
tickLine={false}
axisLine={false}
minTickGap={20}
/>
<YAxis
stroke="#52525b"
fontSize={10}
tickLine={false}
axisLine={false}
domain={['auto', 'auto']}
/>
<Tooltip
contentStyle={{ backgroundColor: '#09090b', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '12px', fontSize: '12px' }}
itemStyle={{ color: '#fff', fontWeight: 'bold' }}
/>
<Legend wrapperStyle={{ fontSize: '12px', paddingTop: '10px' }} />
<Line type="monotone" dataKey="entry" stroke="#3b82f6" name="Entry" strokeWidth={2} dot={{ r: 3 }} />
<Line type="monotone" dataKey="takeProfit" stroke="#22c55e" name="Take Profit" strokeWidth={2} dot={{ r: 3 }} />
<Line type="monotone" dataKey="stopLoss" stroke="#ef4444" name="Stop Loss" strokeWidth={2} dot={{ r: 3 }} />
</LineChart>
</ResponsiveContainer>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="md:col-span-2 glass-card p-5 min-h-[350px] flex flex-col">
<h3 className="font-black italic uppercase tracking-wider text-xs mb-6 text-zinc-400">Curva de Equidade Estimada</h3>
@@ -234,3 +314,4 @@ export const DashboardStats: React.FC = () => {
</div>
);
};
+115 -63
View File
@@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react';
import { Signal, SignalResult, SignalType } from '../types';
import { cn } from '../lib/utils';
import { Clock, TrendingUp, TrendingDown } from 'lucide-react';
import { Clock, TrendingUp, TrendingDown, ChevronDown, ChevronUp } from 'lucide-react';
import { collection, query, where, orderBy, onSnapshot } from 'firebase/firestore';
import { auth, db, handleFirestoreError, OperationType } from '../lib/firebase';
@@ -10,6 +10,7 @@ export const SignalHistory: React.FC = () => {
const [loading, setLoading] = useState(true);
const [startDate, setStartDate] = useState<string>('');
const [endDate, setEndDate] = useState<string>('');
const [expandedSignalIds, setExpandedSignalIds] = useState<string[]>([]);
useEffect(() => {
if (!auth.currentUser) return;
@@ -68,6 +69,12 @@ export const SignalHistory: React.FC = () => {
return true;
});
const toggleSignal = (id: string) => {
setExpandedSignalIds(prev =>
prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id]
);
};
return (
<div className="space-y-6">
<header className="flex flex-col md:flex-row md:items-center justify-between gap-4">
@@ -111,74 +118,119 @@ export const SignalHistory: React.FC = () => {
Nenhum sinal encontrado. Comece realizando um novo scan.
</div>
) : (
filteredSignals.map((signal) => (
<div key={signal.id} className="glass-card p-4 flex flex-col md:flex-row md:items-center gap-6 group hover:border-white/20 transition-all">
<div className={cn(
"w-12 h-12 rounded-xl flex items-center justify-center shrink-0",
signal.type === SignalType.BUY ? "bg-green-500/10 text-green-500" : "bg-brand-red/10 text-brand-red"
)}>
{signal.type === SignalType.BUY ? <TrendingUp size={24} /> : <TrendingDown size={24} />}
</div>
<div className="grid grid-cols-2 lg:grid-cols-7 flex-1 gap-4 items-center">
<div>
<span className="text-[10px] text-zinc-500 font-black uppercase">Ativo / TF</span>
<p className="font-bold text-white text-sm">{signal.pair} <span className="text-zinc-500">· {signal.timeframe}</span></p>
</div>
<div>
<span className="text-[10px] text-zinc-500 font-black uppercase">Tipo</span>
<p className={cn(
"font-bold italic uppercase text-sm",
signal.type === SignalType.BUY ? "text-green-500" : "text-brand-red"
)}>{signal.type}</p>
</div>
<div>
<span className="text-[10px] text-zinc-500 font-black uppercase">Score</span>
<p className="font-bold text-white text-sm">{signal.score}%</p>
</div>
<div>
<span className="text-[10px] text-zinc-500 font-black uppercase">SL / TP</span>
<p className="font-bold text-white text-xs">{signal.stopLoss} <span className="text-zinc-600">/</span> <span className="text-zinc-400">{signal.takeProfit}</span></p>
</div>
<div>
<span className="text-[10px] text-zinc-500 font-black uppercase">Data e Hora</span>
<p className="font-bold text-zinc-300 text-xs whitespace-nowrap">{new Date(signal.timestamp).toLocaleString("pt-BR", { dateStyle: "short", timeStyle: "short" })}</p>
</div>
<div>
<span className="text-[10px] text-zinc-500 font-black uppercase">Status</span>
filteredSignals.map((signal) => {
const isExpanded = expandedSignalIds.includes(signal.id);
return (
<div
key={signal.id}
className="glass-card flex flex-col group hover:border-white/20 transition-all cursor-pointer"
onClick={() => toggleSignal(signal.id)}
>
<div className="p-4 flex flex-col md:flex-row md:items-center gap-6">
<div className={cn(
"inline-flex px-2 py-0.5 rounded-full text-[10px] font-black uppercase tracking-widest",
signal.result === SignalResult.PENDING ? "bg-blue-500/10 text-blue-500 border border-blue-500/20" : "bg-zinc-800 text-zinc-400 border border-zinc-700"
"w-12 h-12 rounded-xl flex items-center justify-center shrink-0",
signal.type === SignalType.BUY ? "bg-green-500/10 text-green-500" : "bg-brand-red/10 text-brand-red"
)}>
{signal.result === SignalResult.PENDING ? "Válido" : "Expirado"}
{signal.type === SignalType.BUY ? <TrendingUp size={24} /> : <TrendingDown size={24} />}
</div>
<div className="grid grid-cols-2 lg:grid-cols-7 flex-1 gap-4 items-center">
<div>
<span className="text-[10px] text-zinc-500 font-black uppercase">Ativo / TF</span>
<p className="font-bold text-white text-sm">{signal.pair} <span className="text-zinc-500">· {signal.timeframe}</span></p>
</div>
<div>
<span className="text-[10px] text-zinc-500 font-black uppercase">Tipo</span>
<p className={cn(
"font-bold italic uppercase text-sm",
signal.type === SignalType.BUY ? "text-green-500" : "text-brand-red"
)}>{signal.type}</p>
</div>
<div>
<span className="text-[10px] text-zinc-500 font-black uppercase">Score</span>
<p className="font-bold text-white text-sm">{signal.score}%</p>
</div>
<div>
<span className="text-[10px] text-zinc-500 font-black uppercase">SL / TP</span>
<p className="font-bold text-white text-xs">{signal.stopLoss} <span className="text-zinc-600">/</span> <span className="text-zinc-400">{signal.takeProfit}</span></p>
</div>
<div>
<span className="text-[10px] text-zinc-500 font-black uppercase">Data e Hora</span>
<p className="font-bold text-zinc-300 text-xs whitespace-nowrap">{new Date(signal.timestamp).toLocaleString("pt-BR", { dateStyle: "short", timeStyle: "short" })}</p>
</div>
<div>
<span className="text-[10px] text-zinc-500 font-black uppercase">Status</span>
<div className={cn(
"inline-flex px-2 py-0.5 rounded-full text-[10px] font-black uppercase tracking-widest",
signal.result === SignalResult.PENDING ? "bg-blue-500/10 text-blue-500 border border-blue-500/20" : "bg-zinc-800 text-zinc-400 border border-zinc-700"
)}>
{signal.result === SignalResult.PENDING ? "Válido" : "Expirado"}
</div>
</div>
<div className="text-right">
<span className="text-[10px] text-zinc-500 font-black uppercase">Resultado</span>
<div className={cn(
"flex items-center justify-end gap-1 font-black italic uppercase text-sm",
signal.result === SignalResult.GAIN ? "text-green-500" :
signal.result === SignalResult.LOSS ? "text-brand-red" :
"text-zinc-500"
)}>
{signal.result}
</div>
</div>
</div>
<div className="h-px w-full md:h-12 md:w-px bg-white/5" />
<div className="flex md:flex-col justify-between items-center md:items-end gap-1 min-w-[80px]">
<div className="flex flex-col text-right">
<span className="text-[10px] text-zinc-500 font-black uppercase">Detalhes</span>
</div>
{isExpanded ? <ChevronUp size={20} className="text-zinc-400" /> : <ChevronDown size={20} className="text-zinc-400" />}
</div>
</div>
<div className="text-right">
<span className="text-[10px] text-zinc-500 font-black uppercase">Resultado</span>
<div className={cn(
"flex items-center justify-end gap-1 font-black italic uppercase text-sm",
signal.result === SignalResult.GAIN ? "text-green-500" :
signal.result === SignalResult.LOSS ? "text-brand-red" :
"text-zinc-500"
)}>
{signal.result}
{isExpanded && (
<div className="p-4 border-t border-white/5 bg-black/20 grid grid-cols-1 md:grid-cols-2 gap-6 text-sm text-zinc-300">
<div className="space-y-4">
{signal.justification && (
<div>
<h4 className="text-[10px] text-zinc-500 font-black uppercase mb-1">Justificativa</h4>
<p className="italic">{signal.justification}</p>
</div>
)}
{signal.analiseGeral && (
<div>
<h4 className="text-[10px] text-zinc-500 font-black uppercase mb-1">Análise Geral</h4>
<p>{signal.analiseGeral}</p>
</div>
)}
</div>
<div className="space-y-4">
{signal.estrutura && (
<div>
<h4 className="text-[10px] text-zinc-500 font-black uppercase mb-1">Estrutura de Mercado</h4>
<p>{signal.estrutura}</p>
</div>
)}
{signal.tecnica && (
<div>
<h4 className="text-[10px] text-zinc-500 font-black uppercase mb-1">Análise Técnica</h4>
<p>{signal.tecnica}</p>
</div>
)}
{signal.fundamental && (
<div>
<h4 className="text-[10px] text-zinc-500 font-black uppercase mb-1">Análise Fundamental</h4>
<p>{signal.fundamental}</p>
</div>
)}
</div>
</div>
</div>
)}
</div>
<div className="h-px w-full md:h-12 md:w-px bg-white/5" />
<div className="flex md:flex-col justify-between text-right gap-1 min-w-[80px]">
<span className="text-[10px] text-zinc-500 font-black uppercase">Data</span>
<span className="text-[10px] text-zinc-400 font-medium">
{new Date(signal.timestamp).toLocaleDateString()}
</span>
<span className="text-[10px] text-zinc-600 font-medium hidden md:block">
{new Date(signal.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</span>
</div>
</div>
))
);
})
)}
</div>
</div>
+1 -1
View File
@@ -4,7 +4,7 @@ import { AnalysisResponse, SignalType } from "../types";
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY || '' });
export const analyzeForexChart = async (imageBase64: string, userNotes?: string, preferredMode?: 'Técnico' | 'Fundamental' | 'Híbrido'): Promise<AnalysisResponse> => {
const model = "gemini-3.1-pro-preview";
const model = "gemini-2.5-flash";
const systemInstruction = `
Você é o QuantScan IA, um sistema avançado de análise de mercado financeiro com inteligência institucional.