Spaces:
Sleeping
Sleeping
Commit
·
704792a
1
Parent(s):
7a1ebee
Init quiz mode
Browse files- .DS_Store +0 -0
- app.py +1 -0
- helpers/models.py +27 -0
- routes/quiz.py +490 -0
- static/index.html +101 -0
- static/quiz.js +546 -0
- static/script.js +12 -0
- static/styles.css +334 -0
- utils/rag/rag.py +23 -0
.DS_Store
CHANGED
|
Binary files a/.DS_Store and b/.DS_Store differ
|
|
|
app.py
CHANGED
|
@@ -12,6 +12,7 @@ import routes.chats as _routes_chat
|
|
| 12 |
import routes.sessions as _routes_sessions
|
| 13 |
import routes.health as _routes_health
|
| 14 |
import routes.analytics as _routes_analytics
|
|
|
|
| 15 |
|
| 16 |
# Local dev
|
| 17 |
# if __name__ == "__main__":
|
|
|
|
| 12 |
import routes.sessions as _routes_sessions
|
| 13 |
import routes.health as _routes_health
|
| 14 |
import routes.analytics as _routes_analytics
|
| 15 |
+
import routes.quiz as _routes_quiz
|
| 16 |
|
| 17 |
# Local dev
|
| 18 |
# if __name__ == "__main__":
|
helpers/models.py
CHANGED
|
@@ -30,6 +30,8 @@ class ChatHistoryResponse(BaseModel):
|
|
| 30 |
|
| 31 |
class MessageResponse(BaseModel):
|
| 32 |
message: str
|
|
|
|
|
|
|
| 33 |
|
| 34 |
class UploadResponse(BaseModel):
|
| 35 |
job_id: str
|
|
@@ -60,4 +62,29 @@ class StatusUpdateResponse(BaseModel):
|
|
| 60 |
message: str
|
| 61 |
progress: Optional[int] = None
|
| 62 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
|
|
|
|
| 30 |
|
| 31 |
class MessageResponse(BaseModel):
|
| 32 |
message: str
|
| 33 |
+
quiz: Optional[Dict[str, Any]] = None
|
| 34 |
+
results: Optional[Dict[str, Any]] = None
|
| 35 |
|
| 36 |
class UploadResponse(BaseModel):
|
| 37 |
job_id: str
|
|
|
|
| 62 |
message: str
|
| 63 |
progress: Optional[int] = None
|
| 64 |
|
| 65 |
+
class QuizQuestionResponse(BaseModel):
|
| 66 |
+
type: str # "mcq" or "self_reflect"
|
| 67 |
+
question: str
|
| 68 |
+
options: Optional[List[str]] = None # For MCQ questions
|
| 69 |
+
correct_answer: Optional[int] = None # For MCQ questions
|
| 70 |
+
topic: str
|
| 71 |
+
complexity: str
|
| 72 |
+
|
| 73 |
+
class QuizResponse(BaseModel):
|
| 74 |
+
quiz_id: str
|
| 75 |
+
user_id: str
|
| 76 |
+
project_id: str
|
| 77 |
+
questions: List[QuizQuestionResponse]
|
| 78 |
+
time_limit: int
|
| 79 |
+
documents: List[str]
|
| 80 |
+
created_at: str
|
| 81 |
+
status: str
|
| 82 |
+
|
| 83 |
+
class QuizResultResponse(BaseModel):
|
| 84 |
+
questions: List[Dict[str, Any]]
|
| 85 |
+
total_score: float
|
| 86 |
+
correct_count: int
|
| 87 |
+
partial_count: int
|
| 88 |
+
incorrect_count: int
|
| 89 |
+
|
| 90 |
|
routes/quiz.py
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# routes/quiz.py
|
| 2 |
+
import json, time, uuid, asyncio
|
| 3 |
+
from datetime import datetime, timezone
|
| 4 |
+
from typing import Any, Dict, List, Optional
|
| 5 |
+
from fastapi import Form, HTTPException
|
| 6 |
+
|
| 7 |
+
from helpers.setup import app, rag, logger, nvidia_rotator, gemini_rotator
|
| 8 |
+
from helpers.models import MessageResponse, QuizResponse, QuizResultResponse
|
| 9 |
+
from utils.api.router import select_model, generate_answer_with_model, NVIDIA_SMALL, NVIDIA_MEDIUM, NVIDIA_LARGE
|
| 10 |
+
from utils.analytics import get_analytics_tracker
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
@app.post("/quiz/create", response_model=MessageResponse)
|
| 14 |
+
async def create_quiz(
|
| 15 |
+
user_id: str = Form(...),
|
| 16 |
+
project_id: str = Form(...),
|
| 17 |
+
questions_input: str = Form(...),
|
| 18 |
+
time_limit: str = Form(...),
|
| 19 |
+
documents: str = Form(...)
|
| 20 |
+
):
|
| 21 |
+
"""Create a quiz from selected documents"""
|
| 22 |
+
try:
|
| 23 |
+
# Parse documents
|
| 24 |
+
selected_docs = json.loads(documents)
|
| 25 |
+
time_limit_minutes = int(time_limit)
|
| 26 |
+
|
| 27 |
+
logger.info(f"[QUIZ] Creating quiz for user {user_id}, project {project_id}")
|
| 28 |
+
logger.info(f"[QUIZ] Documents: {selected_docs}")
|
| 29 |
+
logger.info(f"[QUIZ] Questions input: {questions_input}")
|
| 30 |
+
logger.info(f"[QUIZ] Time limit: {time_limit_minutes} minutes")
|
| 31 |
+
|
| 32 |
+
# Step 1: Parse user input to determine question counts
|
| 33 |
+
question_config = await parse_question_input(questions_input, nvidia_rotator)
|
| 34 |
+
logger.info(f"[QUIZ] Parsed question config: {question_config}")
|
| 35 |
+
|
| 36 |
+
# Step 2: Get document summaries and key topics
|
| 37 |
+
document_summaries = await get_document_summaries(user_id, project_id, selected_docs)
|
| 38 |
+
key_topics = await extract_key_topics(document_summaries, nvidia_rotator)
|
| 39 |
+
logger.info(f"[QUIZ] Extracted {len(key_topics)} key topics")
|
| 40 |
+
|
| 41 |
+
# Step 3: Create question generation plan
|
| 42 |
+
generation_plan = await create_question_plan(
|
| 43 |
+
question_config, key_topics, nvidia_rotator
|
| 44 |
+
)
|
| 45 |
+
logger.info(f"[QUIZ] Created generation plan with {len(generation_plan)} tasks")
|
| 46 |
+
|
| 47 |
+
# Step 4: Generate questions and answers
|
| 48 |
+
questions = await generate_questions_and_answers(
|
| 49 |
+
generation_plan, document_summaries, nvidia_rotator
|
| 50 |
+
)
|
| 51 |
+
logger.info(f"[QUIZ] Generated {len(questions)} questions")
|
| 52 |
+
|
| 53 |
+
# Step 5: Create quiz record
|
| 54 |
+
quiz_id = str(uuid.uuid4())
|
| 55 |
+
quiz_data = {
|
| 56 |
+
"quiz_id": quiz_id,
|
| 57 |
+
"user_id": user_id,
|
| 58 |
+
"project_id": project_id,
|
| 59 |
+
"questions": questions,
|
| 60 |
+
"time_limit": time_limit_minutes,
|
| 61 |
+
"documents": selected_docs,
|
| 62 |
+
"created_at": datetime.now(timezone.utc),
|
| 63 |
+
"status": "ready"
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
# Store quiz in database
|
| 67 |
+
rag.db["quizzes"].insert_one(quiz_data)
|
| 68 |
+
|
| 69 |
+
return MessageResponse(message="Quiz created successfully", quiz=quiz_data)
|
| 70 |
+
|
| 71 |
+
except Exception as e:
|
| 72 |
+
logger.error(f"[QUIZ] Quiz creation failed: {e}")
|
| 73 |
+
raise HTTPException(500, detail=f"Failed to create quiz: {str(e)}")
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
@app.post("/quiz/submit", response_model=MessageResponse)
|
| 77 |
+
async def submit_quiz(
|
| 78 |
+
user_id: str = Form(...),
|
| 79 |
+
project_id: str = Form(...),
|
| 80 |
+
quiz_id: str = Form(...),
|
| 81 |
+
answers: str = Form(...)
|
| 82 |
+
):
|
| 83 |
+
"""Submit quiz answers and get results"""
|
| 84 |
+
try:
|
| 85 |
+
# Parse answers
|
| 86 |
+
user_answers = json.loads(answers)
|
| 87 |
+
|
| 88 |
+
# Get quiz data
|
| 89 |
+
quiz = rag.db["quizzes"].find_one({
|
| 90 |
+
"quiz_id": quiz_id,
|
| 91 |
+
"user_id": user_id,
|
| 92 |
+
"project_id": project_id
|
| 93 |
+
})
|
| 94 |
+
|
| 95 |
+
if not quiz:
|
| 96 |
+
raise HTTPException(404, detail="Quiz not found")
|
| 97 |
+
|
| 98 |
+
# Mark answers
|
| 99 |
+
results = await mark_quiz_answers(quiz["questions"], user_answers, nvidia_rotator)
|
| 100 |
+
|
| 101 |
+
# Store results
|
| 102 |
+
result_data = {
|
| 103 |
+
"quiz_id": quiz_id,
|
| 104 |
+
"user_id": user_id,
|
| 105 |
+
"project_id": project_id,
|
| 106 |
+
"answers": user_answers,
|
| 107 |
+
"results": results,
|
| 108 |
+
"submitted_at": datetime.now(timezone.utc)
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
rag.db["quiz_results"].insert_one(result_data)
|
| 112 |
+
|
| 113 |
+
return MessageResponse(message="Quiz submitted successfully", results=results)
|
| 114 |
+
|
| 115 |
+
except Exception as e:
|
| 116 |
+
logger.error(f"[QUIZ] Quiz submission failed: {e}")
|
| 117 |
+
raise HTTPException(500, detail=f"Failed to submit quiz: {str(e)}")
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
async def parse_question_input(questions_input: str, nvidia_rotator) -> Dict[str, int]:
|
| 121 |
+
"""Parse user input to determine MCQ and self-reflect question counts"""
|
| 122 |
+
system_prompt = """You are an expert at parsing user requests for quiz questions.
|
| 123 |
+
Given a user's input about how many questions they want, extract the number of MCQ and self-reflect questions.
|
| 124 |
+
|
| 125 |
+
Return ONLY a JSON object with this exact format:
|
| 126 |
+
{
|
| 127 |
+
"mcq": <number of multiple choice questions>,
|
| 128 |
+
"sr": <number of self-reflect questions>
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
If the user doesn't specify types, assume they want MCQ questions.
|
| 132 |
+
If they say "total" or "questions", split them roughly 70% MCQ and 30% self-reflect.
|
| 133 |
+
"""
|
| 134 |
+
|
| 135 |
+
user_prompt = f"User input: {questions_input}\n\nExtract the question counts:"
|
| 136 |
+
|
| 137 |
+
try:
|
| 138 |
+
# Use NVIDIA_SMALL for parsing
|
| 139 |
+
response = await generate_answer_with_model(
|
| 140 |
+
selection={"provider": "nvidia", "model": NVIDIA_SMALL},
|
| 141 |
+
system_prompt=system_prompt,
|
| 142 |
+
user_prompt=user_prompt,
|
| 143 |
+
gemini_rotator=gemini_rotator,
|
| 144 |
+
nvidia_rotator=nvidia_rotator,
|
| 145 |
+
user_id="system",
|
| 146 |
+
context="quiz_parsing"
|
| 147 |
+
)
|
| 148 |
+
|
| 149 |
+
# Parse JSON response
|
| 150 |
+
config = json.loads(response.strip())
|
| 151 |
+
return {
|
| 152 |
+
"mcq": int(config.get("mcq", 0)),
|
| 153 |
+
"sr": int(config.get("sr", 0))
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
except Exception as e:
|
| 157 |
+
logger.warning(f"[QUIZ] Failed to parse question input: {e}")
|
| 158 |
+
# Fallback: assume 10 MCQ questions
|
| 159 |
+
return {"mcq": 10, "sr": 0}
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
async def get_document_summaries(user_id: str, project_id: str, documents: List[str]) -> str:
|
| 163 |
+
"""Get summaries from selected documents"""
|
| 164 |
+
summaries = []
|
| 165 |
+
|
| 166 |
+
for doc in documents:
|
| 167 |
+
try:
|
| 168 |
+
# Get file summary
|
| 169 |
+
file_data = rag.get_file_summary(user_id=user_id, project_id=project_id, filename=doc)
|
| 170 |
+
if file_data and file_data.get("summary"):
|
| 171 |
+
summaries.append(f"[{doc}] {file_data['summary']}")
|
| 172 |
+
|
| 173 |
+
# Get additional chunks for more context
|
| 174 |
+
chunks = rag.get_file_chunks(user_id=user_id, project_id=project_id, filename=doc, limit=20)
|
| 175 |
+
if chunks:
|
| 176 |
+
chunk_text = "\n".join([chunk.get("content", "") for chunk in chunks[:10]])
|
| 177 |
+
summaries.append(f"[{doc} - Additional Content] {chunk_text[:2000]}...")
|
| 178 |
+
|
| 179 |
+
except Exception as e:
|
| 180 |
+
logger.warning(f"[QUIZ] Failed to get summary for {doc}: {e}")
|
| 181 |
+
continue
|
| 182 |
+
|
| 183 |
+
return "\n\n".join(summaries)
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
async def extract_key_topics(document_summaries: str, nvidia_rotator) -> List[str]:
|
| 187 |
+
"""Extract key topics from document summaries"""
|
| 188 |
+
system_prompt = """You are an expert at analyzing educational content and extracting key topics.
|
| 189 |
+
Given document summaries, identify the main topics and concepts that would be suitable for quiz questions.
|
| 190 |
+
|
| 191 |
+
Return a JSON array of topic strings, focusing on:
|
| 192 |
+
- Main concepts and theories
|
| 193 |
+
- Important facts and details
|
| 194 |
+
- Key processes and procedures
|
| 195 |
+
- Critical thinking points
|
| 196 |
+
|
| 197 |
+
Limit to 10-15 most important topics.
|
| 198 |
+
"""
|
| 199 |
+
|
| 200 |
+
user_prompt = f"Document summaries:\n{document_summaries}\n\nExtract key topics:"
|
| 201 |
+
|
| 202 |
+
try:
|
| 203 |
+
# Use NVIDIA_MEDIUM for topic extraction
|
| 204 |
+
response = await generate_answer_with_model(
|
| 205 |
+
selection={"provider": "qwen", "model": NVIDIA_MEDIUM},
|
| 206 |
+
system_prompt=system_prompt,
|
| 207 |
+
user_prompt=user_prompt,
|
| 208 |
+
gemini_rotator=gemini_rotator,
|
| 209 |
+
nvidia_rotator=nvidia_rotator,
|
| 210 |
+
user_id="system",
|
| 211 |
+
context="quiz_topic_extraction"
|
| 212 |
+
)
|
| 213 |
+
|
| 214 |
+
topics = json.loads(response.strip())
|
| 215 |
+
return topics if isinstance(topics, list) else []
|
| 216 |
+
|
| 217 |
+
except Exception as e:
|
| 218 |
+
logger.warning(f"[QUIZ] Failed to extract topics: {e}")
|
| 219 |
+
return ["General Knowledge", "Key Concepts", "Important Details"]
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
async def create_question_plan(question_config: Dict, key_topics: List[str], nvidia_rotator) -> List[Dict]:
|
| 223 |
+
"""Create a plan for question generation"""
|
| 224 |
+
system_prompt = """You are an expert at creating quiz question generation plans.
|
| 225 |
+
Given question counts and topics, create a detailed plan for generating questions.
|
| 226 |
+
|
| 227 |
+
Return a JSON array of task objects, each with:
|
| 228 |
+
- description: what type of questions to generate
|
| 229 |
+
- complexity: "high", "medium", or "low"
|
| 230 |
+
- topic: which topic to focus on
|
| 231 |
+
- number_mcq: number of MCQ questions for this task
|
| 232 |
+
- number_sr: number of self-reflect questions for this task
|
| 233 |
+
|
| 234 |
+
Distribute questions across topics and complexity levels.
|
| 235 |
+
"""
|
| 236 |
+
|
| 237 |
+
user_prompt = f"""Question config: {question_config}
|
| 238 |
+
Key topics: {key_topics}
|
| 239 |
+
|
| 240 |
+
Create a generation plan:"""
|
| 241 |
+
|
| 242 |
+
try:
|
| 243 |
+
# Use NVIDIA_MEDIUM for planning
|
| 244 |
+
response = await generate_answer_with_model(
|
| 245 |
+
selection={"provider": "qwen", "model": NVIDIA_MEDIUM},
|
| 246 |
+
system_prompt=system_prompt,
|
| 247 |
+
user_prompt=user_prompt,
|
| 248 |
+
gemini_rotator=gemini_rotator,
|
| 249 |
+
nvidia_rotator=nvidia_rotator,
|
| 250 |
+
user_id="system",
|
| 251 |
+
context="quiz_planning"
|
| 252 |
+
)
|
| 253 |
+
|
| 254 |
+
plan = json.loads(response.strip())
|
| 255 |
+
return plan if isinstance(plan, list) else []
|
| 256 |
+
|
| 257 |
+
except Exception as e:
|
| 258 |
+
logger.warning(f"[QUIZ] Failed to create plan: {e}")
|
| 259 |
+
# Fallback plan
|
| 260 |
+
return [{
|
| 261 |
+
"description": "Generate general questions",
|
| 262 |
+
"complexity": "medium",
|
| 263 |
+
"topic": "General",
|
| 264 |
+
"number_mcq": question_config.get("mcq", 0),
|
| 265 |
+
"number_sr": question_config.get("sr", 0)
|
| 266 |
+
}]
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
async def generate_questions_and_answers(plan: List[Dict], document_summaries: str, nvidia_rotator) -> List[Dict]:
|
| 270 |
+
"""Generate questions and answers based on the plan"""
|
| 271 |
+
all_questions = []
|
| 272 |
+
|
| 273 |
+
for task in plan:
|
| 274 |
+
try:
|
| 275 |
+
# Generate questions for this task
|
| 276 |
+
questions = await generate_task_questions(task, document_summaries, nvidia_rotator)
|
| 277 |
+
all_questions.extend(questions)
|
| 278 |
+
|
| 279 |
+
except Exception as e:
|
| 280 |
+
logger.warning(f"[QUIZ] Failed to generate questions for task: {e}")
|
| 281 |
+
continue
|
| 282 |
+
|
| 283 |
+
return all_questions
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
async def generate_task_questions(task: Dict, document_summaries: str, nvidia_rotator) -> List[Dict]:
|
| 287 |
+
"""Generate questions for a specific task"""
|
| 288 |
+
system_prompt = f"""You are an expert quiz question generator.
|
| 289 |
+
Generate {task.get('number_mcq', 0)} multiple choice questions and {task.get('number_sr', 0)} self-reflection questions.
|
| 290 |
+
|
| 291 |
+
For MCQ questions:
|
| 292 |
+
- Create clear, well-structured questions
|
| 293 |
+
- Provide 4 answer options (A, B, C, D)
|
| 294 |
+
- Mark the correct answer
|
| 295 |
+
- Make distractors plausible but incorrect
|
| 296 |
+
|
| 297 |
+
For self-reflection questions:
|
| 298 |
+
- Create open-ended questions that require critical thinking
|
| 299 |
+
- Focus on analysis, evaluation, and synthesis
|
| 300 |
+
- Encourage personal reflection and application
|
| 301 |
+
|
| 302 |
+
Return a JSON array of question objects with this format:
|
| 303 |
+
{{
|
| 304 |
+
"type": "mcq" or "self_reflect",
|
| 305 |
+
"question": "question text",
|
| 306 |
+
"options": ["option1", "option2", "option3", "option4"] (for MCQ only),
|
| 307 |
+
"correct_answer": 0 (index for MCQ, null for self_reflect),
|
| 308 |
+
"topic": "topic name",
|
| 309 |
+
"complexity": "high/medium/low"
|
| 310 |
+
}}
|
| 311 |
+
"""
|
| 312 |
+
|
| 313 |
+
user_prompt = f"""Task: {task}
|
| 314 |
+
Document content: {document_summaries[:3000]}...
|
| 315 |
+
|
| 316 |
+
Generate questions:"""
|
| 317 |
+
|
| 318 |
+
try:
|
| 319 |
+
# Use appropriate model based on complexity
|
| 320 |
+
if task.get("complexity") == "high":
|
| 321 |
+
model_selection = {"provider": "nvidia_large", "model": NVIDIA_LARGE}
|
| 322 |
+
else:
|
| 323 |
+
model_selection = {"provider": "nvidia", "model": NVIDIA_SMALL}
|
| 324 |
+
|
| 325 |
+
response = await generate_answer_with_model(
|
| 326 |
+
selection=model_selection,
|
| 327 |
+
system_prompt=system_prompt,
|
| 328 |
+
user_prompt=user_prompt,
|
| 329 |
+
gemini_rotator=gemini_rotator,
|
| 330 |
+
nvidia_rotator=nvidia_rotator,
|
| 331 |
+
user_id="system",
|
| 332 |
+
context="quiz_question_generation"
|
| 333 |
+
)
|
| 334 |
+
|
| 335 |
+
questions = json.loads(response.strip())
|
| 336 |
+
return questions if isinstance(questions, list) else []
|
| 337 |
+
|
| 338 |
+
except Exception as e:
|
| 339 |
+
logger.warning(f"[QUIZ] Failed to generate questions for task: {e}")
|
| 340 |
+
return []
|
| 341 |
+
|
| 342 |
+
|
| 343 |
+
async def mark_quiz_answers(questions: List[Dict], user_answers: Dict, nvidia_rotator) -> Dict:
|
| 344 |
+
"""Mark quiz answers and provide feedback"""
|
| 345 |
+
results = {
|
| 346 |
+
"questions": [],
|
| 347 |
+
"total_score": 0,
|
| 348 |
+
"correct_count": 0,
|
| 349 |
+
"partial_count": 0,
|
| 350 |
+
"incorrect_count": 0
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
for i, question in enumerate(questions):
|
| 354 |
+
user_answer = user_answers.get(str(i))
|
| 355 |
+
question_result = await mark_single_question(question, user_answer, nvidia_rotator)
|
| 356 |
+
results["questions"].append(question_result)
|
| 357 |
+
|
| 358 |
+
# Update counts
|
| 359 |
+
if question_result["status"] == "correct":
|
| 360 |
+
results["correct_count"] += 1
|
| 361 |
+
results["total_score"] += 1
|
| 362 |
+
elif question_result["status"] == "partial":
|
| 363 |
+
results["partial_count"] += 1
|
| 364 |
+
results["total_score"] += 0.5
|
| 365 |
+
else:
|
| 366 |
+
results["incorrect_count"] += 1
|
| 367 |
+
|
| 368 |
+
return results
|
| 369 |
+
|
| 370 |
+
|
| 371 |
+
async def mark_single_question(question: Dict, user_answer: Any, nvidia_rotator) -> Dict:
|
| 372 |
+
"""Mark a single question"""
|
| 373 |
+
result = {
|
| 374 |
+
"question": question["question"],
|
| 375 |
+
"type": question["type"],
|
| 376 |
+
"user_answer": user_answer,
|
| 377 |
+
"status": "incorrect",
|
| 378 |
+
"explanation": ""
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
if question["type"] == "mcq":
|
| 382 |
+
# MCQ marking
|
| 383 |
+
correct_index = question.get("correct_answer", 0)
|
| 384 |
+
if user_answer is not None and int(user_answer) == correct_index:
|
| 385 |
+
result["status"] = "correct"
|
| 386 |
+
result["explanation"] = "Correct answer!"
|
| 387 |
+
else:
|
| 388 |
+
result["status"] = "incorrect"
|
| 389 |
+
result["explanation"] = await generate_mcq_explanation(question, user_answer, nvidia_rotator)
|
| 390 |
+
|
| 391 |
+
result["correct_answer"] = correct_index
|
| 392 |
+
result["options"] = question.get("options", [])
|
| 393 |
+
|
| 394 |
+
elif question["type"] == "self_reflect":
|
| 395 |
+
# Self-reflection marking using AI
|
| 396 |
+
result["status"] = await evaluate_self_reflect_answer(question, user_answer, nvidia_rotator)
|
| 397 |
+
result["explanation"] = await generate_self_reflect_feedback(question, user_answer, result["status"], nvidia_rotator)
|
| 398 |
+
|
| 399 |
+
return result
|
| 400 |
+
|
| 401 |
+
|
| 402 |
+
async def generate_mcq_explanation(question: Dict, user_answer: Any, nvidia_rotator) -> str:
|
| 403 |
+
"""Generate explanation for MCQ answer"""
|
| 404 |
+
system_prompt = """You are an expert tutor. Explain why the user's answer was wrong and why the correct answer is right.
|
| 405 |
+
Be concise but helpful. Focus on the key concept being tested."""
|
| 406 |
+
|
| 407 |
+
user_prompt = f"""Question: {question['question']}
|
| 408 |
+
Options: {question.get('options', [])}
|
| 409 |
+
User's answer: {user_answer}
|
| 410 |
+
Correct answer: {question.get('correct_answer', 0)}
|
| 411 |
+
|
| 412 |
+
Explain why the user was wrong:"""
|
| 413 |
+
|
| 414 |
+
try:
|
| 415 |
+
response = await generate_answer_with_model(
|
| 416 |
+
selection={"provider": "nvidia", "model": NVIDIA_SMALL},
|
| 417 |
+
system_prompt=system_prompt,
|
| 418 |
+
user_prompt=user_prompt,
|
| 419 |
+
gemini_rotator=gemini_rotator,
|
| 420 |
+
nvidia_rotator=nvidia_rotator,
|
| 421 |
+
user_id="system",
|
| 422 |
+
context="quiz_explanation"
|
| 423 |
+
)
|
| 424 |
+
return response
|
| 425 |
+
except Exception as e:
|
| 426 |
+
logger.warning(f"[QUIZ] Failed to generate MCQ explanation: {e}")
|
| 427 |
+
return "The correct answer is different from your choice. Please review the material."
|
| 428 |
+
|
| 429 |
+
|
| 430 |
+
async def evaluate_self_reflect_answer(question: Dict, user_answer: str, nvidia_rotator) -> str:
|
| 431 |
+
"""Evaluate self-reflection answer using AI"""
|
| 432 |
+
system_prompt = """You are an expert educator evaluating student responses.
|
| 433 |
+
Evaluate the student's answer and determine if it's correct, partially correct, or incorrect.
|
| 434 |
+
|
| 435 |
+
Return only one word: "correct", "partial", or "incorrect"
|
| 436 |
+
"""
|
| 437 |
+
|
| 438 |
+
user_prompt = f"""Question: {question['question']}
|
| 439 |
+
Student's answer: {user_answer or 'No answer provided'}
|
| 440 |
+
|
| 441 |
+
Evaluate the answer:"""
|
| 442 |
+
|
| 443 |
+
try:
|
| 444 |
+
response = await generate_answer_with_model(
|
| 445 |
+
selection={"provider": "nvidia", "model": NVIDIA_SMALL},
|
| 446 |
+
system_prompt=system_prompt,
|
| 447 |
+
user_prompt=user_prompt,
|
| 448 |
+
gemini_rotator=gemini_rotator,
|
| 449 |
+
nvidia_rotator=nvidia_rotator,
|
| 450 |
+
user_id="system",
|
| 451 |
+
context="quiz_evaluation"
|
| 452 |
+
)
|
| 453 |
+
|
| 454 |
+
response = response.strip().lower()
|
| 455 |
+
if response in ["correct", "partial", "incorrect"]:
|
| 456 |
+
return response
|
| 457 |
+
else:
|
| 458 |
+
return "partial" # Default to partial if unclear
|
| 459 |
+
|
| 460 |
+
except Exception as e:
|
| 461 |
+
logger.warning(f"[QUIZ] Failed to evaluate self-reflect answer: {e}")
|
| 462 |
+
return "partial"
|
| 463 |
+
|
| 464 |
+
|
| 465 |
+
async def generate_self_reflect_feedback(question: Dict, user_answer: str, status: str, nvidia_rotator) -> str:
|
| 466 |
+
"""Generate feedback for self-reflection answer"""
|
| 467 |
+
system_prompt = """You are an expert tutor providing feedback on student responses.
|
| 468 |
+
Provide constructive feedback that helps the student understand their answer and improve.
|
| 469 |
+
Be encouraging but honest about areas for improvement."""
|
| 470 |
+
|
| 471 |
+
user_prompt = f"""Question: {question['question']}
|
| 472 |
+
Student's answer: {user_answer or 'No answer provided'}
|
| 473 |
+
Evaluation: {status}
|
| 474 |
+
|
| 475 |
+
Provide feedback:"""
|
| 476 |
+
|
| 477 |
+
try:
|
| 478 |
+
response = await generate_answer_with_model(
|
| 479 |
+
selection={"provider": "nvidia", "model": NVIDIA_SMALL},
|
| 480 |
+
system_prompt=system_prompt,
|
| 481 |
+
user_prompt=user_prompt,
|
| 482 |
+
gemini_rotator=gemini_rotator,
|
| 483 |
+
nvidia_rotator=nvidia_rotator,
|
| 484 |
+
user_id="system",
|
| 485 |
+
context="quiz_feedback"
|
| 486 |
+
)
|
| 487 |
+
return response
|
| 488 |
+
except Exception as e:
|
| 489 |
+
logger.warning(f"[QUIZ] Failed to generate self-reflect feedback: {e}")
|
| 490 |
+
return "Thank you for your response. Please review the material for a more complete answer."
|
static/index.html
CHANGED
|
@@ -319,6 +319,13 @@
|
|
| 319 |
</svg>
|
| 320 |
<span>Search</span>
|
| 321 |
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 322 |
</div>
|
| 323 |
</div>
|
| 324 |
<div class="chat-hint" id="chat-hint">
|
|
@@ -433,6 +440,99 @@
|
|
| 433 |
</div>
|
| 434 |
</div>
|
| 435 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 436 |
<!-- Loading Overlay -->
|
| 437 |
<div id="loading-overlay" class="loading-overlay hidden">
|
| 438 |
<div class="loading-content">
|
|
@@ -451,5 +551,6 @@
|
|
| 451 |
<script src="/static/script.js"></script>
|
| 452 |
<script src="/static/sessions.js"></script>
|
| 453 |
<script src="/static/analytics.js"></script>
|
|
|
|
| 454 |
</body>
|
| 455 |
</html>
|
|
|
|
| 319 |
</svg>
|
| 320 |
<span>Search</span>
|
| 321 |
</button>
|
| 322 |
+
<button id="quiz-link" class="action-pill" title="Create quiz from selected documents">
|
| 323 |
+
<svg viewBox="0 0 24 24" aria-hidden="true">
|
| 324 |
+
<path d="M9 12l2 2 4-4"></path>
|
| 325 |
+
<path d="M21 12c0 4.97-4.03 9-9 9s-9-4.03-9-9 4.03-9 9-9 9 4.03 9 9z"></path>
|
| 326 |
+
</svg>
|
| 327 |
+
<span>Quiz</span>
|
| 328 |
+
</button>
|
| 329 |
</div>
|
| 330 |
</div>
|
| 331 |
<div class="chat-hint" id="chat-hint">
|
|
|
|
| 440 |
</div>
|
| 441 |
</div>
|
| 442 |
|
| 443 |
+
<!-- Quiz Setup Modal -->
|
| 444 |
+
<div id="quiz-setup-modal" class="modal hidden" aria-hidden="true" role="dialog" aria-labelledby="quiz-setup-title">
|
| 445 |
+
<div class="modal-content">
|
| 446 |
+
<div class="modal-header">
|
| 447 |
+
<h2 id="quiz-setup-title">Create Quiz</h2>
|
| 448 |
+
<p class="modal-subtitle">Configure your quiz settings</p>
|
| 449 |
+
</div>
|
| 450 |
+
<form id="quiz-setup-form">
|
| 451 |
+
<!-- Step 1: Question Configuration -->
|
| 452 |
+
<div class="quiz-step" id="quiz-step-1">
|
| 453 |
+
<div class="form-group">
|
| 454 |
+
<label>How many questions would you like?</label>
|
| 455 |
+
<textarea id="quiz-questions-input" placeholder="Tell me how many MCQ and self-reflect questions you want. For example: 'I want 25 multiple choice questions and 10 self-reflection questions' or 'Give me 30 questions total, split between MCQ and self-reflect questions'" rows="3" required></textarea>
|
| 456 |
+
</div>
|
| 457 |
+
</div>
|
| 458 |
+
|
| 459 |
+
<!-- Step 2: Time Limit -->
|
| 460 |
+
<div class="quiz-step" id="quiz-step-2" style="display:none;">
|
| 461 |
+
<div class="form-group">
|
| 462 |
+
<label>Time Limit (minutes)</label>
|
| 463 |
+
<input type="number" id="quiz-time-limit" placeholder="Enter time limit in minutes (0 for no limit)" min="0" value="0">
|
| 464 |
+
<small>Enter 0 for no time limit</small>
|
| 465 |
+
</div>
|
| 466 |
+
</div>
|
| 467 |
+
|
| 468 |
+
<!-- Step 3: Document Selection -->
|
| 469 |
+
<div class="quiz-step" id="quiz-step-3" style="display:none;">
|
| 470 |
+
<div class="form-group">
|
| 471 |
+
<label>Select Documents</label>
|
| 472 |
+
<div id="quiz-document-list" class="document-checkbox-list">
|
| 473 |
+
<!-- Documents will be populated here -->
|
| 474 |
+
</div>
|
| 475 |
+
</div>
|
| 476 |
+
</div>
|
| 477 |
+
|
| 478 |
+
<div class="form-actions">
|
| 479 |
+
<button type="button" id="quiz-prev-step" class="btn-secondary" style="display:none;">Previous</button>
|
| 480 |
+
<button type="button" id="quiz-next-step" class="btn-primary">Next</button>
|
| 481 |
+
<button type="button" id="quiz-cancel" class="btn-secondary">Cancel</button>
|
| 482 |
+
<button type="submit" id="quiz-submit" class="btn-primary" style="display:none;">Create Quiz</button>
|
| 483 |
+
</div>
|
| 484 |
+
</form>
|
| 485 |
+
</div>
|
| 486 |
+
</div>
|
| 487 |
+
|
| 488 |
+
<!-- Quiz Taking Modal -->
|
| 489 |
+
<div id="quiz-modal" class="modal hidden" aria-hidden="true" role="dialog" aria-labelledby="quiz-title">
|
| 490 |
+
<div class="modal-content quiz-modal-content">
|
| 491 |
+
<div class="modal-header">
|
| 492 |
+
<h2 id="quiz-title">Quiz</h2>
|
| 493 |
+
<div class="quiz-timer" id="quiz-timer">Time: --:--</div>
|
| 494 |
+
</div>
|
| 495 |
+
<div class="quiz-content">
|
| 496 |
+
<div class="quiz-progress">
|
| 497 |
+
<div class="quiz-progress-bar">
|
| 498 |
+
<div class="quiz-progress-fill" id="quiz-progress-fill"></div>
|
| 499 |
+
</div>
|
| 500 |
+
<span class="quiz-progress-text" id="quiz-progress-text">Question 1 of 10</span>
|
| 501 |
+
</div>
|
| 502 |
+
|
| 503 |
+
<div class="quiz-question" id="quiz-question">
|
| 504 |
+
<!-- Question content will be populated here -->
|
| 505 |
+
</div>
|
| 506 |
+
|
| 507 |
+
<div class="quiz-answers" id="quiz-answers">
|
| 508 |
+
<!-- Answer options will be populated here -->
|
| 509 |
+
</div>
|
| 510 |
+
|
| 511 |
+
<div class="quiz-navigation">
|
| 512 |
+
<button id="quiz-prev" class="btn-secondary" disabled>Previous</button>
|
| 513 |
+
<button id="quiz-next" class="btn-primary">Next</button>
|
| 514 |
+
<button id="quiz-submit" class="btn-primary" style="display:none;">Submit Quiz</button>
|
| 515 |
+
</div>
|
| 516 |
+
</div>
|
| 517 |
+
</div>
|
| 518 |
+
</div>
|
| 519 |
+
|
| 520 |
+
<!-- Quiz Results Modal -->
|
| 521 |
+
<div id="quiz-results-modal" class="modal hidden" aria-hidden="true" role="dialog" aria-labelledby="quiz-results-title">
|
| 522 |
+
<div class="modal-content">
|
| 523 |
+
<div class="modal-header">
|
| 524 |
+
<h2 id="quiz-results-title">Quiz Results</h2>
|
| 525 |
+
<p class="modal-subtitle">Your quiz has been completed</p>
|
| 526 |
+
</div>
|
| 527 |
+
<div class="quiz-results-content" id="quiz-results-content">
|
| 528 |
+
<!-- Results will be populated here -->
|
| 529 |
+
</div>
|
| 530 |
+
<div class="form-actions">
|
| 531 |
+
<button type="button" id="quiz-results-close" class="btn-primary">Close</button>
|
| 532 |
+
</div>
|
| 533 |
+
</div>
|
| 534 |
+
</div>
|
| 535 |
+
|
| 536 |
<!-- Loading Overlay -->
|
| 537 |
<div id="loading-overlay" class="loading-overlay hidden">
|
| 538 |
<div class="loading-content">
|
|
|
|
| 551 |
<script src="/static/script.js"></script>
|
| 552 |
<script src="/static/sessions.js"></script>
|
| 553 |
<script src="/static/analytics.js"></script>
|
| 554 |
+
<script src="/static/quiz.js"></script>
|
| 555 |
</body>
|
| 556 |
</html>
|
static/quiz.js
ADDED
|
@@ -0,0 +1,546 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ────────────────────────────── static/quiz.js ──────────────────────────────
|
| 2 |
+
(function() {
|
| 3 |
+
// Quiz state
|
| 4 |
+
let currentQuiz = null;
|
| 5 |
+
let currentQuestionIndex = 0;
|
| 6 |
+
let quizAnswers = {};
|
| 7 |
+
let quizTimer = null;
|
| 8 |
+
let timeRemaining = 0;
|
| 9 |
+
let quizSetupStep = 1;
|
| 10 |
+
|
| 11 |
+
// DOM elements
|
| 12 |
+
const quizLink = document.getElementById('quiz-link');
|
| 13 |
+
const quizSetupModal = document.getElementById('quiz-setup-modal');
|
| 14 |
+
const quizModal = document.getElementById('quiz-modal');
|
| 15 |
+
const quizResultsModal = document.getElementById('quiz-results-modal');
|
| 16 |
+
const quizSetupForm = document.getElementById('quiz-setup-form');
|
| 17 |
+
const quizQuestionsInput = document.getElementById('quiz-questions-input');
|
| 18 |
+
const quizTimeLimit = document.getElementById('quiz-time-limit');
|
| 19 |
+
const quizDocumentList = document.getElementById('quiz-document-list');
|
| 20 |
+
const quizPrevStep = document.getElementById('quiz-prev-step');
|
| 21 |
+
const quizNextStep = document.getElementById('quiz-next-step');
|
| 22 |
+
const quizCancel = document.getElementById('quiz-cancel');
|
| 23 |
+
const quizSubmit = document.getElementById('quiz-submit');
|
| 24 |
+
const quizTimerElement = document.getElementById('quiz-timer');
|
| 25 |
+
const quizProgressFill = document.getElementById('quiz-progress-fill');
|
| 26 |
+
const quizProgressText = document.getElementById('quiz-progress-text');
|
| 27 |
+
const quizQuestion = document.getElementById('quiz-question');
|
| 28 |
+
const quizAnswers = document.getElementById('quiz-answers');
|
| 29 |
+
const quizPrev = document.getElementById('quiz-prev');
|
| 30 |
+
const quizNext = document.getElementById('quiz-next');
|
| 31 |
+
const quizSubmitBtn = document.getElementById('quiz-submit');
|
| 32 |
+
const quizResultsContent = document.getElementById('quiz-results-content');
|
| 33 |
+
const quizResultsClose = document.getElementById('quiz-results-close');
|
| 34 |
+
|
| 35 |
+
// Initialize
|
| 36 |
+
init();
|
| 37 |
+
|
| 38 |
+
function init() {
|
| 39 |
+
setupEventListeners();
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
function setupEventListeners() {
|
| 43 |
+
// Quiz link
|
| 44 |
+
if (quizLink) {
|
| 45 |
+
quizLink.addEventListener('click', (e) => {
|
| 46 |
+
e.preventDefault();
|
| 47 |
+
openQuizSetup();
|
| 48 |
+
});
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
// Quiz setup form
|
| 52 |
+
if (quizSetupForm) {
|
| 53 |
+
quizSetupForm.addEventListener('submit', handleQuizSetupSubmit);
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
// Quiz setup navigation
|
| 57 |
+
if (quizNextStep) {
|
| 58 |
+
quizNextStep.addEventListener('click', nextQuizStep);
|
| 59 |
+
}
|
| 60 |
+
if (quizPrevStep) {
|
| 61 |
+
quizPrevStep.addEventListener('click', prevQuizStep);
|
| 62 |
+
}
|
| 63 |
+
if (quizCancel) {
|
| 64 |
+
quizCancel.addEventListener('click', closeQuizSetup);
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
// Quiz navigation
|
| 68 |
+
if (quizPrev) {
|
| 69 |
+
quizPrev.addEventListener('click', prevQuestion);
|
| 70 |
+
}
|
| 71 |
+
if (quizNext) {
|
| 72 |
+
quizNext.addEventListener('click', nextQuestion);
|
| 73 |
+
}
|
| 74 |
+
if (quizSubmitBtn) {
|
| 75 |
+
quizSubmitBtn.addEventListener('click', submitQuiz);
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
// Quiz results
|
| 79 |
+
if (quizResultsClose) {
|
| 80 |
+
quizResultsClose.addEventListener('click', closeQuizResults);
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
// Close modals on outside click
|
| 84 |
+
document.addEventListener('click', (e) => {
|
| 85 |
+
if (e.target.classList.contains('modal')) {
|
| 86 |
+
closeAllQuizModals();
|
| 87 |
+
}
|
| 88 |
+
});
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
async function openQuizSetup() {
|
| 92 |
+
const user = window.__sb_get_user();
|
| 93 |
+
if (!user) {
|
| 94 |
+
alert('Please sign in to create a quiz');
|
| 95 |
+
window.__sb_show_auth_modal();
|
| 96 |
+
return;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
const currentProject = window.__sb_get_current_project && window.__sb_get_current_project();
|
| 100 |
+
if (!currentProject) {
|
| 101 |
+
alert('Please select a project first');
|
| 102 |
+
return;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
// Load available documents
|
| 106 |
+
await loadQuizDocuments();
|
| 107 |
+
|
| 108 |
+
// Reset form
|
| 109 |
+
quizSetupStep = 1;
|
| 110 |
+
updateQuizSetupStep();
|
| 111 |
+
|
| 112 |
+
// Show modal
|
| 113 |
+
quizSetupModal.classList.remove('hidden');
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
async function loadQuizDocuments() {
|
| 117 |
+
const user = window.__sb_get_user();
|
| 118 |
+
const currentProject = window.__sb_get_current_project && window.__sb_get_current_project();
|
| 119 |
+
|
| 120 |
+
if (!user || !currentProject) return;
|
| 121 |
+
|
| 122 |
+
try {
|
| 123 |
+
const res = await fetch(`/files?user_id=${encodeURIComponent(user.user_id)}&project_id=${encodeURIComponent(currentProject.project_id)}`);
|
| 124 |
+
if (!res.ok) return;
|
| 125 |
+
|
| 126 |
+
const data = await res.json();
|
| 127 |
+
const files = data.files || [];
|
| 128 |
+
|
| 129 |
+
// Clear existing documents
|
| 130 |
+
quizDocumentList.innerHTML = '';
|
| 131 |
+
|
| 132 |
+
if (files.length === 0) {
|
| 133 |
+
quizDocumentList.innerHTML = '<div class="muted">No documents available. Please upload documents first.</div>';
|
| 134 |
+
return;
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
// Create document checkboxes
|
| 138 |
+
files.forEach((file, index) => {
|
| 139 |
+
const item = document.createElement('div');
|
| 140 |
+
item.className = 'document-checkbox-item';
|
| 141 |
+
item.innerHTML = `
|
| 142 |
+
<input type="checkbox" id="quiz-doc-${index}" value="${file.filename}" checked>
|
| 143 |
+
<label for="quiz-doc-${index}">${file.filename}</label>
|
| 144 |
+
`;
|
| 145 |
+
quizDocumentList.appendChild(item);
|
| 146 |
+
});
|
| 147 |
+
} catch (error) {
|
| 148 |
+
console.error('Failed to load documents:', error);
|
| 149 |
+
quizDocumentList.innerHTML = '<div class="muted">Failed to load documents.</div>';
|
| 150 |
+
}
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
function updateQuizSetupStep() {
|
| 154 |
+
// Hide all steps
|
| 155 |
+
document.querySelectorAll('.quiz-step').forEach(step => {
|
| 156 |
+
step.style.display = 'none';
|
| 157 |
+
});
|
| 158 |
+
|
| 159 |
+
// Show current step
|
| 160 |
+
const currentStep = document.getElementById(`quiz-step-${quizSetupStep}`);
|
| 161 |
+
if (currentStep) {
|
| 162 |
+
currentStep.style.display = 'block';
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
// Update navigation buttons
|
| 166 |
+
quizPrevStep.style.display = quizSetupStep > 1 ? 'inline-flex' : 'none';
|
| 167 |
+
quizNextStep.style.display = quizSetupStep < 3 ? 'inline-flex' : 'none';
|
| 168 |
+
quizSubmit.style.display = quizSetupStep === 3 ? 'inline-flex' : 'none';
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
function nextQuizStep() {
|
| 172 |
+
if (quizSetupStep < 3) {
|
| 173 |
+
quizSetupStep++;
|
| 174 |
+
updateQuizSetupStep();
|
| 175 |
+
}
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
function prevQuizStep() {
|
| 179 |
+
if (quizSetupStep > 1) {
|
| 180 |
+
quizSetupStep--;
|
| 181 |
+
updateQuizSetupStep();
|
| 182 |
+
}
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
function closeQuizSetup() {
|
| 186 |
+
quizSetupModal.classList.add('hidden');
|
| 187 |
+
quizSetupStep = 1;
|
| 188 |
+
updateQuizSetupStep();
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
async function handleQuizSetupSubmit(e) {
|
| 192 |
+
e.preventDefault();
|
| 193 |
+
|
| 194 |
+
const user = window.__sb_get_user();
|
| 195 |
+
const currentProject = window.__sb_get_current_project && window.__sb_get_current_project();
|
| 196 |
+
|
| 197 |
+
if (!user || !currentProject) {
|
| 198 |
+
alert('Please sign in and select a project');
|
| 199 |
+
return;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
// Get form data
|
| 203 |
+
const questionsInput = quizQuestionsInput.value.trim();
|
| 204 |
+
const timeLimit = parseInt(quizTimeLimit.value) || 0;
|
| 205 |
+
|
| 206 |
+
// Get selected documents
|
| 207 |
+
const selectedDocs = Array.from(quizDocumentList.querySelectorAll('input[type="checkbox"]:checked'))
|
| 208 |
+
.map(input => input.value);
|
| 209 |
+
|
| 210 |
+
if (selectedDocs.length === 0) {
|
| 211 |
+
alert('Please select at least one document');
|
| 212 |
+
return;
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
if (!questionsInput) {
|
| 216 |
+
alert('Please specify how many questions you want');
|
| 217 |
+
return;
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
// Show loading
|
| 221 |
+
showLoading('Creating quiz...');
|
| 222 |
+
|
| 223 |
+
try {
|
| 224 |
+
// Create quiz
|
| 225 |
+
const formData = new FormData();
|
| 226 |
+
formData.append('user_id', user.user_id);
|
| 227 |
+
formData.append('project_id', currentProject.project_id);
|
| 228 |
+
formData.append('questions_input', questionsInput);
|
| 229 |
+
formData.append('time_limit', timeLimit.toString());
|
| 230 |
+
formData.append('documents', JSON.stringify(selectedDocs));
|
| 231 |
+
|
| 232 |
+
const response = await fetch('/quiz/create', {
|
| 233 |
+
method: 'POST',
|
| 234 |
+
body: formData
|
| 235 |
+
});
|
| 236 |
+
|
| 237 |
+
const data = await response.json();
|
| 238 |
+
|
| 239 |
+
if (response.ok) {
|
| 240 |
+
hideLoading();
|
| 241 |
+
closeQuizSetup();
|
| 242 |
+
|
| 243 |
+
// Start quiz
|
| 244 |
+
currentQuiz = data.quiz;
|
| 245 |
+
currentQuestionIndex = 0;
|
| 246 |
+
quizAnswers = {};
|
| 247 |
+
timeRemaining = timeLimit * 60; // Convert to seconds
|
| 248 |
+
|
| 249 |
+
startQuiz();
|
| 250 |
+
} else {
|
| 251 |
+
hideLoading();
|
| 252 |
+
alert(data.detail || 'Failed to create quiz');
|
| 253 |
+
}
|
| 254 |
+
} catch (error) {
|
| 255 |
+
hideLoading();
|
| 256 |
+
console.error('Quiz creation failed:', error);
|
| 257 |
+
alert('Failed to create quiz. Please try again.');
|
| 258 |
+
}
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
function startQuiz() {
|
| 262 |
+
// Show quiz modal
|
| 263 |
+
quizModal.classList.remove('hidden');
|
| 264 |
+
|
| 265 |
+
// Start timer if time limit is set
|
| 266 |
+
if (timeRemaining > 0) {
|
| 267 |
+
startQuizTimer();
|
| 268 |
+
} else {
|
| 269 |
+
quizTimerElement.textContent = 'No time limit';
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
// Show first question
|
| 273 |
+
showQuestion(0);
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
function startQuizTimer() {
|
| 277 |
+
updateTimerDisplay();
|
| 278 |
+
|
| 279 |
+
quizTimer = setInterval(() => {
|
| 280 |
+
timeRemaining--;
|
| 281 |
+
updateTimerDisplay();
|
| 282 |
+
|
| 283 |
+
if (timeRemaining <= 0) {
|
| 284 |
+
clearInterval(quizTimer);
|
| 285 |
+
timeUp();
|
| 286 |
+
}
|
| 287 |
+
}, 1000);
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
function updateTimerDisplay() {
|
| 291 |
+
const minutes = Math.floor(timeRemaining / 60);
|
| 292 |
+
const seconds = timeRemaining % 60;
|
| 293 |
+
const timeString = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
| 294 |
+
|
| 295 |
+
quizTimerElement.textContent = `Time: ${timeString}`;
|
| 296 |
+
|
| 297 |
+
// Add warning classes
|
| 298 |
+
quizTimerElement.classList.remove('warning', 'danger');
|
| 299 |
+
if (timeRemaining <= 60) {
|
| 300 |
+
quizTimerElement.classList.add('danger');
|
| 301 |
+
} else if (timeRemaining <= 300) { // 5 minutes
|
| 302 |
+
quizTimerElement.classList.add('warning');
|
| 303 |
+
}
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
function timeUp() {
|
| 307 |
+
alert('Time Up!');
|
| 308 |
+
submitQuiz();
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
function showQuestion(index) {
|
| 312 |
+
if (!currentQuiz || !currentQuiz.questions || index >= currentQuiz.questions.length) {
|
| 313 |
+
return;
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
const question = currentQuiz.questions[index];
|
| 317 |
+
currentQuestionIndex = index;
|
| 318 |
+
|
| 319 |
+
// Update progress
|
| 320 |
+
const progress = ((index + 1) / currentQuiz.questions.length) * 100;
|
| 321 |
+
quizProgressFill.style.width = `${progress}%`;
|
| 322 |
+
quizProgressText.textContent = `Question ${index + 1} of ${currentQuiz.questions.length}`;
|
| 323 |
+
|
| 324 |
+
// Show question
|
| 325 |
+
quizQuestion.innerHTML = `
|
| 326 |
+
<h3>Question ${index + 1}</h3>
|
| 327 |
+
<p>${question.question}</p>
|
| 328 |
+
`;
|
| 329 |
+
|
| 330 |
+
// Show answers
|
| 331 |
+
if (question.type === 'mcq') {
|
| 332 |
+
showMCQAnswers(question);
|
| 333 |
+
} else if (question.type === 'self_reflect') {
|
| 334 |
+
showSelfReflectAnswer(question);
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
// Update navigation
|
| 338 |
+
quizPrev.disabled = index === 0;
|
| 339 |
+
quizNext.style.display = index < currentQuiz.questions.length - 1 ? 'inline-flex' : 'none';
|
| 340 |
+
quizSubmitBtn.style.display = index === currentQuiz.questions.length - 1 ? 'inline-flex' : 'none';
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
function showMCQAnswers(question) {
|
| 344 |
+
quizAnswers.innerHTML = '';
|
| 345 |
+
|
| 346 |
+
question.options.forEach((option, index) => {
|
| 347 |
+
const optionDiv = document.createElement('div');
|
| 348 |
+
optionDiv.className = 'quiz-answer-option';
|
| 349 |
+
optionDiv.innerHTML = `
|
| 350 |
+
<input type="radio" name="question-${currentQuestionIndex}" value="${index}" id="option-${currentQuestionIndex}-${index}">
|
| 351 |
+
<label for="option-${currentQuestionIndex}-${index}">${option}</label>
|
| 352 |
+
`;
|
| 353 |
+
|
| 354 |
+
// Check if already answered
|
| 355 |
+
if (quizAnswers[currentQuestionIndex] !== undefined) {
|
| 356 |
+
const radio = optionDiv.querySelector('input[type="radio"]');
|
| 357 |
+
radio.checked = quizAnswers[currentQuestionIndex] === index;
|
| 358 |
+
if (radio.checked) {
|
| 359 |
+
optionDiv.classList.add('selected');
|
| 360 |
+
}
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
// Add click handler
|
| 364 |
+
optionDiv.addEventListener('click', () => {
|
| 365 |
+
// Remove selection from other options
|
| 366 |
+
quizAnswers.querySelectorAll('.quiz-answer-option').forEach(opt => {
|
| 367 |
+
opt.classList.remove('selected');
|
| 368 |
+
});
|
| 369 |
+
|
| 370 |
+
// Select this option
|
| 371 |
+
optionDiv.classList.add('selected');
|
| 372 |
+
const radio = optionDiv.querySelector('input[type="radio"]');
|
| 373 |
+
radio.checked = true;
|
| 374 |
+
|
| 375 |
+
// Save answer
|
| 376 |
+
quizAnswers[currentQuestionIndex] = index;
|
| 377 |
+
});
|
| 378 |
+
|
| 379 |
+
quizAnswers.appendChild(optionDiv);
|
| 380 |
+
});
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
function showSelfReflectAnswer(question) {
|
| 384 |
+
quizAnswers.innerHTML = `
|
| 385 |
+
<textarea class="quiz-text-answer" id="self-reflect-${currentQuestionIndex}" placeholder="Enter your answer here...">${quizAnswers[currentQuestionIndex] || ''}</textarea>
|
| 386 |
+
`;
|
| 387 |
+
|
| 388 |
+
const textarea = quizAnswers.querySelector('textarea');
|
| 389 |
+
textarea.addEventListener('input', (e) => {
|
| 390 |
+
quizAnswers[currentQuestionIndex] = e.target.value;
|
| 391 |
+
});
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
function prevQuestion() {
|
| 395 |
+
if (currentQuestionIndex > 0) {
|
| 396 |
+
showQuestion(currentQuestionIndex - 1);
|
| 397 |
+
}
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
function nextQuestion() {
|
| 401 |
+
if (currentQuestionIndex < currentQuiz.questions.length - 1) {
|
| 402 |
+
showQuestion(currentQuestionIndex + 1);
|
| 403 |
+
}
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
async function submitQuiz() {
|
| 407 |
+
if (quizTimer) {
|
| 408 |
+
clearInterval(quizTimer);
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
+
// Show loading
|
| 412 |
+
showLoading('Submitting quiz...');
|
| 413 |
+
|
| 414 |
+
try {
|
| 415 |
+
const user = window.__sb_get_user();
|
| 416 |
+
const currentProject = window.__sb_get_current_project && window.__sb_get_current_project();
|
| 417 |
+
|
| 418 |
+
const formData = new FormData();
|
| 419 |
+
formData.append('user_id', user.user_id);
|
| 420 |
+
formData.append('project_id', currentProject.project_id);
|
| 421 |
+
formData.append('quiz_id', currentQuiz.quiz_id);
|
| 422 |
+
formData.append('answers', JSON.stringify(quizAnswers));
|
| 423 |
+
|
| 424 |
+
const response = await fetch('/quiz/submit', {
|
| 425 |
+
method: 'POST',
|
| 426 |
+
body: formData
|
| 427 |
+
});
|
| 428 |
+
|
| 429 |
+
const data = await response.json();
|
| 430 |
+
|
| 431 |
+
if (response.ok) {
|
| 432 |
+
hideLoading();
|
| 433 |
+
closeQuizModal();
|
| 434 |
+
showQuizResults(data.results);
|
| 435 |
+
} else {
|
| 436 |
+
hideLoading();
|
| 437 |
+
alert(data.detail || 'Failed to submit quiz');
|
| 438 |
+
}
|
| 439 |
+
} catch (error) {
|
| 440 |
+
hideLoading();
|
| 441 |
+
console.error('Quiz submission failed:', error);
|
| 442 |
+
alert('Failed to submit quiz. Please try again.');
|
| 443 |
+
}
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
function showQuizResults(results) {
|
| 447 |
+
// Show results summary
|
| 448 |
+
const totalQuestions = results.questions.length;
|
| 449 |
+
const correctAnswers = results.questions.filter(q => q.status === 'correct').length;
|
| 450 |
+
const partialAnswers = results.questions.filter(q => q.status === 'partial').length;
|
| 451 |
+
const incorrectAnswers = results.questions.filter(q => q.status === 'incorrect').length;
|
| 452 |
+
const score = Math.round((correctAnswers + partialAnswers * 0.5) / totalQuestions * 100);
|
| 453 |
+
|
| 454 |
+
quizResultsContent.innerHTML = `
|
| 455 |
+
<div class="quiz-result-summary">
|
| 456 |
+
<div class="quiz-result-stat">
|
| 457 |
+
<div class="quiz-result-stat-value">${score}%</div>
|
| 458 |
+
<div class="quiz-result-stat-label">Score</div>
|
| 459 |
+
</div>
|
| 460 |
+
<div class="quiz-result-stat">
|
| 461 |
+
<div class="quiz-result-stat-value">${correctAnswers}</div>
|
| 462 |
+
<div class="quiz-result-stat-label">Correct</div>
|
| 463 |
+
</div>
|
| 464 |
+
<div class="quiz-result-stat">
|
| 465 |
+
<div class="quiz-result-stat-value">${partialAnswers}</div>
|
| 466 |
+
<div class="quiz-result-stat-label">Partial</div>
|
| 467 |
+
</div>
|
| 468 |
+
<div class="quiz-result-stat">
|
| 469 |
+
<div class="quiz-result-stat-value">${incorrectAnswers}</div>
|
| 470 |
+
<div class="quiz-result-stat-label">Incorrect</div>
|
| 471 |
+
</div>
|
| 472 |
+
</div>
|
| 473 |
+
|
| 474 |
+
<div class="quiz-result-questions">
|
| 475 |
+
${results.questions.map((question, index) => `
|
| 476 |
+
<div class="quiz-result-question">
|
| 477 |
+
<div class="quiz-result-question-header">
|
| 478 |
+
<div class="quiz-result-question-title">Question ${index + 1}</div>
|
| 479 |
+
<div class="quiz-result-question-status ${question.status}">${question.status}</div>
|
| 480 |
+
</div>
|
| 481 |
+
<div class="quiz-result-question-text">${question.question}</div>
|
| 482 |
+
|
| 483 |
+
${question.type === 'mcq' ? `
|
| 484 |
+
<div class="quiz-result-answer">
|
| 485 |
+
<div class="quiz-result-answer-label">Your Answer:</div>
|
| 486 |
+
<div class="quiz-result-answer-content">${question.options[question.user_answer] || 'No answer'}</div>
|
| 487 |
+
</div>
|
| 488 |
+
<div class="quiz-result-answer">
|
| 489 |
+
<div class="quiz-result-answer-label">Correct Answer:</div>
|
| 490 |
+
<div class="quiz-result-answer-content">${question.options[question.correct_answer]}</div>
|
| 491 |
+
</div>
|
| 492 |
+
` : `
|
| 493 |
+
<div class="quiz-result-answer">
|
| 494 |
+
<div class="quiz-result-answer-label">Your Answer:</div>
|
| 495 |
+
<div class="quiz-result-answer-content">${question.user_answer || 'No answer'}</div>
|
| 496 |
+
</div>
|
| 497 |
+
`}
|
| 498 |
+
|
| 499 |
+
${question.explanation ? `
|
| 500 |
+
<div class="quiz-result-explanation">
|
| 501 |
+
<strong>Explanation:</strong> ${question.explanation}
|
| 502 |
+
</div>
|
| 503 |
+
` : ''}
|
| 504 |
+
</div>
|
| 505 |
+
`).join('')}
|
| 506 |
+
</div>
|
| 507 |
+
`;
|
| 508 |
+
|
| 509 |
+
quizResultsModal.classList.remove('hidden');
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
function closeQuizModal() {
|
| 513 |
+
quizModal.classList.add('hidden');
|
| 514 |
+
}
|
| 515 |
+
|
| 516 |
+
function closeQuizResults() {
|
| 517 |
+
quizResultsModal.classList.add('hidden');
|
| 518 |
+
}
|
| 519 |
+
|
| 520 |
+
function closeAllQuizModals() {
|
| 521 |
+
closeQuizSetup();
|
| 522 |
+
closeQuizModal();
|
| 523 |
+
closeQuizResults();
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
function showLoading(message = 'Loading...') {
|
| 527 |
+
const loadingOverlay = document.getElementById('loading-overlay');
|
| 528 |
+
const loadingMessage = document.getElementById('loading-message');
|
| 529 |
+
|
| 530 |
+
if (loadingOverlay && loadingMessage) {
|
| 531 |
+
loadingMessage.textContent = message;
|
| 532 |
+
loadingOverlay.classList.remove('hidden');
|
| 533 |
+
}
|
| 534 |
+
}
|
| 535 |
+
|
| 536 |
+
function hideLoading() {
|
| 537 |
+
const loadingOverlay = document.getElementById('loading-overlay');
|
| 538 |
+
if (loadingOverlay) {
|
| 539 |
+
loadingOverlay.classList.add('hidden');
|
| 540 |
+
}
|
| 541 |
+
}
|
| 542 |
+
|
| 543 |
+
// Expose functions globally
|
| 544 |
+
window.__sb_open_quiz_setup = openQuizSetup;
|
| 545 |
+
window.__sb_close_quiz_setup = closeQuizSetup;
|
| 546 |
+
})();
|
static/script.js
CHANGED
|
@@ -132,6 +132,18 @@
|
|
| 132 |
searchLink.classList.toggle('active');
|
| 133 |
});
|
| 134 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
}
|
| 136 |
|
| 137 |
function handleFileSelection(files) {
|
|
|
|
| 132 |
searchLink.classList.toggle('active');
|
| 133 |
});
|
| 134 |
}
|
| 135 |
+
|
| 136 |
+
// Quiz link toggle
|
| 137 |
+
const quizLink = document.getElementById('quiz-link');
|
| 138 |
+
if (quizLink) {
|
| 139 |
+
quizLink.addEventListener('click', (e) => {
|
| 140 |
+
e.preventDefault();
|
| 141 |
+
// Open quiz setup modal
|
| 142 |
+
if (window.__sb_open_quiz_setup) {
|
| 143 |
+
window.__sb_open_quiz_setup();
|
| 144 |
+
}
|
| 145 |
+
});
|
| 146 |
+
}
|
| 147 |
}
|
| 148 |
|
| 149 |
function handleFileSelection(files) {
|
static/styles.css
CHANGED
|
@@ -2424,4 +2424,338 @@
|
|
| 2424 |
.daily-label {
|
| 2425 |
font-size: 0.625rem;
|
| 2426 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2427 |
}
|
|
|
|
| 2424 |
.daily-label {
|
| 2425 |
font-size: 0.625rem;
|
| 2426 |
}
|
| 2427 |
+
}
|
| 2428 |
+
|
| 2429 |
+
/* Quiz Styles */
|
| 2430 |
+
.quiz-modal-content {
|
| 2431 |
+
max-width: 800px;
|
| 2432 |
+
width: 90vw;
|
| 2433 |
+
max-height: 90vh;
|
| 2434 |
+
overflow-y: auto;
|
| 2435 |
+
}
|
| 2436 |
+
|
| 2437 |
+
.quiz-step {
|
| 2438 |
+
margin-bottom: 24px;
|
| 2439 |
+
}
|
| 2440 |
+
|
| 2441 |
+
.document-checkbox-list {
|
| 2442 |
+
display: flex;
|
| 2443 |
+
flex-direction: column;
|
| 2444 |
+
gap: 12px;
|
| 2445 |
+
max-height: 300px;
|
| 2446 |
+
overflow-y: auto;
|
| 2447 |
+
border: 1px solid var(--border);
|
| 2448 |
+
border-radius: var(--radius);
|
| 2449 |
+
padding: 16px;
|
| 2450 |
+
background: var(--bg-secondary);
|
| 2451 |
+
}
|
| 2452 |
+
|
| 2453 |
+
.document-checkbox-item {
|
| 2454 |
+
display: flex;
|
| 2455 |
+
align-items: center;
|
| 2456 |
+
gap: 12px;
|
| 2457 |
+
padding: 8px 12px;
|
| 2458 |
+
background: var(--card);
|
| 2459 |
+
border-radius: var(--radius);
|
| 2460 |
+
border: 1px solid var(--border);
|
| 2461 |
+
transition: all 0.2s ease;
|
| 2462 |
+
}
|
| 2463 |
+
|
| 2464 |
+
.document-checkbox-item:hover {
|
| 2465 |
+
background: var(--card-hover);
|
| 2466 |
+
border-color: var(--border-light);
|
| 2467 |
+
}
|
| 2468 |
+
|
| 2469 |
+
.document-checkbox-item input[type="checkbox"] {
|
| 2470 |
+
margin: 0;
|
| 2471 |
+
transform: scale(1.2);
|
| 2472 |
+
}
|
| 2473 |
+
|
| 2474 |
+
.document-checkbox-item label {
|
| 2475 |
+
flex: 1;
|
| 2476 |
+
margin: 0;
|
| 2477 |
+
cursor: pointer;
|
| 2478 |
+
font-weight: 500;
|
| 2479 |
+
}
|
| 2480 |
+
|
| 2481 |
+
.quiz-timer {
|
| 2482 |
+
font-size: 18px;
|
| 2483 |
+
font-weight: 600;
|
| 2484 |
+
color: var(--accent);
|
| 2485 |
+
background: var(--bg-secondary);
|
| 2486 |
+
padding: 8px 16px;
|
| 2487 |
+
border-radius: var(--radius);
|
| 2488 |
+
border: 1px solid var(--border);
|
| 2489 |
+
}
|
| 2490 |
+
|
| 2491 |
+
.quiz-timer.warning {
|
| 2492 |
+
color: var(--warning);
|
| 2493 |
+
background: rgba(245, 158, 11, 0.1);
|
| 2494 |
+
border-color: var(--warning);
|
| 2495 |
+
}
|
| 2496 |
+
|
| 2497 |
+
.quiz-timer.danger {
|
| 2498 |
+
color: var(--error);
|
| 2499 |
+
background: rgba(239, 68, 68, 0.1);
|
| 2500 |
+
border-color: var(--error);
|
| 2501 |
+
}
|
| 2502 |
+
|
| 2503 |
+
.quiz-progress {
|
| 2504 |
+
margin-bottom: 24px;
|
| 2505 |
+
}
|
| 2506 |
+
|
| 2507 |
+
.quiz-progress-bar {
|
| 2508 |
+
height: 8px;
|
| 2509 |
+
background: var(--border);
|
| 2510 |
+
border-radius: 4px;
|
| 2511 |
+
overflow: hidden;
|
| 2512 |
+
margin-bottom: 8px;
|
| 2513 |
+
}
|
| 2514 |
+
|
| 2515 |
+
.quiz-progress-fill {
|
| 2516 |
+
height: 100%;
|
| 2517 |
+
background: var(--gradient-accent);
|
| 2518 |
+
transition: width 0.3s ease;
|
| 2519 |
+
width: 0%;
|
| 2520 |
+
}
|
| 2521 |
+
|
| 2522 |
+
.quiz-progress-text {
|
| 2523 |
+
font-size: 14px;
|
| 2524 |
+
color: var(--text-secondary);
|
| 2525 |
+
font-weight: 500;
|
| 2526 |
+
}
|
| 2527 |
+
|
| 2528 |
+
.quiz-question {
|
| 2529 |
+
margin-bottom: 24px;
|
| 2530 |
+
padding: 20px;
|
| 2531 |
+
background: var(--card);
|
| 2532 |
+
border-radius: var(--radius-lg);
|
| 2533 |
+
border: 1px solid var(--border);
|
| 2534 |
+
}
|
| 2535 |
+
|
| 2536 |
+
.quiz-question h3 {
|
| 2537 |
+
font-size: 18px;
|
| 2538 |
+
font-weight: 600;
|
| 2539 |
+
margin-bottom: 16px;
|
| 2540 |
+
color: var(--text);
|
| 2541 |
+
}
|
| 2542 |
+
|
| 2543 |
+
.quiz-question p {
|
| 2544 |
+
font-size: 16px;
|
| 2545 |
+
line-height: 1.6;
|
| 2546 |
+
color: var(--text-secondary);
|
| 2547 |
+
margin-bottom: 0;
|
| 2548 |
+
}
|
| 2549 |
+
|
| 2550 |
+
.quiz-answers {
|
| 2551 |
+
margin-bottom: 24px;
|
| 2552 |
+
}
|
| 2553 |
+
|
| 2554 |
+
.quiz-answer-option {
|
| 2555 |
+
display: flex;
|
| 2556 |
+
align-items: flex-start;
|
| 2557 |
+
gap: 12px;
|
| 2558 |
+
padding: 16px;
|
| 2559 |
+
margin-bottom: 12px;
|
| 2560 |
+
background: var(--card);
|
| 2561 |
+
border: 2px solid var(--border);
|
| 2562 |
+
border-radius: var(--radius);
|
| 2563 |
+
cursor: pointer;
|
| 2564 |
+
transition: all 0.2s ease;
|
| 2565 |
+
}
|
| 2566 |
+
|
| 2567 |
+
.quiz-answer-option:hover {
|
| 2568 |
+
background: var(--card-hover);
|
| 2569 |
+
border-color: var(--border-light);
|
| 2570 |
+
}
|
| 2571 |
+
|
| 2572 |
+
.quiz-answer-option.selected {
|
| 2573 |
+
border-color: var(--accent);
|
| 2574 |
+
background: rgba(59, 130, 246, 0.1);
|
| 2575 |
+
}
|
| 2576 |
+
|
| 2577 |
+
.quiz-answer-option.correct {
|
| 2578 |
+
border-color: var(--success);
|
| 2579 |
+
background: rgba(16, 185, 129, 0.1);
|
| 2580 |
+
}
|
| 2581 |
+
|
| 2582 |
+
.quiz-answer-option.incorrect {
|
| 2583 |
+
border-color: var(--error);
|
| 2584 |
+
background: rgba(239, 68, 68, 0.1);
|
| 2585 |
+
}
|
| 2586 |
+
|
| 2587 |
+
.quiz-answer-option input[type="radio"] {
|
| 2588 |
+
margin: 0;
|
| 2589 |
+
transform: scale(1.2);
|
| 2590 |
+
}
|
| 2591 |
+
|
| 2592 |
+
.quiz-answer-option label {
|
| 2593 |
+
flex: 1;
|
| 2594 |
+
margin: 0;
|
| 2595 |
+
cursor: pointer;
|
| 2596 |
+
font-weight: 500;
|
| 2597 |
+
line-height: 1.5;
|
| 2598 |
+
}
|
| 2599 |
+
|
| 2600 |
+
.quiz-text-answer {
|
| 2601 |
+
width: 100%;
|
| 2602 |
+
min-height: 120px;
|
| 2603 |
+
padding: 16px;
|
| 2604 |
+
border: 2px solid var(--border);
|
| 2605 |
+
border-radius: var(--radius);
|
| 2606 |
+
background: var(--card);
|
| 2607 |
+
color: var(--text);
|
| 2608 |
+
font-size: 16px;
|
| 2609 |
+
font-family: inherit;
|
| 2610 |
+
resize: vertical;
|
| 2611 |
+
transition: border-color 0.2s ease;
|
| 2612 |
+
}
|
| 2613 |
+
|
| 2614 |
+
.quiz-text-answer:focus {
|
| 2615 |
+
outline: none;
|
| 2616 |
+
border-color: var(--accent);
|
| 2617 |
+
}
|
| 2618 |
+
|
| 2619 |
+
.quiz-navigation {
|
| 2620 |
+
display: flex;
|
| 2621 |
+
justify-content: space-between;
|
| 2622 |
+
align-items: center;
|
| 2623 |
+
gap: 16px;
|
| 2624 |
+
}
|
| 2625 |
+
|
| 2626 |
+
.quiz-results-content {
|
| 2627 |
+
max-height: 60vh;
|
| 2628 |
+
overflow-y: auto;
|
| 2629 |
+
}
|
| 2630 |
+
|
| 2631 |
+
.quiz-result-summary {
|
| 2632 |
+
display: grid;
|
| 2633 |
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
| 2634 |
+
gap: 16px;
|
| 2635 |
+
margin-bottom: 24px;
|
| 2636 |
+
}
|
| 2637 |
+
|
| 2638 |
+
.quiz-result-stat {
|
| 2639 |
+
text-align: center;
|
| 2640 |
+
padding: 16px;
|
| 2641 |
+
background: var(--card);
|
| 2642 |
+
border-radius: var(--radius);
|
| 2643 |
+
border: 1px solid var(--border);
|
| 2644 |
+
}
|
| 2645 |
+
|
| 2646 |
+
.quiz-result-stat-value {
|
| 2647 |
+
font-size: 24px;
|
| 2648 |
+
font-weight: 700;
|
| 2649 |
+
color: var(--accent);
|
| 2650 |
+
margin-bottom: 4px;
|
| 2651 |
+
}
|
| 2652 |
+
|
| 2653 |
+
.quiz-result-stat-label {
|
| 2654 |
+
font-size: 14px;
|
| 2655 |
+
color: var(--text-secondary);
|
| 2656 |
+
text-transform: uppercase;
|
| 2657 |
+
letter-spacing: 0.5px;
|
| 2658 |
+
}
|
| 2659 |
+
|
| 2660 |
+
.quiz-result-questions {
|
| 2661 |
+
margin-top: 24px;
|
| 2662 |
+
}
|
| 2663 |
+
|
| 2664 |
+
.quiz-result-question {
|
| 2665 |
+
margin-bottom: 24px;
|
| 2666 |
+
padding: 20px;
|
| 2667 |
+
background: var(--card);
|
| 2668 |
+
border-radius: var(--radius-lg);
|
| 2669 |
+
border: 1px solid var(--border);
|
| 2670 |
+
}
|
| 2671 |
+
|
| 2672 |
+
.quiz-result-question-header {
|
| 2673 |
+
display: flex;
|
| 2674 |
+
justify-content: space-between;
|
| 2675 |
+
align-items: center;
|
| 2676 |
+
margin-bottom: 12px;
|
| 2677 |
+
}
|
| 2678 |
+
|
| 2679 |
+
.quiz-result-question-title {
|
| 2680 |
+
font-weight: 600;
|
| 2681 |
+
color: var(--text);
|
| 2682 |
+
}
|
| 2683 |
+
|
| 2684 |
+
.quiz-result-question-status {
|
| 2685 |
+
padding: 4px 12px;
|
| 2686 |
+
border-radius: 999px;
|
| 2687 |
+
font-size: 12px;
|
| 2688 |
+
font-weight: 600;
|
| 2689 |
+
text-transform: uppercase;
|
| 2690 |
+
}
|
| 2691 |
+
|
| 2692 |
+
.quiz-result-question-status.correct {
|
| 2693 |
+
background: rgba(16, 185, 129, 0.1);
|
| 2694 |
+
color: var(--success);
|
| 2695 |
+
}
|
| 2696 |
+
|
| 2697 |
+
.quiz-result-question-status.incorrect {
|
| 2698 |
+
background: rgba(239, 68, 68, 0.1);
|
| 2699 |
+
color: var(--error);
|
| 2700 |
+
}
|
| 2701 |
+
|
| 2702 |
+
.quiz-result-question-status.partial {
|
| 2703 |
+
background: rgba(245, 158, 11, 0.1);
|
| 2704 |
+
color: var(--warning);
|
| 2705 |
+
}
|
| 2706 |
+
|
| 2707 |
+
.quiz-result-question-text {
|
| 2708 |
+
margin-bottom: 16px;
|
| 2709 |
+
color: var(--text-secondary);
|
| 2710 |
+
line-height: 1.6;
|
| 2711 |
+
}
|
| 2712 |
+
|
| 2713 |
+
.quiz-result-answer {
|
| 2714 |
+
margin-bottom: 12px;
|
| 2715 |
+
padding: 12px;
|
| 2716 |
+
border-radius: var(--radius);
|
| 2717 |
+
border: 1px solid var(--border);
|
| 2718 |
+
}
|
| 2719 |
+
|
| 2720 |
+
.quiz-result-answer-label {
|
| 2721 |
+
font-weight: 600;
|
| 2722 |
+
margin-bottom: 4px;
|
| 2723 |
+
color: var(--text);
|
| 2724 |
+
}
|
| 2725 |
+
|
| 2726 |
+
.quiz-result-answer-content {
|
| 2727 |
+
color: var(--text-secondary);
|
| 2728 |
+
line-height: 1.5;
|
| 2729 |
+
}
|
| 2730 |
+
|
| 2731 |
+
.quiz-result-explanation {
|
| 2732 |
+
margin-top: 12px;
|
| 2733 |
+
padding: 12px;
|
| 2734 |
+
background: var(--bg-secondary);
|
| 2735 |
+
border-radius: var(--radius);
|
| 2736 |
+
border-left: 4px solid var(--accent);
|
| 2737 |
+
font-size: 14px;
|
| 2738 |
+
color: var(--text-secondary);
|
| 2739 |
+
line-height: 1.5;
|
| 2740 |
+
}
|
| 2741 |
+
|
| 2742 |
+
/* Quiz responsive */
|
| 2743 |
+
@media (max-width: 768px) {
|
| 2744 |
+
.quiz-modal-content {
|
| 2745 |
+
width: 95vw;
|
| 2746 |
+
max-height: 95vh;
|
| 2747 |
+
}
|
| 2748 |
+
|
| 2749 |
+
.quiz-navigation {
|
| 2750 |
+
flex-direction: column;
|
| 2751 |
+
gap: 12px;
|
| 2752 |
+
}
|
| 2753 |
+
|
| 2754 |
+
.quiz-navigation button {
|
| 2755 |
+
width: 100%;
|
| 2756 |
+
}
|
| 2757 |
+
|
| 2758 |
+
.quiz-result-summary {
|
| 2759 |
+
grid-template-columns: 1fr;
|
| 2760 |
+
}
|
| 2761 |
}
|
utils/rag/rag.py
CHANGED
|
@@ -78,6 +78,29 @@ class RAGStore:
|
|
| 78 |
return serializable_doc
|
| 79 |
return None
|
| 80 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
def list_files(self, user_id: str, project_id: str):
|
| 82 |
"""List all files for a project with their summaries"""
|
| 83 |
files_cursor = self.files.find(
|
|
|
|
| 78 |
return serializable_doc
|
| 79 |
return None
|
| 80 |
|
| 81 |
+
def get_file_chunks(self, user_id: str, project_id: str, filename: str, limit: int = 20) -> List[Dict[str, Any]]:
|
| 82 |
+
"""Get chunks for a specific file"""
|
| 83 |
+
cursor = self.chunks.find({
|
| 84 |
+
"user_id": user_id,
|
| 85 |
+
"project_id": project_id,
|
| 86 |
+
"filename": filename
|
| 87 |
+
}).limit(limit)
|
| 88 |
+
|
| 89 |
+
chunks = []
|
| 90 |
+
for doc in cursor:
|
| 91 |
+
# Convert MongoDB document to JSON-serializable format
|
| 92 |
+
serializable_doc = {}
|
| 93 |
+
for key, value in doc.items():
|
| 94 |
+
if key == '_id':
|
| 95 |
+
serializable_doc[key] = str(value)
|
| 96 |
+
elif hasattr(value, 'isoformat'):
|
| 97 |
+
serializable_doc[key] = value.isoformat()
|
| 98 |
+
else:
|
| 99 |
+
serializable_doc[key] = value
|
| 100 |
+
chunks.append(serializable_doc)
|
| 101 |
+
|
| 102 |
+
return chunks
|
| 103 |
+
|
| 104 |
def list_files(self, user_id: str, project_id: str):
|
| 105 |
"""List all files for a project with their summaries"""
|
| 106 |
files_cursor = self.files.find(
|