Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions frontend/components/quizzes/quiz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,19 @@ export class Quiz {
}

hidePools() {
// Check if any pools have groups defined
const hasGroupedPools = this.pools().some((pool: QuestionPool) => pool.group);

if (hasGroupedPools) {
this.hidePoolsGroups();
} else {
// Original behavior for backward compatibility
this.hidePoolsIndividually();
}
}

// Original pool hiding logic, extracted for clarity
private hidePoolsIndividually() {
this.questions().forEach((question: Question) => {
question.visible(true);
});
Expand All @@ -287,7 +300,66 @@ export class Quiz {
}

hidePoolsGroups() {
// First, make all questions visible
this.questions().forEach((question: Question) => {
question.visible(true);
});

// Group pools by their group field
const poolsByGroup: Map<string, QuestionPool[]> = new Map();
const ungroupedPools: QuestionPool[] = [];

this.pools().forEach((pool: QuestionPool) => {
if (pool.group) {
if (!poolsByGroup.has(pool.group)) {
poolsByGroup.set(pool.group, []);
}
poolsByGroup.get(pool.group)!.push(pool);
} else {
ungroupedPools.push(pool);
}
});

// Calculate the base seed based on pool randomness
const baseSeed = this.poolRandomness() === QuizPoolRandomness.SEED ?
this.seed() :
this.poolRandomness() === QuizPoolRandomness.ATTEMPT ?
this.seed() + this.attemptCount() :
0;

// Handle grouped pools - use same seed for all pools in the same group
poolsByGroup.forEach((groupPools: QuestionPool[], groupName: string) => {
// Use group name as additional seed modifier for consistency
const groupSeed = baseSeed + this.hashString(groupName);

groupPools.forEach((pool: QuestionPool) => {
const allQuestions = pool.questions;
const chosenQuestions = subsetRandomly(allQuestions, pool.amount, groupSeed);
allQuestions.forEach((questionId: string) => {
this.questionMap[questionId].visible(chosenQuestions.includes(questionId));
});
});
});

// Handle ungrouped pools individually (original behavior)
ungroupedPools.forEach((pool: QuestionPool) => {
const allQuestions = pool.questions;
const chosenQuestions = subsetRandomly(allQuestions, pool.amount, baseSeed);
allQuestions.forEach((questionId: string) => {
this.questionMap[questionId].visible(chosenQuestions.includes(questionId));
});
});
}

// Simple hash function to convert group name to a consistent number
private hashString(str: string): number {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32-bit integer
}
return Math.abs(hash);
}

submissionAsJson(): string {
Expand Down
1 change: 1 addition & 0 deletions frontend/components/quizzes/quiz_schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ Remember, these objects are in a LIST.
* `"name"`: An optional string name for the pool. I usually name the questions stuff like `PoolName_1` and then the pool `PoolName`, using CapitalCamelCase.
* `"amount"`: The number of questions to select from this pool for each attempt.
* `"questions"`: A list of strings representing the IDs of the questions (eg.g., `["PoolName_1", "PoolName_2"]` or whatever you're calling them.)
* `"group"`: An optional string that allows multiple pools to "vary together". When pools share the same group name, they will use the same seed for question selection, ensuring their random choices are coordinated. This is useful for multi-question scenarios where you want to show related content (e.g., a code block in one pool and follow-up questions in another pool). Pools without a group field work independently as before.

### Questions Instructions

Expand Down