feat: Añadir selección de cantidad de preguntas en el formulario de plantillas de prueba y mejorar la gestión de edición

This commit is contained in:
2026-04-02 12:21:45 -03:00
parent d0a8e13fb6
commit 4470e3d20b
4 changed files with 52 additions and 11 deletions
@@ -901,6 +901,7 @@ pub async fn generate_questions_with_rag(
) -> Result<Json<Vec<TestTemplateQuestion>>, (StatusCode, String)> { ) -> Result<Json<Vec<TestTemplateQuestion>>, (StatusCode, String)> {
use common::ai::{self, generate_embedding}; use common::ai::{self, generate_embedding};
use serde_json::json; use serde_json::json;
let requested_num_questions = payload.num_questions.unwrap_or(5).clamp(1, 20);
let mut mysql_questions: Vec<QuestionBankForRAG>; let mut mysql_questions: Vec<QuestionBankForRAG>;
@@ -952,7 +953,7 @@ pub async fn generate_questions_with_rag(
.bind(&pgvector) .bind(&pgvector)
.bind(org_ctx.id) .bind(org_ctx.id)
.bind(payload.course_id) .bind(payload.course_id)
.bind(payload.num_questions.unwrap_or(5) * 3) // Get more for diversity .bind(requested_num_questions * 3) // Get more for diversity
.fetch_all(&pool) .fetch_all(&pool)
.await .await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Semantic search failed: {}", e)))?; .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Semantic search failed: {}", e)))?;
@@ -997,7 +998,7 @@ pub async fn generate_questions_with_rag(
.bind(org_ctx.id) .bind(org_ctx.id)
.bind(payload.course_id) .bind(payload.course_id)
.bind(&format!("%{}%", topic)) .bind(&format!("%{}%", topic))
.bind(payload.num_questions.unwrap_or(5) * 3) .bind(requested_num_questions * 3)
.fetch_all(&pool) .fetch_all(&pool)
.await .await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Keyword fallback failed: {}", e)))?; .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Keyword fallback failed: {}", e)))?;
@@ -1081,7 +1082,7 @@ pub async fn generate_questions_with_rag(
.bind(org_ctx.id) .bind(org_ctx.id)
.bind(payload.course_id) .bind(payload.course_id)
.bind(&format!("%{}%", topic)) .bind(&format!("%{}%", topic))
.bind(payload.num_questions.unwrap_or(5) * 3) .bind(requested_num_questions * 3)
.fetch_all(&pool) .fetch_all(&pool)
.await .await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Keyword search failed: {}", e)))?; .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Keyword search failed: {}", e)))?;
@@ -1283,7 +1284,7 @@ pub async fn generate_questions_with_rag(
// Save topic for later use // Save topic for later use
let topic = payload.topic.clone().unwrap_or_else(|| "English grammar".to_string()); let topic = payload.topic.clone().unwrap_or_else(|| "English grammar".to_string());
let num_questions = payload.num_questions.unwrap_or(5); let num_questions = requested_num_questions;
let requested_question_type = match payload.question_type.as_deref() { let requested_question_type = match payload.question_type.as_deref() {
Some("multiple-choice") => "multiple-choice".to_string(), Some("multiple-choice") => "multiple-choice".to_string(),
Some("true-false") => "true-false".to_string(), Some("true-false") => "true-false".to_string(),
+19 -4
View File
@@ -6,18 +6,33 @@ import TestTemplateManager from '@/components/TestTemplates/TestTemplateManager'
import TestTemplateForm from '@/components/TestTemplates/TestTemplateForm'; import TestTemplateForm from '@/components/TestTemplates/TestTemplateForm';
export default function TestTemplatesPage() { export default function TestTemplatesPage() {
const [view, setView] = useState<'list' | 'create'>('list'); const [view, setView] = useState<'list' | 'create' | 'edit'>('list');
const [editingTemplateId, setEditingTemplateId] = useState<string | null>(null);
return ( return (
<PageLayout title="Plantillas de Pruebas"> <PageLayout title="Plantillas de Pruebas">
{view === 'list' ? ( {view === 'list' ? (
<TestTemplateManager <TestTemplateManager
onCreateTemplate={() => setView('create')} onCreateTemplate={() => {
setEditingTemplateId(null);
setView('create');
}}
onEditTemplate={(template) => {
setEditingTemplateId(template.id);
setView('edit');
}}
/> />
) : ( ) : (
<TestTemplateForm <TestTemplateForm
onSuccess={() => setView('list')} templateId={view === 'edit' ? editingTemplateId || undefined : undefined}
onCancel={() => setView('list')} onSuccess={() => {
setEditingTemplateId(null);
setView('list');
}}
onCancel={() => {
setEditingTemplateId(null);
setView('list');
}}
/> />
)} )}
</PageLayout> </PageLayout>
@@ -53,6 +53,7 @@ export default function TestTemplateForm({ onSuccess, onCancel }: TestTemplateFo
const [expandedQuestion, setExpandedQuestion] = useState<string | null>(null); const [expandedQuestion, setExpandedQuestion] = useState<string | null>(null);
const [aiContext, setAiContext] = useState(''); const [aiContext, setAiContext] = useState('');
const [aiQuestionType, setAiQuestionType] = useState<QuestionType>('multiple-choice'); const [aiQuestionType, setAiQuestionType] = useState<QuestionType>('multiple-choice');
const [aiQuestionCount, setAiQuestionCount] = useState<number>(5);
// MySQL course selection state // MySQL course selection state
const [mysqlPlans, setMysqlPlans] = useState<MySqlPlan[]>([]); const [mysqlPlans, setMysqlPlans] = useState<MySqlPlan[]>([]);
@@ -249,7 +250,7 @@ export default function TestTemplateForm({ onSuccess, onCancel }: TestTemplateFo
}, },
body: JSON.stringify({ body: JSON.stringify({
topic: aiContext, topic: aiContext,
num_questions: 5, num_questions: aiQuestionCount,
question_type: aiQuestionType, question_type: aiQuestionType,
}), }),
}); });
@@ -577,6 +578,26 @@ export default function TestTemplateForm({ onSuccess, onCancel }: TestTemplateFo
<option value="fill-in-the-blanks">Completar espacios</option> <option value="fill-in-the-blanks">Completar espacios</option>
<option value="audio-response">Respuesta de audio</option> <option value="audio-response">Respuesta de audio</option>
</select> </select>
<select
value={aiQuestionCount}
onChange={(e) => setAiQuestionCount(Number(e.target.value))}
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 bg-white"
disabled={generatingAI}
>
<option value={1}>1</option>
<option value={2}>2</option>
<option value={3}>3</option>
<option value={4}>4</option>
<option value={5}>5</option>
<option value={6}>6</option>
<option value={7}>7</option>
<option value={8}>8</option>
<option value={9}>9</option>
<option value={10}>10</option>
<option value={12}>12</option>
<option value={15}>15</option>
<option value={20}>20</option>
</select>
<button <button
type="button" type="button"
onClick={handleGenerateWithAI} onClick={handleGenerateWithAI}
@@ -590,6 +611,9 @@ export default function TestTemplateForm({ onSuccess, onCancel }: TestTemplateFo
<p className="text-xs text-gray-500"> <p className="text-xs text-gray-500">
La IA genera varios tipos de ejercicios. Hotspot y Code Lab quedan para creacion manual del instructor. La IA genera varios tipos de ejercicios. Hotspot y Code Lab quedan para creacion manual del instructor.
</p> </p>
<p className="text-xs text-gray-500">
Puedes elegir entre 1 y 20 preguntas por generacion.
</p>
</div> </div>
{/* Sections */} {/* Sections */}
@@ -16,9 +16,10 @@ import { Plus, Search, Filter, Edit2, Trash2, Eye, Copy, BookOpen, Clock, Target
interface TestTemplateManagerProps { interface TestTemplateManagerProps {
onSelectTemplate?: (template: TestTemplate) => void; onSelectTemplate?: (template: TestTemplate) => void;
onCreateTemplate?: () => void; onCreateTemplate?: () => void;
onEditTemplate?: (template: TestTemplate) => void;
} }
export default function TestTemplateManager({ onSelectTemplate, onCreateTemplate }: TestTemplateManagerProps) { export default function TestTemplateManager({ onSelectTemplate, onCreateTemplate, onEditTemplate }: TestTemplateManagerProps) {
const [templates, setTemplates] = useState<TestTemplate[]>([]); const [templates, setTemplates] = useState<TestTemplate[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -346,7 +347,7 @@ export default function TestTemplateManager({ onSelectTemplate, onCreateTemplate
<Eye className="w-4 h-4" /> <Eye className="w-4 h-4" />
</button> </button>
<button <button
onClick={() => alert(`Implementar: Editar plantilla ${template.id}`)} onClick={() => onEditTemplate?.(template)}
className="p-1 text-gray-400 hover:text-green-600 transition-colors" className="p-1 text-gray-400 hover:text-green-600 transition-colors"
title="Editar" title="Editar"
> >