diff --git a/docs/BROCHURE_STUDIO.docx b/docs/BROCHURE_STUDIO.docx new file mode 100644 index 0000000..42e74f9 Binary files /dev/null and b/docs/BROCHURE_STUDIO.docx differ diff --git a/docs/BROCHURE_STUDIO.md b/docs/BROCHURE_STUDIO.md new file mode 100644 index 0000000..ce77dc2 --- /dev/null +++ b/docs/BROCHURE_STUDIO.md @@ -0,0 +1,66 @@ +# Brochure Comercial - Plataforma Studio + +## 1) Qué es Studio +Studio es la plataforma de autoría académica de OpenCCB para diseñar, publicar y administrar cursos de alta fidelidad. Está orientada a instituciones educativas, equipos académicos y docentes que necesitan construir experiencias de aprendizaje interactivas sin fricción técnica. + +## 2) Propuesta de valor +- Diseño instruccional en un entorno moderno y visual. +- Creación de lecciones modulares con bloques interactivos. +- Banco de preguntas reutilizable para evaluaciones. +- Gestión de usuarios, cursos y configuraciones institucionales. +- Integración con LMS Experience para consumo de contenidos por estudiantes. + +## 3) Capacidades principales +- Gestión de cursos: creación, edición, estructura por módulos y lecciones. +- Constructor de lecciones: bloques de texto, multimedia, quiz, respuestas cortas, ordenamiento, matching, hotspot, audio-response, peer review, role-playing y mermaid. +- Plantillas: plantillas de curso y de pruebas para estandarizar procesos. +- Biblioteca de assets: administración y reutilización de recursos multimedia. +- Banco de preguntas: catálogo de preguntas filtrable por tipo y dificultad. +- Analítica y seguimiento: soporte para revisión de progreso y evaluaciones. +- Branding institucional: logo, favicon y colores de marca. + +## 4) Beneficios para la institución +- Reduce tiempos de producción académica. +- Estandariza calidad de contenidos. +- Facilita colaboración entre docentes y equipos de diseño instruccional. +- Mejora la trazabilidad de evaluaciones y resultados. +- Acelera despliegues de nuevos programas y cursos. + +## 5) Flujo recomendado de uso +1. Configurar branding institucional. +2. Crear curso y definir módulos. +3. Diseñar lecciones con bloques interactivos. +4. Configurar evaluaciones y rúbricas. +5. Publicar y monitorear desempeño. + +## 6) Capturas sugeridas (fotos) +Pantalla de acceso + +![Pantalla de acceso](assets/studio/01-login-studio.png) + +Vista principal de cursos + +![Dashboard de cursos](assets/studio/02-dashboard-cursos.png) + +Editor de lección con bloques + +![Editor de leccion](assets/studio/03-editor-leccion.png) + +Banco de preguntas y filtros + +![Banco de preguntas](assets/studio/04-banco-preguntas.png) + +Librería de recursos + +![Libreria de assets](assets/studio/05-library-assets.png) + +Editor de rúbricas + +![Rubricas](assets/studio/06-rubricas.png) + +Administración de usuarios + +![Administracion de usuarios](assets/studio/07-admin-usuarios.png) + +## 7) Mensaje comercial breve +Studio convierte el diseño académico en un proceso más rápido, controlado y escalable. Permite a la institución pasar de la idea al curso publicado con calidad pedagógica y consistencia operacional. diff --git a/docs/MANUAL_USUARIO_STUDIO.docx b/docs/MANUAL_USUARIO_STUDIO.docx new file mode 100644 index 0000000..3d68978 Binary files /dev/null and b/docs/MANUAL_USUARIO_STUDIO.docx differ diff --git a/docs/MANUAL_USUARIO_STUDIO.md b/docs/MANUAL_USUARIO_STUDIO.md new file mode 100644 index 0000000..c3c45ef --- /dev/null +++ b/docs/MANUAL_USUARIO_STUDIO.md @@ -0,0 +1,140 @@ +# Manual de Usuario - Plataforma Studio + +## 1) Objetivo +Este manual explica cómo usar Studio para crear y administrar cursos, lecciones y evaluaciones. + +## 2) Perfil de usuario +- Docentes +- Diseñadores instruccionales +- Administradores académicos + +## 3) Acceso a la plataforma +1. Ir a Studio en el navegador. +2. Iniciar sesión con credenciales institucionales. +3. Verificar que el menú principal muestre los módulos habilitados para el rol. + +Captura recomendada: + +![Login Studio](assets/studio/01-login-studio.png) + +## 4) Navegación principal +- Cursos: listado y gestión de cursos. +- Librería: administración de archivos y recursos. +- Banco de preguntas: creación y reutilización de preguntas. +- Plantillas: estandarización de contenidos. +- Configuración: opciones de perfil y parámetros generales. + +Captura recomendada: + +![Dashboard de cursos](assets/studio/02-dashboard-cursos.png) + +## 5) Crear un curso +1. Entrar a Cursos. +2. Seleccionar Crear curso. +3. Definir título y descripción. +4. Guardar para habilitar el editor completo. + +Buenas prácticas: +- Usar nombres claros por nivel/tema. +- Definir objetivos de aprendizaje desde el inicio. + +## 6) Editar estructura (módulos y lecciones) +1. Abrir un curso. +2. Crear módulos. +3. Agregar lecciones por módulo. +4. Ordenar secuencia de aprendizaje. + +Captura recomendada: + +![Editor de lección](assets/studio/03-editor-leccion.png) + +## 7) Constructor de lecciones por bloques +Tipos recomendados de bloque: +- Description +- Media +- Quiz +- Fill in the blanks +- Matching +- Ordering +- Short answer +- Hotspot +- Audio response +- Peer review +- Role playing +- Mermaid + +Flujo: +1. Agregar bloque. +2. Configurar contenido e instrucciones. +3. Guardar cambios. +4. Previsualizar. + +## 8) Banco de preguntas +1. Entrar a Banco de preguntas. +2. Crear nueva pregunta. +3. Definir tipo, dificultad y enunciado. +4. Guardar y reutilizar en evaluaciones. + +Captura recomendada: + +![Banco de preguntas](assets/studio/04-banco-preguntas.png) + +## 9) Libreria de assets +1. Subir recursos (imágenes, audio, documentos). +2. Etiquetar y organizar. +3. Insertar recursos desde lecciones. + +Captura recomendada: + +![Librería de assets](assets/studio/05-library-assets.png) + +## 10) Rúbricas de evaluación +1. Crear rúbrica por curso o actividad. +2. Definir criterios y niveles. +3. Asignar puntajes por nivel. +4. Guardar al finalizar la edición. + +Captura recomendada: + +![Rúbricas](assets/studio/06-rubricas.png) + +## 11) Administración de usuarios (rol admin) +1. Entrar al panel de administración. +2. Buscar usuario. +3. Editar rol o estado. +4. Guardar cambios. + +Captura recomendada: + +![Administración de usuarios](assets/studio/07-admin-usuarios.png) + +## 12) Solución de problemas comunes +- No veo menús esperados: + Verificar sesión activa y rol del usuario. +- No se guardan cambios: + Confirmar conexión y permisos del curso. +- Recursos no cargan: + Revisar formatos permitidos y tamaño del archivo. + +## 13) Recomendaciones operativas +- Definir convención de nombres para cursos y módulos. +- Usar plantillas para mantener estándar pedagógico. +- Revisar cada lección en previsualización antes de publicar. +- Mantener el banco de preguntas limpio y etiquetado. + +## 14) Checklist de publicacion +- Objetivos definidos. +- Lecciones ordenadas. +- Bloques revisados. +- Evaluaciones probadas. +- Rúbricas listas. +- Recursos multimedia verificados. + +## 15) Anexo: inventario de fotos para documentación +- docs/assets/studio/01-login-studio.png +- docs/assets/studio/02-dashboard-cursos.png +- docs/assets/studio/03-editor-leccion.png +- docs/assets/studio/04-banco-preguntas.png +- docs/assets/studio/05-library-assets.png +- docs/assets/studio/06-rubricas.png +- docs/assets/studio/07-admin-usuarios.png diff --git a/docs/assets/studio/01-login-studio.png b/docs/assets/studio/01-login-studio.png new file mode 100644 index 0000000..e0e97b6 Binary files /dev/null and b/docs/assets/studio/01-login-studio.png differ diff --git a/docs/assets/studio/02-dashboard-cursos.png b/docs/assets/studio/02-dashboard-cursos.png new file mode 100644 index 0000000..4abd19c Binary files /dev/null and b/docs/assets/studio/02-dashboard-cursos.png differ diff --git a/docs/assets/studio/03-editor-leccion.png b/docs/assets/studio/03-editor-leccion.png new file mode 100644 index 0000000..d590c36 Binary files /dev/null and b/docs/assets/studio/03-editor-leccion.png differ diff --git a/docs/assets/studio/04-banco-preguntas.png b/docs/assets/studio/04-banco-preguntas.png new file mode 100644 index 0000000..dc1c04a Binary files /dev/null and b/docs/assets/studio/04-banco-preguntas.png differ diff --git a/docs/assets/studio/05-library-assets.png b/docs/assets/studio/05-library-assets.png new file mode 100644 index 0000000..00ed10d Binary files /dev/null and b/docs/assets/studio/05-library-assets.png differ diff --git a/docs/assets/studio/06-rubricas.png b/docs/assets/studio/06-rubricas.png new file mode 100644 index 0000000..f0041bb Binary files /dev/null and b/docs/assets/studio/06-rubricas.png differ diff --git a/docs/assets/studio/07-admin-usuarios.png b/docs/assets/studio/07-admin-usuarios.png new file mode 100644 index 0000000..616e3e6 Binary files /dev/null and b/docs/assets/studio/07-admin-usuarios.png differ diff --git a/nginx/learning.conf b/nginx/learning.conf index 2a81622..3ae6cbc 100644 --- a/nginx/learning.conf +++ b/nginx/learning.conf @@ -13,6 +13,29 @@ location /lms-api/ { } location /cms-api/ { + # CORS safety net at proxy level for CMS API. + set $cors_origin ""; + if ($http_origin ~* "^https?://([a-z0-9-]+\\.)?norteamericano\\.(com|cl)$") { + set $cors_origin $http_origin; + } + if ($http_origin ~* "^http://localhost(:[0-9]+)?$") { + set $cors_origin $http_origin; + } + + if ($request_method = OPTIONS) { + add_header Access-Control-Allow-Origin $cors_origin always; + add_header Vary "Origin, Access-Control-Request-Method, Access-Control-Request-Headers" always; + add_header Access-Control-Allow-Methods "GET,POST,PUT,DELETE,OPTIONS,PATCH,HEAD" always; + add_header Access-Control-Allow-Headers "content-type,authorization,x-requested-with,x-organization-id,range" always; + add_header Access-Control-Max-Age 86400 always; + add_header Content-Length 0 always; + add_header Content-Type "text/plain; charset=utf-8" always; + return 204; + } + + add_header Access-Control-Allow-Origin $cors_origin always; + add_header Vary "Origin" always; + rewrite ^/cms-api/(.*)$ /$1 break; proxy_pass http://openccb-studio:3001; proxy_set_header Host $host; diff --git a/nginx/studio.conf b/nginx/studio.conf index 02b29b8..f08734c 100644 --- a/nginx/studio.conf +++ b/nginx/studio.conf @@ -9,6 +9,29 @@ client_body_timeout 1800s; # Prefer the explicit `/cms-api/*` prefix for frontend fetches. This avoids collisions # with Next.js pages like `/courses` and `/admin` that share the same host. location /cms-api/ { + # CORS safety net at proxy level for CMS API. + set $cors_origin ""; + if ($http_origin ~* "^https?://([a-z0-9-]+\\.)?norteamericano\\.(com|cl)$") { + set $cors_origin $http_origin; + } + if ($http_origin ~* "^http://localhost(:[0-9]+)?$") { + set $cors_origin $http_origin; + } + + if ($request_method = OPTIONS) { + add_header Access-Control-Allow-Origin $cors_origin always; + add_header Vary "Origin, Access-Control-Request-Method, Access-Control-Request-Headers" always; + add_header Access-Control-Allow-Methods "GET,POST,PUT,DELETE,OPTIONS,PATCH,HEAD" always; + add_header Access-Control-Allow-Headers "content-type,authorization,x-requested-with,x-organization-id,range" always; + add_header Access-Control-Max-Age 86400 always; + add_header Content-Length 0 always; + add_header Content-Type "text/plain; charset=utf-8" always; + return 204; + } + + add_header Access-Control-Allow-Origin $cors_origin always; + add_header Vary "Origin" always; + rewrite ^/cms-api/(.*)$ /$1 break; proxy_pass http://openccb-studio:3001; proxy_request_buffering off; diff --git a/services/cms-service/src/handlers_question_bank.rs b/services/cms-service/src/handlers_question_bank.rs index d56fbc8..8ecb776 100644 --- a/services/cms-service/src/handlers_question_bank.rs +++ b/services/cms-service/src/handlers_question_bank.rs @@ -47,6 +47,47 @@ source_asset_id, unit_number "#; +fn normalize_answer_keywords_value(value: serde_json::Value) -> serde_json::Value { + match value { + serde_json::Value::Array(items) => serde_json::Value::Array( + items + .into_iter() + .map(normalize_answer_keywords_value) + .collect(), + ), + serde_json::Value::Object(map) => { + let answer_text = map + .get("answer") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + + if map.contains_key("keywords") { + if let Some(answer) = answer_text { + return serde_json::Value::String(answer); + } + } + + let normalized_map = map + .into_iter() + .map(|(k, v)| (k, normalize_answer_keywords_value(v))) + .collect(); + + serde_json::Value::Object(normalized_map) + } + other => other, + } +} + +fn normalize_question_bank_payload_values( + options: Option, + correct_answer: Option, +) -> (Option, Option) { + ( + options.map(normalize_answer_keywords_value), + correct_answer.map(normalize_answer_keywords_value), + ) +} + async fn connect_mysql_pool(env_var: &str) -> Result { use sqlx::mysql::MySqlPoolOptions; @@ -288,6 +329,23 @@ pub async fn create_question( State(pool): State, Json(payload): Json, ) -> Result, (StatusCode, String)> { + let CreateQuestionBankPayload { + question_text, + question_type, + options, + correct_answer, + explanation, + points, + difficulty, + tags, + media_url, + media_type, + skill_assessed, + } = payload; + + let (normalized_options, normalized_correct_answer) = + normalize_question_bank_payload_values(options, correct_answer); + let create_question_sql = format!( r#" INSERT INTO question_bank ( @@ -306,17 +364,17 @@ pub async fn create_question( ) .bind(org_ctx.id) .bind(claims.sub) - .bind(&payload.question_text) - .bind(&payload.question_type) - .bind(&payload.options) - .bind(&payload.correct_answer) - .bind(&payload.explanation) - .bind(payload.points.unwrap_or(1)) - .bind(payload.difficulty.as_deref().unwrap_or("medium")) - .bind(payload.tags.as_deref()) - .bind(payload.skill_assessed.as_deref()) - .bind(payload.media_url.as_deref()) - .bind(payload.media_type.as_deref()) + .bind(&question_text) + .bind(&question_type) + .bind(&normalized_options) + .bind(&normalized_correct_answer) + .bind(&explanation) + .bind(points.unwrap_or(1)) + .bind(difficulty.as_deref().unwrap_or("medium")) + .bind(tags.as_deref()) + .bind(skill_assessed.as_deref()) + .bind(media_url.as_deref()) + .bind(media_type.as_deref()) .fetch_one(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; @@ -486,6 +544,22 @@ pub async fn update_question( State(pool): State, Json(payload): Json, ) -> Result, (StatusCode, String)> { + let UpdateQuestionBankPayload { + question_text, + question_type, + options, + correct_answer, + explanation, + points, + difficulty, + tags, + is_active, + is_archived, + } = payload; + + let (normalized_options, normalized_correct_answer) = + normalize_question_bank_payload_values(options, correct_answer); + let update_question_sql = format!( r#" UPDATE question_bank @@ -512,16 +586,16 @@ pub async fn update_question( ) .bind(id) .bind(org_ctx.id) - .bind(payload.question_text) - .bind(payload.question_type.map(|t| t.to_string())) - .bind(&payload.options) - .bind(&payload.correct_answer) - .bind(&payload.explanation) - .bind(payload.points) - .bind(payload.difficulty) - .bind(payload.tags.as_deref()) - .bind(payload.is_active) - .bind(payload.is_archived) + .bind(question_text) + .bind(question_type.map(|t| t.to_string())) + .bind(&normalized_options) + .bind(&normalized_correct_answer) + .bind(&explanation) + .bind(points) + .bind(difficulty) + .bind(tags.as_deref()) + .bind(is_active) + .bind(is_archived) .fetch_one(&pool) .await .map_err(|e| match e { diff --git a/services/cms-service/src/handlers_test_templates.rs b/services/cms-service/src/handlers_test_templates.rs index 3bab76e..2601d60 100644 --- a/services/cms-service/src/handlers_test_templates.rs +++ b/services/cms-service/src/handlers_test_templates.rs @@ -13,6 +13,47 @@ use sqlx::PgPool; use std::time::Duration; use uuid::Uuid; +fn normalize_answer_keywords_value(value: serde_json::Value) -> serde_json::Value { + match value { + serde_json::Value::Array(items) => serde_json::Value::Array( + items + .into_iter() + .map(normalize_answer_keywords_value) + .collect(), + ), + serde_json::Value::Object(map) => { + let answer_text = map + .get("answer") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + + if map.contains_key("keywords") { + if let Some(answer) = answer_text { + return serde_json::Value::String(answer); + } + } + + let normalized_map = map + .into_iter() + .map(|(k, v)| (k, normalize_answer_keywords_value(v))) + .collect(); + + serde_json::Value::Object(normalized_map) + } + other => other, + } +} + +fn normalize_question_bank_payload_values( + options: Option, + correct_answer: Option, +) -> (Option, Option) { + ( + options.map(normalize_answer_keywords_value), + correct_answer.map(normalize_answer_keywords_value), + ) +} + // ==================== Query Parameters ==================== #[derive(Debug, Deserialize)] @@ -1736,6 +1777,12 @@ pub async fn generate_questions_with_rag( _ => common::models::QuestionBankType::MultipleChoice, }; + let (normalized_options, normalized_correct_answer) = + normalize_question_bank_payload_values( + question.options.clone(), + question.correct_answer.clone(), + ); + let result = sqlx::query( r#" INSERT INTO question_bank ( @@ -1750,8 +1797,8 @@ pub async fn generate_questions_with_rag( .bind(claims.sub) .bind(&question.question_text) .bind(&question_type) - .bind(&question.options) - .bind(&question.correct_answer) + .bind(&normalized_options) + .bind(&normalized_correct_answer) .bind(&question.explanation) .bind(question.points) .bind("medium") diff --git a/services/cms-service/src/main.rs b/services/cms-service/src/main.rs index 02f2e8d..ddf62cb 100644 --- a/services/cms-service/src/main.rs +++ b/services/cms-service/src/main.rs @@ -27,7 +27,7 @@ use sqlx::postgres::PgPoolOptions; use std::env; use std::net::SocketAddr; use std::time::Duration; -use tower_http::cors::{Any, CorsLayer}; +use tower_http::cors::CorsLayer; use tower_http::set_header::SetResponseHeaderLayer; use tower_http::trace::TraceLayer; @@ -114,8 +114,14 @@ async fn main() { "http://192.168.0.254:3000", "http://192.168.0.254:3003", "http://192.168.0.254", - // Production - Norteamericano domains (HTTPS) + // Production - Norteamericano domains (.cl and .com) + "http://studio.norteamericano.com", + "https://studio.norteamericano.com", + "http://learning.norteamericano.com", + "https://learning.norteamericano.com", + "http://studio.norteamericano.cl", "https://studio.norteamericano.cl", + "http://learning.norteamericano.cl", "https://learning.norteamericano.cl", ]; @@ -124,17 +130,21 @@ async fn main() { return true; } - // Check wildcard for subdomains: https://*.norteamericano.cl - if origin_str.starts_with("https://") && origin_str.ends_with(".norteamericano.cl") { - let subdomain = origin_str - .strip_prefix("https://") - .unwrap_or("") - .strip_suffix(".norteamericano.cl") - .unwrap_or(""); - - // Allow any subdomain (e.g., api., cdn., admin., etc.) - if !subdomain.is_empty() && !subdomain.contains('/') { - return true; + // Check wildcard for subdomains in norteamericano.cl/.com over HTTP(S) + for scheme in ["http://", "https://"] { + for domain in [".norteamericano.cl", ".norteamericano.com"] { + if origin_str.starts_with(scheme) && origin_str.ends_with(domain) { + let subdomain = origin_str + .strip_prefix(scheme) + .unwrap_or("") + .strip_suffix(domain) + .unwrap_or(""); + + // Allow any subdomain (e.g., api., cdn., admin., etc.) + if !subdomain.is_empty() && !subdomain.contains('/') { + return true; + } + } } } diff --git a/web/studio/src/app/question-bank/page.tsx b/web/studio/src/app/question-bank/page.tsx index 6e3f830..2a74f32 100644 --- a/web/studio/src/app/question-bank/page.tsx +++ b/web/studio/src/app/question-bank/page.tsx @@ -1,11 +1,10 @@ 'use client'; -import React, { useState, useEffect } from 'react'; -import { questionBankApi, QuestionBank, QuestionBankFilters, QuestionBankType } from '@/lib/api'; +import React, { useState, useEffect, useCallback } from 'react'; +import { questionBankApi, QuestionBank, QuestionBankFilters } from '@/lib/api'; import { - Plus, Search, Filter, Edit2, Trash2, Download, - Upload, Sparkles, ChevronDown, ChevronUp, X, Check, AlertCircle, - Headphones, BookOpen, Tag, Hash, Globe + Plus, Search, Filter, + Upload, BookOpen } from 'lucide-react'; import QuestionBankEditor from '@/components/QuestionBank/QuestionBankEditor'; import QuestionBankCard from '@/components/QuestionBank/QuestionBankCard'; @@ -15,6 +14,68 @@ const isMySqlOrigin = (source?: string) => source === 'imported-mysql' || source const isMaterialsOrigin = (source?: string) => source === 'imported-material'; const isAiOrigin = (source?: string) => source === 'ai-generated'; +const toSafeText = (value: unknown): string => { + if (value === null || value === undefined) return ''; + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + return String(value); + } + if (Array.isArray(value)) { + return value.map((v) => toSafeText(v)).filter(Boolean).join(', '); + } + if (typeof value === 'object') { + const obj = value as Record; + if (typeof obj.answer === 'string') return obj.answer; + if (typeof obj.text === 'string') return obj.text; + if (typeof obj.label === 'string') return obj.label; + try { + return JSON.stringify(obj); + } catch { + return ''; + } + } + return ''; +}; + +const sanitizeQuestion = (q: QuestionBank): QuestionBank => { + const safeTags = Array.isArray(q.tags) + ? q.tags.map((t) => toSafeText(t)).filter(Boolean) + : []; + + const safeOptions = Array.isArray(q.options) + ? q.options.map((opt) => { + if (typeof opt === 'object' && opt !== null) { + const item = opt as Record; + if ('answer' in item || 'keywords' in item) { + return toSafeText(item.answer ?? item.text ?? item.label ?? item); + } + } + return opt; + }) + : q.options; + + const safeCorrectAnswer = (() => { + const raw = q.correct_answer; + if (Array.isArray(raw)) { + return raw.map((item) => toSafeText(item)); + } + if (raw && typeof raw === 'object') { + const item = raw as Record; + if ('answer' in item || 'keywords' in item || 'text' in item || 'label' in item) { + return toSafeText(item); + } + } + return raw; + })(); + + return { + ...q, + question_text: toSafeText(q.question_text), + tags: safeTags, + options: safeOptions, + correct_answer: safeCorrectAnswer, + }; +}; + export default function QuestionBankPage() { const [questions, setQuestions] = useState([]); const [loading, setLoading] = useState(true); @@ -24,22 +85,25 @@ export default function QuestionBankPage() { const [showEditor, setShowEditor] = useState(false); const [editingQuestion, setEditingQuestion] = useState(null); const [showImportModal, setShowImportModal] = useState(false); + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(20); - const loadQuestions = async () => { + const loadQuestions = useCallback(async () => { try { setLoading(true); const data = await questionBankApi.list(filters); - setQuestions(data); + setQuestions(data.map(sanitizeQuestion)); + setCurrentPage(1); } catch (error) { console.error('Failed to load questions:', error); } finally { setLoading(false); } - }; + }, [filters]); useEffect(() => { loadQuestions(); - }, [filters]); + }, [loadQuestions]); const handleCreate = () => { setEditingQuestion(null); @@ -68,31 +132,6 @@ export default function QuestionBankPage() { await loadQuestions(); }; - const getQuestionTypeLabel = (type: QuestionBankType) => { - 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', - 'audio-response': 'Respuesta Audio', - 'hotspot': 'Hotspot', - 'code-lab': 'Código', - }; - return labels[type] || type; - }; - - const getDifficultyColor = (difficulty?: string) => { - switch (difficulty) { - case 'easy': return 'bg-green-100 text-green-800'; - case 'medium': return 'bg-yellow-100 text-yellow-800'; - case 'hard': return 'bg-red-100 text-red-800'; - default: return 'bg-gray-100 text-gray-800'; - } - }; - return (
{/* Header */} @@ -187,7 +226,7 @@ export default function QuestionBankPage() {