diff --git a/manager/components/Exams.tsx b/manager/components/Exams.tsx index 1152a97..3577449 100644 --- a/manager/components/Exams.tsx +++ b/manager/components/Exams.tsx @@ -21,13 +21,11 @@ const Exams: React.FC = ({ data, updateData }) => { const { showAlert, showConfirm } = useDialog(); const [showAIConfigModal, setShowAIConfigModal] = useState(false); - const [aiConfig, setAIConfig] = useState<{ provider: 'gemini' | 'openai'; apiKey: string }>(() => { - try { - const saved = localStorage.getItem('edumanager_ai_config'); - return saved ? JSON.parse(saved) : { provider: 'gemini', apiKey: '' }; - } catch { - return { provider: 'gemini', apiKey: '' }; - } + const [aiForm, setAiForm] = useState({ + activeProvider: data.aiConfig?.activeProvider || 'gemini', + geminiKey: data.aiConfig?.geminiKey ? '********' : '', + openaiKey: data.aiConfig?.openaiKey ? '********' : '', + claudeKey: data.aiConfig?.claudeKey ? '********' : '' }); const [isGeneratingAI, setIsGeneratingAI] = useState(false); @@ -35,12 +33,32 @@ const Exams: React.FC = ({ data, updateData }) => { const [aiQuantity, setAiQuantity] = useState(5); const [aiDifficulty, setAiDifficulty] = useState('medium'); - const handleSaveAIConfig = (provider: 'gemini' | 'openai', apiKey: string) => { - const newConfig = { provider, apiKey }; - setAIConfig(newConfig); - localStorage.setItem('edumanager_ai_config', JSON.stringify(newConfig)); - setShowAIConfigModal(false); - showAlert('Sucesso', 'Configurações de IA salvas com sucesso!', 'success'); + const handleOpenAIConfig = () => { + setAiForm({ + activeProvider: data.aiConfig?.activeProvider || 'gemini', + geminiKey: data.aiConfig?.geminiKey ? '********' : '', + openaiKey: data.aiConfig?.openaiKey ? '********' : '', + claudeKey: data.aiConfig?.claudeKey ? '********' : '' + }); + setShowAIConfigModal(true); + }; + + const handleSaveAIConfig = async () => { + const newConfig = { + activeProvider: aiForm.activeProvider, + geminiKey: aiForm.geminiKey === '********' ? (data.aiConfig?.geminiKey || '') : aiForm.geminiKey, + openaiKey: aiForm.openaiKey === '********' ? (data.aiConfig?.openaiKey || '') : aiForm.openaiKey, + claudeKey: aiForm.claudeKey === '********' ? (data.aiConfig?.claudeKey || '') : aiForm.claudeKey + }; + + try { + updateData({ aiConfig: newConfig }); + setShowAIConfigModal(false); + showAlert('Sucesso', 'Configurações de IA salvas com sucesso no banco de dados!', 'success'); + } catch (e) { + console.error(e); + showAlert('Erro', 'Falha ao salvar configurações de IA.', 'error'); + } }; const [dbClasses, setDbClasses] = useState(data?.classes || []); @@ -250,10 +268,18 @@ const Exams: React.FC = ({ data, updateData }) => { }; const handleGenerateQuestionsAI = async (quantityOverride?: number) => { - if (!aiConfig.apiKey) { - showAlert('IA não configurada', 'Por favor, configure sua API Key clicando no botão "Configurar IA" no cabeçalho da página.', 'warning'); + const activeProvider = data.aiConfig?.activeProvider || 'gemini'; + const hasKey = !!( + (activeProvider === 'gemini' && data.aiConfig?.geminiKey) || + (activeProvider === 'openai' && data.aiConfig?.openaiKey) || + (activeProvider === 'claude' && data.aiConfig?.claudeKey) + ); + + if (!hasKey) { + showAlert('IA não configurada', 'Por favor, configure a chave de API do seu provedor ativo clicando no botão "Configurar IA" no cabeçalho.', 'warning'); return; } + if (!aiTopic.trim()) { showAlert('Atenção', 'Informe o tema ou assunto das questões a serem geradas.', 'warning'); return; @@ -267,8 +293,6 @@ const Exams: React.FC = ({ data, updateData }) => { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - apiKey: aiConfig.apiKey, - provider: aiConfig.provider, topic: aiTopic, quantity: qty, difficulty: aiDifficulty @@ -310,10 +334,18 @@ const Exams: React.FC = ({ data, updateData }) => { }; const handleRegenerateQuestionAI = async (qIndex: number) => { - if (!aiConfig.apiKey) { - showAlert('IA não configurada', 'Por favor, configure sua API Key clicando no botão "Configurar IA" no cabeçalho da página.', 'warning'); + const activeProvider = data.aiConfig?.activeProvider || 'gemini'; + const hasKey = !!( + (activeProvider === 'gemini' && data.aiConfig?.geminiKey) || + (activeProvider === 'openai' && data.aiConfig?.openaiKey) || + (activeProvider === 'claude' && data.aiConfig?.claudeKey) + ); + + if (!hasKey) { + showAlert('IA não configurada', 'Por favor, configure a chave de API do seu provedor ativo clicando no botão "Configurar IA" no cabeçalho.', 'warning'); return; } + const topic = aiTopic.trim() || editingExam?.title || 'Conhecimentos Gerais'; setIsGeneratingAI(true); @@ -322,8 +354,6 @@ const Exams: React.FC = ({ data, updateData }) => { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - apiKey: aiConfig.apiKey, - provider: aiConfig.provider, topic: topic, quantity: 1, difficulty: aiDifficulty @@ -519,6 +549,12 @@ const Exams: React.FC = ({ data, updateData }) => { }; if (currentView === 'builder' && editingExam) { + const activeProvider = data.aiConfig?.activeProvider || 'gemini'; + const hasAPIKey = !!( + (activeProvider === 'gemini' && data.aiConfig?.geminiKey) || + (activeProvider === 'openai' && data.aiConfig?.openaiKey) || + (activeProvider === 'claude' && data.aiConfig?.claudeKey) + ); return (
@@ -643,7 +679,7 @@ const Exams: React.FC = ({ data, updateData }) => {

Use nossa inteligência artificial para criar questões completas instantaneamente.

- {!aiConfig.apiKey && ( + {!hasAPIKey && ( IA não configurada @@ -688,10 +724,10 @@ const Exams: React.FC = ({ data, updateData }) => {
- {!aiConfig.apiKey ? ( + {!hasAPIKey ? (
diff --git a/manager/server.selfhosted.js b/manager/server.selfhosted.js index 736f5c2..4ff24a8 100644 --- a/manager/server.selfhosted.js +++ b/manager/server.selfhosted.js @@ -1261,11 +1261,8 @@ app.post('/api/provas/:id/questoes', async (req, res) => { }); app.post('/api/ai/generate-questions', async (req, res) => { - const { apiKey, provider, topic, quantity, difficulty } = req.body; + const { topic, quantity, difficulty } = req.body; - if (!apiKey) { - return res.status(400).json({ error: 'API Key não informada' }); - } if (!topic) { return res.status(400).json({ error: 'Tema não informado' }); } @@ -1273,6 +1270,19 @@ app.post('/api/ai/generate-questions', async (req, res) => { const limitQty = Math.min(Math.max(Number(quantity) || 1, 1), 20); try { + const appData = await getSchoolData(); + const aiConfig = appData.aiConfig || {}; + const provider = aiConfig.activeProvider || 'gemini'; + + let apiKey = ''; + if (provider === 'gemini') apiKey = aiConfig.geminiKey; + else if (provider === 'openai') apiKey = aiConfig.openaiKey; + else if (provider === 'claude') apiKey = aiConfig.claudeKey; + + if (!apiKey) { + return res.status(400).json({ error: `Chave de API não configurada para o provedor ${provider.toUpperCase()} no painel da escola.` }); + } + if (provider === 'openai') { const payload = { model: 'gpt-4o-mini', @@ -1329,6 +1339,64 @@ Exemplo de formato esperado: const questions = parsed.questions || parsed || []; return res.json({ questions }); + } else if (provider === 'claude') { + const payload = { + model: 'claude-3-5-sonnet-20241022', + max_tokens: 4096, + system: `Você é um gerador de questões escolares de múltipla escolha para provas de alunos. +Retorne APENAS um array de objetos JSON estruturado (sem blocos de código markdown como \`\`\`json ou texto explicativo extra). +Cada questão do array deve conter obrigatoriamente: +- "text": o enunciado da questão. +- "options": um array com exatamente 4 opções de resposta. +- "correctOptionIndex": o índice da opção correta (de 0 a 3). + +Exemplo de formato esperado: +[ + { + "text": "Qual é a principal função do processador?", + "options": ["Processar dados", "Armazenar arquivos", "Exibir imagens", "Conectar à internet"], + "correctOptionIndex": 0 + } +]`, + messages: [ + { + role: 'user', + content: `Gere exatamente ${limitQty} questões escolares de múltipla escolha sobre o tema "${topic}" com nível de dificuldade "${difficulty}" em Português do Brasil.` + } + ] + }; + + const response = await fetch('https://api.anthropic.com/v1/messages', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': apiKey, + 'anthropic-version': '2023-06-01' + }, + body: JSON.stringify(payload) + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Claude API retornou erro: ${response.status} - ${errorText}`); + } + + const resJson = await response.json(); + let contentText = resJson.content?.[0]?.text || ''; + contentText = contentText.trim(); + if (contentText.startsWith('```json')) { + contentText = contentText.substring(7); + } else if (contentText.startsWith('```')) { + contentText = contentText.substring(3); + } + if (contentText.endsWith('```')) { + contentText = contentText.substring(0, contentText.length - 3); + } + contentText = contentText.trim(); + + const questions = JSON.parse(contentText); + return res.json({ questions }); + } else { // Provedor padrão: Google Gemini const geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${apiKey}`; diff --git a/manager/types.ts b/manager/types.ts index f3e9d61..356cb8f 100644 --- a/manager/types.ts +++ b/manager/types.ts @@ -297,6 +297,13 @@ export interface PreMatriculaInscricao { createdAt: string; } +export interface AIConfig { + activeProvider: 'gemini' | 'openai' | 'claude'; + geminiKey?: string; + openaiKey?: string; + claudeKey?: string; +} + export interface SchoolData { users: User[]; courses: Course[]; @@ -327,6 +334,7 @@ export interface SchoolData { instanceName: string; apiKey: string; }; + aiConfig?: AIConfig; messageTemplates?: { boletoGerado: string; pagamentoConfirmado: string;