feat: trata cota de IA amigavelmente e corrige downgrade do Gemini 2.5 Flash no backend
Build and Deploy (Gitea) / build-and-deploy (push) Successful in 2m4s Details

This commit is contained in:
Sidney 2026-07-18 18:29:23 -03:00
parent 961cbfdbdd
commit 45be343a2b
4 changed files with 68 additions and 7 deletions

View File

@ -84,3 +84,9 @@
50. **Pré-Matrícula - Máscaras de Entrada e Single Event Binding**: Para evitar comportamentos instáveis em teclados móveis (como Gboard no Android ou Autoresumos no iOS), as máscaras de campos estruturados (telefone, CPF, CEP) devem ser gerenciadas por um único escopo de eventos global (`document.addEventListener`). É estritamente proibido realizar vinculações duplas (Double Binding) com ouvintes locais diretos concorrentes nos elementos dinâmicos, para evitar a perda de foco, cursor saltando, ou substituição indesejada de caracteres digitados. A máscara de telefone deve diferenciar e suportar dinamicamente tanto telefones fixos (10 dígitos) quanto celulares (11 dígitos). 50. **Pré-Matrícula - Máscaras de Entrada e Single Event Binding**: Para evitar comportamentos instáveis em teclados móveis (como Gboard no Android ou Autoresumos no iOS), as máscaras de campos estruturados (telefone, CPF, CEP) devem ser gerenciadas por um único escopo de eventos global (`document.addEventListener`). É estritamente proibido realizar vinculações duplas (Double Binding) com ouvintes locais diretos concorrentes nos elementos dinâmicos, para evitar a perda de foco, cursor saltando, ou substituição indesejada de caracteres digitados. A máscara de telefone deve diferenciar e suportar dinamicamente tanto telefones fixos (10 dígitos) quanto celulares (11 dígitos).
51. **Encargos Financeiros e Simulação Asaas**: Se nenhuma taxa de juro ou multa estiver cadastrada no curso do aluno, o fallback padrão é obrigatoriamente 0% (sem encargos). Em simulações de atraso, o desconto perde a validade automaticamente, calculando multa/juros sobre o valor bruto original. Se o vencimento cair em finais de semana, a contagem de encargos é postergada em um dia útil (considerando segunda-feira em dia, aplicando juros apenas a partir de terça-feira), refletido em paridade absoluta entre Manager e Portal. 51. **Encargos Financeiros e Simulação Asaas**: Se nenhuma taxa de juro ou multa estiver cadastrada no curso do aluno, o fallback padrão é obrigatoriamente 0% (sem encargos). Em simulações de atraso, o desconto perde a validade automaticamente, calculando multa/juros sobre o valor bruto original. Se o vencimento cair em finais de semana, a contagem de encargos é postergada em um dia útil (considerando segunda-feira em dia, aplicando juros apenas a partir de terça-feira), refletido em paridade absoluta entre Manager e Portal.
52. **Simulação de Encargos no Dashboard do Portal**: O dashboard principal do Portal do Aluno (`portal/src/pages/Dashboard.tsx`) deve calcular e refletir em tempo real os juros, multas e perda de desconto para parcelas em atraso, mantendo paridade absoluta com os valores exibidos na aba Financeiro e prevenindo divergências visuais. 52. **Simulação de Encargos no Dashboard do Portal**: O dashboard principal do Portal do Aluno (`portal/src/pages/Dashboard.tsx`) deve calcular e refletir em tempo real os juros, multas e perda de desconto para parcelas em atraso, mantendo paridade absoluta com os valores exibidos na aba Financeiro e prevenindo divergências visuais.
53. **Tratamento Resumido de Erros de Cota de IA**: Ao ocorrer falha em rotas de IA, o sistema deve capturar e identificar erros de limite ou cota excedida (como HTTP 429, `Quota exceeded`, `insufficient_quota`, etc.) e exibir um alerta resumido contendo o provedor ativo, o problema, o motivo, a solução prática recomendada e um detalhe técnico reduzido a 250 caracteres.
54. **Suporte Direto ao Gemini 2.5 Flash**: O backend em `server.selfhosted.js` não deve realizar downgrade forçado do modelo `gemini-2.5-flash` para `gemini-1.5-flash` na geração de questões, permitindo o suporte nativo às chaves com acesso exclusivo às versões mais recentes da API do Gemini.
55. **Migração Automática de Chaves de IA**: O sistema deve migrar automaticamente as configurações e chaves de IA legadas salvas no arquivo JSON para a tabela relacional `configuracoes_ia` do PostgreSQL para centralizar as credenciais.
56. **Geração Híbrida de Imagens Gemini/Imagen**: O backend deve suportar chamadas de imagem nativas (endpoint `:generateImages` do Imagen 3) para modelos `imagen-` e chamadas multimodais para modelos Gemini (`responseModalities`), aplicando fallbacks automáticos em cascata caso o modelo primário selecionado falhe.
57. **Correção de Autofill no Chrome**: Modais de IA devem desativar ou evitar elementos de formulário que induzam o Chrome a preencher incorretamente campos de texto irrelevantes (como busca de provas) com dados armazenados de login (ex: 'admin').

