'use client'; import React, { useState, useEffect } from 'react'; import { cmsApi, questionBankApi, CreateTestTemplatePayload, CourseLevel, CourseType, TestType, QuestionType, MySqlPlan, MySqlCourse } from '@/lib/api'; import { X, Save, Plus, Trash2, Sparkles, ChevronDown, ChevronUp, Copy, GripVertical, Edit2 } from 'lucide-react'; interface Section { id: string; title: string; description?: string; section_order: number; points: number; instructions?: string; } interface Question { id: string; section_id?: string; question_order: number; question_type: QuestionType; question_text: string; options?: unknown; correct_answer?: unknown; explanation?: string; points: number; metadata?: any; } interface TestTemplateFormProps { templateId?: string; onSuccess?: () => void; onCancel?: () => void; } export default function TestTemplateForm({ templateId, onSuccess, onCancel }: TestTemplateFormProps) { const [formData, setFormData] = useState({ name: '', description: '', mysql_course_id: undefined, test_type: 'CA', duration_minutes: 60, passing_score: 70, total_points: 100, instructions: '', template_data: { sections: [], questions: [] }, tags: [], }); const [sections, setSections] = useState([]); const [questions, setQuestions] = useState([]); const [newTag, setNewTag] = useState(''); const [saving, setSaving] = useState(false); const [generatingAI, setGeneratingAI] = useState(false); const [expandedQuestion, setExpandedQuestion] = useState(null); const [aiContext, setAiContext] = useState(''); const [aiQuestionType, setAiQuestionType] = useState('multiple-choice'); const [aiQuestionCount, setAiQuestionCount] = useState(5); // MySQL course selection state const [mysqlPlans, setMysqlPlans] = useState([]); const [mysqlCourses, setMysqlCourses] = useState([]); const [selectedPlanId, setSelectedPlanId] = useState(''); const [selectedCourseId, setSelectedCourseId] = useState(''); const [loadingPlans, setLoadingPlans] = useState(false); const [loadingCourses, setLoadingCourses] = useState(false); const isEditing = Boolean(templateId); // Load MySQL plans on mount useEffect(() => { const loadPlans = async () => { try { setLoadingPlans(true); const plans = await questionBankApi.getMySQLPlans(); setMysqlPlans(plans); } catch (error) { console.error('Failed to load MySQL plans:', error); } finally { setLoadingPlans(false); } }; loadPlans(); }, []); // Load courses when plan is selected useEffect(() => { const loadCourses = async () => { if (!selectedPlanId) { setMysqlCourses([]); return; } try { setLoadingCourses(true); const courses = await questionBankApi.getMySQLCoursesByPlan(selectedPlanId as number); setMysqlCourses(courses); } catch (error) { console.error('Failed to load MySQL courses:', error); } finally { setLoadingCourses(false); } }; loadCourses(); }, [selectedPlanId]); useEffect(() => { if (!templateId) return; const loadTemplateForEdit = async () => { try { setSaving(true); const data = await cmsApi.getTestTemplate(templateId); setFormData({ name: data.template.name, description: data.template.description || '', mysql_course_id: data.template.mysql_course_id, level: data.template.level, course_type: data.template.course_type, test_type: data.template.test_type, duration_minutes: data.template.duration_minutes, passing_score: data.template.passing_score, total_points: data.template.total_points, instructions: data.template.instructions || '', template_data: data.template.template_data || { sections: [], questions: [] }, tags: data.template.tags || [], }); setSections( (data.sections || []).map((section) => ({ id: section.id, title: section.title, description: section.description || '', section_order: section.section_order, points: section.points, instructions: section.instructions || '', })) ); setQuestions( (data.questions || []).map((question) => { const allowedTypes: QuestionType[] = [ 'multiple-choice', 'true-false', 'short-answer', 'essay', 'matching', 'ordering', 'fill-in-the-blanks', 'audio-response', ]; const normalizedType = allowedTypes.includes(question.question_type) ? question.question_type : 'multiple-choice'; return { id: question.id, section_id: question.section_id, question_order: question.question_order, question_type: normalizedType, question_text: question.question_text, options: question.options, correct_answer: question.correct_answer, explanation: question.explanation || '', points: question.points, metadata: question.metadata, }; }) ); if (data.template.mysql_course_id) { setSelectedCourseId(data.template.mysql_course_id); } } catch (error) { console.error('Failed to load template for edit:', error); alert('No se pudo cargar la plantilla para editar'); onCancel?.(); } finally { setSaving(false); } }; loadTemplateForEdit(); }, [templateId, onCancel]); // Handle course selection - store mysql_course_id (preferred approach) const handleCourseSelect = (courseId: number | '') => { setSelectedCourseId(courseId); // Store the MySQL course ID directly - level/course_type can be derived from mysql_courses table setFormData((prev) => ({ ...prev, mysql_course_id: courseId === '' ? undefined : courseId, })); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!formData.name.trim()) { alert('El nombre es obligatorio'); return; } if (questions.length === 0) { alert('Debes agregar al menos una pregunta'); return; } // Business rules by test type: // - CA must have at least 4 questions. // - MWT/MOT/FOT/FWT must have exactly 1 question. if (formData.test_type === 'CA' && questions.length < 4) { alert('Las plantillas CA deben tener minimo 4 preguntas.'); return; } if (formData.test_type !== 'CA' && questions.length !== 1) { alert('Las plantillas MWT, MOT, FOT y FWT deben tener exactamente 1 pregunta.'); return; } // Validate: either mysql_course_id OR level+course_type must be provided if (!formData.mysql_course_id && (!formData.level || !formData.course_type)) { alert('Debes seleccionar un curso de MySQL o especificar nivel y tipo de curso manualmente'); return; } try { setSaving(true); let targetTemplateId = templateId; if (isEditing && templateId) { await cmsApi.updateTestTemplate(templateId, { name: formData.name, description: formData.description, mysql_course_id: formData.mysql_course_id, level: formData.level, course_type: formData.course_type, test_type: formData.test_type, duration_minutes: formData.duration_minutes, passing_score: formData.passing_score, total_points: formData.total_points, instructions: formData.instructions, template_data: formData.template_data, tags: formData.tags, }); const existingData = await cmsApi.getTestTemplate(templateId); for (const existingQuestion of existingData.questions) { await cmsApi.deleteTemplateQuestion(templateId, existingQuestion.id); } for (const existingSection of existingData.sections) { await cmsApi.deleteTemplateSection(templateId, existingSection.id); } } else { const template = await cmsApi.createTestTemplate(formData); targetTemplateId = template.id; } if (!targetTemplateId) { throw new Error('No se pudo determinar la plantilla a guardar'); } const sectionIdMap = new Map(); for (const section of sections) { const createdSection = await cmsApi.createTemplateSection(targetTemplateId, { title: section.title, description: section.description, section_order: section.section_order, points: section.points, instructions: section.instructions, }); sectionIdMap.set(section.id, createdSection.id); } for (const question of questions) { await cmsApi.createTemplateQuestion(targetTemplateId, { section_id: question.section_id ? sectionIdMap.get(question.section_id) : undefined, question_order: question.question_order, question_type: question.question_type, question_text: question.question_text, options: question.options, correct_answer: question.correct_answer, explanation: question.explanation, points: question.points, metadata: question.metadata, }); } alert(isEditing ? 'Plantilla actualizada exitosamente' : 'Plantilla creada exitosamente'); onSuccess?.(); } catch (error) { console.error('Failed to create template:', error); const message = error instanceof Error ? error.message : 'Error al crear la plantilla'; alert(message); } finally { setSaving(false); } }; const handleAddTag = () => { if (newTag.trim() && !formData.tags?.includes(newTag.trim())) { setFormData({ ...formData, tags: [...(formData.tags || []), newTag.trim()], }); setNewTag(''); } }; const handleRemoveTag = (tagToRemove: string) => { setFormData({ ...formData, tags: formData.tags?.filter(tag => tag !== tagToRemove) || [], }); }; const handleAddSection = () => { const newSection: Section = { id: `section-${Date.now()}`, title: `Sección ${sections.length + 1}`, description: '', section_order: sections.length, points: 0, instructions: '', }; setSections([...sections, newSection]); }; const handleRemoveSection = (sectionId: string) => { setSections(sections.filter(s => s.id !== sectionId)); setQuestions(questions.filter(q => q.section_id !== sectionId)); }; const handleUpdateSection = (sectionId: string, updates: Partial
) => { setSections(sections.map(s => s.id === sectionId ? { ...s, ...updates } : s)); }; const handleAddQuestion = (sectionId?: string) => { const newQuestion: Question = { id: `question-${Date.now()}`, section_id: sectionId, question_order: questions.filter(q => q.section_id === sectionId).length, question_type: 'multiple-choice', question_text: '', options: ['Opción 1', 'Opción 2', 'Opción 3', 'Opción 4'], correct_answer: 0, explanation: '', points: 1, }; setQuestions([...questions, newQuestion]); setExpandedQuestion(newQuestion.id); }; const handleGenerateWithAI = async () => { if (!aiContext.trim()) { alert('Ingresa el contexto para generar las preguntas (ej: tema de la lección, contenido, etc.)'); return; } const token = localStorage.getItem('studio_token'); if (!token) { alert('No hay sesión activa. Por favor inicia sesión nuevamente.'); return; } try { setGeneratingAI(true); // Usar el endpoint RAG de generación de preguntas desde banco MySQL const response = await fetch(`${process.env.NEXT_PUBLIC_CMS_API_URL || 'http://localhost:3001'}/test-templates/generate-with-rag`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, }, body: JSON.stringify({ topic: aiContext, num_questions: aiQuestionCount, question_type: aiQuestionType, }), }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Error ${response.status}: ${errorText}`); } const generatedQuestions = await response.json(); // Parsear las preguntas generadas if (Array.isArray(generatedQuestions) && generatedQuestions.length > 0) { const questionsToAdd: Question[] = generatedQuestions.map((q: any, idx: number) => ({ id: `q-${Date.now()}-${idx}`, section_id: undefined, question_order: questions.length + idx, question_type: q.question_type || 'multiple-choice', question_text: q.question_text || q.text, options: Array.isArray(q.options) ? q.options : [], correct_answer: q.correct_answer ?? q.correct, explanation: q.explanation || '', points: q.points || 1, metadata: q.metadata, })); setQuestions([...questions, ...questionsToAdd]); alert(`Se generaron ${questionsToAdd.length} preguntas con IA`); } } catch (error) { console.error('AI generation error:', error); alert(`Error al generar preguntas con IA: ${error instanceof Error ? error.message : 'Verifica que Ollama esté configurado y el banco de preguntas MySQL tenga datos'}`); } finally { setGeneratingAI(false); } }; const handleDuplicateQuestion = (question: Question) => { const duplicate: Question = { ...question, id: `question-${Date.now()}`, question_order: questions.length, question_text: `${question.question_text} (copia)`, }; setQuestions([...questions, duplicate]); }; const handleUpdateQuestion = (questionId: string, updates: Partial) => { setQuestions(questions.map(q => q.id === questionId ? { ...q, ...updates } : q )); }; const handleRemoveQuestion = (questionId: string) => { setQuestions(questions.filter(q => q.id !== questionId)); if (expandedQuestion === questionId) { setExpandedQuestion(null); } }; const getQuestionTypeLabel = (type: QuestionType) => { const labels: Record = { 'multiple-choice': 'Opción Múltiple', 'true-false': 'Verdadero/Falso', 'short-answer': 'Respuesta Corta', 'essay': 'Ensayo', 'matching': 'Emparejamiento', 'ordering': 'Ordenar', 'fill-in-the-blanks': 'Completar espacios', 'audio-response': 'Respuesta de audio', }; return labels[type] || type; }; return (
{/* Header */}

{isEditing ? 'Editar Plantilla de Prueba' : 'Nueva Plantilla de Prueba'}

{isEditing ? 'Modifica preguntas y secciones de tu evaluación' : 'Crea preguntas y secciones para tu evaluación'}

{/* Form */}
{/* Basic Info */}

Información Básica

setFormData({ ...formData, name: e.target.value })} className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500" placeholder="Ej: Final Exam - Beginner 1" required />