diff --git a/manager/components/Exams.tsx b/manager/components/Exams.tsx index 0602e55..1152a97 100644 --- a/manager/components/Exams.tsx +++ b/manager/components/Exams.tsx @@ -1,6 +1,6 @@ import React, { useState, useRef } 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 } from 'lucide-react'; +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'; import { useDialog } from '../DialogContext'; @@ -20,6 +20,29 @@ const Exams: React.FC = ({ data, updateData }) => { const [activeTab, setActiveTab] = useState<'ativos' | 'lixeira'>('ativos'); 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 [isGeneratingAI, setIsGeneratingAI] = useState(false); + const [aiTopic, setAiTopic] = useState(''); + 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 [dbClasses, setDbClasses] = useState(data?.classes || []); const [dbCourses, setDbCourses] = useState(data?.courses || []); const [dbSubjects, setDbSubjects] = useState(data?.subjects || []); @@ -226,6 +249,123 @@ const Exams: React.FC = ({ data, updateData }) => { setTargetClassId(''); }; + 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'); + return; + } + if (!aiTopic.trim()) { + showAlert('Atenção', 'Informe o tema ou assunto das questões a serem geradas.', 'warning'); + return; + } + + const qty = quantityOverride || aiQuantity; + + setIsGeneratingAI(true); + try { + const response = await fetch('/api/ai/generate-questions', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + apiKey: aiConfig.apiKey, + provider: aiConfig.provider, + topic: aiTopic, + quantity: qty, + difficulty: aiDifficulty + }) + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || 'Erro desconhecido na geração de questões.'); + } + + const { questions } = await response.json(); + if (!questions || questions.length === 0) { + throw new Error('Nenhuma questão retornada pelo servidor.'); + } + + if (editingExam) { + const formattedQuestions = questions.map((q: any) => ({ + id: Date.now().toString() + Math.random().toString(36).substring(7), + text: q.text || '', + options: q.options && q.options.length === 4 ? q.options : ['Opção A', 'Opção B', 'Opção C', 'Opção D'], + correctOptionIndex: q.correctOptionIndex !== undefined ? Math.min(Math.max(q.correctOptionIndex, 0), 3) : 0 + })); + + setEditingExam({ + ...editingExam, + questions: [...editingExam.questions, ...formattedQuestions] + }); + + showAlert('Sucesso', `${formattedQuestions.length} questões geradas e inseridas com sucesso!`, 'success'); + } + + } catch (e: any) { + 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'); + } finally { + setIsGeneratingAI(false); + } + }; + + 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'); + return; + } + const topic = aiTopic.trim() || editingExam?.title || 'Conhecimentos Gerais'; + + setIsGeneratingAI(true); + try { + const response = await fetch('/api/ai/generate-questions', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + apiKey: aiConfig.apiKey, + provider: aiConfig.provider, + topic: topic, + quantity: 1, + difficulty: aiDifficulty + }) + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || 'Erro desconhecido na geração.'); + } + + const { questions } = await response.json(); + if (!questions || questions.length === 0) { + throw new Error('Nenhuma questão retornada pela IA.'); + } + + const newQ = questions[0]; + if (editingExam) { + const newQuestions = [...editingExam.questions]; + newQuestions[qIndex] = { + ...newQuestions[qIndex], + text: newQ.text || '', + options: newQ.options && newQ.options.length === 4 ? newQ.options : ['Opção A', 'Opção B', 'Opção C', 'Opção D'], + correctOptionIndex: newQ.correctOptionIndex !== undefined ? Math.min(Math.max(newQ.correctOptionIndex, 0), 3) : 0 + }; + + setEditingExam({ + ...editingExam, + questions: newQuestions + }); + + showAlert('Sucesso', 'Questão regenerada com sucesso!', 'success'); + } + + } catch (e: any) { + console.error(e); + showAlert('Erro ao regenerar', e.message || 'Falha ao se comunicar com a IA.', 'error'); + } finally { + setIsGeneratingAI(false); + } + }; + const handleAddQuestion = () => { if (!editingExam) return; setEditingExam({ @@ -492,6 +632,105 @@ const Exams: React.FC = ({ data, updateData }) => { + {/* Painel do Gerador de Questões por IA */} +
+
+
+
+

+ + Gerador de Questões Inteligente +

+

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

+
+ {!aiConfig.apiKey && ( + + IA não configurada + + )} +
+ +
+
+ + setAiTopic(e.target.value)} + placeholder="Ex: Introdução ao Excel, Programação em Python, Redes..." + className="w-full px-4 py-3 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-sm" + /> +
+
+ + +
+
+ + +
+
+ +
+ {!aiConfig.apiKey ? ( + + ) : ( + <> + + + + )} +
+
+ {/* Questões */}
{editingExam.questions.map((question, qIndex) => ( @@ -503,13 +742,25 @@ const Exams: React.FC = ({ data, updateData }) => { {qIndex + 1} Questão - +
+ {aiConfig.apiKey && ( + + )} + +
@@ -672,6 +923,14 @@ const Exams: React.FC = ({ data, updateData }) => { onChange={(e) => setSearchTerm(e.target.value)} />
+ + + +
+
+ + +
+ +
+ + setAIConfig({ ...aiConfig, apiKey: e.target.value })} + placeholder={aiConfig.provider === 'gemini' ? 'AIzaSy...' : 'sk-...'} + className="w-full px-4 py-3 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" + /> +

+ Sua chave é armazenada de forma segura apenas localmente no seu navegador. +

+
+ +
+ + +
+
+ + + )} ); }; diff --git a/manager/server.selfhosted.js b/manager/server.selfhosted.js index 5214d4f..736f5c2 100644 --- a/manager/server.selfhosted.js +++ b/manager/server.selfhosted.js @@ -1260,6 +1260,146 @@ 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; + + 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' }); + } + + const limitQty = Math.min(Math.max(Number(quantity) || 1, 1), 20); + + try { + if (provider === 'openai') { + const payload = { + model: 'gpt-4o-mini', + messages: [ + { + role: 'system', + content: `Você é um gerador de questões escolares de múltipla escolha para provas de alunos. +Retorne um objeto JSON contendo uma lista de questões na chave "questions". +Cada questão deve conter: +- "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: +{ + "questions": [ + { + "text": "Qual é a principal função do processador?", + "options": ["Processar dados", "Armazenar arquivos", "Exibir imagens", "Conectar à internet"], + "correctOptionIndex": 0 + } + ] +}` + }, + { + role: 'user', + content: `Gere exatamente ${limitQty} questões sobre o tema "${topic}" com nível de dificuldade "${difficulty}" em Português do Brasil.` + } + ], + response_format: { type: 'json_object' } + }; + + const response = await fetch('https://api.openai.com/v1/chat/completions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${apiKey}` + }, + body: JSON.stringify(payload) + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`OpenAI API retornou erro: ${response.status} - ${errorText}`); + } + + const resJson = await response.json(); + const contentText = resJson.choices?.[0]?.message?.content; + if (!contentText) { + throw new Error('Retorno vazio da API do OpenAI'); + } + + const parsed = JSON.parse(contentText); + const questions = parsed.questions || parsed || []; + 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}`; + const payload = { + contents: [ + { + parts: [ + { + text: `Gere exatamente ${limitQty} questões de múltipla escolha escolares sobre o tema "${topic}" com nível de dificuldade "${difficulty}" em Português do Brasil. +Apenas retorne o JSON estruturado contendo a lista de questões no seguinte formato de array: +[ + { + "text": "Enunciado da questão", + "options": ["opção 1", "opção 2", "opção 3", "opção 4"], + "correctOptionIndex": 0 + } +]` + } + ] + } + ], + generationConfig: { + responseMimeType: 'application/json', + responseSchema: { + type: 'ARRAY', + items: { + type: 'OBJECT', + properties: { + text: { type: 'STRING' }, + options: { + type: 'ARRAY', + items: { type: 'STRING' }, + minItems: 4, + maxItems: 4 + }, + correctOptionIndex: { type: 'INTEGER' } + }, + required: ['text', 'options', 'correctOptionIndex'] + } + } + } + }; + + const response = await fetch(geminiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(payload) + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Gemini API retornou erro: ${response.status} - ${errorText}`); + } + + const resJson = await response.json(); + const textResponse = resJson.candidates?.[0]?.content?.parts?.[0]?.text; + if (!textResponse) { + throw new Error('Retorno vazio da API do Gemini'); + } + + const questions = JSON.parse(textResponse); + return res.json({ questions }); + } + } catch (error) { + console.error('Erro na geração de IA:', error); + res.status(500).json({ error: error.message || 'Erro ao processar requisição com a IA' }); + } +}); + // Helper para formatar datas para o JSON const formatDateForJSON = (val) => { if (!val) return '';