Skip to content

Commit

Permalink
Add frontend support for multi-part questions
Browse files Browse the repository at this point in the history
  • Loading branch information
willcrichton committed Sep 21, 2023
1 parent f459829 commit 8b5f373
Show file tree
Hide file tree
Showing 9 changed files with 140 additions and 21 deletions.
1 change: 1 addition & 0 deletions crates/mdbook-quiz/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ fn main() -> Result<()> {
}
}

println!("cargo:rerun-if-changed={JS_DIST_DIR}");
let entries = fs::read_dir(js_dist_dir)?;
let local_js_dist_dir = Path::new(LOCAL_JS_DIST_DIR);
fs::create_dir_all(local_js_dist_dir)?;
Expand Down
10 changes: 9 additions & 1 deletion js/packages/quiz-embed/src/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -458,5 +458,13 @@ $error-color: #e16969;
}
}


.multipart-context {
padding-left: 1em;
.multipart-context-content {
border: 1px solid $light-border-color;
padding: 0.5em;
background-color: var(--theme-popup-bg);
margin-bottom: 1em;
}
}
}
40 changes: 38 additions & 2 deletions js/packages/quiz/src/components/quiz.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import React, {
useState,
} from "react";

import type { Question } from "../bindings/Question";
import type { Quiz } from "../bindings/Quiz";
import {
AnswerView,
Expand Down Expand Up @@ -203,6 +204,7 @@ let AnswerReview = ({
<button onClick={onGiveUp}>see the correct answers</button>.
</p>
) : null;
let questionTitles = generateQuestionTitles(quiz);
return (
<>
<h3>Answer Review</h3>
Expand All @@ -220,6 +222,7 @@ let AnswerReview = ({
<div className="answer-wrapper" key={i}>
<AnswerView
index={i + 1}
title={questionTitles[i]}
quizName={name}
question={question}
userAnswer={answer}
Expand Down Expand Up @@ -289,6 +292,35 @@ export interface QuizViewProps {
onFinish?: (answers: TaggedAnswer[]) => void;
}

let aCode = "a".charCodeAt(0);
export let generateQuestionTitles = (quiz: Quiz): string[] => {
let groups: Question[][] = [];
let group = undefined;
let part = undefined;
quiz.questions.forEach(q => {
if (q.multipart) {
if (q.multipart === part) {
group.push(q);
} else {
group = [q];
groups.push(group);
}
part = q.multipart;
} else {
group = [q];
groups.push(group);
}
});

return groups.flatMap((g, i) =>
g.map((q, j) => {
let title = (i + 1).toString();
if (q.multipart) title += String.fromCharCode(aCode + j);
return title;
})
);
};

export let QuizView: React.FC<QuizViewProps> = observer(
({ quiz, name, fullscreen, cacheAnswers, allowRetry, onFinish }) => {
let [quizHash] = useState(() => hash.MD5(quiz));
Expand Down Expand Up @@ -378,7 +410,9 @@ export let QuizView: React.FC<QuizViewProps> = observer(

// HACK: need this component to observe confirmedDone
// on first render...
state.confirmedDone;
state.confirmedDone;

let questionTitles = generateQuestionTitles(quiz);

let body = (
<section>
Expand All @@ -402,7 +436,9 @@ export let QuizView: React.FC<QuizViewProps> = observer(
<QuestionView
key={state.index}
quizName={name}
index={state.index + 1}
multipart={quiz.multipart}
index={state.index}
title={questionTitles[state.index]}
attempt={state.attempt}
question={quiz.questions[state.index]}
questionState={questionStates[state.index]}
Expand Down
53 changes: 46 additions & 7 deletions js/packages/quiz/src/questions/mod.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import _ from "lodash";
import React, { useId, useMemo, useRef, useState } from "react";
import { RegisterOptions, useForm } from "react-hook-form";

import { Question } from "../bindings/Question";
import type { Question } from "../bindings/Question";
import type { Quiz } from "../bindings/Quiz";
import { MarkdownView } from "../components/markdown";
import { MoreInfo } from "../components/more-info";
import { useCaptureMdbookShortcuts } from "../lib";
Expand Down Expand Up @@ -106,14 +107,27 @@ This explanation helps us understand *why* a reader answers a particular way, so
we can better improve the surrounding text.
`.trim();

export let QuestionView: React.FC<{
interface QuestionViewProps {
quizName: string;
multipart: Quiz["multipart"];
question: Question;
index: number;
title: string;
attempt: number;
questionState?: any;
onSubmit: (answer: TaggedAnswer) => void;
}> = ({ quizName, question, index, attempt, questionState, onSubmit }) => {
}

export let QuestionView: React.FC<QuestionViewProps> = ({
quizName,
multipart,
question,
index,
title,
attempt,
questionState,
onSubmit,
}) => {
let start = useMemo(now, [quizName, question, index]);
let ref = useRef<HTMLFormElement>(null);
let [showExplanation, setShowExplanation] = useState(false);
Expand Down Expand Up @@ -156,10 +170,24 @@ export let QuestionView: React.FC<{

let explanationId = useId();

let titleNumber = title.substring(0, 1);
let promptContext = question.multipart ? (
<div className="multipart-context">
<p>
<strong>Question {titleNumber} has multiple parts.</strong> The box
below contains the shared context for each part.
</p>
<div className="multipart-context-content">
<MarkdownView markdown={multipart[question.multipart]} />
</div>
</div>
) : null;

return (
<div className={classNames("question", questionClass)}>
<div className="prompt">
<h4>Question {index}</h4>
<h4>Question {title}</h4>
{promptContext}
<methods.PromptView prompt={question.prompt} />
{window.telemetry ? (
<BugReporter quizName={quizName} question={index} />
Expand Down Expand Up @@ -204,21 +232,32 @@ export let QuestionView: React.FC<{
);
};

export let AnswerView: React.FC<{
interface AnswerViewProps {
quizName: string;
question: Question;
index: number;
title: string;
userAnswer: Question["answer"];
correct: boolean;
showCorrect: boolean;
}> = ({ quizName, question, index, userAnswer, correct, showCorrect }) => {
}

export let AnswerView: React.FC<AnswerViewProps> = ({
quizName,
question,
index,
title,
userAnswer,
correct,
showCorrect,
}) => {
let methods = getQuestionMethods(question.type);
let questionClass = questionNameToCssClass(question.type);

return (
<div className={classNames("answer", questionClass)}>
<div className="prompt">
<h4>Question {index}</h4>
<h4>Question {title}</h4>
<methods.PromptView prompt={question.prompt} />
{window.telemetry ? (
<BugReporter quizName={quizName} question={index} />
Expand Down
10 changes: 7 additions & 3 deletions js/packages/quiz/tests/multiple-choice.test.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { render, screen, waitFor } from "@testing-library/react";
import user from "@testing-library/user-event";
import { MultipleChoice } from "@wcrichto/quiz-schema";
import React from "react";
import { beforeEach, describe, expect, it } from "vitest";

import { MultipleChoice } from "../src/bindings/MultipleChoice";
import { MultipleChoiceMethods, QuestionView } from "../src/lib";
import { submitButton } from "./utils";

describe("MultipleChoice", () => {
let question: MultipleChoice = {
let question: MultipleChoice & { type: "MultipleChoice" } = {
type: "MultipleChoice",
prompt: { prompt: "Hello world", distractors: ["B", "C"] },
answer: { answer: "A" },
Expand All @@ -25,6 +25,8 @@ describe("MultipleChoice", () => {
<QuestionView
quizName={"Foobar"}
question={question}
multipart={{}}
title={"1"}
index={1}
attempt={0}
questionState={state}
Expand Down Expand Up @@ -55,7 +57,7 @@ describe("MultipleChoice", () => {
});

describe("MultipleChoice multi-answer", () => {
let question: MultipleChoice = {
let question: MultipleChoice & { type: "MultipleChoice" } = {
type: "MultipleChoice",
prompt: { prompt: "Hello world", distractors: ["C", "D"] },
answer: { answer: ["A", "B"] },
Expand All @@ -72,7 +74,9 @@ describe("MultipleChoice multi-answer", () => {
<QuestionView
quizName={"Foobar"}
question={question}
multipart={{}}
index={1}
title="1"
attempt={0}
questionState={state}
onSubmit={answer => {
Expand Down
6 changes: 4 additions & 2 deletions js/packages/quiz/tests/question.test.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { render, screen, waitFor } from "@testing-library/react";
import user from "@testing-library/user-event";
import { ShortAnswer } from "@wcrichto/quiz-schema";
import React from "react";
import { beforeEach, describe, expect, it } from "vitest";

import { Question } from "../src/bindings/Question";
import { QuestionView } from "../src/lib";
import { submitButton } from "./utils";

describe("Question prompt for explanation", () => {
let question: ShortAnswer = {
let question: Question = {
type: "ShortAnswer",
prompt: { prompt: "Hello world" },
answer: { answer: "Yes" },
Expand All @@ -22,7 +22,9 @@ describe("Question prompt for explanation", () => {
<QuestionView
quizName={"Foobar"}
question={question}
multipart={{}}
index={1}
title="1"
attempt={0}
onSubmit={answer => {
submitted = answer;
Expand Down
29 changes: 27 additions & 2 deletions js/packages/quiz/tests/quiz.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { render, screen, waitFor } from "@testing-library/react";
import user from "@testing-library/user-event";
import { Quiz } from "@wcrichto/quiz-schema";
import React from "react";
import { beforeEach, describe, expect, it } from "vitest";

import { QuizView } from "../src/lib";
import { Question } from "../src/bindings/Question";
import { Quiz } from "../src/bindings/Quiz";
import { QuizView, generateQuestionTitles } from "../src/lib";
import { startButton, submitButton } from "./utils";

let quiz: Quiz = {
Expand Down Expand Up @@ -116,3 +117,27 @@ describe("Quiz retry", () => {
).toThrow();
});
});

describe("generateQuestionTitles", () => {
it("handles multi-part questions", () => {
let template: Question = {
type: "ShortAnswer",
prompt: { prompt: "" },
answer: { answer: "" },
};
let questions: Question[] = [
{ ...template, multipart: "a" },
{ ...template, multipart: "a" },
{ ...template, multipart: "b" },
{ ...template },
{ ...template, multipart: "c" },
];
expect(generateQuestionTitles({ questions })).toStrictEqual([
"1a",
"1b",
"2a",
"3",
"4a",
]);
});
});
6 changes: 4 additions & 2 deletions js/packages/quiz/tests/short-answer.test.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { render, screen, waitFor } from "@testing-library/react";
import user from "@testing-library/user-event";
import { ShortAnswer } from "@wcrichto/quiz-schema";
import React from "react";
import { beforeEach, describe, expect, it } from "vitest";

import { ShortAnswer } from "../src/bindings/ShortAnswer";
import { QuestionView } from "../src/lib";
import { submitButton } from "./utils";

describe("ShortAnswer", () => {
let question: ShortAnswer = {
let question: ShortAnswer & { type: "ShortAnswer" } = {
type: "ShortAnswer",
prompt: { prompt: "Hello world" },
answer: { answer: "Yes", alternatives: ["Ok"] },
Expand All @@ -21,7 +21,9 @@ describe("ShortAnswer", () => {
<QuestionView
quizName={"Foobar"}
question={question}
multipart={{}}
index={1}
title="1"
attempt={0}
onSubmit={answer => {
submitted = answer;
Expand Down
6 changes: 4 additions & 2 deletions js/packages/quiz/tests/tracing.test.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { render, screen, waitFor } from "@testing-library/react";
import user from "@testing-library/user-event";
import { Tracing } from "@wcrichto/quiz-schema";
import React from "react";
import { beforeEach, describe, expect, it } from "vitest";

import { Tracing } from "../src/bindings/Tracing";
import { QuestionView } from "../src/lib";
import { submitButton } from "./utils";

describe("Tracing", () => {
let question: Tracing = {
let question: Tracing & { type: "Tracing" } = {
type: "Tracing",
prompt: { program: "fn main(){}" },
answer: { doesCompile: true, stdout: "Yes" },
Expand All @@ -21,7 +21,9 @@ describe("Tracing", () => {
<QuestionView
quizName={"Foobar"}
question={question}
multipart={{}}
index={1}
title="1"
attempt={0}
onSubmit={answer => {
submitted = answer;
Expand Down

0 comments on commit 8b5f373

Please sign in to comment.