View File

@ -99,3 +99,11 @@ Nesta sessão de trabalho, realizamos o "core" da transição do sistema EduMana
### Fase 12: Simulação de Encargos no Dashboard do Portal do Aluno (SQL-First) ### Fase 12: Simulação de Encargos no Dashboard do Portal do Aluno (SQL-First)
- **Correção do Dashboard do Portal**: Atualizado o painel principal do Portal do Aluno (`portal/src/pages/Dashboard.tsx`) para embutir as funções de simulação matemática de juros/multa de atraso e expiração de desconto. O dashboard agora reflete exatamente o mesmo valor com encargos calculado na página Financeiro, unificando os totais pendentes e os valores de parcelas vencidas do card "Próximo Vencimento". - **Correção do Dashboard do Portal**: Atualizado o painel principal do Portal do Aluno (`portal/src/pages/Dashboard.tsx`) para embutir as funções de simulação matemática de juros/multa de atraso e expiração de desconto. O dashboard agora reflete exatamente o mesmo valor com encargos calculado na página Financeiro, unificando os totais pendentes e os valores de parcelas vencidas do card "Próximo Vencimento".
- **Declaração de Tipos**: Atualizada a interface `Payment` em `portal/src/types.ts` para opcionalmente expor as propriedades `lateFee`, `interest` e `valor_pago`, garantindo a validação estrita do compilador TypeScript e prevenindo quebras de tela (White Screen). - **Declaração de Tipos**: Atualizada a interface `Payment` em `portal/src/types.ts` para opcionalmente expor as propriedades `lateFee`, `interest` e `valor_pago`, garantindo a validação estrita do compilador TypeScript e prevenindo quebras de tela (White Screen).
### Fase 13: Consolidação e Resiliência do Módulo de Inteligência Artificial (Julho de 2026)
- **Mensagem Amigável de Cota Excedida**: Adicionada a função helper `handleAiError` em `Exams.tsx` para interceptar falhas em todas as chamadas de IA. Erros de limite e cota (HTTP 429) do Gemini, OpenAI, Claude e OpenRouter são formatados em um alerta limpo que explica o erro e fornece a solução correspondente (ex: ativar faturamento ou adicionar saldo).
- **Remoção de Downgrade do Gemini 2.5 Flash**: Corrigido bug crítico na rota `/api/ai/generate-questions` em `server.selfhosted.js` que reescrevia silenciosamente o modelo selecionado `gemini-2.5-flash` para o antigo `gemini-1.5-flash`, o que causava erros 404 em contas de API novas onde o 1.5 não está provisionado.
- **Geração Híbrida de Imagens Gemini/Imagen**: Implementação de rotas no backend que suportam a geração nativa de imagens via API do Imagen 3 (`:generateImages`) para modelos `imagen-`, e via multimodalidade (`responseModalities`) para modelos Gemini. Adicionado fallback em cascata automático entre modelos (`imagen-3.0-generate-002`, `gemini-2.5-flash-image`, `gemini-3.1-flash-image`).
- **Migração de Chaves de IA para o PostgreSQL**: Adicionada rotina de migração transparente que move as credenciais e configurações de IA do antigo cache JSON de configurações para a tabela relacional `configuracoes_ia`.
- **Correção de Autofill no Chrome**: Corrigido bug onde o recurso de preenchimento automático do Chrome inseria o usuário 'admin' na barra de busca de provas ao carregar o modal de configuração de IA.

View File

