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:
@@ -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(),
|
||||||
|
|||||||
@@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user