diff --git a/manager/components/Exams.tsx b/manager/components/Exams.tsx index 33e2e1f..62c3439 100644 --- a/manager/components/Exams.tsx +++ b/manager/components/Exams.tsx @@ -1,4 +1,4 @@ -import React, { useState, useRef } from 'react'; +import React, { useState, useRef, useEffect } from 'react'; import { SchoolData, Exam, Question } from '../types'; import { FileText, Plus, Search, BookOpen, Upload, Trash2, ArrowLeft, Save, CheckCircle, Image as ImageIcon, X, RefreshCw, Lock, Unlock, AlertTriangle, Copy, Bell, Sparkles, Wand2 } from 'lucide-react'; import { uploadExamImage } from '../services/supabase'; @@ -87,7 +87,7 @@ const Exams: React.FC = ({ data, updateData }) => { setShowAIConfigModal(true); }; - const handleFetchModels = async (provider: 'gemini' | 'openai' | 'claude' | 'openrouter') => { + const handleFetchModels = async (provider: 'gemini' | 'openai' | 'claude' | 'openrouter', silent = false) => { setLoadingModels(prev => ({ ...prev, [provider]: true })); try { let key = ''; @@ -110,18 +110,38 @@ const Exams: React.FC = ({ data, updateData }) => { const { models } = await response.json(); if (models && models.length > 0) { setDynamicModels(prev => ({ ...prev, [provider]: models })); - showAlert('Sucesso', `Lista de modelos para ${provider.toUpperCase()} atualizada!`, 'success'); + if (!silent) { + showAlert('Sucesso', `Lista de modelos para ${provider.toUpperCase()} atualizada!`, 'success'); + } } else { throw new Error('Nenhum modelo compatível retornado.'); } } catch (e: any) { console.error(e); - showAlert('Erro ao listar modelos', e.message || 'Falha ao buscar modelos, verifique sua chave de API.', 'error'); + if (!silent) { + showAlert('Erro ao listar modelos', e.message || 'Falha ao buscar modelos, verifique sua chave de API.', 'error'); + } } finally { setLoadingModels(prev => ({ ...prev, [provider]: false })); } }; + useEffect(() => { + if (showAIConfigModal) { + const provider = aiForm.activeProvider; + let hasKey = false; + if (provider === 'gemini') hasKey = !!aiForm.geminiKey; + else if (provider === 'openai') hasKey = !!aiForm.openaiKey; + else if (provider === 'claude') hasKey = !!aiForm.claudeKey; + else if (provider === 'openrouter') hasKey = !!aiForm.openrouterKey; + + // Disparar carregamento automático e silencioso em background + if (hasKey) { + handleFetchModels(provider, true); + } + } + }, [aiForm.activeProvider, showAIConfigModal]); + const handleSaveAIConfig = async () => { const newConfig = { activeProvider: aiForm.activeProvider, @@ -1411,197 +1431,205 @@ const Exams: React.FC = ({ data, updateData }) => { -
-

Google Gemini

-
-
- - setAiForm({ ...aiForm, geminiKey: e.target.value })} - placeholder="Cole a chave (AIzaSy...)" - className="w-full px-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:outline-none transition-all font-medium text-slate-800 text-xs" - /> -
-
-
- - + {aiForm.activeProvider === 'gemini' && ( +
+

Google Gemini

+
+
+ + setAiForm({ ...aiForm, geminiKey: e.target.value })} + placeholder="Cole a chave (AIzaSy...)" + className="w-full px-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:outline-none transition-all font-medium text-slate-800 text-xs" + /> +
+
+
+ + +
+
-
-
+ )} -
-

OpenAI (ChatGPT)

-
-
- - setAiForm({ ...aiForm, openaiKey: e.target.value })} - placeholder="Cole a chave (sk-...)" - className="w-full px-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:outline-none transition-all font-medium text-slate-800 text-xs" - /> -
-
-
- - + {aiForm.activeProvider === 'openai' && ( +
+

OpenAI (ChatGPT)

+
+
+ + setAiForm({ ...aiForm, openaiKey: e.target.value })} + placeholder="Cole a chave (sk-...)" + className="w-full px-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:outline-none transition-all font-medium text-slate-800 text-xs" + /> +
+
+
+ + +
+
-
-
+ )} -
-

Anthropic Claude

-
-
- - setAiForm({ ...aiForm, claudeKey: e.target.value })} - placeholder="Cole a chave (sk-ant-...)" - className="w-full px-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:outline-none transition-all font-medium text-slate-800 text-xs" - /> -
-
-
- - + {aiForm.activeProvider === 'claude' && ( +
+

Anthropic Claude

+
+
+ + setAiForm({ ...aiForm, claudeKey: e.target.value })} + placeholder="Cole a chave (sk-ant-...)" + className="w-full px-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:outline-none transition-all font-medium text-slate-800 text-xs" + /> +
+
+
+ + +
+
-
-
+ )} -
-

