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} +
+ +
+
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";