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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,10 +38,36 @@ 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)
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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,8 +118,15 @@ 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">
|
||||
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(
|
||||
"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"
|
||||
@@ -168,17 +182,55 @@ export const SignalHistory: React.FC = () => {
|
||||
|
||||
<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 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>
|
||||
))
|
||||
|
||||
{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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user