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, Sparkles, Wand2 } from 'lucide-react'; import { uploadExamImage } from '../services/supabase'; import { useDialog } from '../DialogContext'; interface ExamsProps { data: SchoolData; updateData: (newData: Partial) => void; } const Exams: React.FC = ({ data, updateData }) => { const [searchTerm, setSearchTerm] = useState(''); const [currentView, setCurrentView] = useState<'list' | 'builder'>('list'); const [editingExam, setEditingExam] = useState(null); const [isUploading, setIsUploading] = useState(false); const [duplicatingExam, setDuplicatingExam] = useState(null); const [targetClassId, setTargetClassId] = useState(''); const [activeTab, setActiveTab] = useState<'ativos' | 'lixeira'>('ativos'); const { showAlert, showConfirm } = useDialog(); const [showAIConfigModal, setShowAIConfigModal] = useState(false); const [aiForm, setAiForm] = useState({ activeProvider: data.aiConfig?.activeProvider || 'gemini', geminiKey: data.aiConfig?.geminiKey ? '********' : '', openaiKey: data.aiConfig?.openaiKey ? '********' : '', claudeKey: data.aiConfig?.claudeKey ? '********' : '', openrouterKey: data.aiConfig?.openrouterKey ? '********' : '', geminiModel: data.aiConfig?.geminiModel || 'gemini-1.5-flash', openaiModel: data.aiConfig?.openaiModel || 'gpt-4o-mini', claudeModel: data.aiConfig?.claudeModel || 'claude-3-5-sonnet-20241022', openrouterModel: data.aiConfig?.openrouterModel || 'google/gemini-2.5-flash', activeImageProvider: data.aiConfig?.activeImageProvider || 'gemini' }); const [dynamicModels, setDynamicModels] = useState<{ gemini: any[]; openai: any[]; claude: any[]; openrouter: any[]; }>({ gemini: [ { 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)' } ], openai: [ { 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)' } ], claude: [ { id: 'claude-3-5-sonnet-20241022', name: 'claude-3-5-sonnet-20241022' }, { id: 'claude-3-5-haiku-20241022', name: 'claude-3-5-haiku-20241022' } ], openrouter: [ { 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' } ] }); const [loadingModels, setLoadingModels] = useState<{[key: string]: boolean}>({}); const [isGeneratingAI, setIsGeneratingAI] = useState(false); const [aiTopic, setAiTopic] = useState(''); const [aiQuantity, setAiQuantity] = useState(5); const [aiDifficulty, setAiDifficulty] = useState('medium'); const [aiContextText, setAiContextText] = useState(''); const [isGeneratingImage, setIsGeneratingImage] = useState<{[key: string]: boolean}>({}); const handleOpenAIConfig = () => { setAiForm({ activeProvider: data.aiConfig?.activeProvider || 'gemini', geminiKey: data.aiConfig?.geminiKey ? '********' : '', openaiKey: data.aiConfig?.openaiKey ? '********' : '', claudeKey: data.aiConfig?.claudeKey ? '********' : '', openrouterKey: data.aiConfig?.openrouterKey ? '********' : '', geminiModel: data.aiConfig?.geminiModel || 'gemini-1.5-flash', openaiModel: data.aiConfig?.openaiModel || 'gpt-4o-mini', claudeModel: data.aiConfig?.claudeModel || 'claude-3-5-sonnet-20241022', openrouterModel: data.aiConfig?.openrouterModel || 'google/gemini-2.5-flash', activeImageProvider: data.aiConfig?.activeImageProvider || 'gemini' }); setShowAIConfigModal(true); }; const handleFetchModels = async (provider: 'gemini' | 'openai' | 'claude' | 'openrouter') => { setLoadingModels(prev => ({ ...prev, [provider]: true })); try { let key = ''; if (provider === 'gemini') key = aiForm.geminiKey; else if (provider === 'openai') key = aiForm.openaiKey; else if (provider === 'claude') key = aiForm.claudeKey; else if (provider === 'openrouter') key = aiForm.openrouterKey; const response = await fetch('/api/ai/list-models', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ provider, apiKey: key }) }); if (!response.ok) { const err = await response.json(); throw new Error(err.error || 'Erro ao carregar modelos da API.'); } 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'); } 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'); } finally { setLoadingModels(prev => ({ ...prev, [provider]: false })); } }; 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, openrouterKey: aiForm.openrouterKey === '********' ? (data.aiConfig?.openrouterKey || '') : aiForm.openrouterKey, geminiModel: aiForm.geminiModel, openaiModel: aiForm.openaiModel, claudeModel: aiForm.claudeModel, openrouterModel: aiForm.openrouterModel, activeImageProvider: aiForm.activeImageProvider }; 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 || []); const [dbCourses, setDbCourses] = useState(data?.courses || []); const [dbSubjects, setDbSubjects] = useState(data?.subjects || []); const [dbExams, setDbExams] = useState(data.exams || []); const loadExams = async () => { try { const res = await fetch(`/api/provas?t=${Date.now()}`); if (res.ok) { const { provas } = await res.json(); setDbExams(provas.map((p: any) => ({ id: p.id, classId: p.classId || p.turma_id, subjectId: p.subjectId || p.disciplina_id, periodId: p.periodId || p.periodo_id, title: p.title || p.titulo, durationMinutes: p.durationMinutes || p.duracao_minutos, status: p.status, allowRetake: p.allowRetake ?? p.permitir_refacao ?? false, isDeleted: p.isDeleted ?? p.is_deleted ?? false, evaluationType: p.evaluationType || p.evaluation_type || 'exam', maxScore: p.maxScore !== undefined && p.maxScore !== null ? Number(p.maxScore) : (p.max_score !== undefined && p.max_score !== null ? Number(p.max_score) : 10), questions: p.questions || [] }))); } } catch(e) { console.error(e); } }; React.useEffect(() => { loadExams(); }, []); React.useEffect(() => { Promise.all([ fetch('/api/turmas').catch(() => ({ ok: false, json: async () => ({}) })), fetch('/api/cursos').catch(() => ({ ok: false, json: async () => ({}) })), fetch('/api/disciplinas').catch(() => ({ ok: false, json: async () => ({}) })) ]).then(async (responses) => { const [resT, resC, resS] = responses; if (resT && resT.ok) { const jsonT = await resT.json(); if (jsonT.turmas) setDbClasses(jsonT.turmas.map((t: any) => ({ id: t.id, name: t.nome, courseId: t.curso_id, maxStudents: Number(t.max_alunos || 0) }))); } if (resC && resC.ok) { const jsonC = await resC.json(); if (jsonC.cursos) setDbCourses(jsonC.cursos.map((c: any) => ({ id: c.id, name: c.nome, monthlyFee: Number(c.mensalidade || 0), registrationFee: Number(c.taxa_matricula || 0) }))); } if (resS && resS.ok) { const jsonS = await resS.json(); if (jsonS.disciplinas) setDbSubjects(jsonS.disciplinas.map((d: any) => ({ id: d.id, name: d.nome }))); } }).catch(console.error); }, []); const normalizePhotoUrl = (url?: string) => { if (!url) return ''; if (url.startsWith('data:image') || url.startsWith('/storage')) return url; try { const match = url.match(/^https?:\/\/[^\/]+\/(.+)$/); if (match) return `/storage/${match[1]}`; } catch (e) { } return url; }; const exams = dbExams || []; const filteredExams = exams.filter(exam => (activeTab === 'ativos' ? !exam.isDeleted : !!exam.isDeleted) && (exam.title.toLowerCase().includes(searchTerm.toLowerCase()) || dbClasses.find(c => c.id === exam.classId)?.name.toLowerCase().includes(searchTerm.toLowerCase())) ); const handleStartCreate = () => { setEditingExam({ id: Date.now().toString(), title: '', classId: dbClasses[0]?.id || '', durationMinutes: 60, status: 'draft', questions: [], evaluationType: 'exam', maxScore: 10, allowRetake: false } as any); setCurrentView('builder'); }; const handleEditExam = (exam: Exam) => { setEditingExam({ ...exam }); setCurrentView('builder'); }; const handleToggleRetake = async (examId: string) => { const targetExam = exams.find(e => e.id === examId); if (!targetExam) return; try { await fetch(`/api/provas/${examId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...targetExam, allowRetake: !targetExam.allowRetake }) }); await loadExams(); } catch (e) { console.error('Erro ao alterar refação:', e); } }; const handleDeleteExam = (examId: string) => { showConfirm( 'Mover para Lixeira', 'Tem certeza que deseja mover esta avaliação para a lixeira? Ela será ocultada para os alunos, mas as notas no boletim continuarão intactas.', async () => { try { const targetExam = exams.find(e => e.id === examId); if (targetExam) { await fetch(`/api/provas/${examId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...targetExam, isDeleted: true }) }); } } catch (e) { console.error('Erro ao deletar prova:', e); } await loadExams(); showAlert('Sucesso', 'Avaliação movida para a lixeira.', 'success'); } ); }; const handleRestoreExam = async (examId: string) => { try { const targetExam = exams.find(e => e.id === examId); if (targetExam) { await fetch(`/api/provas/${examId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...targetExam, isDeleted: false }) }); } } catch (e) { console.error('Erro ao restaurar prova:', e); } await loadExams(); showAlert('Sucesso', 'Avaliação reativada.', 'success'); }; const handlePermanentDelete = (examId: string) => { showConfirm( 'Excluir Permanentemente', '⚠️ Atenção: Esta ação irá apagar esta avaliação e todas as suas questões PERMANENTEMENTE do banco de dados. As submissões dos alunos também serão removidas. Esta ação NÃO pode ser desfeita. Deseja continuar?', async () => { try { const response = await fetch(`/api/provas/${examId}`, { method: 'DELETE' }); if (!response.ok) throw new Error('Falha ao excluir'); await loadExams(); showAlert('Sucesso', 'Avaliação excluída permanentemente.', 'success'); } catch (e) { console.error('Erro ao excluir permanentemente:', e); showAlert('Erro', 'Falha ao excluir a avaliação do servidor.', 'error'); } } ); }; const handleDuplicateExam = async () => { if (!duplicatingExam || !targetClassId) return; const newExam: Exam = { ...duplicatingExam, id: Date.now().toString() + Math.random().toString(36).substring(7), classId: targetClassId, status: 'draft', title: `${duplicatingExam.title} (Cópia)`, questions: (duplicatingExam.questions || []).map((q: any) => ({ ...q, id: Date.now().toString() + Math.random().toString(36).substring(2, 10) + Math.random().toString(36).substring(2, 6) })) }; try { const response = await fetch('/api/provas', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(newExam) }); if (!response.ok) throw new Error('Falha ao duplicar prova'); await loadExams(); showAlert('Sucesso', 'Avaliação duplicada com sucesso!', 'success'); } catch (e) { console.error('Erro ao duplicar:', e); showAlert('Erro', 'Falha ao duplicar a avaliação.', 'error'); } setDuplicatingExam(null); setTargetClassId(''); }; const handleGenerateQuestionsAI = async (quantityOverride?: number) => { 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; } 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({ topic: aiTopic, quantity: qty, difficulty: aiDifficulty, contextText: aiContextText }) }); 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) => { 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); try { const response = await fetch('/api/ai/generate-questions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ topic: topic, quantity: 1, difficulty: aiDifficulty, contextText: aiContextText }) }); 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 handleGenerateImageAI = async (qIndex: number) => { if (!editingExam) return; const question = editingExam.questions[qIndex]; if (!question.text.trim()) { showAlert('Atenção', 'Digite o enunciado da questão para gerar a imagem.', 'warning'); return; } 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 sua chave de API clicando no botão "Configurar IA" no cabeçalho.', 'warning'); return; } setIsGeneratingImage(prev => ({ ...prev, [question.id]: true })); try { const response = await fetch('/api/ai/generate-image', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: question.text }) }); if (!response.ok) { const errData = await response.json(); throw new Error(errData.error || 'Erro ao gerar imagem por IA.'); } const { url } = await response.json(); if (!url) { throw new Error('URL da imagem não retornada.'); } const newQuestions = [...editingExam.questions]; newQuestions[qIndex] = { ...newQuestions[qIndex], imageUrl: url }; setEditingExam({ ...editingExam, questions: newQuestions }); showAlert('Sucesso', 'Imagem gerada por IA e anexada com sucesso!', 'success'); } catch (e: any) { console.error(e); showAlert('Erro ao gerar imagem', e.message || 'Houve uma falha de comunicação com o servidor para gerar a imagem.', 'error'); } finally { setIsGeneratingImage(prev => ({ ...prev, [question.id]: false })); } }; const handleAddQuestion = () => { if (!editingExam) return; setEditingExam({ ...editingExam, questions: [ ...editingExam.questions, { id: Date.now().toString() + Math.random().toString(36).substring(7), text: '', options: ['', '', '', ''], correctOptionIndex: 0 } ] }); }; const handleRemoveQuestion = (qIndex: number) => { if (!editingExam) return; const newQuestions = [...editingExam.questions]; newQuestions.splice(qIndex, 1); setEditingExam({ ...editingExam, questions: newQuestions }); }; const handleQuestionChange = (qIndex: number, field: keyof Question, value: any) => { if (!editingExam) return; const newQuestions = [...editingExam.questions]; newQuestions[qIndex] = { ...newQuestions[qIndex], [field]: value }; setEditingExam({ ...editingExam, questions: newQuestions }); }; const handleOptionChange = (qIndex: number, oIndex: number, value: string) => { if (!editingExam) return; const newQuestions = [...editingExam.questions]; const newOptions = [...newQuestions[qIndex].options]; newOptions[oIndex] = value; newQuestions[qIndex].options = newOptions; setEditingExam({ ...editingExam, questions: newQuestions }); }; const handleAddOption = (qIndex: number) => { if (!editingExam) return; const newQuestions = [...editingExam.questions]; newQuestions[qIndex].options.push(''); setEditingExam({ ...editingExam, questions: newQuestions }); }; const handleRemoveOption = (qIndex: number, oIndex: number) => { if (!editingExam) return; const newQuestions = [...editingExam.questions]; newQuestions[qIndex].options.splice(oIndex, 1); // Adjust correctOptionIndex if needed if (newQuestions[qIndex].correctOptionIndex >= newQuestions[qIndex].options.length) { newQuestions[qIndex].correctOptionIndex = Math.max(0, newQuestions[qIndex].options.length - 1); } else if (newQuestions[qIndex].correctOptionIndex === oIndex) { newQuestions[qIndex].correctOptionIndex = 0; } setEditingExam({ ...editingExam, questions: newQuestions }); }; const handleImageUpload = async (qIndex: number, event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; setIsUploading(true); try { const url = await uploadExamImage(file); if (url) { handleQuestionChange(qIndex, 'imageUrl', url); } else { alert('Falha ao obter URL pública da imagem após o upload.'); } } catch (error: any) { console.error(error); const errorMessage = error.message || 'Erro desconhecido'; alert(`Erro ao enviar imagem: ${errorMessage}\n\nVerifique sua conexão ou a configuração do bucket "exames" no MinIO.`); } finally { setIsUploading(false); if (event.target) { event.target.value = ''; // Reset file input } } }; const handleNotifyStudents = async (exam: Exam) => { const classObj = (dbClasses || []).find(c => c.id === exam.classId); if (!classObj) return; showConfirm( 'Notificar Turma', `Deseja enviar WhatsApp e notificação no portal para os alunos da turma ${classObj.name} informando sobre esta avaliação?`, async () => { try { const resp = await fetch('/api/exames/notificar', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ examId: exam.id }) }); const resData = await resp.json(); if (resp.ok) { showAlert('Sucesso', 'Notificações enviadas com sucesso!', 'success'); } else { showAlert('Erro', resData.error || 'Erro ao notificar turma.', 'error'); } } catch (e) { showAlert('Erro', 'Erro de conexão.', 'error'); } } ); }; const handleSave = async (status: 'draft' | 'published') => { if (!editingExam) return; if (!editingExam.title || !editingExam.classId) { showAlert('Atenção', 'Preencha o título e a turma antes de salvar.', 'warning'); return; } if (status === 'published' && (!editingExam.subjectId || !editingExam.periodId)) { showAlert( 'Vínculo Obrigatório', 'Para PUBLICAR a avaliação e permitir que as notas entrem no Boletim Escolar, você precisa vincular uma Disciplina e um Período.', 'warning' ); return; } const finalExam = { ...editingExam, status }; const isNew = !dbExams.some(e => e.id === finalExam.id); const endpoint = isNew ? '/api/provas' : `/api/provas/${finalExam.id}`; const method = isNew ? 'POST' : 'PUT'; try { const response = await fetch(endpoint, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(finalExam) }); if (!response.ok) throw new Error('Falha ao salvar avaliação'); await loadExams(); showAlert('Sucesso', status === 'published' ? 'Avaliação publicada com sucesso!' : 'Rascunho salvo com sucesso!', 'success'); } catch (e) { console.error('Erro ao salvar prova:', e); showAlert('Erro', 'Falha ao salvar a avaliação no servidor.', 'error'); return; } setCurrentView('list'); setEditingExam(null); }; 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 (

Criador de Provas

Configure os detalhes e as questões da avaliação.

{/* Informações Básicas */}

Informações Básicas

setEditingExam({ ...editingExam, title: e.target.value })} placeholder="Ex: Prova Bimestral de Matemática" 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" />
setEditingExam({ ...editingExam, maxScore: parseFloat(e.target.value) || 0 } as any)} min="0" step="0.1" 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" />
setEditingExam({ ...editingExam, durationMinutes: parseInt(e.target.value) || 0 })} min="0" 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" />

Obrigatório para Publicar. A nota irá automaticamente para o boletim.

Obrigatório para Publicar. Define em qual coluna do boletim a nota entra.

{/* Painel do Gerador de Questões por IA */}

Gerador de Questões Inteligente

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

{!hasAPIKey && ( 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" />