@ -130,7 +130,7 @@ const Exams: React.FC<ExamsProps> = ({ data, updateData }) => {
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
if (!silent) { if (!silent) {
showAlert('Erro ao listar modelos', e.message || 'Falha ao buscar modelos, verifique sua chave de API.', 'error'); handleAiError('Erro ao listar modelos', e, 'Falha ao buscar modelos, verifique sua chave de API.');
} }
} finally { } finally {
setLoadingModels(prev => ({ ...prev, [provider]: false })); setLoadingModels(prev => ({ ...prev, [provider]: false }));
@ -385,6 +385,56 @@ const Exams: React.FC<ExamsProps> = ({ data, updateData }) => {
setTargetClassId(''); setTargetClassId('');
}; };
const handleAiError = (title: string, error: any, defaultMsg: string) => {
const errorMsg = error?.message || String(error);
const lowercaseMsg = errorMsg.toLowerCase();
const isQuotaError =
lowercaseMsg.includes('429') ||
lowercaseMsg.includes('quota') ||
lowercaseMsg.includes('resource_exhausted') ||
lowercaseMsg.includes('rate_limit') ||
lowercaseMsg.includes('rate-limit') ||
lowercaseMsg.includes('insufficient_quota') ||
lowercaseMsg.includes('exceeded');
if (isQuotaError) {
let providerName = 'Inteligência Artificial';
let reason = 'A cota de uso gratuita ou o limite de requisições da sua chave de API foi excedido.';
let solution = 'Para resolver, você precisa habilitar o faturamento (plano de cobrança) no painel do provedor de IA ou adicionar créditos à sua conta.';
if (lowercaseMsg.includes('gemini') || lowercaseMsg.includes('google') || lowercaseMsg.includes('imagen')) {
providerName = 'Google Gemini/Imagen';
reason = 'A cota gratuita para o modelo selecionado está zerada (limit: 0) ou foi totalmente excedida.';
solution = 'Ative o faturamento (Pay-as-you-go) no painel do Google AI Studio para este projeto.';
} else if (lowercaseMsg.includes('openai') || lowercaseMsg.includes('dall-e') || lowercaseMsg.includes('dall_e') || lowercaseMsg.includes('dall-e-3') || lowercaseMsg.includes('dall-e-2')) {
providerName = 'OpenAI (DALL-E)';
reason = 'Seu limite de cota da OpenAI foi atingido ou os créditos da sua conta expiraram.';
solution = 'Adicione fundos/créditos ou atualize seu plano no painel do OpenAI Developer Console.';
} else if (lowercaseMsg.includes('claude') || lowercaseMsg.includes('anthropic')) {
providerName = 'Anthropic Claude';
reason = 'A cota de uso ou limite de requisições da API do Claude foi excedido.';
solution = 'Verifique seu plano ou saldo de créditos no console da Anthropic.';
} else if (lowercaseMsg.includes('openrouter')) {
providerName = 'OpenRouter';
reason = 'O limite de cota da sua conta do OpenRouter foi excedido ou o saldo de créditos acabou.';
solution = 'Adicione saldo ou configure um limite maior no painel do OpenRouter.';
}
showAlert(
'Limite de Cota Excedido',
`⚠️ Provedor: ${providerName}\n\n` +
`O que houve: A requisição à IA falhou porque a cota limite de uso foi atingida.\n\n` +
`Motivo: ${reason}\n\n` +
`Como resolver: ${solution}\n\n` +
`Detalhe do erro técnico: ${errorMsg.length > 250 ? errorMsg.substring(0, 250) + '...' : errorMsg}`,
'warning'
);
} else {
showAlert(title, errorMsg || defaultMsg, 'error');
}
};
const handleGenerateQuestionsAI = async (quantityOverride?: number) => { const handleGenerateQuestionsAI = async (quantityOverride?: number) => {
const activeProvider = data.aiConfig?.activeProvider || 'gemini'; const activeProvider = data.aiConfig?.activeProvider || 'gemini';
const hasKey = !!( const hasKey = !!(
@ -446,7 +496,7 @@ const Exams: React.FC<ExamsProps> = ({ data, updateData }) => {
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
showAlert('Erro ao gerar questões', e.message || 'Houve uma falha de comunicação com a inteligência artificial. Verifique sua chave de API e tente novamente.', 'error'); handleAiError('Erro ao gerar questões', e, 'Houve uma falha de comunicação com a inteligência artificial. Verifique sua chave de API e tente novamente.');
} finally { } finally {
setIsGeneratingAI(false); setIsGeneratingAI(false);
} }
@ -510,7 +560,7 @@ const Exams: React.FC<ExamsProps> = ({ data, updateData }) => {
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
showAlert('Erro ao regenerar', e.message || 'Falha ao se comunicar com a IA.', 'error'); handleAiError('Erro ao regenerar', e, 'Falha ao se comunicar com a IA.');
} finally { } finally {
setIsGeneratingAI(false); setIsGeneratingAI(false);
} }
@ -561,7 +611,7 @@ const Exams: React.FC<ExamsProps> = ({ data, updateData }) => {
showAlert('Sucesso', 'Imagem gerada por IA e anexada com sucesso!', 'success'); showAlert('Sucesso', 'Imagem gerada por IA e anexada com sucesso!', 'success');
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
showAlert('Erro ao gerar imagem', e.message || 'Houve uma falha de comunicação com o servidor para gerar a imagem.', 'error'); handleAiError('Erro ao gerar imagem', e, 'Houve uma falha de comunicação com o servidor para gerar a imagem.');
} finally { } finally {
setIsGeneratingImage(prev => ({ ...prev, [question.id]: false })); setIsGeneratingImage(prev => ({ ...prev, [question.id]: false }));
} }

View File

@ -1280,9 +1280,6 @@ app.post('/api/ai/generate-questions', async (req, res) => {
if (provider === 'gemini') { if (provider === 'gemini') {
apiKey = aiConfig.geminiKey; apiKey = aiConfig.geminiKey;
modelName = aiConfig.geminiModel || 'gemini-1.5-flash'; modelName = aiConfig.geminiModel || 'gemini-1.5-flash';
if (modelName === 'gemini-2.5-flash') {
modelName = 'gemini-1.5-flash';
}
} else if (provider === 'openai') { } else if (provider === 'openai') {
apiKey = aiConfig.openaiKey; apiKey = aiConfig.openaiKey;
modelName = aiConfig.openaiModel || 'gpt-4o-mini'; modelName = aiConfig.openaiModel || 'gpt-4o-mini';