OpenRouter

-
-
- - setAiForm({ ...aiForm, openrouterKey: e.target.value })} - placeholder="Cole a chave (sk-or-...)" - className="w-full px-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:outline-none transition-all font-medium text-slate-800 text-xs" - /> -
-
-
- - + {aiForm.activeProvider === 'openrouter' && ( +
+

OpenRouter

+
+
+ + setAiForm({ ...aiForm, openrouterKey: e.target.value })} + placeholder="Cole a chave (sk-or-...)" + className="w-full px-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:outline-none transition-all font-medium text-slate-800 text-xs" + /> +
+
+
+ + +
+
-
-
+ )}
diff --git a/manager/server.selfhosted.js b/manager/server.selfhosted.js index 32fa155..b18e8c8 100644 --- a/manager/server.selfhosted.js +++ b/manager/server.selfhosted.js @@ -1753,12 +1753,20 @@ app.post('/api/ai/list-models', async (req, res) => { throw new Error(`Erro Gemini: ${response.status} - ${errText}`); } const json = await response.json(); - const models = (json.models || []) - .filter(m => m.supportedMethods && m.supportedMethods.includes('generateContent')) + let models = (json.models || []) + .filter(m => m.supportedGenerationMethods && m.supportedGenerationMethods.includes('generateContent')) .map(m => ({ id: m.name.replace('models/', ''), name: m.displayName || m.name })); + if (models.length === 0) { + models = [ + { id: 'gemini-1.5-flash', name: 'gemini-1.5-flash (Mais Rápido & Gratuito)' }, + { id: 'gemini-1.5-pro', name: 'gemini-1.5-pro (Multimodal)' }, + { id: 'gemini-2.0-flash-exp', name: 'gemini-2.0-flash-exp (Experimental)' }, + { id: 'gemini-2.5-flash', name: 'gemini-2.5-flash (Se ativo na sua conta)' } + ]; + } return res.json({ models }); } else if (provider === 'openai') { @@ -1771,12 +1779,19 @@ app.post('/api/ai/list-models', async (req, res) => { throw new Error(`Erro OpenAI: ${response.status} - ${errText}`); } const json = await response.json(); - const models = (json.data || []) + let models = (json.data || []) .filter(m => m.id.includes('gpt') || m.id.includes('o1') || m.id.includes('o3')) .map(m => ({ id: m.id, name: m.id })); + if (models.length === 0) { + models = [ + { id: 'gpt-4o-mini', name: 'gpt-4o-mini (Recomendado)' }, + { id: 'gpt-4o', name: 'gpt-4o (Completo)' }, + { id: 'gpt-3.5-turbo', name: 'gpt-3.5-turbo (Legado)' } + ]; + } return res.json({ models }); } else if (provider === 'claude') { @@ -1797,10 +1812,16 @@ app.post('/api/ai/list-models', async (req, res) => { return res.json({ models: defaultModels }); } const json = await response.json(); - const models = (json.data || []).map(m => ({ + let models = (json.data || []).map(m => ({ id: m.id, name: m.display_name || m.id })); + if (models.length === 0) { + models = [ + { id: 'claude-3-5-sonnet-20241022', name: 'claude-3-5-sonnet-20241022' }, + { id: 'claude-3-5-haiku-20241022', name: 'claude-3-5-haiku-20241022' } + ]; + } return res.json({ models }); } else if (provider === 'openrouter') { @@ -1813,10 +1834,17 @@ app.post('/api/ai/list-models', async (req, res) => { throw new Error(`Erro OpenRouter: ${response.status} - ${errText}`); } const json = await response.json(); - const models = (json.data || []).map(m => ({ + let models = (json.data || []).map(m => ({ id: m.id, name: `${m.name || m.id} (${m.id})` })); + if (models.length === 0) { + models = [ + { id: 'google/gemini-2.5-flash', name: 'Gemini 2.5 Flash' }, + { id: 'openai/gpt-4o-mini', name: 'GPT-4o Mini' }, + { id: 'anthropic/claude-3.5-sonnet', name: 'Claude 3.5 Sonnet' } + ]; + } return res.json({ models }); }