feat: implementing embedding AI
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import PageLayout from '@/components/PageLayout';
|
||||
import { TestTemplateManager, TestTemplateForm } from '@/components/TestTemplates';
|
||||
import { Plus } from 'lucide-react';
|
||||
//import { Plus } from 'lucide-react';
|
||||
|
||||
export default function TestTemplatesPage() {
|
||||
const [showCreateForm, setShowCreateForm] = useState(false);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { cmsApi, CreateTestTemplatePayload, CourseLevel, CourseType, TestType, QuestionType } from '@/lib/api';
|
||||
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 {
|
||||
@@ -35,8 +35,7 @@ export default function TestTemplateForm({ onSuccess, onCancel }: TestTemplateFo
|
||||
const [formData, setFormData] = useState<CreateTestTemplatePayload>({
|
||||
name: '',
|
||||
description: '',
|
||||
level: 'beginner',
|
||||
course_type: 'regular',
|
||||
mysql_course_id: undefined,
|
||||
test_type: 'CA',
|
||||
duration_minutes: 60,
|
||||
passing_score: 70,
|
||||
@@ -53,6 +52,61 @@ export default function TestTemplateForm({ onSuccess, onCancel }: TestTemplateFo
|
||||
const [generatingAI, setGeneratingAI] = useState(false);
|
||||
const [expandedQuestion, setExpandedQuestion] = useState<string | null>(null);
|
||||
const [aiContext, setAiContext] = useState('');
|
||||
|
||||
// MySQL course selection state
|
||||
const [mysqlPlans, setMysqlPlans] = useState<MySqlPlan[]>([]);
|
||||
const [mysqlCourses, setMysqlCourses] = useState<MySqlCourse[]>([]);
|
||||
const [selectedPlanId, setSelectedPlanId] = useState<number | ''>('');
|
||||
const [selectedCourseId, setSelectedCourseId] = useState<number | ''>('');
|
||||
const [loadingPlans, setLoadingPlans] = useState(false);
|
||||
const [loadingCourses, setLoadingCourses] = useState(false);
|
||||
|
||||
// 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]);
|
||||
|
||||
// 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({
|
||||
...formData,
|
||||
mysql_course_id: courseId,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -67,9 +121,15 @@ export default function TestTemplateForm({ onSuccess, onCancel }: TestTemplateFo
|
||||
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);
|
||||
|
||||
|
||||
// Primero crear la plantilla
|
||||
const template = await cmsApi.createTestTemplate(formData);
|
||||
|
||||
@@ -233,6 +293,12 @@ export default function TestTemplateForm({ onSuccess, onCancel }: TestTemplateFo
|
||||
setQuestions([...questions, duplicate]);
|
||||
};
|
||||
|
||||
const handleUpdateQuestion = (questionId: string, updates: Partial<Question>) => {
|
||||
setQuestions(questions.map(q =>
|
||||
q.id === questionId ? { ...q, ...updates } : q
|
||||
));
|
||||
};
|
||||
|
||||
const getQuestionTypeLabel = (type: QuestionType) => {
|
||||
const labels: Record<QuestionType, string> = {
|
||||
'multiple-choice': 'Opción Múltiple',
|
||||
@@ -317,40 +383,109 @@ export default function TestTemplateForm({ onSuccess, onCancel }: TestTemplateFo
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* MySQL Course Selection */}
|
||||
<div className="bg-blue-50 p-4 rounded-lg border border-blue-200">
|
||||
<h4 className="text-sm font-medium text-blue-900 mb-3">
|
||||
📚 Seleccionar Curso desde MySQL (Opcional)
|
||||
</h4>
|
||||
<p className="text-xs text-blue-700 mb-3">
|
||||
Selecciona un curso para autocompletar automáticamente el Nivel y Tipo de Curso
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-blue-800 mb-1">
|
||||
Plan de Estudios *
|
||||
</label>
|
||||
<select
|
||||
value={selectedPlanId}
|
||||
onChange={(e) => {
|
||||
setSelectedPlanId(e.target.value ? Number(e.target.value) : '');
|
||||
setSelectedCourseId('');
|
||||
}}
|
||||
disabled={loadingPlans}
|
||||
className="w-full px-3 py-2 border border-blue-300 rounded-lg focus:ring-2 focus:ring-blue-500 bg-white"
|
||||
>
|
||||
<option value="">-- Seleccionar Plan --</option>
|
||||
{mysqlPlans.map(plan => (
|
||||
<option key={plan.idPlanDeEstudios} value={plan.idPlanDeEstudios}>
|
||||
{plan.NombrePlan}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{loadingPlans && <p className="text-xs text-blue-600 mt-1">Cargando planes...</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-blue-800 mb-1">
|
||||
Curso *
|
||||
</label>
|
||||
<select
|
||||
value={selectedCourseId}
|
||||
onChange={(e) => handleCourseSelect(e.target.value ? Number(e.target.value) : '')}
|
||||
disabled={!selectedPlanId || loadingCourses}
|
||||
className="w-full px-3 py-2 border border-blue-300 rounded-lg focus:ring-2 focus:ring-blue-500 bg-white disabled:bg-gray-100"
|
||||
>
|
||||
<option value="">-- Seleccionar Curso --</option>
|
||||
{mysqlCourses.map(course => (
|
||||
<option key={course.idCursos} value={course.idCursos}>
|
||||
{course.NombreCurso}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{loadingCourses && <p className="text-xs text-blue-600 mt-1">Cargando cursos...</p>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Nivel *
|
||||
Nivel {formData.mysql_course_id ? '(del curso seleccionado)' : '*'}
|
||||
</label>
|
||||
<select
|
||||
value={formData.level}
|
||||
onChange={(e) => setFormData({ ...formData, level: e.target.value as CourseLevel })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
value={formData.level || ''}
|
||||
onChange={(e) => setFormData({ ...formData, level: e.target.value as CourseLevel || undefined })}
|
||||
disabled={!!formData.mysql_course_id}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100"
|
||||
>
|
||||
<option value="">Seleccionar nivel</option>
|
||||
<option value="beginner">Beginner</option>
|
||||
<option value="beginner1">Beginner 1</option>
|
||||
<option value="beginner2">Beginner 2</option>
|
||||
<option value="beginner_1">Beginner 1</option>
|
||||
<option value="beginner_2">Beginner 2</option>
|
||||
<option value="intermediate">Intermediate</option>
|
||||
<option value="intermediate1">Intermediate 1</option>
|
||||
<option value="intermediate2">Intermediate 2</option>
|
||||
<option value="intermediate_1">Intermediate 1</option>
|
||||
<option value="intermediate_2">Intermediate 2</option>
|
||||
<option value="advanced">Advanced</option>
|
||||
<option value="advanced1">Advanced 1</option>
|
||||
<option value="advanced2">Advanced 2</option>
|
||||
<option value="advanced_1">Advanced 1</option>
|
||||
<option value="advanced_2">Advanced 2</option>
|
||||
</select>
|
||||
{formData.mysql_course_id && (
|
||||
<p className="text-xs text-green-600 mt-1">
|
||||
✓ Nivel determinado automáticamente desde el curso MySQL
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Tipo de Curso *
|
||||
Tipo de Curso {formData.mysql_course_id ? '(del curso seleccionado)' : '*'}
|
||||
</label>
|
||||
<select
|
||||
value={formData.course_type}
|
||||
onChange={(e) => setFormData({ ...formData, course_type: e.target.value as CourseType })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
value={formData.course_type || ''}
|
||||
onChange={(e) => setFormData({ ...formData, course_type: e.target.value as CourseType || undefined })}
|
||||
disabled={!!formData.mysql_course_id}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100"
|
||||
>
|
||||
<option value="">Seleccionar tipo</option>
|
||||
<option value="regular">Regular</option>
|
||||
<option value="intensive">Intensivo</option>
|
||||
</select>
|
||||
{formData.mysql_course_id && (
|
||||
<p className="text-xs text-green-600 mt-1">
|
||||
✓ Tipo determinado automáticamente desde el curso MySQL
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
@@ -90,14 +90,14 @@ export default function TestTemplateManager({ onSelectTemplate, onCreateTemplate
|
||||
const getLevelLabel = (level: CourseLevel) => {
|
||||
const labels: Record<CourseLevel, string> = {
|
||||
beginner: 'Beginner',
|
||||
beginner1: 'Beginner 1',
|
||||
beginner2: 'Beginner 2',
|
||||
beginner_1: 'Beginner 1',
|
||||
beginner_2: 'Beginner 2',
|
||||
intermediate: 'Intermediate',
|
||||
intermediate1: 'Intermediate 1',
|
||||
intermediate2: 'Intermediate 2',
|
||||
intermediate_1: 'Intermediate 1',
|
||||
intermediate_2: 'Intermediate 2',
|
||||
advanced: 'Advanced',
|
||||
advanced1: 'Advanced 1',
|
||||
advanced2: 'Advanced 2',
|
||||
advanced_1: 'Advanced 1',
|
||||
advanced_2: 'Advanced 2',
|
||||
};
|
||||
return labels[level] || level;
|
||||
};
|
||||
@@ -185,14 +185,14 @@ export default function TestTemplateManager({ onSelectTemplate, onCreateTemplate
|
||||
>
|
||||
<option value="">Todos los niveles</option>
|
||||
<option value="beginner">Beginner</option>
|
||||
<option value="beginner1">Beginner 1</option>
|
||||
<option value="beginner2">Beginner 2</option>
|
||||
<option value="beginner_1">Beginner 1</option>
|
||||
<option value="beginner_2">Beginner 2</option>
|
||||
<option value="intermediate">Intermediate</option>
|
||||
<option value="intermediate1">Intermediate 1</option>
|
||||
<option value="intermediate2">Intermediate 2</option>
|
||||
<option value="intermediate_1">Intermediate 1</option>
|
||||
<option value="intermediate_2">Intermediate 2</option>
|
||||
<option value="advanced">Advanced</option>
|
||||
<option value="advanced1">Advanced 1</option>
|
||||
<option value="advanced2">Advanced 2</option>
|
||||
<option value="advanced_1">Advanced 1</option>
|
||||
<option value="advanced_2">Advanced 2</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -1015,8 +1015,26 @@ export const questionBankApi = {
|
||||
apiFetch(`/question-bank/${id}`, { method: 'DELETE' }, false),
|
||||
importFromMySQL: (courseId?: number, questionIds?: number[], importAll?: boolean): Promise<QuestionBank[]> =>
|
||||
apiFetch('/question-bank/import-mysql', { method: 'POST', body: JSON.stringify({ mysql_course_id: courseId, question_ids: questionIds, import_all: importAll }) }, false),
|
||||
getMySQLPlans: (): Promise<MySqlPlan[]> =>
|
||||
apiFetch('/question-bank/mysql-plans', {}, false),
|
||||
getMySQLCoursesByPlan: (planId: number): Promise<MySqlCourse[]> =>
|
||||
apiFetch(`/question-bank/mysql-courses?plan_id=${planId}`, {}, false),
|
||||
};
|
||||
|
||||
export interface MySqlPlan {
|
||||
idPlanDeEstudios: number;
|
||||
NombrePlan: string;
|
||||
}
|
||||
|
||||
export interface MySqlCourse {
|
||||
idCursos: number;
|
||||
NombreCurso: string;
|
||||
NivelCurso?: number;
|
||||
idPlanDeEstudios: number;
|
||||
NombrePlan: string;
|
||||
Duracion?: number; // Duration in hours (40=regular, 80=intensive)
|
||||
}
|
||||
|
||||
export const lmsApi = {
|
||||
getCohorts: (): Promise<Cohort[]> => apiFetch('/cohorts', {}, true),
|
||||
createCohort: (payload: CreateCohortPayload): Promise<Cohort> => apiFetch('/cohorts', { method: 'POST', body: JSON.stringify(payload) }, true),
|
||||
@@ -1135,7 +1153,7 @@ export interface BackgroundTask {
|
||||
|
||||
// ==================== Test Templates ====================
|
||||
|
||||
export type CourseLevel = 'beginner' | 'beginner1' | 'beginner2' | 'intermediate' | 'intermediate1' | 'intermediate2' | 'advanced' | 'advanced1' | 'advanced2';
|
||||
export type CourseLevel = 'beginner' | 'beginner_1' | 'beginner_2' | 'intermediate' | 'intermediate_1' | 'intermediate_2' | 'advanced' | 'advanced_1' | 'advanced_2';
|
||||
export type CourseType = 'intensive' | 'regular';
|
||||
export type TestType = 'CA' | 'MWT' | 'MOT' | 'FOT' | 'FWT';
|
||||
export type QuestionType = 'multiple-choice' | 'true-false' | 'short-answer' | 'essay' | 'matching' | 'ordering';
|
||||
@@ -1143,10 +1161,11 @@ export type QuestionType = 'multiple-choice' | 'true-false' | 'short-answer' | '
|
||||
export interface TestTemplate {
|
||||
id: string;
|
||||
organization_id: string;
|
||||
mysql_course_id?: number; // Reference to imported MySQL course
|
||||
name: string;
|
||||
description?: string;
|
||||
level: CourseLevel;
|
||||
course_type: CourseType;
|
||||
level?: CourseLevel; // Deprecated: use mysql_course_id instead
|
||||
course_type?: CourseType; // Deprecated: use mysql_course_id instead
|
||||
test_type: TestType;
|
||||
duration_minutes: number;
|
||||
passing_score: number;
|
||||
@@ -1197,8 +1216,9 @@ export interface TestTemplateWithQuestions {
|
||||
export interface CreateTestTemplatePayload {
|
||||
name: string;
|
||||
description?: string;
|
||||
level: CourseLevel;
|
||||
course_type: CourseType;
|
||||
mysql_course_id?: number; // Reference to imported MySQL course (preferred)
|
||||
level?: CourseLevel; // Fallback if mysql_course_id not provided
|
||||
course_type?: CourseType; // Fallback if mysql_course_id not provided
|
||||
test_type: TestType;
|
||||
duration_minutes: number;
|
||||
passing_score: number;
|
||||
@@ -1211,6 +1231,7 @@ export interface CreateTestTemplatePayload {
|
||||
export interface UpdateTestTemplatePayload {
|
||||
name?: string;
|
||||
description?: string;
|
||||
mysql_course_id?: number;
|
||||
level?: CourseLevel;
|
||||
course_type?: CourseType;
|
||||
test_type?: TestType;
|
||||
|
||||
Reference in New Issue
Block a user