add dropdown menu to the frontend of the quiz

parent 66689c39
......@@ -338,25 +338,27 @@ def create_app() -> FastAPI:
@app.post("/mcq/generate")
async def generate_mcqs_handler(
request: Request,
grade: int = Form(...),
curriculum: str = Form(...),
grade: str = Form(...), # Changed to str
subject: str = Form(...),
unit: str = Form(...),
concept: str = Form(...),
count: int = Form(5),
is_arabic: bool = Form(False)
is_arabic: bool = Form(False),
):
"""
Generates and stores a new set of MCQs for a specific topic.
Generates and stores a new set of MCQs for a specific topic, using the new schema.
"""
container = request.app.state.container
try:
generated_questions = container.agent_service.generate_and_store_mcqs(
curriculum=curriculum,
grade=grade,
subject=subject,
unit=unit,
concept=concept,
num_questions=count,
is_arabic=is_arabic
is_arabic=is_arabic,
)
return {
"status": "success",
......@@ -364,7 +366,7 @@ def create_app() -> FastAPI:
"questions": generated_questions
}
except HTTPException as e:
raise e # Re-raise FastAPI specific exceptions
raise e
except Exception as e:
logger.error(f"Error in generate_mcqs_handler: {e}")
raise HTTPException(status_code=500, detail=str(e))
......@@ -372,27 +374,27 @@ def create_app() -> FastAPI:
@app.get("/mcq")
async def get_mcqs_handler(
request: Request,
grade: int,
curriculum: str,
grade: str,
subject: str,
unit: str,
concept: str,
is_arabic: bool,
# Make limit optional. If not provided, it will be None.
limit: Optional[int] = None
):
"""
Retrieves existing MCQs for a specific topic and language from the database.
If no limit is provided, retrieves all questions.
Retrieves existing MCQs for a specific topic, now filtering by curriculum.
"""
container = request.app.state.container
try:
questions = container.agent_service.pgvector.get_mcqs(
curriculum=curriculum,
grade=grade,
subject=subject,
unit=unit,
concept=concept,
is_arabic=is_arabic,
limit=limit # Pass the limit (which could be None)
limit=limit
)
return {
"status": "success",
......@@ -406,7 +408,8 @@ def create_app() -> FastAPI:
@app.post("/quiz/dynamic")
async def get_dynamic_quiz_handler(
request: Request,
grade: int = Form(...),
curriculum: str = Form(...),
grade: str = Form(...),
subject: str = Form(...),
unit: str = Form(...),
concept: str = Form(...),
......@@ -414,15 +417,12 @@ def create_app() -> FastAPI:
count: int = Form(5)
):
"""
Generates a dynamic quiz for a topic.
This endpoint ensures freshness by generating a few new questions
and then randomly selects the total requested 'count' from the
entire pool of available questions (new and old).
Generates a dynamic quiz, now using curriculum as a key identifier.
"""
container = request.app.state.container
try:
quiz_questions = container.agent_service.get_dynamic_quiz(
curriculum=curriculum,
grade=grade,
subject=subject,
unit=unit,
......@@ -436,7 +436,7 @@ def create_app() -> FastAPI:
"quiz": quiz_questions
}
except HTTPException as e:
raise e # Re-raise FastAPI specific exceptions
raise e
except Exception as e:
logger.error(f"Error in get_dynamic_quiz_handler: {e}")
raise HTTPException(status_code=500, detail=str(e))
......@@ -459,6 +459,37 @@ def create_app() -> FastAPI:
except Exception as e:
print(f"Error serving quiz interface: {e}")
raise HTTPException(status_code=500, detail=f"Error serving interface: {str(e)}")
@app.get("/quiz/options/curricula")
async def get_curricula_options(request: Request):
container = request.app.state.container
options = container.agent_service.pgvector.get_distinct_curricula_from_structure()
return {"options": options}
@app.get("/quiz/options/grades")
async def get_grades_options(request: Request, curriculum: str):
container = request.app.state.container
options = container.agent_service.pgvector.get_distinct_grades_from_structure(curriculum)
return {"options": options}
@app.get("/quiz/options/subjects")
async def get_subjects_options(request: Request, curriculum: str, grade: str):
container = request.app.state.container
options = container.agent_service.pgvector.get_distinct_subjects_from_structure(curriculum, grade)
return {"options": options}
@app.get("/quiz/options/units")
async def get_units_options(request: Request, curriculum: str, grade: str, subject: str):
container = request.app.state.container
options = container.agent_service.pgvector.get_distinct_units_from_structure(curriculum, grade, subject)
return {"options": options}
@app.get("/quiz/options/concepts")
async def get_concepts_options(request: Request, curriculum: str, grade: str, subject: str, unit: str):
container = request.app.state.container
options = container.agent_service.pgvector.get_distinct_concepts_from_structure(curriculum, grade, subject, unit)
return {"options": options}
@app.options("/get-audio-response")
async def audio_response_options():
......
......@@ -6,6 +6,7 @@ import sys
import json
import random
import math
import re
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from core import StudentNationality, Models
......@@ -110,135 +111,188 @@ class AgentService:
except Exception as e:
logger.error(f"Error closing connection pools: {e}")
def _extract_grade_integer(self, grade_str: str) -> int:
"""
Safely extracts the first integer from a grade string like '4th Grade'.
This acts as a translator between the string-based MCQ schema and the
integer-based vector DB schema.
"""
if not isinstance(grade_str, str):
raise ValueError(f"Grade must be a string, but got {type(grade_str)}")
numbers = re.findall(r'\d+', grade_str)
if numbers:
return int(numbers[0])
# If no numbers are found, we cannot query the vector DB. This is an invalid input.
raise ValueError(f"Could not extract a numeric grade from the input string: '{grade_str}'")
def generate_and_store_mcqs(
self, grade: int, subject: str, unit: str, concept: str, is_arabic: bool, num_questions: int = 5
self, curriculum: str, grade: str, subject: str, unit: str, concept: str,
is_arabic: bool, num_questions: int = 5
) -> List[Dict]:
"""
Generates NEW, UNIQUE MCQs for a topic by first retrieving existing ones
and instructing the AI to avoid generating duplicates.
Generates NEW, UNIQUE MCQs with the full schema. The AI is now responsible
for assigning a balanced difficulty level to each question.
"""
if not self.pgvector:
raise HTTPException(status_code=503, detail="Vector service is not available for context retrieval.")
# === STEP 1: RETRIEVE EXISTING QUESTIONS ===
logger.info(f"Checking for existing questions for: {grade}/{subject}/{unit}/{concept}")
# ... (Step 1 and 2 for getting existing questions and context remain the same) ...
logger.info(f"Checking for existing questions for: {curriculum}/{grade}/{subject}/{unit}/{concept}")
existing_questions = self.pgvector.get_mcqs(
grade, subject, unit, concept, is_arabic, limit=None # Fetch ALL existing questions
curriculum, grade, subject, unit, concept, is_arabic, limit=None
)
existing_questions_text = "No existing questions found."
if existing_questions:
# Format the existing questions into a simple list for the prompt
q_list = [f"- {q['question_text']}" for q in existing_questions]
existing_questions_text = "\n".join(q_list)
logger.info(f"Found {len(existing_questions)} existing questions. Will instruct AI to generate different ones.")
# === STEP 2: RETRIEVE CURRICULUM CONTEXT ===
search_query = f"summary of {concept} in {unit}"
query_embedding = self.openai_service.generate_embedding(search_query)
search_query = f"summary of {concept} in {unit} for {subject}"
query_embedding = self.openai_service.generate_embedding(search_query)
try:
grade_for_search = self._extract_grade_integer(grade)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
context_chunks = self.pgvector.search_filtered_nearest(
query_embedding, grade, subject, is_arabic, limit=10
query_embedding, grade_for_search, subject, is_arabic, limit=10
)
if not context_chunks:
raise HTTPException(status_code=404, detail="No curriculum context found for this topic in the specified language.")
raise HTTPException(status_code=404, detail="No curriculum context found for this topic.")
full_context = "\n---\n".join([chunk['chunk_text'] for chunk in context_chunks])
# === STEP 3: CREATE THE ADVANCED, AWARE PROMPT ===
# --- STEP 3: THE PROMPT IS UPDATED TO HANDLE DIFFICULTY INTERNALLY ---
if is_arabic:
prompt = f"""
أنت خبير في تطوير المناهج ومهمتك إنشاء أسئلة اختيار من متعدد جديدة ومختلفة.
هذه هي الأسئلة الموجودة حاليًا في قاعدة البيانات حول المفهوم "{concept}":
--- الأسئلة الحالية ---
{existing_questions_text}
--- نهاية الأسئلة الحالية ---
اعتمادًا فقط على السياق التالي من المنهج:
--- السياق ---
{full_context}
--- نهاية السياق ---
يرجى توليد {num_questions} من أسئلة الاختيار من متعدد **الجديدة والمختلفة تمامًا** عن الأسئلة الموجودة أعلاه.
يجب أن تكون كل الأسئلة قابلة للإجابة مباشرة من السياق المقدم.
يجب أن يكون ردك مصفوفة JSON صحيحة. كل كائن يجب أن يحتوي على المفاتيح التالية:
- "question_text": نص السؤال.
- "correct_answer": الإجابة الصحيحة.
- "wrong_answer_1": إجابة خاطئة.
- "wrong_answer_2": إجابة خاطئة.
- "wrong_answer_3": إجابة خاطئة.
لا تكتب أي نص أو شرح خارج مصفوفة الـ JSON.
"""
# (A similar detailed prompt in Arabic would be needed here)
prompt =f"""
أنت خبير في تطوير المناهج التعليمية، ومهمتك هي إنشاء **أسئلة اختيار من متعدد جديدة بالكامل** (أسئلة لم تظهر من قبل).
هذه هي الأسئلة الموجودة بالفعل لمفهوم "{concept}":
--- الأسئلة الموجودة ---
{existing_questions_text}
--- نهاية الأسئلة الموجودة ---
استنادًا فقط إلى المعلومات التالية:
--- السياق ---
{full_context}
--- نهاية السياق ---
قم بإنشاء {num_questions} سؤالًا جديدًا تمامًا من نوع الاختيار من متعدد (MCQ)، **مختلفة كليًا عن الأسئلة الموجودة أعلاه**.
⚠️ **مهم جدًا**:
يجب أن تتضمن الأسئلة مستويات صعوبة متنوعة وفق التوزيع التالي تقريبًا:
- ٤٠٪ أسئلة سهلة (مستوى صعوبة من ١ إلى ٤)
- ٣٠٪ أسئلة متوسطة (مستوى صعوبة من ٥ إلى ٧)
- ٣٠٪ أسئلة صعبة (مستوى صعوبة من ٨ إلى ١٠)
**صيغة الإخراج مطلوبة أن تكون مصفوفة JSON صالحة** (JSON array) من الكائنات (objects).
كل كائن يجب أن يحتوي على المفاتيح التالية **بالضبط**:
- "question_text": نص السؤال.
- "difficulty_level": رقم صحيح من ١ إلى ١٠ يمثل مستوى الصعوبة.
- "question_type": نوع السؤال (مثلاً: "multiple_choice").
- "correct_answer": الإجابة الصحيحة الوحيدة.
- "wrong_answer_1": إجابة خاطئة ولكن معقولة.
- "wrong_answer_2": إجابة خاطئة ولكن معقولة.
- "wrong_answer_3": إجابة خاطئة ولكن معقولة.
- "wrong_answer_4": إجابة خاطئة ولكن معقولة.
- "hint": تلميح أو مساعدة للطالب لفهم السؤال.
- "question_image_url": اترك هذا الحقل كسلسلة فارغة "".
- "correct_image_url": اترك هذا الحقل كسلسلة فارغة "".
- "wrong_image_url_1": اترك هذا الحقل كسلسلة فارغة "".
- "wrong_image_url_2": اترك هذا الحقل كسلسلة فارغة "".
- "wrong_image_url_3": اترك هذا الحقل كسلسلة فارغة "".
- "wrong_image_url_4": اترك هذا الحقل كسلسلة فارغة "".
لا تكتب أي نص خارج مصفوفة JSON.
"""
else:
prompt = f"""
You are an expert curriculum developer tasked with creating new and unique multiple-choice questions.
You are an expert curriculum developer creating new multiple-choice questions.
Here are the questions that ALREADY EXIST in the database for the concept "{concept}":
Here are the questions that ALREADY EXIST for the concept "{concept}":
--- EXISTING QUESTIONS ---
{existing_questions_text}
--- END EXISTING QUESTIONS ---
Based ONLY on the following context from the curriculum:
Based ONLY on the following context:
--- CONTEXT ---
{full_context}
--- END CONTEXT ---
Please generate {num_questions} NEW and COMPLETELY DIFFERENT multiple-choice questions from the list of existing ones above.
Each question must be answerable directly from the provided context. The questions and all answers MUST be in English.
Generate {num_questions} NEW and COMPLETELY DIFFERENT multiple-choice questions from the list above.
**IMPORTANT**: For the {num_questions} questions you generate, assign a difficulty level to each one. The distribution should be approximately:
- 40% easy (difficulty 1-4)
- 30% medium (difficulty 5-7)
- 30% hard (difficulty 8-10)
Your response MUST be a valid JSON array of objects with these keys:
- "question_text"
- "correct_answer"
- "wrong_answer_1"
- "wrong_answer_2"
- "wrong_answer_3"
Your response MUST be a valid JSON array of objects. Each object must have these exact keys:
- "question_text": The text of the question.
- "difficulty_level": An integer between 1 and 10, based on the distribution rule.
- "question_type": The type of question (e.g., "multiple_choice").
- "correct_answer": The single correct answer.
- "wrong_answer_1": A plausible wrong answer.
- "wrong_answer_2": Another plausible wrong answer.
- "wrong_answer_3": A third plausible wrong answer.
- "wrong_answer_4": A fourth plausible wrong answer.
- "hint": A helpful hint for the student.
- "question_image_url": Leave this as an empty string "".
- "correct_image_url": Leave this as an empty string "".
- "wrong_image_url_1": Leave this as an empty string "".
- "wrong_image_url_2": Leave this as an empty string "".
- "wrong_image_url_3": Leave this as an empty string "".
- "wrong_image_url_4": Leave this as an empty string "".
Do not include any text outside of the JSON array.
"""
# === STEP 4 & 5: CALL LLM, PARSE, and STORE (No changes here) ===
# --- STEP 4: CALL LLM and PARSE (unchanged) ---
try:
# ... (The entire try/except block for calling the LLM remains exactly the same)
response = self.openai_service.client.chat.completions.create(
model=Models.chat,
messages=[{"role": "user", "content": prompt}],
temperature=0.5, # Slightly higher temp for more creativity
response_format={"type": "json_object"}
model=Models.chat, messages=[{"role": "user", "content": prompt}],
temperature=0.7, response_format={"type": "json_object"}
)
response_content = response.choices[0].message.content
json_response = json.loads(response_content)
generated_questions = []
for key, value in json_response.items():
if isinstance(value, list):
generated_questions = value
break
generated_questions = next((v for v in json_response.values() if isinstance(v, list)), None)
if not generated_questions:
raise ValueError("LLM did not return a list of questions in the JSON response.")
except (json.JSONDecodeError, ValueError, KeyError) as e:
except (json.JSONDecodeError, ValueError, KeyError, StopIteration) as e:
logger.error(f"Failed to parse MCQ response from LLM: {e}\nRaw Response: {response_content}")
raise HTTPException(status_code=500, detail="Failed to generate or parse MCQs from AI.")
# --- STEP 5: PREPARE FOR STORAGE (Now gets difficulty_level from the AI response) ---
mcqs_to_store = []
for q in generated_questions:
mcqs_to_store.append({
"grade": grade, "is_arabic": is_arabic, "subject": subject,
"unit": unit, "concept": concept, "question_text": q["question_text"],
"correct_answer": q["correct_answer"], "wrong_answer_1": q["wrong_answer_1"],
"wrong_answer_2": q["wrong_answer_2"], "wrong_answer_3": q["wrong_answer_3"],
"curriculum": curriculum, "grade": grade, "subject": subject, "unit": unit,
"concept": concept, "is_arabic": is_arabic,
"difficulty_level": q.get("difficulty_level"), # <-- AI now provides this
"question_text": q.get("question_text"),
"question_type": q.get("question_type", "multiple_choice"),
"correct_answer": q.get("correct_answer"),
"wrong_answer_1": q.get("wrong_answer_1"),
"wrong_answer_2": q.get("wrong_answer_2"),
"wrong_answer_3": q.get("wrong_answer_3"),
"wrong_answer_4": q.get("wrong_answer_4"),
"hint": q.get("hint"),
"question_image_url": q.get("question_image_url"),
"correct_image_url": q.get("correct_image_url"),
"wrong_image_url_1": q.get("wrong_image_url_1"),
"wrong_image_url_2": q.get("wrong_image_url_2"),
"wrong_image_url_3": q.get("wrong_image_url_3"),
"wrong_image_url_4": q.get("wrong_image_url_4"),
})
self.pgvector.insert_mcqs(mcqs_to_store)
return mcqs_to_store
def handle_ask_for_question(self, student_id: str) -> Dict:
......@@ -254,7 +308,13 @@ class AgentService:
student_info = self.db_service.get_student_info(student_id)
if not student_info: raise HTTPException(status_code=404, detail="Student not found.")
grade, is_arabic, subject = student_info['grade'], student_info['is_arabic'], "Science"
grade_str = f"{grade}th Grade"
nationality = student_info['nationality']
if nationality == StudentNationality.EGYPTIAN:
curriculum = "EGYPTIAN National"
else:
curriculum = "SAUDI National"
recent_history = self.db_service.get_chat_history(student_id, limit=6)
if not recent_history: raise HTTPException(status_code=400, detail="Cannot ask a question without conversation context.")
......@@ -278,12 +338,12 @@ class AgentService:
# 2. Generate one new question to enrich the pool (No changes here)
try:
self.generate_and_store_mcqs(grade, subject, unit, concept, is_arabic, num_questions=1)
self.generate_and_store_mcqs(curriculum, grade_str, subject, unit, concept, is_arabic, num_questions=1)
except Exception as e:
logger.warning(f"Non-critical error: Failed to generate a new background MCQ: {e}")
# 3. Retrieve and filter the pool of available questions (No changes here)
all_mcqs = self.pgvector.get_mcqs(grade, subject, unit, concept, is_arabic, limit=None)
all_mcqs = self.pgvector.get_mcqs(curriculum, grade_str, subject, unit, concept, is_arabic, limit=None)
if not all_mcqs: raise HTTPException(status_code=404, detail="No questions found for the current topic.")
asked_question_texts = {msg['content'] for msg in recent_history if msg['role'] == 'assistant'}
......@@ -299,7 +359,6 @@ class AgentService:
relevant_question_texts = []
last_user_message = recent_history[-1]['content']
# --- THIS IS THE FIX ---
# Dynamically determine how many questions to ask for.
# Ask for up to 3, but no more than the number of available questions.
num_to_select = min(3, len(unasked_mcqs))
......@@ -360,82 +419,63 @@ class AgentService:
def get_dynamic_quiz(
self, grade: int, subject: str, unit: str, concept: str, is_arabic: bool, count: int
self, curriculum: str, grade: str, subject: str, unit: str, concept: str, is_arabic: bool, count: int
) -> List[Dict]:
"""
Generates a dynamic quiz of 'count' questions using a hybrid approach with BATCHED generation:
1. Always generates a "freshness batch" of new questions.
2. Retrieves all questions and checks if the total meets the 'count'.
3. If not, generates the remaining number of questions needed IN BATCHES.
Generates a dynamic quiz of 'count' questions using a hybrid approach with BATCHED generation.
"""
if not self.pgvector:
raise HTTPException(status_code=503, detail="Vector service is not available for this feature.")
# Define maximum questions per batch to avoid token limits
MAX_QUESTIONS_PER_BATCH = 10
# --- PART 1: Generate freshness questions ---
# 1. Calculate how many new questions to generate for freshness.
num_fresh_questions = min(max(1, math.floor(count / 3)), 5)
logger.info(f"Request for {count} questions. Step 1: Generating {num_fresh_questions} new 'freshness' questions.")
# 2. Generate and store these new "freshness" questions.
try:
# --- FIX #1: Removed the erroneous 'difficulty_level' argument ---
self.generate_and_store_mcqs(
grade=grade, subject=subject, unit=unit, concept=concept,
curriculum=curriculum, grade=grade, subject=subject, unit=unit, concept=concept,
is_arabic=is_arabic, num_questions=num_fresh_questions
)
except Exception as e:
logger.warning(f"Could not generate 'freshness' questions for the quiz due to an error: {e}")
# --- PART 2: Check for shortfall and generate more if needed (WITH BATCHING) ---
# 3. Retrieve ALL available questions for the topic from the database.
all_mcqs_after_freshness = self.pgvector.get_mcqs(
curriculum=curriculum,
grade=grade, subject=subject, unit=unit, concept=concept,
is_arabic=is_arabic, limit=None
)
# 4. Calculate if there is still a shortfall.
questions_still_needed = count - len(all_mcqs_after_freshness)
# 5. If we still need more questions, generate them IN BATCHES.
if questions_still_needed > 0:
logger.info(f"After freshness batch, have {len(all_mcqs_after_freshness)} questions. Need to generate {questions_still_needed} more to meet count of {count}.")
total_generated = 0
remaining = questions_still_needed
while remaining > 0:
# Determine batch size (cap at MAX_QUESTIONS_PER_BATCH and also cap total at 20 per session)
batch_size = min(remaining, MAX_QUESTIONS_PER_BATCH)
try:
logger.info(f"Generating batch {total_generated // MAX_QUESTIONS_PER_BATCH + 1} of {batch_size} questions...")
logger.info(f"Generating batch of {remaining // MAX_QUESTIONS_PER_BATCH + 1} of {batch_size} questions...")
# --- FIX #2: Added the missing 'curriculum' argument ---
self.generate_and_store_mcqs(
grade=grade, subject=subject, unit=unit, concept=concept,
is_arabic=is_arabic, num_questions=batch_size
curriculum=curriculum,
grade=grade,
subject=subject,
unit=unit,
concept=concept,
is_arabic=is_arabic,
num_questions=batch_size
)
total_generated += batch_size
remaining -= batch_size
logger.info(f"Successfully generated batch. Total gap-filling questions generated: {total_generated}")
except Exception as e:
logger.error(f"Failed to generate batch of {batch_size} questions: {e}")
# If we've generated at least some questions, continue
if total_generated > 0:
logger.warning(f"Continuing with {total_generated} gap-filling questions generated so far.")
break
else:
logger.warning(f"Could not generate gap-filling questions: {e}")
break
# --- PART 3: Final Assembly and Return ---
# Break the loop if generation fails to prevent an infinite loop
break
# 6. Retrieve the final, complete list of ALL questions.
final_pool = self.pgvector.get_mcqs(
curriculum=curriculum,
grade=grade, subject=subject, unit=unit, concept=concept,
is_arabic=is_arabic, limit=None
)
......@@ -443,11 +483,9 @@ class AgentService:
if not final_pool:
raise HTTPException(status_code=404, detail="No questions could be found or generated for this topic.")
# Check if we have enough questions
if len(final_pool) < count:
logger.warning(f"Could only gather {len(final_pool)} questions out of {count} requested. Returning all available questions.")
# 7. Randomly select the desired number of questions from the final pool.
random.shuffle(final_pool)
final_quiz = final_pool[:min(count, len(final_pool))]
......
......@@ -527,26 +527,37 @@ class PGVectorService:
def insert_mcqs(self, mcq_list: List[Dict]):
"""
Inserts a batch of MCQs, now including the language flag.
Inserts a batch of MCQs, now including ALL new fields from the updated schema.
"""
if not mcq_list:
return
with self.pool_handler.get_connection() as conn:
with conn.cursor() as cur:
# --- UPDATED QUERY ---
# --- UPDATED INSERT QUERY WITH ALL NEW COLUMNS ---
insert_query = """
INSERT INTO mcq_questions (
grade, is_arabic, subject, unit, concept, question_text,
correct_answer, wrong_answer_1, wrong_answer_2, wrong_answer_3
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s);
curriculum, grade, subject, unit, concept, question_text,
question_type, difficulty_level, is_arabic, correct_answer,
wrong_answer_1, wrong_answer_2, wrong_answer_3, wrong_answer_4,
question_image_url, correct_image_url, wrong_image_url_1,
wrong_image_url_2, wrong_image_url_3, wrong_image_url_4, hint
) VALUES (
%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
%s, %s, %s, %s, %s, %s, %s
);
"""
# --- UPDATED DATA PREPARATION ---
# --- UPDATED DATA PREPARATION TO MATCH THE NEW SCHEMA ---
# Using .get() provides safety against missing keys from the LLM response
data_to_insert = [
(
q['grade'], q['is_arabic'], q['subject'], q['unit'], q['concept'],
q['question_text'], q['correct_answer'], q['wrong_answer_1'],
q['wrong_answer_2'], q['wrong_answer_3']
q.get('curriculum'), q.get('grade'), q.get('subject'), q.get('unit'), q.get('concept'),
q.get('question_text'), q.get('question_type'), q.get('difficulty_level'),
q.get('is_arabic'), q.get('correct_answer'), q.get('wrong_answer_1'),
q.get('wrong_answer_2'), q.get('wrong_answer_3'), q.get('wrong_answer_4'),
q.get('question_image_url'), q.get('correct_image_url'), q.get('wrong_image_url_1'),
q.get('wrong_image_url_2'), q.get('wrong_image_url_3'), q.get('wrong_image_url_4'),
q.get('hint')
) for q in mcq_list
]
......@@ -554,23 +565,23 @@ class PGVectorService:
conn.commit()
logger.info(f"Successfully inserted {len(mcq_list)} MCQs into the database.")
def get_mcqs(self, grade: int, subject: str, unit: str, concept: str, is_arabic: bool, limit: Optional[int] = 10) -> List[Dict]:
def get_mcqs(self, curriculum: str, grade: str, subject: str, unit: str, concept: str, is_arabic: bool, limit: Optional[int] = 10) -> List[Dict]:
"""
Retrieves MCQs for a specific topic and language.
Retrieves MCQs for a specific topic and language, now filtering by curriculum.
If limit is None, it retrieves all matching questions.
"""
with self.pool_handler.get_connection() as conn:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
# Dynamically build the query based on the limit
# --- UPDATED SELECT AND WHERE CLAUSE ---
query = """
SELECT id, question_text, correct_answer, wrong_answer_1, wrong_answer_2, wrong_answer_3
SELECT *
FROM mcq_questions
WHERE grade = %s AND subject = %s AND unit = %s AND concept = %s AND is_arabic = %s
WHERE curriculum = %s AND grade = %s AND subject = %s AND unit = %s AND concept = %s AND is_arabic = %s
ORDER BY created_at DESC
"""
params = (grade, subject, unit, concept, is_arabic)
params = (curriculum, grade, subject, unit, concept, is_arabic)
if limit is not None:
query += " LIMIT %s;"
......@@ -579,4 +590,64 @@ class PGVectorService:
query += ";"
cur.execute(query, params)
return cur.fetchall()
\ No newline at end of file
return cur.fetchall()
def get_distinct_curricula_from_structure(self) -> List[str]:
"""Gets distinct curriculum names from the curriculum_structure table."""
with self.pool_handler.get_connection() as conn:
with conn.cursor() as cur:
cur.execute("SELECT DISTINCT curriculum_data->>'title' FROM curriculum_structure ORDER BY 1;")
return [row[0] for row in cur.fetchall() if row[0]]
def get_distinct_grades_from_structure(self, curriculum: str) -> List[str]:
"""Gets distinct grades for a given curriculum from the curriculum_structure table."""
with self.pool_handler.get_connection() as conn:
with conn.cursor() as cur:
# We assume grade is stored as an integer, but return as string for consistency
cur.execute("""
SELECT DISTINCT grade::text FROM curriculum_structure
WHERE curriculum_data->>'title' = %s ORDER BY 1;
""", (curriculum,))
return [row[0] for row in cur.fetchall() if row[0]]
def get_distinct_subjects_from_structure(self, curriculum: str, grade: str) -> List[str]:
"""Gets distinct subjects for a given curriculum and grade."""
with self.pool_handler.get_connection() as conn:
with conn.cursor() as cur:
cur.execute("""
SELECT DISTINCT subject FROM curriculum_structure
WHERE curriculum_data->>'title' = %s AND grade = %s ORDER BY 1;
""", (curriculum, int(grade))) # Grade is an integer in this table
return [row[0] for row in cur.fetchall() if row[0]]
def get_distinct_units_from_structure(self, curriculum: str, grade: str, subject: str) -> List[str]:
"""Gets distinct unit names from the JSONB data in curriculum_structure."""
with self.pool_handler.get_connection() as conn:
with conn.cursor() as cur:
# This query uses jsonb_array_elements to expand the 'units' array
cur.execute("""
SELECT DISTINCT unit->>'name'
FROM curriculum_structure, jsonb_array_elements(curriculum_data->'units') AS unit
WHERE curriculum_data->>'title' = %s AND grade = %s AND subject = %s
ORDER BY 1;
""", (curriculum, int(grade), subject))
return [row[0] for row in cur.fetchall() if row[0]]
def get_distinct_concepts_from_structure(self, curriculum: str, grade: str, subject: str, unit: str) -> List[str]:
"""Gets distinct concept names for a given unit from the JSONB data."""
with self.pool_handler.get_connection() as conn:
with conn.cursor() as cur:
# This is a more complex query that expands both units and concepts
cur.execute("""
SELECT DISTINCT concept->>'name'
FROM curriculum_structure,
jsonb_array_elements(curriculum_data->'units') AS u,
jsonb_array_elements(u->'concepts') AS concept
WHERE curriculum_data->>'title' = %s
AND grade = %s
AND subject = %s
AND u->>'name' = %s
ORDER BY 1;
""", (curriculum, int(grade), subject, unit))
return [row[0] for row in cur.fetchall() if row[0]]
\ No newline at end of file
......@@ -6,7 +6,7 @@ load_dotenv()
def setup_mcq_table(drop_existing_table: bool = False):
"""
Sets up the mcq_questions table in the database.
Sets up the mcq_questions table with the final, comprehensive schema.
"""
try:
conn = psycopg2.connect(
......@@ -24,30 +24,41 @@ def setup_mcq_table(drop_existing_table: bool = False):
cur.execute("DROP TABLE IF EXISTS mcq_questions CASCADE;")
print("Table dropped.")
print("Creating mcq_questions table...")
# --- THIS IS THE UPDATED TABLE SCHEMA ---
print("Creating mcq_questions table with the NEW COMPREHENSIVE schema...")
# --- THIS IS THE FULLY UPDATED TABLE SCHEMA ---
cur.execute("""
CREATE TABLE IF NOT EXISTS mcq_questions (
id SERIAL PRIMARY KEY,
grade INTEGER NOT NULL,
is_arabic BOOLEAN NOT NULL, -- <-- ADDED THIS LINE
curriculum TEXT,
grade TEXT NOT NULL,
subject TEXT NOT NULL,
unit TEXT NOT NULL,
concept TEXT NOT NULL,
question_text TEXT NOT NULL,
question_type TEXT,
difficulty_level INTEGER,
is_arabic BOOLEAN NOT NULL,
correct_answer TEXT NOT NULL,
wrong_answer_1 TEXT NOT NULL,
wrong_answer_2 TEXT NOT NULL,
wrong_answer_3 TEXT NOT NULL,
wrong_answer_1 TEXT,
wrong_answer_2 TEXT,
wrong_answer_3 TEXT,
wrong_answer_4 TEXT,
question_image_url TEXT, -- Placeholder for MinIO URL
correct_image_url TEXT, -- Placeholder for MinIO URL
wrong_image_url_1 TEXT, -- Placeholder for MinIO URL
wrong_image_url_2 TEXT, -- Placeholder for MinIO URL
wrong_image_url_3 TEXT, -- Placeholder for MinIO URL
wrong_image_url_4 TEXT, -- Placeholder for MinIO URL
hint TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
""")
print("Creating indexes on mcq_questions table...")
# --- THIS IS THE UPDATED INDEX ---
# --- UPDATED INDEX TO INCLUDE CURRICULUM ---
cur.execute("""
CREATE INDEX IF NOT EXISTS idx_mcq_topic
ON mcq_questions(grade, is_arabic, subject, unit, concept); -- <-- ADDED is_arabic
ON mcq_questions(curriculum, grade, is_arabic, subject, unit, concept);
""")
print("MCQ table setup complete.")
......@@ -60,7 +71,7 @@ def setup_mcq_table(drop_existing_table: bool = False):
print("Database connection closed.")
if __name__ == "__main__":
# To apply the changes, it's best to drop and recreate the table.
# Be careful if you have existing data you want to keep!
print("Creating MCQ table...")
setup_mcq_table(drop_existing_table=False)
\ No newline at end of file
# To apply the new schema, run this script.
# Set drop_existing_table=True to ensure a clean recreation.
print("Setting up the new MCQ table structure...")
setup_mcq_table(drop_existing_table=True)
\ No newline at end of file
......@@ -5,124 +5,33 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dynamic Quiz Generator</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
max-width: 800px;
margin: 40px auto;
padding: 20px;
background-color: #f9f9f9;
color: #333;
line-height: 1.6;
}
.container {
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}
h1 {
text-align: center;
color: #2c3e50;
}
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
.form-group {
display: flex;
flex-direction: column;
}
.form-group.full-width {
grid-column: 1 / -1;
}
label {
margin-bottom: 5px;
font-weight: bold;
color: #555;
}
input[type="text"], input[type="number"] {
width: 95%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
font-size: 16px;
}
.checkbox-group {
display: flex;
align-items: center;
gap: 10px;
margin-top: 10px;
}
button {
display: block;
width: 100%;
padding: 12px;
font-size: 16px;
font-weight: bold;
background: #007bff;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background 0.2s;
}
button:hover {
background: #0056b3;
}
button:disabled {
background: #95a5a6;
cursor: not-allowed;
}
button.secondary {
background: #28a745;
margin-top: 10px;
}
button.secondary:hover {
background: #218838;
}
button.secondary:disabled {
background: #95a5a6;
}
.status {
margin-top: 20px;
padding: 15px;
border-radius: 5px;
font-weight: bold;
display: none;
}
.status.success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
.status.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
.status.processing { background-color: #e7f3ff; color: #004085; border: 1px solid #b3d9ff; }
#resultsContainer {
margin-top: 30px;
border-top: 2px solid #eee;
padding-top: 20px;
}
.quiz-question {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 5px;
padding: 20px;
margin-bottom: 20px;
}
.quiz-question h3 {
margin-top: 0;
color: #343a40;
}
.quiz-question ul {
list-style-type: none;
padding: 0;
}
.quiz-question li {
padding: 8px 0;
}
.correct-answer {
color: #28a745;
font-weight: bold;
}
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; max-width: 800px; margin: 40px auto; padding: 20px; background-color: #f9f9f9; color: #333; line-height: 1.6; }
.container { background: white; padding: 30px; border-radius: 8px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); }
h1 { text-align: center; color: #2c3e50; }
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px; }
.form-group { display: flex; flex-direction: column; }
.form-group.full-width { grid-column: 1 / -1; }
label { margin-bottom: 5px; font-weight: bold; color: #555; }
input[type="number"], select { width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 5px; font-size: 16px; background-color: #fff; }
select:disabled { background-color: #e9ecef; }
.checkbox-group { display: flex; align-items: center; gap: 10px; margin-top: 10px; }
.button-group { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 10px; }
button { display: block; width: 100%; padding: 12px; font-size: 16px; font-weight: bold; color: white; border: none; border-radius: 5px; cursor: pointer; transition: background 0.2s; }
#generateButton { background: #007bff; }
#generateButton:hover { background: #0056b3; }
#downloadButton { background: #6c757d; }
#downloadButton:hover { background: #5a6268; }
button:disabled { background: #95a5a6; cursor: not-allowed; }
.status { margin-top: 20px; padding: 15px; border-radius: 5px; font-weight: bold; display: none; }
.status.success { background-color: #d4edda; color: #155724; }
.status.error { background-color: #f8d7da; color: #721c24; }
.status.processing { background-color: #e7f3ff; color: #004085; }
#resultsContainer { margin-top: 30px; border-top: 2px solid #eee; padding-top: 20px; }
.quiz-question { background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 5px; padding: 20px; margin-bottom: 20px; }
.quiz-question h3 { margin-top: 0; color: #343a40; }
.quiz-question ul { list-style-type: none; padding: 0; }
.quiz-question li { padding: 8px 0; }
.correct-answer { color: #28a745; font-weight: bold; }
</style>
</head>
<body>
......@@ -130,21 +39,25 @@
<h1>Dynamic Quiz Generator</h1>
<div class="form-grid">
<div class="form-group full-width">
<label for="curriculumSelect">Curriculum:</label>
<select id="curriculumSelect"></select>
</div>
<div class="form-group">
<label for="gradeInput">Grade:</label>
<input type="number" id="gradeInput" value="4">
<label for="gradeSelect">Grade:</label>
<select id="gradeSelect" disabled></select>
</div>
<div class="form-group">
<label for="subjectInput">Subject:</label>
<input type="text" id="subjectInput" value="Science">
<label for="subjectSelect">Subject:</label>
<select id="subjectSelect" disabled></select>
</div>
<div class="form-group full-width">
<label for="unitInput">Unit:</label>
<input type="text" id="unitInput" placeholder="e.g., الوحدة الأولى: الأنظمة الحية">
<label for="unitSelect">Unit:</label>
<select id="unitSelect" disabled></select>
</div>
<div class="form-group full-width">
<label for="conceptInput">Concept:</label>
<input type="text" id="conceptInput" placeholder="e.g., المفهوم الأول: التكيف والبقاء">
<label for="conceptSelect">Concept:</label>
<select id="conceptSelect" disabled></select>
</div>
<div class="form-group">
<label for="countInput">Number of Questions:</label>
......@@ -159,48 +72,135 @@
</div>
</div>
<button id="generateButton">Generate Dynamic Quiz</button>
<button id="downloadCsvButton" class="secondary" style="display:none;">Download Quiz as CSV</button>
<div class="button-group">
<button id="generateButton">Generate Dynamic Quiz</button>
<button id="downloadButton" style="display:none;">Download as CSV</button>
</div>
<div id="status"></div>
<div id="resultsContainer" style="display:none;"></div>
</div>
<script>
const gradeInput = document.getElementById('gradeInput');
const subjectInput = document.getElementById('subjectInput');
const unitInput = document.getElementById('unitInput');
const conceptInput = document.getElementById('conceptInput');
// --- UI Elements ---
const curriculumSelect = document.getElementById('curriculumSelect');
const gradeSelect = document.getElementById('gradeSelect');
const subjectSelect = document.getElementById('subjectSelect');
const unitSelect = document.getElementById('unitSelect');
const conceptSelect = document.getElementById('conceptSelect');
const countInput = document.getElementById('countInput');
const isArabicInput = document.getElementById('isArabicInput');
const generateButton = document.getElementById('generateButton');
const downloadCsvButton = document.getElementById('downloadCsvButton');
const downloadButton = document.getElementById('downloadButton');
const statusDiv = document.getElementById('status');
const resultsContainer = document.getElementById('resultsContainer');
let currentQuizData = null;
let currentQuizData = [];
// --- Helper Functions ---
const showStatus = (message, type) => {
statusDiv.className = `status ${type}`;
statusDiv.textContent = message;
statusDiv.style.display = 'block';
};
const populateDropdown = (selectElement, options, placeholder) => {
selectElement.innerHTML = `<option value="">-- ${placeholder} --</option>`;
options.forEach(option => {
const opt = document.createElement('option');
opt.value = option;
opt.textContent = option;
selectElement.appendChild(opt);
});
selectElement.disabled = options.length === 0;
};
const fetchOptions = async (url) => {
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`Server error: ${response.status}`);
const data = await response.json();
return data.options || [];
} catch (error) {
showStatus(`Failed to load options: ${error.message}`, 'error');
return [];
}
};
// --- Cascading Dropdown Logic ---
document.addEventListener('DOMContentLoaded', async () => {
const curricula = await fetchOptions('/quiz/options/curricula');
populateDropdown(curriculumSelect, curricula, 'Select a Curriculum');
});
curriculumSelect.addEventListener('change', async () => {
const curriculum = curriculumSelect.value;
populateDropdown(gradeSelect, [], 'Loading...');
populateDropdown(subjectSelect, [], 'Select Grade First');
populateDropdown(unitSelect, [], 'Select Subject First');
populateDropdown(conceptSelect, [], 'Select Unit First');
if (curriculum) {
const grades = await fetchOptions(`/quiz/options/grades?curriculum=${encodeURIComponent(curriculum)}`);
populateDropdown(gradeSelect, grades, 'Select a Grade');
}
});
gradeSelect.addEventListener('change', async () => {
const curriculum = curriculumSelect.value;
const grade = gradeSelect.value;
populateDropdown(subjectSelect, [], 'Loading...');
populateDropdown(unitSelect, [], 'Select Subject First');
populateDropdown(conceptSelect, [], 'Select Unit First');
if (grade) {
const subjects = await fetchOptions(`/quiz/options/subjects?curriculum=${encodeURIComponent(curriculum)}&grade=${encodeURIComponent(grade)}`);
populateDropdown(subjectSelect, subjects, 'Select a Subject');
}
});
subjectSelect.addEventListener('change', async () => {
const curriculum = curriculumSelect.value;
const grade = gradeSelect.value;
const subject = subjectSelect.value;
populateDropdown(unitSelect, [], 'Loading...');
populateDropdown(conceptSelect, [], 'Select Unit First');
if (subject) {
const units = await fetchOptions(`/quiz/options/units?curriculum=${encodeURIComponent(curriculum)}&grade=${encodeURIComponent(grade)}&subject=${encodeURIComponent(subject)}`);
populateDropdown(unitSelect, units, 'Select a Unit');
}
});
unitSelect.addEventListener('change', async () => {
const curriculum = curriculumSelect.value;
const grade = gradeSelect.value;
const subject = subjectSelect.value;
const unit = unitSelect.value;
populateDropdown(conceptSelect, [], 'Loading...');
if (unit) {
const concepts = await fetchOptions(`/quiz/options/concepts?curriculum=${encodeURIComponent(curriculum)}&grade=${encodeURIComponent(grade)}&subject=${encodeURIComponent(subject)}&unit=${encodeURIComponent(unit)}`);
populateDropdown(conceptSelect, concepts, 'Select a Concept');
}
});
// --- Main Action: Generate Quiz ---
generateButton.addEventListener('click', async () => {
const grade = gradeInput.value;
const subject = subjectInput.value;
const unit = unitInput.value.trim();
const concept = conceptInput.value.trim();
const count = countInput.value;
const isArabic = isArabicInput.checked;
const [curriculum, grade, subject, unit, concept, count, isArabic] = [
curriculumSelect.value, gradeSelect.value, subjectSelect.value,
unitSelect.value, conceptSelect.value, countInput.value, isArabicInput.checked
];
if (!unit || !concept) {
showStatus('Please fill in both the Unit and Concept fields.', 'error');
if (!curriculum || !grade || !subject || !unit || !concept) {
showStatus('Please make a selection in all dropdown menus.', 'error');
return;
}
showStatus('Generating dynamic quiz... This may take a moment.', 'processing');
generateButton.disabled = true;
downloadCsvButton.style.display = 'none';
resultsContainer.innerHTML = '';
downloadButton.style.display = 'none';
resultsContainer.style.display = 'none';
currentQuizData = null;
currentQuizData = [];
const formData = new FormData();
formData.append('curriculum', curriculum);
formData.append('grade', grade);
formData.append('subject', subject);
formData.append('unit', unit);
......@@ -209,22 +209,18 @@
formData.append('is_arabic', isArabic);
try {
const response = await fetch('/quiz/dynamic', {
method: 'POST',
body: formData,
});
const responseData = await response.json();
const response = await fetch('/quiz/dynamic', { method: 'POST', body: formData });
if (!response.ok) {
throw new Error(responseData.detail || `Server error: ${response.statusText}`);
const err = await response.json().catch(() => ({ detail: `Server error: ${response.status}`}));
throw new Error(err.detail);
}
const responseData = await response.json();
showStatus(responseData.message, 'success');
currentQuizData = responseData.quiz;
displayQuizResults(responseData.quiz);
downloadCsvButton.style.display = 'block';
if (currentQuizData && currentQuizData.length > 0) {
downloadButton.style.display = 'block';
}
displayQuizResults(currentQuizData);
} catch (error) {
showStatus(`An error occurred: ${error.message}`, 'error');
} finally {
......@@ -232,77 +228,65 @@
}
});
downloadCsvButton.addEventListener('click', () => {
// --- Download and Display Logic (RESTORED) ---
downloadButton.addEventListener('click', () => {
if (!currentQuizData || currentQuizData.length === 0) {
showStatus('No quiz data available to download.', 'error');
return;
}
const csvContent = convertQuizToCSV(currentQuizData);
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const blob = new Blob(["\uFEFF" + csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
const filename = `quiz_${subjectInput.value}_${Date.now()}.csv`;
const filename = `quiz_${subjectSelect.value.replace(/ /g, '_')}.csv`;
link.setAttribute('href', url);
link.setAttribute('download', filename);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
showStatus('Quiz downloaded successfully!', 'success');
});
function convertQuizToCSV(quiz) {
const headers = ['Question Number', 'Question Text', 'Correct Answer', 'Wrong Answer 1', 'Wrong Answer 2', 'Wrong Answer 3'];
const headers = ['Question', 'Correct Answer', 'Wrong Answer 1', 'Wrong Answer 2', 'Wrong Answer 3'];
const escapeCSV = (str) => {
if (str === null || str === undefined) return '';
const stringValue = String(str);
if (stringValue.includes(',') || stringValue.includes('"') || stringValue.includes('\n')) {
return `"${stringValue.replace(/"/g, '""')}"`;
let result = str.toString();
if (result.includes(',') || result.includes('"') || result.includes('\n')) {
result = '"' + result.replace(/"/g, '""') + '"';
}
return stringValue;
return result;
};
const rows = quiz.map((q, index) => {
return [
index + 1,
escapeCSV(q.question_text),
escapeCSV(q.correct_answer),
escapeCSV(q.wrong_answer_1),
escapeCSV(q.wrong_answer_2),
escapeCSV(q.wrong_answer_3)
].join(',');
});
return '\uFEFF' + [headers.join(','), ...rows].join('\n');
const rows = quiz.map(q => [
escapeCSV(q.question_text),
escapeCSV(q.correct_answer),
escapeCSV(q.wrong_answer_1),
escapeCSV(q.wrong_answer_2),
escapeCSV(q.wrong_answer_3)
].join(','));
return [headers.join(','), ...rows].join('\n');
}
function displayQuizResults(quiz) {
resultsContainer.innerHTML = ''; // Clear previous results
if (!quiz || quiz.length === 0) {
resultsContainer.innerHTML = '<p>No questions were returned.</p>';
resultsContainer.style.display = 'block';
return;
}
quiz.forEach((question, index) => {
const questionDiv = document.createElement('div');
questionDiv.className = 'quiz-question';
const questionTitle = document.createElement('h3');
questionTitle.textContent = `Question ${index + 1}: ${question.question_text}`;
const optionsList = document.createElement('ul');
const options = [
{ text: question.correct_answer, isCorrect: true },
{ text: question.wrong_answer_1, isCorrect: false },
{ text: question.wrong_answer_2, isCorrect: false },
{ text: question.wrong_answer_3, isCorrect: false }
].sort(() => Math.random() - 0.5);
].filter(opt => opt.text).sort(() => Math.random() - 0.5);
options.forEach(option => {
const listItem = document.createElement('li');
listItem.textContent = option.text;
......@@ -312,20 +296,13 @@
}
optionsList.appendChild(listItem);
});
questionDiv.appendChild(questionTitle);
questionDiv.appendChild(optionsList);
resultsContainer.appendChild(questionDiv);
});
resultsContainer.style.display = 'block';
}
function showStatus(message, type) {
statusDiv.className = `status ${type}`;
statusDiv.textContent = message;
statusDiv.style.display = 'block';
}
</script>
</body>
</html>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment