diff --git a/app/package-lock.json b/app/package-lock.json
index 15a6809c..6631c71e 100644
--- a/app/package-lock.json
+++ b/app/package-lock.json
@@ -8,6 +8,7 @@
"name": "@example/basics",
"version": "0.0.1",
"dependencies": {
+ "@lottiefiles/svelte-lottie-player": "^0.2.0",
"commander": "^9.3.0",
"d3": "^7.6.1",
"mathjax": "^3.2.1",
@@ -549,6 +550,14 @@
"integrity": "sha512-4/RWEeXDO6bocPONheFe6gX/oQdP/bEpv0oL4HqjPP5DCenBSt0mHgahppY49N0CpsaqffdwPq+TlX9CYOq2Dw==",
"dev": true
},
+ "node_modules/@lottiefiles/svelte-lottie-player": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/@lottiefiles/svelte-lottie-player/-/svelte-lottie-player-0.2.0.tgz",
+ "integrity": "sha512-sV8YkO8hCDGq67LCCqjT+JW6cAbVCIw9wtgrOWz+Ao6Q8cD7ZvdsCiipSIMIH55Ja6MWwEXP4N4nKD+jL3XQLg==",
+ "dependencies": {
+ "lottie-web": "^5.7.8"
+ }
+ },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -3649,6 +3658,11 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/lottie-web": {
+ "version": "5.9.6",
+ "resolved": "https://registry.npmjs.org/lottie-web/-/lottie-web-5.9.6.tgz",
+ "integrity": "sha512-JFs7KsHwflugH5qIXBpB4905yC1Sub2MZWtl/elvO/QC6qj1ApqbUZJyjzJseJUtVpgiDaXQLjBlIJGS7UUUXA=="
+ },
"node_modules/lower-case": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
@@ -4885,9 +4899,9 @@
}
},
"node_modules/node-fetch": {
- "version": "3.2.5",
- "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.2.5.tgz",
- "integrity": "sha512-u7zCHdJp8JXBwF09mMfo2CL6kp37TslDl1KP3hRGTlCInBtag+UO3LGVy+NF0VzvnL3PVMpA2hXh1EtECFnyhQ==",
+ "version": "3.2.10",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.2.10.tgz",
+ "integrity": "sha512-MhuzNwdURnZ1Cp4XTazr69K0BTizsBroX7Zx3UgDSVcZYKF/6p0CBe4EUb/hLqmzVhl0UpYfgRljQ4yxE+iCxA==",
"dev": true,
"dependencies": {
"data-uri-to-buffer": "^4.0.0",
@@ -6331,9 +6345,9 @@
}
},
"node_modules/svelte": {
- "version": "3.48.0",
- "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.48.0.tgz",
- "integrity": "sha512-fN2YRm/bGumvjUpu6yI3BpvZnpIm9I6A7HR4oUNYd7ggYyIwSA/BX7DJ+UXXffLp6XNcUijyLvttbPVCYa/3xQ==",
+ "version": "3.49.0",
+ "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.49.0.tgz",
+ "integrity": "sha512-+lmjic1pApJWDfPCpUUTc1m8azDqYCG1JN9YEngrx/hUyIcFJo6VZhj0A1Ai0wqoHcEIuQy+e9tk+4uDgdtsFA==",
"engines": {
"node": ">= 8"
}
@@ -7658,6 +7672,14 @@
"integrity": "sha512-4/RWEeXDO6bocPONheFe6gX/oQdP/bEpv0oL4HqjPP5DCenBSt0mHgahppY49N0CpsaqffdwPq+TlX9CYOq2Dw==",
"dev": true
},
+ "@lottiefiles/svelte-lottie-player": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/@lottiefiles/svelte-lottie-player/-/svelte-lottie-player-0.2.0.tgz",
+ "integrity": "sha512-sV8YkO8hCDGq67LCCqjT+JW6cAbVCIw9wtgrOWz+Ao6Q8cD7ZvdsCiipSIMIH55Ja6MWwEXP4N4nKD+jL3XQLg==",
+ "requires": {
+ "lottie-web": "^5.7.8"
+ }
+ },
"@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -9888,6 +9910,11 @@
"integrity": "sha512-cHlYSUpL2s7Fb3394mYxwTYj8niTaNHUCLr0qdiCXQfSjfuA7CKofpX2uSwEfFDQ0EB7JcnMnm+GjbqqoinYYg==",
"dev": true
},
+ "lottie-web": {
+ "version": "5.9.6",
+ "resolved": "https://registry.npmjs.org/lottie-web/-/lottie-web-5.9.6.tgz",
+ "integrity": "sha512-JFs7KsHwflugH5qIXBpB4905yC1Sub2MZWtl/elvO/QC6qj1ApqbUZJyjzJseJUtVpgiDaXQLjBlIJGS7UUUXA=="
+ },
"lower-case": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
@@ -10711,9 +10738,9 @@
"dev": true
},
"node-fetch": {
- "version": "3.2.5",
- "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.2.5.tgz",
- "integrity": "sha512-u7zCHdJp8JXBwF09mMfo2CL6kp37TslDl1KP3hRGTlCInBtag+UO3LGVy+NF0VzvnL3PVMpA2hXh1EtECFnyhQ==",
+ "version": "3.2.10",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.2.10.tgz",
+ "integrity": "sha512-MhuzNwdURnZ1Cp4XTazr69K0BTizsBroX7Zx3UgDSVcZYKF/6p0CBe4EUb/hLqmzVhl0UpYfgRljQ4yxE+iCxA==",
"dev": true,
"requires": {
"data-uri-to-buffer": "^4.0.0",
@@ -11743,9 +11770,9 @@
"dev": true
},
"svelte": {
- "version": "3.48.0",
- "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.48.0.tgz",
- "integrity": "sha512-fN2YRm/bGumvjUpu6yI3BpvZnpIm9I6A7HR4oUNYd7ggYyIwSA/BX7DJ+UXXffLp6XNcUijyLvttbPVCYa/3xQ=="
+ "version": "3.49.0",
+ "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.49.0.tgz",
+ "integrity": "sha512-+lmjic1pApJWDfPCpUUTc1m8azDqYCG1JN9YEngrx/hUyIcFJo6VZhj0A1Ai0wqoHcEIuQy+e9tk+4uDgdtsFA=="
},
"svelte-hmr": {
"version": "0.14.12",
diff --git a/app/package.json b/app/package.json
index da09a3de..3b5e634d 100644
--- a/app/package.json
+++ b/app/package.json
@@ -17,6 +17,7 @@
"svelte": "^3.48.0"
},
"dependencies": {
+ "@lottiefiles/svelte-lottie-player": "^0.2.0",
"commander": "^9.3.0",
"d3": "^7.6.1",
"mathjax": "^3.2.1",
diff --git a/app/src/apps/Examinations/components/LinkedQuestion/LinkedQuestion.svelte b/app/src/apps/Examinations/components/LinkedQuestion/LinkedQuestion.svelte
new file mode 100644
index 00000000..a364f7de
--- /dev/null
+++ b/app/src/apps/Examinations/components/LinkedQuestion/LinkedQuestion.svelte
@@ -0,0 +1,80 @@
+
+
+
+
Question {number}
+
{@html question.body}
+
+
+ {#each question.prompts as prompt}
+
+ {#if submitted && !prompt.correct}
+
{getPromptAnswer(prompt) + 1}
+ {/if}
+
+
+
+
+
{@html prompt.body}
+
+ {/each}
+
+
+ {#each question.options as option, index}
+ -
+
{index + 1}.
+ {@html option.body}
+
+ {/each}
+
+
+
diff --git a/app/src/apps/Examinations/components/LinkedQuestion/index.js b/app/src/apps/Examinations/components/LinkedQuestion/index.js
new file mode 100644
index 00000000..ef5086f8
--- /dev/null
+++ b/app/src/apps/Examinations/components/LinkedQuestion/index.js
@@ -0,0 +1 @@
+export { default as LinkedQuestion } from "./LinkedQuestion.svelte";
diff --git a/app/src/apps/Examinations/components/LinkedQuestion/styles.scss b/app/src/apps/Examinations/components/LinkedQuestion/styles.scss
new file mode 100644
index 00000000..61c044df
--- /dev/null
+++ b/app/src/apps/Examinations/components/LinkedQuestion/styles.scss
@@ -0,0 +1,120 @@
+.linked-question {
+ font-family: "Helvetica Neue", serif;
+ margin-left: 0.25rem;
+ width: 50rem;
+}
+
+.linked-question h2 {
+ margin-bottom: 0.5rem;
+}
+
+.linked-question-text {
+ font-size: 1.25rem;
+ margin-top: 0;
+ margin-left: 1rem;
+}
+
+.linked-question-body {
+ margin-left: 3rem;
+ display: flex;
+ flex-direction: row;
+ font-size: 1.25rem;
+}
+
+.linked-question-prompts {
+ min-height: 90%;
+ margin: 5% 0;
+ width: 50%;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ align-items: flex-start;
+}
+
+.linked-question-prompt {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ position: relative;
+}
+
+.linked-question-prompt div.correct-option {
+ position: absolute;
+ font-weight: bold;
+ height: 100%;
+ top: 0;
+ left: -15%;
+ margin: 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ color: rgb(56, 232, 51);
+ font-size: 1.35rem;
+}
+
+.linked-question-prompt-body {
+ margin-left: 1rem;
+}
+
+// Style div surrounding each select element
+.select-wrapper {
+ background-color: transparent;
+ width: 4.5rem;
+ height: 2.5rem;
+ font-family: inherit;
+ font-size: 1.25rem;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ border-radius: 0.35rem;
+}
+
+.select-wrapper.correct {
+ background: rgb(70, 255, 60);
+}
+
+.select-wrapper.incorrect {
+ background: rgb(255, 69, 69);
+}
+
+.select-wrapper select {
+ width: 100%;
+ height: 100%;
+ padding-left: 15%;
+ background: transparent;
+ font-family: inherit;
+ font-size: inherit;
+ border: 2px solid rgb(157, 157, 157);
+ border-radius: inherit;
+ appearance: none;
+ z-index: 1;
+}
+
+.select-wrapper svg {
+ width: 20px;
+ height: 20px;
+ margin-left: -40%;
+}
+
+.select-wrapper select:focus {
+ border-radius: inherit;
+ border: 3px solid rgb(31, 107, 249);
+}
+
+.linked-question-options {
+ width: 50%;
+ list-style: none;
+ margin: 0;
+ padding-left: 0;
+}
+
+.linked-question-options li {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+}
+
+.linked-question-options li p {
+ font-weight: bold;
+ margin-right: 1rem;
+}
diff --git a/app/src/apps/Examinations/lib/factories/StringStartOrEnd.js b/app/src/apps/Examinations/lib/factories/StringStartOrEnd.js
new file mode 100644
index 00000000..5fc97555
--- /dev/null
+++ b/app/src/apps/Examinations/lib/factories/StringStartOrEnd.js
@@ -0,0 +1,138 @@
+import Factory from "../Factory";
+import { MultipleChoiceQuestion } from "../questions";
+
+const MIN_STRING_SIZE = 3;
+const MAX_STRING_SIZE = 16;
+const MIN_ALPHABET_SIZE = 2;
+const MAX_ALPHABET_SIZE = 6;
+const FULL_ALPHABET = "abcdefghijklmnopqrstuvwxyz";
+
+class StringStartOrEnd extends Factory {
+ create() {
+ const content = this.generateQuestionContent();
+ const body = this.generateQuestionBody(content);
+ const options = this.generateQuestionOptions(content);
+ const question = new MultipleChoiceQuestion(body, options);
+ return question;
+ }
+
+ getRandomAlphabetSize() {
+ return (
+ Math.floor(Math.random() * (MAX_ALPHABET_SIZE - MIN_ALPHABET_SIZE + 1)) +
+ MIN_ALPHABET_SIZE
+ );
+ }
+
+ getRandomStringLength() {
+ return (
+ Math.floor(Math.random() * (MAX_STRING_SIZE - MIN_STRING_SIZE + 1)) +
+ MIN_STRING_SIZE
+ );
+ }
+
+ // Generates random string based on the given alphabet and desired string length
+ getRandomString(alphabet, length) {
+ let result = "";
+ for (let i = 0; i < length; ++i)
+ result += alphabet.at(Math.floor(Math.random() * alphabet.length));
+ return result;
+ }
+
+ getSubstrings(strLength, alphabetSize) {
+ // generate available alphabet
+ let alphabet = FULL_ALPHABET.slice(0, alphabetSize);
+
+ // start substring length is at max 2 characters shorter than the total length of the string
+ let startSubstrLength = Math.floor(Math.random() * (strLength - 2)) + 1;
+ // end substring length will depend on the start substring length
+ let endSubstrLength =
+ Math.floor(Math.random() * (strLength - startSubstrLength - 1)) + 1;
+
+ let startSubstr = this.getRandomString(alphabet, startSubstrLength);
+ let endSubstr = this.getRandomString(alphabet, endSubstrLength);
+ return [startSubstr, endSubstr];
+ }
+
+ generateQuestionContent() {
+ const strLength = this.getRandomStringLength();
+ const alphabetSize = this.getRandomAlphabetSize();
+ const [startSubstr, endSubstr] = this.getSubstrings(
+ strLength,
+ alphabetSize
+ );
+ return { strLength, alphabetSize, startSubstr, endSubstr };
+ }
+
+ generateQuestionBody(questionContent) {
+ const { strLength, alphabetSize, startSubstr, endSubstr } = questionContent;
+ let alphabetString = FULL_ALPHABET.slice(0, alphabetSize)
+ .split("")
+ .join(", ");
+ return String.raw`Consider strings of length ${strLength} over the alphabet $\{${alphabetString}\}$. How many such strings are there that start with $${startSubstr}$ or end with $${endSubstr}$?`;
+ }
+
+ generateQuestionOptions(questionContent) {
+ const { strLength, alphabetSize, startSubstr, endSubstr } = questionContent;
+
+ let options = [];
+
+ options.push(this.generateOptionOne(strLength, alphabetSize, startSubstr));
+ options.push(
+ this.generateOptionTwo(strLength, alphabetSize, startSubstr, endSubstr)
+ );
+ options.push(
+ this.generateOptionThree(strLength, alphabetSize, startSubstr, endSubstr)
+ );
+ options.push(
+ this.generateOptionFour(strLength, alphabetSize, startSubstr, endSubstr)
+ );
+
+ options.sort(() => Math.random() - 0.5);
+
+ return options;
+ }
+
+ generateOptionOne(strLength, alphabetSize, startSubstr) {
+ // this generates the number of strings that start with the given substring
+ let result = alphabetSize ** (strLength - startSubstr.length);
+ return {
+ body: String.raw`$ {${result}} $`,
+ correct: false,
+ };
+ }
+
+ generateOptionTwo(strLength, alphabetSize, startSubstr, endSubstr) {
+ // this generates a falsy value that counts strings that start AND end with the given substrings
+ let result =
+ alphabetSize ** (strLength - startSubstr.length - endSubstr.length);
+ return {
+ body: String.raw`$ {${result}} $`,
+ correct: false,
+ };
+ }
+
+ generateOptionThree(strLength, alphabetSize, startSubstr, endSubstr) {
+ // this generates the correct answer
+ let result =
+ alphabetSize ** (strLength - startSubstr.length) +
+ alphabetSize ** (strLength - endSubstr.length) -
+ alphabetSize ** (strLength - startSubstr.length - endSubstr.length);
+ return {
+ body: String.raw`$ {${result}} $`,
+ correct: true,
+ };
+ }
+
+ generateOptionFour(strLength, alphabetSize, startSubstr, endSubstr) {
+ // this generates a falsy value that double counts strings that start AND end with the given substrings
+ let result =
+ alphabetSize ** (strLength - startSubstr.length) +
+ alphabetSize ** (strLength - endSubstr.length);
+ return {
+ body: String.raw`$ {${result}} $`,
+ correct: false,
+ };
+ }
+}
+
+export { StringStartOrEnd };
diff --git a/app/src/apps/Examinations/lib/questions/LinkedQuestion.js b/app/src/apps/Examinations/lib/questions/LinkedQuestion.js
new file mode 100644
index 00000000..b77f9b36
--- /dev/null
+++ b/app/src/apps/Examinations/lib/questions/LinkedQuestion.js
@@ -0,0 +1,70 @@
+import { Question } from "../Question";
+
+class LinkedQuestion extends Question {
+ prompts = [];
+ options = [];
+ answers = [];
+
+ constructor(body, answers, labId) {
+ super(body, labId);
+ this.answers = this.constructAnswers(answers);
+ // Randomly sort prompts and options after creation
+ this.prompts.sort(() => 0.5 - Math.random());
+ this.options.sort(() => 0.5 - Math.random());
+ }
+
+ constructAnswers(answers) {
+ // Extract prompts and options and return mapping between them
+ return answers
+ .map((answer) => {
+ const option = new LinkedQuestionOption(answer.option.body);
+ this.options.push(option);
+
+ if (answer.prompt) {
+ const prompt = new LinkedQuestionPrompt(
+ answer.prompt.body,
+ undefined
+ );
+ this.prompts.push(prompt);
+ return {
+ prompt,
+ option,
+ };
+ }
+
+ return null;
+ })
+ .filter((answer) => answer);
+ }
+
+ // Checks user answers and marks each prompt as correct/incorrect
+ checkAnswers(userAnswers) {
+ userAnswers.forEach((userAnswer) => {
+ const correctAnswer = this.answers.find(
+ (answer) => answer.prompt === userAnswer.prompt
+ );
+ userAnswer.prompt.correct = userAnswer.option === correctAnswer.option;
+ return userAnswer;
+ });
+ }
+}
+
+class LinkedQuestionOption {
+ body;
+
+ constructor(body) {
+ this.body = body;
+ }
+}
+
+class LinkedQuestionPrompt {
+ body;
+ correct;
+
+ constructor(body, correct) {
+ this.body = body;
+ this.correct = correct;
+ }
+}
+
+export { LinkedQuestion };
diff --git a/app/src/apps/Examinations/lib/questions/index.js b/app/src/apps/Examinations/lib/questions/index.js
index e4a856f0..c7d440ad 100644
--- a/app/src/apps/Examinations/lib/questions/index.js
+++ b/app/src/apps/Examinations/lib/questions/index.js
@@ -1,3 +1,4 @@
export { DesmosAsymtopicAnalysisQuestion } from "./DesmosAsymtopicAnalysisQuestion";
export { MultipleChoiceQuestion } from "./MultipleChoiceQuestion";
export { WrittenQuestion } from "./WrittenQuestion";
+export { LinkedQuestion } from "./LinkedQuestion";
diff --git a/app/src/components/GlobeAnimation/GlobeAnimation.svelte b/app/src/components/GlobeAnimation/GlobeAnimation.svelte
new file mode 100644
index 00000000..30cb8f79
--- /dev/null
+++ b/app/src/components/GlobeAnimation/GlobeAnimation.svelte
@@ -0,0 +1,25 @@
+
+
+{#if LottiePlayer}
+
+
+
+{/if}
diff --git a/app/src/components/GlobeAnimation/index.js b/app/src/components/GlobeAnimation/index.js
new file mode 100644
index 00000000..f7ec60dd
--- /dev/null
+++ b/app/src/components/GlobeAnimation/index.js
@@ -0,0 +1 @@
+export { default as GlobeAnimation } from "./GlobeAnimation.svelte";
diff --git a/app/src/components/LinkedQuestion/index.js b/app/src/components/LinkedQuestion/index.js
new file mode 100644
index 00000000..ef5086f8
--- /dev/null
+++ b/app/src/components/LinkedQuestion/index.js
@@ -0,0 +1 @@
+export { default as LinkedQuestion } from "./LinkedQuestion.svelte";
diff --git a/app/src/pages/index.astro b/app/src/pages/index.astro
index 6d9180d4..5eb58762 100644
--- a/app/src/pages/index.astro
+++ b/app/src/pages/index.astro
@@ -2,6 +2,7 @@
import BaseLayout from "../layouts/BaseLayout.astro";
import HomeLayout from "../layouts/HomeLayout.astro";
import { Button } from "../components";
+import { GlobeAnimation } from "../components/GlobeAnimation";
---
@@ -22,7 +23,7 @@ import { Button } from "../components";
- "globe animation here"
+