diff --git a/backend/app.js b/backend/app.js
index f4a6923..bfa1308 100644
--- a/backend/app.js
+++ b/backend/app.js
@@ -42,6 +42,9 @@ const populateClosedDays = require('./schedule/populateClosedDays');
//Register focus routes
app.use('/', require('./endpoints/checkFocus/route')(databaseConnection));
+ //Register Quiz routes
+ app.use('/', require('./endpoints/hpcQuestion/route')(databaseConnection));
+
// Register report routes
app.use('/', require('./endpoints/report/routes')(databaseConnection));
diff --git a/backend/db/mongodb-schema.json b/backend/db/mongodb-schema.json
index e041a3a..a2293f3 100644
--- a/backend/db/mongodb-schema.json
+++ b/backend/db/mongodb-schema.json
@@ -177,6 +177,47 @@
"validationLevel": "moderate",
"validationAction": "error"
},
+ {
+ "name": "hpcQuestion",
+ "validator": {
+ "$jsonSchema": {
+ "bsonType": "object",
+ "required": [
+ "_id",
+ "question",
+ "options",
+ "correctAnswerIndex",
+ "explanation",
+ "active"
+ ],
+ "properties": {
+ "_id": {
+ "bsonType": "objectId"
+ },
+ "question": {
+ "bsonType": "string"
+ },
+ "options": {
+ "bsonType": "array",
+ "items": {
+ "bsonType": "string"
+ }
+ },
+ "correctAnswerIndex": {
+ "bsonType": "int"
+ },
+ "explanation": {
+ "bsonType": "string"
+ },
+ "active": {
+ "bsonType": "bool"
+ }
+ }
+ }
+ },
+ "validationLevel": "moderate",
+ "validationAction": "error"
+ },
{
"name": "method",
"validator": {
diff --git a/backend/endpoints/hpcQuestion/controller/checkAnswer.js b/backend/endpoints/hpcQuestion/controller/checkAnswer.js
new file mode 100644
index 0000000..a6b9667
--- /dev/null
+++ b/backend/endpoints/hpcQuestion/controller/checkAnswer.js
@@ -0,0 +1,112 @@
+const { ObjectId } = require('mongodb');
+
+/**
+ * @param {import('mongodb').Db} db
+ */
+module.exports = (db) => {
+ /**
+ * @param {import('express').Request} req
+ * @param {import('express').Response} res
+ * @returns {Promise}
+ */
+ return async (req, res) => {
+ try {
+ const { questionId, selectedAnswerIndex, optionOrder } = req.body || {};
+
+ if (!questionId) {
+ return res.status(400).json({
+ success: false,
+ error: 'Missing question id'
+ });
+ }
+
+ const sanitizedQuestionId = String(questionId).trim();
+
+ if (!ObjectId.isValid(sanitizedQuestionId)) {
+ return res.status(400).json({
+ success: false,
+ error: 'Invalid question id'
+ });
+ }
+
+ if (!Number.isInteger(selectedAnswerIndex)) {
+ return res.status(400).json({
+ success: false,
+ error: 'Selected answer index must be an integer'
+ });
+ }
+
+ if (!Array.isArray(optionOrder)) {
+ return res.status(400).json({
+ success: false,
+ error: 'Option order must be an array'
+ });
+ }
+
+ if (!optionOrder.every((value) => Number.isInteger(value))) {
+ return res.status(400).json({
+ success: false,
+ error: 'Option order must only contain integers'
+ });
+ }
+
+ const question = await db.collection('hpcQuestion').findOne({
+ _id: new ObjectId(sanitizedQuestionId),
+ active: true
+ });
+
+ if (!question) {
+ return res.status(404).json({
+ success: false,
+ error: 'HPC question does not exist'
+ });
+ }
+
+ if (selectedAnswerIndex < 0 || selectedAnswerIndex >= optionOrder.length) {
+ return res.status(400).json({
+ success: false,
+ error: 'Selected answer index is out of range'
+ });
+ }
+
+ if (optionOrder.length !== question.options.length) {
+ return res.status(400).json({
+ success: false,
+ error: 'Option order length does not match question options'
+ });
+ }
+
+ const originalSelectedAnswerIndex = optionOrder[selectedAnswerIndex];
+
+ if (
+ originalSelectedAnswerIndex < 0 ||
+ originalSelectedAnswerIndex >= question.options.length
+ ) {
+ return res.status(400).json({
+ success: false,
+ error: 'Original selected answer index is out of range'
+ });
+ }
+
+ const correct = originalSelectedAnswerIndex === question.correctAnswerIndex;
+
+ const correctAnswerDisplayIndex = optionOrder.findIndex(
+ (originalIndex) => originalIndex === question.correctAnswerIndex
+ );
+
+ return res.status(200).json({
+ success: true,
+ body: {
+ correct,
+ correctAnswerIndex: correctAnswerDisplayIndex,
+ explanation: question.explanation
+ }
+ });
+ } catch (error) {
+ return res.status(500).json({
+ success: false,
+ error: error.message
+ });
+ }
+ };
+};
diff --git a/backend/endpoints/hpcQuestion/controller/getRandomQuestion.js b/backend/endpoints/hpcQuestion/controller/getRandomQuestion.js
new file mode 100644
index 0000000..dfa34da
--- /dev/null
+++ b/backend/endpoints/hpcQuestion/controller/getRandomQuestion.js
@@ -0,0 +1,61 @@
+/**
+ * @param {import('mongodb').Db} db
+ */
+module.exports = (db) => {
+ /**
+ * @param {import('express').Request} req
+ * @param {import('express').Response} res
+ * @returns {Promise}
+ */
+ return async (req, res) => {
+ try {
+ if (Math.random() >= 0.35) {
+ return res.status(200).json({
+ success: true,
+ body: null
+ });
+ }
+
+ const [question] = await db
+ .collection('hpcQuestion')
+ .aggregate([{ $match: { active: true } }, { $sample: { size: 1 } }])
+ .toArray();
+
+ if (!question) {
+ return res.status(404).json({
+ success: false,
+ error: 'No active HPC questions found'
+ });
+ }
+
+ const optionOrder = question.options.map((_, index) => index);
+
+ for (let i = optionOrder.length - 1; i > 0; i--) {
+ const randomIndex = Math.floor(Math.random() * (i + 1));
+ const temporaryValue = optionOrder[i];
+
+ optionOrder[i] = optionOrder[randomIndex];
+ optionOrder[randomIndex] = temporaryValue;
+ }
+
+ const shuffledOptions = optionOrder.map(
+ (originalIndex) => question.options[originalIndex]
+ );
+
+ return res.status(200).json({
+ success: true,
+ body: {
+ id: question._id.toString(),
+ question: question.question,
+ options: shuffledOptions,
+ optionOrder
+ }
+ });
+ } catch (error) {
+ return res.status(500).json({
+ success: false,
+ error: error.message
+ });
+ }
+ };
+};
diff --git a/backend/endpoints/hpcQuestion/route.js b/backend/endpoints/hpcQuestion/route.js
new file mode 100644
index 0000000..937a1f5
--- /dev/null
+++ b/backend/endpoints/hpcQuestion/route.js
@@ -0,0 +1,8 @@
+/**
+ * @param {import('mongodb').Db} db
+ */
+module.exports = (db) =>
+ require('express')
+ .Router()
+ .get('/hpc-question/random', require('./controller/getRandomQuestion')(db))
+ .post('/hpc-question/check', require('./controller/checkAnswer')(db));
diff --git a/backend/scripts/data/hpcQuestion.js b/backend/scripts/data/hpcQuestion.js
new file mode 100644
index 0000000..5fc746b
--- /dev/null
+++ b/backend/scripts/data/hpcQuestion.js
@@ -0,0 +1,407 @@
+const { ObjectId } = require('mongodb');
+
+const id = () => new ObjectId();
+
+const hpcQuestions = [
+ {
+ _id: id(),
+ question: 'What does squeue show?',
+ options: [
+ 'Jobs currently known to the scheduler',
+ 'Available disk space',
+ 'Loaded software modules',
+ 'CPU temperature'
+ ],
+ correctAnswerIndex: 0,
+ explanation:
+ 'squeue shows jobs in the scheduler queue, including jobs that are pending, running, or in other scheduler states.',
+ active: true
+ },
+ {
+ _id: id(),
+ question: 'What is sinfo commonly used for?',
+ options: [
+ 'Checking Slurm partitions and node states',
+ 'Viewing shell command history',
+ 'Copying files between servers',
+ 'Editing files'
+ ],
+ correctAnswerIndex: 0,
+ explanation:
+ 'sinfo is commonly used to inspect Slurm partitions, node availability, and node states such as idle, allocated, mixed, or down.',
+ active: true
+ },
+ {
+ _id: id(),
+ question: 'What command is commonly used to submit a Slurm job script?',
+ options: ['sbatch', 'squeue', 'scancel', 'sinfo'],
+ correctAnswerIndex: 0,
+ explanation: 'sbatch submits a batch job script to Slurm.',
+ active: true
+ },
+ {
+ _id: id(),
+ question: 'What command is commonly used to view your current Slurm jobs?',
+ options: ['squeue -u $USER', 'chmod +x $USER', 'module load $USER', 'df -h $USER'],
+ correctAnswerIndex: 0,
+ explanation: 'squeue -u $USER shows jobs belonging to the current user.',
+ active: true
+ },
+ {
+ _id: id(),
+ question: 'What does an array job in Slurm allow you to do?',
+ options: [
+ 'Run many similar tasks using one job submission',
+ 'Store output in an array file format',
+ 'Load multiple modules at the same time',
+ 'Split one CPU core into multiple virtual cores'
+ ],
+ correctAnswerIndex: 0,
+ explanation:
+ 'A Slurm array job lets you run many similar tasks from one submission, each with its own array task ID.',
+ active: true
+ },
+ {
+ _id: id(),
+ question: 'What does %j commonly mean in a Slurm output filename?',
+ options: ['The Slurm job ID', 'The job name', 'The username', 'The node name'],
+ correctAnswerIndex: 0,
+ explanation: 'In Slurm output filename patterns, %j is commonly replaced with the job ID.',
+ active: true
+ },
+ {
+ _id: id(),
+ question: 'What does module list usually show?',
+ options: [
+ 'Software modules currently loaded in your environment',
+ 'All jobs currently running on the cluster',
+ 'All files in the current directory',
+ 'All available Slurm partitions'
+ ],
+ correctAnswerIndex: 0,
+ explanation:
+ 'module list shows which software modules are currently loaded in your shell environment.',
+ active: true
+ },
+ {
+ _id: id(),
+ question: 'What does module purge usually do?',
+ options: [
+ 'Removes loaded modules from the current environment',
+ 'Deletes job output files',
+ 'Cancels every pending job',
+ 'Clears disk quota usage'
+ ],
+ correctAnswerIndex: 0,
+ explanation:
+ 'module purge unloads currently loaded modules, giving you a cleaner environment.',
+ active: true
+ },
+ {
+ _id: id(),
+ question: 'What does ls -la usually show?',
+ options: [
+ 'A detailed listing of files, including hidden files',
+ 'Only running Slurm jobs',
+ 'Only available modules',
+ 'The current memory usage'
+ ],
+ correctAnswerIndex: 0,
+ explanation:
+ 'ls -la lists files in long format and includes hidden files that begin with a dot.',
+ active: true
+ },
+ {
+ _id: id(),
+ question: 'What does grep usually help with?',
+ options: [
+ 'Searching text for matching words or patterns',
+ 'Submitting a batch job',
+ 'Changing a file owner',
+ 'Showing all partitions'
+ ],
+ correctAnswerIndex: 0,
+ explanation:
+ 'grep searches text for matching words or patterns, which is useful when checking logs.',
+ active: true
+ },
+ {
+ _id: id(),
+ question: 'What does tail -f usually do?',
+ options: [
+ 'Shows new lines added to a file as they appear',
+ 'Deletes the end of a file',
+ 'Shows only hidden files',
+ 'Follows a Slurm partition'
+ ],
+ correctAnswerIndex: 0,
+ explanation:
+ 'tail -f follows a file and displays new lines as they are written, which is useful for watching logs.',
+ active: true
+ },
+ {
+ _id: id(),
+ question: 'What does cp source.txt copy.txt do?',
+ options: [
+ 'Copies source.txt to copy.txt',
+ 'Moves source.txt into copy.txt',
+ 'Deletes source.txt after creating copy.txt',
+ 'Compares the two files'
+ ],
+ correctAnswerIndex: 0,
+ explanation:
+ 'cp copies files or directories. In this example it creates copy.txt from source.txt.',
+ active: true
+ },
+ {
+ _id: id(),
+ question: 'What does less file.txt help you do?',
+ options: [
+ 'View a file page by page',
+ 'Make the file smaller',
+ 'Delete lines from the file',
+ 'Submit the file as a job'
+ ],
+ correctAnswerIndex: 0,
+ explanation:
+ 'less lets you view text files page by page without loading everything into the terminal at once.',
+ active: true
+ },
+ {
+ _id: id(),
+ question: 'What does df -h help check?',
+ options: [
+ 'Filesystem disk usage in a human-readable format',
+ 'Loaded software modules',
+ 'Current job queue',
+ 'File permissions'
+ ],
+ correctAnswerIndex: 0,
+ explanation:
+ 'df -h shows filesystem disk usage in human-readable units, which helps identify full or nearly full filesystems.',
+ active: true
+ },
+ {
+ _id: id(),
+ question: 'In JavaScript, what does === check?',
+ options: [
+ 'Strict equality without type coercion',
+ 'Assignment of a new value',
+ 'Greater than or equal comparison',
+ 'Loose equality with type conversion'
+ ],
+ correctAnswerIndex: 0,
+ explanation:
+ '=== checks strict equality. It compares both value and type without converting between types.',
+ active: true
+ },
+ {
+ _id: id(),
+ question: 'What does JSON.stringify do?',
+ options: [
+ 'Converts a JavaScript value into a JSON string',
+ 'Converts JSON text into a JavaScript object',
+ 'Deletes empty JSON fields',
+ 'Checks if JSON is valid without changing it'
+ ],
+ correctAnswerIndex: 0,
+ explanation:
+ 'JSON.stringify converts JavaScript values such as objects or arrays into JSON text.',
+ active: true
+ },
+ {
+ _id: id(),
+ question: 'What does JSON.parse do?',
+ options: [
+ 'Converts JSON text into a JavaScript value',
+ 'Converts an object into JSON text',
+ 'Deletes JSON from memory',
+ 'Uploads JSON to a server'
+ ],
+ correctAnswerIndex: 0,
+ explanation:
+ 'JSON.parse converts valid JSON text into a JavaScript value such as an object or array.',
+ active: true
+ },
+ {
+ _id: id(),
+ question: 'In Express, what does res.status(404) usually mean?',
+ options: [
+ 'The requested resource was not found',
+ 'The request succeeded',
+ 'The server crashed',
+ 'The user is not allowed to access the route'
+ ],
+ correctAnswerIndex: 0,
+ explanation:
+ 'HTTP 404 means Not Found. It is used when the requested resource or route cannot be found.',
+ active: true
+ },
+ {
+ _id: id(),
+ question: 'In Express, what does res.status(500) usually mean?',
+ options: [
+ 'An internal server error occurred',
+ 'The client sent no body',
+ 'The request succeeded',
+ 'The user cancelled the request'
+ ],
+ correctAnswerIndex: 0,
+ explanation:
+ 'HTTP 500 means Internal Server Error. It usually means something went wrong on the server.',
+ active: true
+ },
+ {
+ _id: id(),
+ question: 'In Ruby, what does .each commonly do?',
+ options: [
+ 'Iterates over each item in a collection',
+ 'Deletes each item in a collection',
+ 'Sorts a collection automatically',
+ 'Converts a collection into a string'
+ ],
+ correctAnswerIndex: 0,
+ explanation: '.each loops through each item in a collection such as an array or hash.',
+ active: true
+ },
+ {
+ _id: id(),
+ question: 'In Ruby, what does .map usually return?',
+ options: [
+ 'A new array containing the transformed values',
+ 'The original array unchanged every time',
+ 'Only the first matching value',
+ 'A boolean true or false'
+ ],
+ correctAnswerIndex: 0,
+ explanation:
+ '.map transforms each item in a collection and returns a new array with the results.',
+ active: true
+ },
+ {
+ _id: id(),
+ question: 'In Ruby, what does .select usually return?',
+ options: [
+ 'Items that match the condition',
+ 'The first item only',
+ 'The number of items in an array',
+ 'The original string split into words'
+ ],
+ correctAnswerIndex: 0,
+ explanation: '.select returns the items for which the block condition is truthy.',
+ active: true
+ },
+ {
+ _id: id(),
+ question: 'In Ruby, what does .include? check?',
+ options: [
+ 'Whether a collection or string contains a value',
+ 'Whether a method has been defined privately',
+ 'Whether a file exists on disk',
+ 'Whether a class has been inherited'
+ ],
+ correctAnswerIndex: 0,
+ explanation:
+ '.include? checks whether a value exists inside a collection or string and returns true or false.',
+ active: true
+ },
+ {
+ _id: id(),
+ question: 'In Ruby, what does split usually do to a string?',
+ options: [
+ 'Breaks the string into an array of smaller strings',
+ 'Joins an array into one string',
+ 'Deletes the string',
+ 'Converts the string into an integer automatically'
+ ],
+ correctAnswerIndex: 0,
+ explanation:
+ 'split breaks a string into an array, usually using whitespace or a provided separator.',
+ active: true
+ },
+ {
+ _id: id(),
+ question: 'In Ruby, what does join usually do?',
+ options: [
+ 'Combines array elements into one string',
+ 'Splits a string into an array',
+ 'Starts a new thread',
+ 'Connects to a database automatically'
+ ],
+ correctAnswerIndex: 0,
+ explanation:
+ 'join combines array elements into a single string, optionally using a separator.',
+ active: true
+ },
+ {
+ _id: id(),
+ question: 'In Ruby, what does chomp usually remove?',
+ options: [
+ 'A trailing newline from a string',
+ 'All spaces from a string',
+ 'The first character from a string',
+ 'All numbers from a string'
+ ],
+ correctAnswerIndex: 0,
+ explanation:
+ 'chomp commonly removes a trailing newline from a string, which is useful when reading lines from files.',
+ active: true
+ },
+ {
+ _id: id(),
+ question: 'In Ruby, what does File.readlines usually return?',
+ options: [
+ 'An array containing the lines of a file',
+ 'A single boolean showing whether the file exists',
+ 'The filename without its extension',
+ 'The file permissions only'
+ ],
+ correctAnswerIndex: 0,
+ explanation: 'File.readlines reads a file and returns its lines as an array.',
+ active: true
+ },
+ {
+ _id: id(),
+ question: 'In Ruby, what is a symbol such as :name commonly used for?',
+ options: [
+ 'A lightweight identifier often used as a hash key',
+ 'A string that automatically changes value',
+ 'A hidden password field',
+ 'A special type of integer'
+ ],
+ correctAnswerIndex: 0,
+ explanation:
+ 'Symbols are lightweight identifiers. They are commonly used as hash keys and method names.',
+ active: true
+ },
+ {
+ _id: id(),
+ question: 'In Ruby, what does attr_reader create?',
+ options: [
+ 'Getter methods for instance variables',
+ 'Setter methods only',
+ 'A new class automatically',
+ 'A private constant'
+ ],
+ correctAnswerIndex: 0,
+ explanation:
+ 'attr_reader creates getter methods so instance variables can be read from outside the object.',
+ active: true
+ },
+ {
+ _id: id(),
+ question: 'In Ruby, what does a hash store?',
+ options: [
+ 'Key-value pairs',
+ 'Only ordered numbers',
+ 'Only one string value',
+ 'Only method definitions'
+ ],
+ correctAnswerIndex: 0,
+ explanation:
+ 'A Ruby hash stores key-value pairs, similar to a dictionary or object map in other languages.',
+ active: true
+ }
+];
+
+module.exports = hpcQuestions;
diff --git a/backend/scripts/testData.js b/backend/scripts/testData.js
index 5f24ae6..c8be15a 100644
--- a/backend/scripts/testData.js
+++ b/backend/scripts/testData.js
@@ -1,4 +1,5 @@
const { ObjectId } = require('mongodb');
+const hpcQuestions = require('./data/hpcQuestion');
module.exports.seedData = async (db) => {
const personCol = db.collection('person');
@@ -10,6 +11,8 @@ module.exports.seedData = async (db) => {
const methodCol = db.collection('method');
const bonusChallengeCol = db.collection('bonusChallenge');
+ const hpcQuestionCol = db.collection('hpcQuestion');
+
const checkFocusCol = db.collection('checkFocus');
// -----------------------------
@@ -230,6 +233,11 @@ module.exports.seedData = async (db) => {
await bonusChallengeCol.insertMany(bonusChallenges);
+ // -----------------------------
+ // HPC QUICK CHECK QUESTIONS
+ // -----------------------------
+ await hpcQuestionCol.insertMany(hpcQuestions);
+
// -----------------------------
// CHECKING OPERATIONAL FOCUS OF THE CHECK
// E.G. GRUMPY OLD RESEARCHER
diff --git a/frontend/app/components/Form.jsx b/frontend/app/components/Form.jsx
index 6512dc2..c40f84e 100644
--- a/frontend/app/components/Form.jsx
+++ b/frontend/app/components/Form.jsx
@@ -98,6 +98,10 @@ export default function Form() {
const [checkFocus, setCheckFocus] = useState(null);
const [focusReflection, setFocusReflection] = useState('');
+ const [hpcQuestion, setHpcQuestion] = useState(null);
+ const [selectedAnswerIndex, setSelectedAnswerIndex] = useState(null);
+ const [hpcQuestionResult, setHpcQuestionResult] = useState(null);
+
//METHOD-HIDING SETTINGS
const [hiddenMethodIds, setHiddenMethodIds] = useState([]);
const [revealedMethodIds, setRevealedMethodIds] = useState([]);
@@ -288,6 +292,56 @@ export default function Form() {
getBonusChallenge();
}, []);
+ useEffect(() => {
+ async function getHpcQuestion() {
+ try {
+ const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/hpc-question/random`);
+ const data = await res.json();
+
+ if (!data.success) {
+ setHpcQuestion(null);
+ return;
+ }
+
+ setHpcQuestion(data.body);
+ } catch (error) {
+ console.error('Failed to fetch HPC question', error);
+ setHpcQuestion(null);
+ }
+ }
+ getHpcQuestion();
+ }, []);
+
+ async function checkHpcAnswer(answerIndex) {
+ if (!hpcQuestion || hpcQuestionResult) return;
+
+ try {
+ setSelectedAnswerIndex(answerIndex);
+
+ const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/hpc-question/check`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ questionId: hpcQuestion.id,
+ selectedAnswerIndex: answerIndex,
+ optionOrder: hpcQuestion.optionOrder
+ })
+ });
+
+ const data = await res.json();
+
+ if (!res.ok) {
+ throw new Error(data.error || 'Failed to check answer');
+ }
+
+ setHpcQuestionResult(data.body);
+ } catch (error) {
+ console.error('Failed to check HPC answer:', error);
+ }
+ }
+
useEffect(() => {
async function getCheckFocus() {
try {
@@ -509,7 +563,7 @@ export default function Form() {
- {/* Focus*/}
+ {/* Focus */}
{checkFocus && (
@@ -527,89 +581,95 @@ export default function Form() {
{steps.length == 0 && (
No instruction available
)}
+
{steps.map((step, index) => {
const isCompleted = Boolean(completedSteps[step.id]);
-
const isEditing = editingStepID === step.id;
const isAddingMethod = addMethodStepID === step.id;
+ const shouldShowQuiz =
+ hpcQuestion && index === Math.floor(steps.length / 2) - 1;
return (
-
-
-
- {index + 1}
-
-
-
{step.title}
- {step.expectedTime && (
-
- {step.expectedTime}
+
+
+
+
+ {index + 1}
- )}
-
-
- {step.description}
-
-
-
- View Methods
-
-
-
- {(step.methods || []).map((method, i) => {
- const isMethodHidden = hiddenMethodIds.includes(method.id);
- const isMethodRevealed = revealedMethodIds.includes(method.id);
- return (
- -
- {i > 0 &&
}
-
- {editingMethodId === method.id ? (
-
- ) : (
- <>
+ ) : (
{isMethodHidden && !isMethodRevealed ? (
@@ -618,6 +678,7 @@ export default function Form() {
Independent challenge — work this
method out yourself.
+
- >
- )}
+ )}
- {isEditing && !(editingMethodId === method.id) && (
-
-
-
-
-
- )}
-
- );
- })}
-
- {!isEditing ? (
-
-
-
- ) : (
- <>
+ {isEditing && !(editingMethodId === method.id) && (
+
+
+
+
+
+ )}
+
+ );
+ })}
+
+ {!isEditing ? (
+
+
+
+ ) : (
{!isAddingMethod ? (
+
- >
+ )}
+
+
+
+
+ {step.good && (
+
+
+ Good
+
+
+
+ {step.good}
+
+
)}
-
-
-
-
- {step.good && (
-
-
- Good
-
-
-
- {step.good}
-
-
- )}
- {step.bad && (
-
-
- Bad
-
+ {step.bad && (
+
+
+ Bad
+
-
- {step.bad}
-
+
+ {step.bad}
+
+
+ )}
+
+
+ {/* toggle */}
+
+
+ Completed Successfully
+
+
+
+
+
+ {/* notes */}
+
+
+
+ {/* Questionnaire */}
+ {shouldShowQuiz && (
+
+
+ HPC Quick Check
+
+
+
+ {hpcQuestion.question}
+
+
+
+ {hpcQuestion.options.map((option, optionIndex) => {
+ const isSelected = selectedAnswerIndex === optionIndex;
+ const isCorrectAnswer =
+ hpcQuestionResult?.correctAnswerIndex === optionIndex;
+
+ return (
+
+ );
+ })}
- )}
-
-
- {/* toggle */}
-
-
- Completed Successfully
-
-
-
-
-
- {/* notes */}
-
-
+
+ {hpcQuestionResult && (
+
+
+ {hpcQuestionResult.correct
+ ? 'Correct ✅'
+ : 'Not quite ❌'}
+
+
+
+ {hpcQuestionResult.explanation}
+
+
+ )}
+
+ )}
+
);
})}
@@ -897,6 +1027,7 @@ export default function Form() {
Time remaining
+