feat: adiciona gerador de questoes por IA no modulo de provas
Build and Deploy (Gitea) / build-and-deploy (push) Successful in 2m2s
Details
Build and Deploy (Gitea) / build-and-deploy (push) Successful in 2m2s
Details
This commit is contained in:
parent
c71259bd63
commit
a6dbd80362
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useState, useRef } from 'react';
|
import React, { useState, useRef } from 'react';
|
||||||
import { SchoolData, Exam, Question } from '../types';
|
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 { uploadExamImage } from '../services/supabase';
|
||||||
import { useDialog } from '../DialogContext';
|
import { useDialog } from '../DialogContext';
|
||||||
|
|
||||||
|
|
@ -20,6 +20,29 @@ const Exams: React.FC<ExamsProps> = ({ data, updateData }) => {
|
||||||
const [activeTab, setActiveTab] = useState<'ativos' | 'lixeira'>('ativos');
|
const [activeTab, setActiveTab] = useState<'ativos' | 'lixeira'>('ativos');
|
||||||
const { showAlert, showConfirm } = useDialog();
|
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<any[]>(data?.classes || []);
|
const [dbClasses, setDbClasses] = useState<any[]>(data?.classes || []);
|
||||||
const [dbCourses, setDbCourses] = useState<any[]>(data?.courses || []);
|
const [dbCourses, setDbCourses] = useState<any[]>(data?.courses || []);
|
||||||
const [dbSubjects, setDbSubjects] = useState<any[]>(data?.subjects || []);
|
const [dbSubjects, setDbSubjects] = useState<any[]>(data?.subjects || []);
|
||||||
|
|
@ -226,6 +249,123 @@ const Exams: React.FC<ExamsProps> = ({ data, updateData }) => {
|
||||||
setTargetClassId('');
|
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 = () => {
|
const handleAddQuestion = () => {
|
||||||
if (!editingExam) return;
|
if (!editingExam) return;
|
||||||
setEditingExam({
|
setEditingExam({
|
||||||
|
|
@ -492,6 +632,105 @@ const Exams: React.FC<ExamsProps> = ({ data, updateData }) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Painel do Gerador de Questões por IA */}
|
||||||
|
<div className="bg-white rounded-2xl p-8 shadow-sm border border-slate-200 mb-8 overflow-hidden relative">
|
||||||
|
<div className="absolute top-0 left-0 w-full h-1.5 bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500"></div>
|
||||||
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-black text-slate-800 flex items-center gap-2 tracking-tight">
|
||||||
|
<Sparkles className="text-indigo-600 animate-pulse" size={22} />
|
||||||
|
Gerador de Questões Inteligente
|
||||||
|
</h3>
|
||||||
|
<p className="text-slate-500 text-sm mt-1 font-medium">Use nossa inteligência artificial para criar questões completas instantaneamente.</p>
|
||||||
|
</div>
|
||||||
|
{!aiConfig.apiKey && (
|
||||||
|
<span className="inline-flex items-center gap-1.5 px-3 py-1.5 bg-amber-50 text-amber-700 rounded-lg text-xs font-bold border border-amber-200">
|
||||||
|
<AlertTriangle size={14} /> IA não configurada
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 items-end">
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<label className="block text-xs font-bold text-slate-500 uppercase tracking-wider mb-2">Tema ou Assunto da Avaliação</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={aiTopic}
|
||||||
|
onChange={e => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-bold text-slate-500 uppercase tracking-wider mb-2">Quantidade de Questões</label>
|
||||||
|
<select
|
||||||
|
value={aiQuantity}
|
||||||
|
onChange={e => setAiQuantity(Number(e.target.value))}
|
||||||
|
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-bold text-slate-700 text-sm"
|
||||||
|
>
|
||||||
|
{[1, 2, 3, 5, 10, 15, 20].map(qty => (
|
||||||
|
<option key={qty} value={qty}>{qty} {qty === 1 ? 'Questão' : 'Questões'}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-bold text-slate-500 uppercase tracking-wider mb-2">Nível de Dificuldade</label>
|
||||||
|
<select
|
||||||
|
value={aiDifficulty}
|
||||||
|
onChange={e => setAiDifficulty(e.target.value)}
|
||||||
|
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-bold text-slate-700 text-sm"
|
||||||
|
>
|
||||||
|
<option value="fácil">Fácil</option>
|
||||||
|
<option value="médio">Médio</option>
|
||||||
|
<option value="difícil">Difícil</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4 mt-6 pt-6 border-t border-slate-100 justify-end">
|
||||||
|
{!aiConfig.apiKey ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowAIConfigModal(true)}
|
||||||
|
className="flex items-center justify-center gap-2 px-6 py-3 bg-indigo-50 text-indigo-700 rounded-xl font-bold hover:bg-indigo-100 transition-all text-sm"
|
||||||
|
>
|
||||||
|
<Sparkles size={18} />
|
||||||
|
Configurar Chave de IA para Começar
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={isGeneratingAI}
|
||||||
|
onClick={() => handleGenerateQuestionsAI(1)}
|
||||||
|
className="flex items-center justify-center gap-2 px-5 py-3 bg-slate-100 text-slate-700 rounded-xl font-bold hover:bg-slate-200 disabled:opacity-50 transition-all text-sm"
|
||||||
|
>
|
||||||
|
{isGeneratingAI ? <RefreshCw className="animate-spin" size={16} /> : <Wand2 size={16} />}
|
||||||
|
Gerar +1 Individual
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={isGeneratingAI}
|
||||||
|
onClick={() => handleGenerateQuestionsAI()}
|
||||||
|
className="flex items-center justify-center gap-2 px-6 py-3 bg-indigo-600 text-white rounded-xl font-black hover:bg-indigo-700 disabled:opacity-50 transition-all shadow-lg shadow-indigo-200 text-sm"
|
||||||
|
>
|
||||||
|
{isGeneratingAI ? (
|
||||||
|
<>
|
||||||
|
<RefreshCw className="animate-spin" size={18} />
|
||||||
|
Gerando Questões...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Sparkles size={18} />
|
||||||
|
Gerar Todas ({aiQuantity})
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Questões */}
|
{/* Questões */}
|
||||||
<div className="space-y-6 mb-8">
|
<div className="space-y-6 mb-8">
|
||||||
{editingExam.questions.map((question, qIndex) => (
|
{editingExam.questions.map((question, qIndex) => (
|
||||||
|
|
@ -503,6 +742,17 @@ const Exams: React.FC<ExamsProps> = ({ data, updateData }) => {
|
||||||
<span className="w-8 h-8 rounded-lg bg-indigo-100 text-indigo-700 flex items-center justify-center text-sm">{qIndex + 1}</span>
|
<span className="w-8 h-8 rounded-lg bg-indigo-100 text-indigo-700 flex items-center justify-center text-sm">{qIndex + 1}</span>
|
||||||
Questão
|
Questão
|
||||||
</h4>
|
</h4>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{aiConfig.apiKey && (
|
||||||
|
<button
|
||||||
|
onClick={() => handleRegenerateQuestionAI(qIndex)}
|
||||||
|
disabled={isGeneratingAI}
|
||||||
|
className="text-slate-400 hover:text-indigo-600 transition-colors p-2 disabled:opacity-50"
|
||||||
|
title="Regenerar esta questão com IA"
|
||||||
|
>
|
||||||
|
<Wand2 size={20} className={isGeneratingAI ? 'animate-spin' : ''} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={() => handleRemoveQuestion(qIndex)}
|
onClick={() => handleRemoveQuestion(qIndex)}
|
||||||
className="text-slate-400 hover:text-red-500 transition-colors p-2"
|
className="text-slate-400 hover:text-red-500 transition-colors p-2"
|
||||||
|
|
@ -511,6 +761,7 @@ const Exams: React.FC<ExamsProps> = ({ data, updateData }) => {
|
||||||
<Trash2 size={20} />
|
<Trash2 size={20} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Enunciado */}
|
{/* Enunciado */}
|
||||||
|
|
@ -672,6 +923,14 @@ const Exams: React.FC<ExamsProps> = ({ data, updateData }) => {
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAIConfigModal(true)}
|
||||||
|
className="flex items-center justify-center gap-2 px-4 py-3 bg-slate-100 text-slate-700 rounded-xl font-bold hover:bg-slate-200 transition-all border border-slate-200"
|
||||||
|
title="Configurar Chaves de IA"
|
||||||
|
>
|
||||||
|
<Sparkles size={20} className="text-indigo-600" />
|
||||||
|
<span className="hidden sm:inline">Configurar IA</span>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleStartCreate}
|
onClick={handleStartCreate}
|
||||||
className="flex items-center justify-center gap-2 px-6 py-3 bg-indigo-600 text-white rounded-xl font-bold hover:bg-indigo-700 transition-all shadow-lg shadow-indigo-200"
|
className="flex items-center justify-center gap-2 px-6 py-3 bg-indigo-600 text-white rounded-xl font-bold hover:bg-indigo-700 transition-all shadow-lg shadow-indigo-200"
|
||||||
|
|
@ -873,6 +1132,71 @@ const Exams: React.FC<ExamsProps> = ({ data, updateData }) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* MODAL CONFIGURAÇÃO IA */}
|
||||||
|
{showAIConfigModal && (
|
||||||
|
<div className="fixed inset-0 bg-transparent z-[60] flex items-center justify-center p-4 animate-in fade-in duration-200">
|
||||||
|
<div className="bg-white rounded-3xl w-full max-w-md p-8 shadow-2xl border border-slate-100 animate-slide-up relative">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="p-3 bg-indigo-100 text-indigo-600 rounded-xl">
|
||||||
|
<Sparkles size={24} />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-black text-slate-800">Configuração de IA</h3>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAIConfigModal(false)}
|
||||||
|
className="text-slate-400 hover:text-slate-600 p-1 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<X size={20} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-5">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-bold text-slate-700 mb-2">Provedor de IA</label>
|
||||||
|
<select
|
||||||
|
value={aiConfig.provider}
|
||||||
|
onChange={e => setAIConfig({ ...aiConfig, provider: e.target.value as any })}
|
||||||
|
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-bold text-slate-700"
|
||||||
|
>
|
||||||
|
<option value="gemini">Google Gemini (Recomendado)</option>
|
||||||
|
<option value="openai">OpenAI (ChatGPT)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-bold text-slate-700 mb-2">Chave de API (API Key)</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={aiConfig.apiKey}
|
||||||
|
onChange={e => 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"
|
||||||
|
/>
|
||||||
|
<p className="text-[11px] text-slate-500 mt-2 font-medium">
|
||||||
|
Sua chave é armazenada de forma segura apenas localmente no seu navegador.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3 pt-4">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAIConfigModal(false)}
|
||||||
|
className="flex-1 px-6 py-3 bg-slate-100 text-slate-600 rounded-xl font-bold hover:bg-slate-200 transition-all"
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleSaveAIConfig(aiConfig.provider, aiConfig.apiKey)}
|
||||||
|
className="flex-1 px-6 py-3 bg-indigo-600 text-white rounded-xl font-bold hover:bg-indigo-700 transition-all shadow-lg shadow-indigo-100"
|
||||||
|
>
|
||||||
|
Salvar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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
|
// Helper para formatar datas para o JSON
|
||||||
const formatDateForJSON = (val) => {
|
const formatDateForJSON = (val) => {
|
||||||
if (!val) return '';
|
if (!val) return '';
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue