Implement Server-Side AI Question Bank Generator, Adaptive Weakness Sampling 60/40, and Curriculum Studio One-Click Generation

This commit is contained in:
Hamza-Ayed
2026-09-03 15:38:35 +03:00
parent c9c03892d1
commit 8d7ffcf2eb
7 changed files with 603 additions and 266 deletions
+61 -16
View File
@@ -70,7 +70,61 @@ class ExamController
return;
}
$questions = Database::select("SELECT * FROM questions WHERE exam_id = ? ORDER BY id ASC", [$examId]);
$allQuestions = Database::select("SELECT * FROM questions WHERE exam_id = ? ORDER BY id ASC", [$examId]);
$questions = [];
if (count($allQuestions) > 15) {
// Adaptive Sampling Engine based on Student Weakness History
$studentId = $request->user_id ?? 0;
$weakTopics = [];
if ($studentId > 0) {
// Fetch failed topics from student's answers
$fails = Database::select(
"SELECT q.topic_tag, COUNT(*) as cnt
FROM student_question_answers sqa
JOIN questions q ON sqa.question_id = q.id
WHERE sqa.student_id = ? AND sqa.is_correct = 0 AND q.topic_tag IS NOT NULL
GROUP BY q.topic_tag ORDER BY cnt DESC LIMIT 5",
[$studentId]
);
foreach ($fails as $f) {
$weakTopics[] = $f['topic_tag'];
}
}
// Split questions into weak vs other topics
$weakPool = [];
$otherPool = [];
foreach ($allQuestions as $q) {
if (!empty($weakTopics) && in_array($q['topic_tag'], $weakTopics)) {
$weakPool[] = $q;
} else {
$otherPool[] = $q;
}
}
shuffle($weakPool);
shuffle($otherPool);
if (!empty($weakPool)) {
// 60% focused on weak concepts for adaptive remediation
$targetWeakCount = min(count($weakPool), 9);
$questions = array_slice($weakPool, 0, $targetWeakCount);
// 40% from rest of unit concepts
$needed = 15 - count($questions);
$questions = array_merge($questions, array_slice($otherPool, 0, $needed));
} else {
// Stratified random sampling from the comprehensive bank
$questions = array_slice($otherPool, 0, 15);
}
shuffle($questions);
} else {
$questions = $allQuestions;
}
foreach ($questions as &$q) {
$options = Database::select("SELECT id, option_text, feedback_text" . ($isTeacher ? ", is_correct" : "") . " FROM question_options WHERE question_id = ? ORDER BY id ASC", [$q['id']]);
@@ -78,6 +132,7 @@ class ExamController
}
$exam['questions'] = $questions;
$exam['questions_count'] = count($questions);
$response->json([
'status' => 'success',
@@ -394,27 +449,17 @@ class ExamController
}
}
if (count($existingQuestions) < 15 || $hasCalculus) {
if (count($existingQuestions) < 45 || $hasCalculus) {
// Wipe legacy corrupt questions
foreach ($existingQuestions as $oq) {
Database::query("DELETE FROM question_options WHERE question_id = ?", [$oq['id']]);
}
Database::query("DELETE FROM questions WHERE exam_id = ?", [$examId]);
$bank = self::getUnit1QuestionBank();
foreach ($bank as $qData) {
$qId = (int)Database::insert(
"INSERT INTO questions (uuid, exam_id, question_text, question_type, bloom_taxonomy, topic_tag, explanation_text, points)
VALUES (UUID(), ?, ?, 'multiple_choice', ?, ?, ?, ?)",
[$examId, $qData['question_text'], $qData['bloom'], $qData['topic'], $qData['explanation'], $qData['points']]
);
foreach ($qData['options'] as $idx => $optText) {
Database::insert(
"INSERT INTO question_options (question_id, option_text, is_correct) VALUES (?, ?, ?)",
[$qId, $optText, ($idx === $qData['correct_index']) ? 1 : 0]
);
}
}
require_once __DIR__ . '/../Services/AiQuestionBankGeneratorService.php';
$unitPath = 'grade_10/math_10/semester_1/unit_01';
$res = \App\Services\AiQuestionBankGeneratorService::generateUnitQuestionBank($unitPath, 1, 50);
return (int)$res['exam_id'];
}
return $examId;