diff --git a/client/.eslintrc.json b/client/.eslintrc.json index bdd629ed9..7e7be7d7e 100644 --- a/client/.eslintrc.json +++ b/client/.eslintrc.json @@ -13,7 +13,11 @@ ], "overrides": [ { - "files": ["**/*.spec.js", "**/*.spec.jsx", "**/*.test.js"], + "files": [ + "**/*.spec.js", + "**/*.spec.jsx", + "**/*.test.js" + ], "env": { "jest": true } @@ -23,7 +27,10 @@ "ecmaVersion": "latest", "sourceType": "module" }, - "plugins": ["react", "jest"], + "plugins": [ + "react", + "jest" + ], "settings": { "react": { "createClass": "createReactClass", diff --git a/client/package-lock.json b/client/package-lock.json index c0f28b1d2..a52cf928e 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -7498,9 +7498,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -10724,9 +10724,9 @@ } }, "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -10857,7 +10857,9 @@ } }, "node_modules/flatted": { - "version": "3.3.1", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "license": "ISC" }, "node_modules/follow-redirects": { @@ -17228,9 +17230,9 @@ } }, "node_modules/jsonpath": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/jsonpath/-/jsonpath-1.2.1.tgz", - "integrity": "sha512-Jl6Jhk0jG+kP3yk59SSeGq7LFPR4JQz1DU0K+kXTysUhMostbhU3qh5mjTuf0PqFcXpAT7kvmMt9WxV10NyIgQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/jsonpath/-/jsonpath-1.3.0.tgz", + "integrity": "sha512-0kjkYHJBkAy50Z5QzArZ7udmvxrJzkpKYW27fiF//BrMY7TQibYLl+FYIXN2BiYmwMIVzSfD8aDRj6IzgBX2/w==", "license": "MIT", "dependencies": { "esprima": "1.2.5", @@ -17472,9 +17474,9 @@ } }, "node_modules/lint-staged/node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", "dev": true, "license": "ISC", "bin": { @@ -18052,9 +18054,9 @@ } }, "node_modules/node-forge": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz", - "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", + "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { "node": ">= 6.13.0" @@ -18505,9 +18507,9 @@ "license": "ISC" }, "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", "license": "MIT" }, "node_modules/path-type": { @@ -18528,7 +18530,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -19147,13 +19151,18 @@ } }, "node_modules/postcss-load-config/node_modules/yaml": { - "version": "2.4.1", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", "license": "ISC", "bin": { "yaml": "bin.mjs" }, "engines": { - "node": ">= 14" + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" } }, "node_modules/postcss-loader": { @@ -23243,9 +23252,9 @@ } }, "node_modules/sucrase/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -25574,7 +25583,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "1.10.2", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", "license": "ISC", "engines": { "node": ">= 6" diff --git a/client/public/img/lab_images/what-is-cognitive-bias-image.png b/client/public/img/lab_images/what-is-cognitive-bias-image.png new file mode 100644 index 000000000..5e6ef9e92 Binary files /dev/null and b/client/public/img/lab_images/what-is-cognitive-bias-image.png differ diff --git a/client/public/img/lab_thumbnails/cognitivebiasai.jpg b/client/public/img/lab_thumbnails/cognitivebiasai.jpg new file mode 100644 index 000000000..ccdb8fa96 Binary files /dev/null and b/client/public/img/lab_thumbnails/cognitivebiasai.jpg differ diff --git a/client/src/App.js b/client/src/App.js index 2db10d0e2..6f7378d38 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -21,6 +21,7 @@ import { default as ExerciseLab9 } from "./components/exercise/lab9/Main"; import { default as ExerciseLab10 } from "./components/exercise/lab10/Main"; import { default as ExerciseLab11 } from "./components/exercise/lab11/Main"; import { default as ExerciseLab12 } from "./components/exercise/lab12/Main"; +import { default as ExerciseLab13 } from "./components/exercise/lab13/Main"; import { default as ExerciseLab14 } from "./components/exercise/lab14/Main"; import { Sections } from "./constants/index"; @@ -115,7 +116,7 @@ const App = () => { const renderLabs = () => { return ( -
+
@@ -142,6 +143,7 @@ const App = () => { + { - + { <> {isLoaded ? (
}>
-
-
+
+
{labInProgress ? ( { canDismiss, timeOutTime: startTime, timeOutMessage, + width, } = props; const [seconds, updateSeconds] = useState(startTime); @@ -90,7 +91,10 @@ const ALLModal = (props) => { }} >
-
+
{/*Header*/} {showHeader && ( <> @@ -249,6 +253,7 @@ ALLModal.propTypes = { secondaryAction: PropTypes.func, timeOutTime: PropTypes.number, timeOutMessage: PropTypes.string, + width: PropTypes.string, }; export default ALLModal; diff --git a/client/src/components/all-components/DragAndDrop/DragDropGame.js b/client/src/components/all-components/DragAndDrop/DragDropGame.js index eee223053..202299634 100644 --- a/client/src/components/all-components/DragAndDrop/DragDropGame.js +++ b/client/src/components/all-components/DragAndDrop/DragDropGame.js @@ -52,6 +52,7 @@ const DragDropGame = ({ handleNav, colContainerStyle, gameStyle, + cardIcon, }) => { const [columns, setColumns] = useState(arrayToObject(cols, "id")); const [bank, setBank] = useState(initialBank); @@ -193,6 +194,7 @@ const DragDropGame = ({ bank={bank} bankStyle={bankStyle} cardStyle={bankCardStyle} + cardIcon={cardIcon} />
@@ -243,6 +245,7 @@ DragDropGame.propTypes = { handleNav: PropTypes.func.isRequired, colContainerStyle: PropTypes.string, gameStyle: PropTypes.string, + cardIcon: PropTypes.any, }; export default DragDropGame; diff --git a/client/src/components/all-components/DragAndDrop/DraggableCard.js b/client/src/components/all-components/DragAndDrop/DraggableCard.js index 5ef98fb2a..3411fed91 100644 --- a/client/src/components/all-components/DragAndDrop/DraggableCard.js +++ b/client/src/components/all-components/DragAndDrop/DraggableCard.js @@ -2,6 +2,7 @@ import { useDraggable } from "@dnd-kit/core"; import React, { useState, useEffect } from "react"; import PropTypes from "prop-types"; import { twMerge } from "tailwind-merge"; +import DragIndicatorRoundedIcon from "@mui/icons-material/DragIndicatorRounded"; const DraggableCard = ({ card, cardStyle }) => { const [borderColor, setBorderColor] = useState(""); @@ -38,7 +39,9 @@ const DraggableCard = ({ card, cardStyle }) => { )} > {card.content} -

{card.title}

+

+ {card.title} +

         {card.body}
       
diff --git a/client/src/components/all-components/DragAndDrop/DroppableBank.js b/client/src/components/all-components/DragAndDrop/DroppableBank.js index 993efb2bd..2dacf5c45 100644 --- a/client/src/components/all-components/DragAndDrop/DroppableBank.js +++ b/client/src/components/all-components/DragAndDrop/DroppableBank.js @@ -3,13 +3,18 @@ import React from "react"; import DraggableCard from "./DraggableCard"; import PropTypes from "prop-types"; -const DroppableBank = ({ bank, bankStyle, cardStyle }) => { +const DroppableBank = ({ bank, bankStyle, cardStyle, cardIcon }) => { const { setNodeRef } = useDroppable({ id: "bank" }); return (
{bank.map((card) => ( - + ))}
); @@ -24,6 +29,7 @@ DroppableBank.propTypes = { ).isRequired, bankStyle: PropTypes.string.isRequired, cardStyle: PropTypes.string.isRequired, + cardIcon: PropTypes.any, }; export default DroppableBank; diff --git a/client/src/components/all-components/Lab/LabWindow.js b/client/src/components/all-components/Lab/LabWindow.js index 9df4bc134..aef60f4ab 100644 --- a/client/src/components/all-components/Lab/LabWindow.js +++ b/client/src/components/all-components/Lab/LabWindow.js @@ -17,21 +17,19 @@ const LabWindow = (props) => { } = props; return ( -
+

{props.title}

-
+
{/* Blue and Yellow stripes*/}
{ {/* Nav Pane and Lab Window */}
- +
+ +
{body !== 2 && }
{children} diff --git a/client/src/components/all-components/Lab/NavigationPane.jsx b/client/src/components/all-components/Lab/NavigationPane.jsx index b2f90d1dc..becd9aebf 100644 --- a/client/src/components/all-components/Lab/NavigationPane.jsx +++ b/client/src/components/all-components/Lab/NavigationPane.jsx @@ -79,19 +79,19 @@ const NavigationPane = (props) => { "tw-flex tw-flex-col tw-gap-y-3 tw-text-left xs:tw-hidden md:tw-flex tw-max-w-[20rem] tw-h-full tw-justify-between tw-z-10" } > -
+
{/* Title Block */}
-

{props.title}

+

{props.title}

{/* Table of Contents Block */}
@@ -115,7 +115,7 @@ const NavigationPane = (props) => { >

{ - const options = [ - "Strongly Disagree", - "Disagree", - "Neutral", - "Agree", - "Strongly Agree", - ]; +const defaultOptions = [ + "Strongly Disagree", + "Disagree", + "Neutral", + "Agree", + "Strongly Agree", +]; +const Likert = ({ + options = defaultOptions, + onAnswerSelected, + name = "likert", +}) => { return (

@@ -19,12 +23,12 @@ const Likert = (props) => { -
))}
@@ -33,7 +37,9 @@ const Likert = (props) => { }; Likert.propTypes = { + options: PropTypes.arrayOf(PropTypes.string), onAnswerSelected: PropTypes.func.isRequired, + name: PropTypes.string, }; export default Likert; diff --git a/client/src/components/all-components/ProgressBar.js b/client/src/components/all-components/ProgressBar.js index 409b120e3..035886b00 100644 --- a/client/src/components/all-components/ProgressBar.js +++ b/client/src/components/all-components/ProgressBar.js @@ -63,7 +63,7 @@ const ProgressBar = ({ return complete ? ( <> ) : ( -
+
{!disableTitle && (

Time Remaining: {elapsed}

)} diff --git a/client/src/components/all-components/UserPfp.js b/client/src/components/all-components/UserPfp.js new file mode 100644 index 000000000..ae429abba --- /dev/null +++ b/client/src/components/all-components/UserPfp.js @@ -0,0 +1,33 @@ +import useMainStateContext from "src/reducers/MainContext"; +import PropTypes from "prop-types"; +import DefaultUser from "../../assets/images/DefaultUser.png"; + +// User profile picture circle +const UserPfp = ({ onClick }) => { + const { state } = useMainStateContext(); + const user = state.main.user; + const className = `tw-w-full tw-h-full tw-object-cover tw-rounded-full ${onClick ? "tw-cursor-pointer" : ""}`; + + return user?.userpfp ? ( + User Profile Picture + ) : user.firstname && user.lastinitial ? ( +
{user?.firstname[0] + user?.lastinitial}
+ ) : ( + Default User + ); +}; + +UserPfp.propTypes = { + onClick: PropTypes.func, +}; + +export default UserPfp; diff --git a/client/src/components/all-components/imagine-components/Ranking.js b/client/src/components/all-components/imagine-components/Ranking.js index 6fdaa4b4a..69717785a 100644 --- a/client/src/components/all-components/imagine-components/Ranking.js +++ b/client/src/components/all-components/imagine-components/Ranking.js @@ -54,43 +54,80 @@ const RankingEntry = (option, length, handleOption, currrentValue) => { }; const RankingQuestion = (props) => { - //Both are used in order to ensure mutual exculivity between a value and it's key + //Both are used in order to ensure mutual exclusivity between a value and its key //These are answers in {option: ranking} eg: {"Option11": 2} const [selectedAnswers, setSelectedAnswers] = useState({}); //These are the answers in the inverted order {ranking: option} eg {2: "Option1"} const [availableAnswers, setAvailableAnswers] = useState({}); - //sets the base value of each hashmap. for selcted answers 0 is the defalut value, and for available answers "" is the default + // Work with a local copy of options to avoid mutating props.options directly + const [displayOptions, setDisplayOptions] = useState([...props.options]); + + // Shuffle displayOptions on mount unless disabled + useEffect(() => { + if (props.disableShuffle) { + setDisplayOptions([...props.options]); + return; + } + const arr = [...props.options]; + for (let i = arr.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + const temp = arr[i]; + arr[i] = arr[j]; + arr[j] = temp; + } + setDisplayOptions(arr); + }, [props.options]); + + // sets the base value of each hashmap. for selected answers 0 is the default value, and for available answers "" is the default + // If props.initialSelectedAnswers is provided, use it to pre-populate both maps useEffect(() => { const selectedOptions = {}; const availableOptions = {}; - for (let i = 0; i < props.options.length; i++) { - availableOptions[i + 1] = ""; - selectedOptions[props.options[i]] = 0; + + for (let i = 0; i < displayOptions.length; i++) { + const opt = displayOptions[i]; + const initialVal = props.initialSelectedAnswers?.[opt] ?? 0; + selectedOptions[opt] = initialVal; } + for (let i = 0; i < displayOptions.length; i++) { + const rank = i + 1; + // find which option (if any) is assigned to this rank from initialSelectedAnswers + let assigned = ""; + if (props.initialSelectedAnswers) { + for (const [k, v] of Object.entries(props.initialSelectedAnswers)) { + if (v === rank) { + assigned = k; + break; + } + } + } + availableOptions[rank] = assigned; + } + setSelectedAnswers(selectedOptions); setAvailableAnswers(availableOptions); - }, [props.options.length]); + }, [displayOptions, props.initialSelectedAnswers]); - //anytime selected answers are updated, notify the registered observer + // anytime selected answers are updated, notify the registered observer useEffect(() => { props.updatedSelectedAnswers?.(selectedAnswers); }, [selectedAnswers]); - //handles selection to ensure mutal exclusivity + // handles selection to ensure mutual exclusivity const handleSelection = (rankingNumber, option) => { const prevSelectedAnswer = selectedAnswers[option]; const prevAvailableAnswer = availableAnswers[rankingNumber]; - //if current ranking is already taken, remove is - if (prevAvailableAnswer != 0) { + // if current ranking is already taken, remove it + if (prevAvailableAnswer && prevAvailableAnswer !== "") { setSelectedAnswers((prevState) => ({ ...prevState, [prevAvailableAnswer]: 0, })); } - //if current option is already taken, remove it - if (prevSelectedAnswer != "") { + // if current option is already taken, remove it + if (prevSelectedAnswer && prevSelectedAnswer !== 0) { setAvailableAnswers((prevState) => ({ ...prevState, [prevSelectedAnswer]: "", @@ -106,24 +143,14 @@ const RankingQuestion = (props) => { })); }; - useEffect(() => { - for (let i = props.options.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - const temp = props.options[i]; - props.options[i] = props.options[j]; - props.options[j] = temp; - } - return; - }, []); - return (
- {props.options.map((option) => ( + {displayOptions.map((option) => (
{RankingEntry( option, - props.options.length, + displayOptions.length, handleSelection, selectedAnswers[option], )} @@ -137,6 +164,9 @@ const RankingQuestion = (props) => { RankingQuestion.propTypes = { options: PropTypes.array.isRequired, updatedSelectedAnswers: PropTypes.func, + initialSelectedAnswers: PropTypes.object, + // when true, do not shuffle options on mount + disableShuffle: PropTypes.bool, }; export default RankingQuestion; diff --git a/client/src/components/all-components/imagine-components/Survey.js b/client/src/components/all-components/imagine-components/Survey.js index 1130a35c4..6ae3e8857 100644 --- a/client/src/components/all-components/imagine-components/Survey.js +++ b/client/src/components/all-components/imagine-components/Survey.js @@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react"; import PropTypes from "prop-types"; import QuestionCount from "../../quiz/components/QuestionCount"; import AnswerOption from "./AnswerOption"; -import Likert from "./Likert"; +import Likert from "../Likert"; import Avatar from "avataaars"; import ImagineService from "src/services/ImagineService"; import RankingQuestion from "./Ranking"; diff --git a/client/src/components/body/About.js b/client/src/components/body/About.js index 22c06abb5..cd8023850 100644 --- a/client/src/components/body/About.js +++ b/client/src/components/body/About.js @@ -31,12 +31,9 @@ const About = (props) => { return (
-
-
-

- {" "} - About This Lab -

+
+
+

About This Lab

{aboutText?.about}

diff --git a/client/src/components/body/Reading/Reading.js b/client/src/components/body/Reading/Reading.js index 1cec2257e..15e350804 100644 --- a/client/src/components/body/Reading/Reading.js +++ b/client/src/components/body/Reading/Reading.js @@ -240,14 +240,6 @@ const Reading = (props) => { positionPercentage: scrollPositionPercentage, }, ]); - console.log( - "Scroll position percentage: " + - JSON.stringify(pagePosition) + - "\n" + - "at " + - seconds + - " seconds", - ); }, 1000); return () => { @@ -294,19 +286,19 @@ const Reading = (props) => { navigate("/Imagine2023/PostSurvey"); }; + const hasPiechartInBody = () => { + return readingData?.body?.some((item) => item.type === "piechart"); + }; + return (
-

- Reading -

+

Reading

-
+
{readingData?.description !== "" ? ( <>

{readingData?.description.header}

@@ -317,7 +309,7 @@ const Reading = (props) => { ) : ( <> )} - {readingData?.piechart && ( + {!hasPiechartInBody() && readingData?.piechart?.header && ( <> {mobileView ? ( <> @@ -415,6 +407,26 @@ const Reading = (props) => { )} {data.type === "image" && } {data.type === "links" && } + {data.type === "piechart" && data.content && ( + <> +
+
+ +
+
+ {data.content.caption && ( +
+ {data.content.caption} +
+ )} + + )} ); }) diff --git a/client/src/components/body/Reinforcement.js b/client/src/components/body/Reinforcement.js index 5d21ae13f..b35085c20 100644 --- a/client/src/components/body/Reinforcement.js +++ b/client/src/components/body/Reinforcement.js @@ -28,13 +28,11 @@ const Reinforcement = (props) => { } return ( -
-

Reinforcement

-
-

- Here is some supplemental material to reinforce the topic. -

-
+
+

Reinforcement

+

+ Here is some supplemental material to reinforce the topic. +

{reinforcement.map((data, index) => { return ( diff --git a/client/src/components/body/lab/Lab.js b/client/src/components/body/lab/Lab.js index 2ebbbb1d6..e7cb1ad0c 100644 --- a/client/src/components/body/lab/Lab.js +++ b/client/src/components/body/lab/Lab.js @@ -161,7 +161,7 @@ const Lab = (props) => { case "FEATURED_LABS": default: return ( -
- +
); @@ -80,6 +80,7 @@ LoginBody.propTypes = { body: PropTypes.number, }), }), + closeModal: PropTypes.function, }; export default connect(mapStateToProps, mapDispatchToProps)(LoginBody); diff --git a/client/src/components/body/profilepage/ProfileHeader.js b/client/src/components/body/profilepage/ProfileHeader.js index c158a474f..2a211893e 100644 --- a/client/src/components/body/profilepage/ProfileHeader.js +++ b/client/src/components/body/profilepage/ProfileHeader.js @@ -1,5 +1,6 @@ import React from "react"; import PropTypes from "prop-types"; +import UserPfp from "src/components/all-components/UserPfp"; const ProfileHeader = (props) => { const { user } = props; @@ -26,18 +27,7 @@ const ProfileHeader = (props) => { tw-rounded-full tw-border-solid xs:tw-mx-[2rem] md:tw-mx-[4rem] tw-border-primary-yellow tw-z-[1rem] tw-flex tw-flex-row tw-overflow-hidden" > - {user?.userpfp ? ( - {`${user.firstname}'s - ) : ( -
- {/* get user's first and last initials */} - {user?.firstname[0] + user?.lastinitial} -
- )} +
diff --git a/client/src/components/exercise/lab0/Main.js b/client/src/components/exercise/lab0/Main.js index 46bc87890..9b6a6c908 100644 --- a/client/src/components/exercise/lab0/Main.js +++ b/client/src/components/exercise/lab0/Main.js @@ -89,7 +89,7 @@ const Main = (props) => { const [newLabTopics, setNewLabTopics] = useState([]); return ( -
+
+
{ return ( -
+
{/* Exercise Start */} diff --git a/client/src/components/exercise/lab11/Main.js b/client/src/components/exercise/lab11/Main.js index 977b4ea28..4e3c0d482 100644 --- a/client/src/components/exercise/lab11/Main.js +++ b/client/src/components/exercise/lab11/Main.js @@ -44,7 +44,7 @@ const Main = () => { useScroll(); return ( -
+
{ const [gradTerm, setGradTerm] = useState(""); return ( -
+
{}, + firstName: "", + setFirstName: () => {}, + lastName: "", + setLastName: () => {}, + preferredName: "", + setPreferredName: () => {}, + pronouns: "", + setPronouns: () => {}, + college: "", + setCollege: () => {}, + major: "", + setMajor: () => {}, + gradTerm: "", + setGradTerm: () => {}, + + // Ranking state + rankingSuccess: false, + setRankingSuccess: () => {}, + rankingColumns: [], + setRankingColumns: () => {}, + rankingBank: [], + setRankingBank: () => {}, + rankingComplete: false, + setRankingComplete: () => {}, + resetRanking: () => {}, + + // Save chat history + chatMessages: [], + setChatMessages: () => {}, + resetChatMessages: () => {}, + + // Wikpedia page states + currentPhase: 1, + setCurrentPhase: () => {}, + hasVisitedWikipedia: false, + setHasVisitedWikipedia: () => {}, + wikipediaTimeSpent: 0, + setWikipediaTimeSpent: () => {}, + + // IDE fix states + showConfidenceScore: false, + setShowConfidenceScore: () => {}, + showCitations: false, + setShowCitations: () => {}, + disclaimerMessage: "", + setDisclaimerMessage: () => {}, + // Question tracking states + askedQuestions: [], + setAskedQuestions: () => {}, +}); + +export const ExerciseStateProvider = ({ children }) => { + const [exerciseState, setExerciseState] = useState("submitting"); + const [firstName, setFirstName] = useState(""); + const [lastName, setLastName] = useState(""); + const [preferredName, setPreferredName] = useState(""); + const [pronouns, setPronouns] = useState(""); + const [college, setCollege] = useState(""); + const [major, setMajor] = useState(""); + const [gradTerm, setGradTerm] = useState(""); + + // Ranking state + const [rankingSuccess, setRankingSuccess] = useState(false); + const [rankingColumns, setRankingColumns] = useState(() => []); // Initialize as empty array + const [rankingBank, setRankingBank] = useState(() => []); // Initialize as empty array + const [rankingComplete, setRankingComplete] = useState(false); + + // Chat history state + const [chatMessages, setChatMessages] = useState([]); + + // Wikpedia page tracking states + const [wikipediaTimeSpent, setWikipediaTimeSpent] = useState(0); + const [currentPhase, setCurrentPhase] = useState(1); + const [hasVisitedWikipedia, setHasVisitedWikipedia] = useState(false); + const [wikipediaAccumulatedTime, setWikipediaAccumulatedTime] = useState(0); + const [wikipediaSessionStart, setWikipediaSessionStart] = useState(null); + + // Post IDE fix tracking states + const [showConfidenceScore, setShowConfidenceScore] = useState(false); + const [showCitations, setShowCitations] = useState(false); + const [disclaimerMessage, setDisclaimerMessage] = useState(""); + + // Question tracking states + const [askedQuestions, setAskedQuestions] = useState([]); + const [topicIndex, setTopicIndex] = useState(0); + + // Reset ranking function + const resetRanking = () => { + setRankingSuccess(false); + setRankingColumns([]); + setRankingBank([]); + setRankingComplete(false); + }; + + const resetChatMessages = () => { + setChatMessages([]); + }; + + const [exercisePromptsState, setExercisePromptsState] = useState([ + { + id: "disclaimer", + fileId: 0, + value: "", + }, + { + id: "confidence", + fileId: 0, + value: false, + }, + { + id: "citations", + fileId: 0, + value: false, + }, + ]); + + const [validInputs, setValidInputs] = useState({ + disclaimer: null, + confidence: null, + citations: null, + }); + const [isFirst, setIsFirst] = useState(true); + + const handleUserInputChange = (id, value) => { + setExercisePromptsState((prev) => + prev.map((item) => (item.id === id ? { ...item, value } : item)), + ); + setIsFirst(false); + }; + + const checkInputValid = () => { + const disclaimerValue = exercisePromptsState + .find((i) => i.id === "disclaimer") + .value.trim() + .toLowerCase(); + // The disclaimer should be at least 20 characters, and include the words "verify" and "output" + const disclaimerValid = + disclaimerValue.length >= 20 && + /\bverify\b/.test(disclaimerValue) && + /\boutput\b/.test(disclaimerValue); + const confidenceValid = + exercisePromptsState.find((i) => i.id === "confidence").value.trim() === + "true"; + const citationsValid = + exercisePromptsState.find((i) => i.id === "citations").value.trim() === + "true"; + + setValidInputs({ + disclaimer: disclaimerValid, + confidence: confidenceValid, + citations: citationsValid, + }); + return disclaimerValid && confidenceValid && citationsValid; + }; + + // No-ops for fetchRepair/postRepair for this exercise + const fetchRepair = () => {}; + const postRepair = () => {}; + + return ( + + {children} + + ); +}; + +ExerciseStateProvider.propTypes = { + children: PropTypes.node.isRequired, +}; + +export const useLab13 = () => useContext(ExerciseStateContext); + +export default ExerciseStateContext; diff --git a/client/src/components/exercise/lab13/Main.js b/client/src/components/exercise/lab13/Main.js new file mode 100644 index 000000000..f95f5f95b --- /dev/null +++ b/client/src/components/exercise/lab13/Main.js @@ -0,0 +1,33 @@ +import React from "react"; +import { Router } from "@reach/router"; +import { ExerciseStateProvider } from "./Lab13Context"; + +import ExerciseIntroduction from "./pages/ExerciseIntroduction"; +import ConfidenceRanking from "./pages/ConfidenceRanking"; +import AIPanel from "./pages/AIPanel.js"; +import Conclusion from "./pages/Conclusion.js"; +import IDEExercise from "./pages/IDEExercise"; +import IDEIntroduction from "./pages/IDEIntroduction"; + +/** + * Main(): is the routing component for managing the lab exercise progression, + * this will be responsible for iterating through the different stages of the lab + * and acting as the container managing the state of the user. + */ +const Main = () => { + return ( +
+ + + + + + + + + + +
+ ); +}; +export default Main; diff --git a/client/src/components/exercise/lab13/components/AIChatBot.js b/client/src/components/exercise/lab13/components/AIChatBot.js new file mode 100644 index 000000000..93683a38b --- /dev/null +++ b/client/src/components/exercise/lab13/components/AIChatBot.js @@ -0,0 +1,400 @@ +import React, { useState, useRef, useEffect } from "react"; +import PropTypes from "prop-types"; +import BlobLoader from "./BlobLoader"; +import Avatar from "./Avatar"; +import { AvatarType } from "src/constants/lab13/AvatarType"; +import HyperLinkImage from "src/assets/images/lab13/HyperLink.png"; +import TypingMessage from "./TypingMessage"; + +/** + * Chatbot component that displays user-ai messages + * and responds to user interactions + * @param {*} userQuestions : Array of question objects to show on dropdown menu + * @param {*} fixedAIResponse : Array of response objects corresponding to user questions + * @param {*} onAnswerSelected : Callback function when an answer is displayed + * @returns + */ +const AIChatBot = ({ + userQuestions, + fixedAIResponse, + onAnswerDataChange, + onTypingChange, + onThinkingChange, + messages = [], + setMessages, + canSelectQuestion = true, + showConfidenceScore = false, + showCitations = false, + disclaimerMessage = "", + onCitationClick = null, + onQuestionAsked = null, +}) => { + const [isTyping, setIsTyping] = useState(false); + const [isThinking, setIsThinking] = useState(false); + const [showQuestionOptions, setShowQuestionOptions] = useState(false); + const messagesContainerRef = useRef(null); + + // Auto-trigger typing animation for any new bot message flagged as isNew + useEffect(() => { + if (messages.length > 0) { + const lastMessage = messages[messages.length - 1]; + if (lastMessage.sender === "bot" && lastMessage.isNew) { + setIsTyping(true); + } + } + }, [messages]); + + useEffect(() => { + if (onTypingChange) { + onTypingChange(isTyping); + } + }, [isTyping, onTypingChange]); + + useEffect(() => { + if (onThinkingChange) { + onThinkingChange(isThinking); + } + }, [isThinking, onThinkingChange]); + + // Show question options when messages end with a bot message + useEffect(() => { + if (messages.length > 0 && canSelectQuestion) { + const lastMessage = messages[messages.length - 1]; + if (lastMessage.sender === "bot" && !isTyping && !isThinking) { + // Small delay to show questions after bot finishes typing + const timer = setTimeout(() => { + setShowQuestionOptions(true); + }, 500); + return () => clearTimeout(timer); + } else { + // Last message is a user message, or bot is still typing/thinking — hide immediately + setShowQuestionOptions(false); + } + } else { + setShowQuestionOptions(false); + } + }, [messages, isTyping, isThinking, canSelectQuestion]); + + // Scroll function to show the most recent message + const scrollToBottom = () => { + if (messagesContainerRef.current) { + messagesContainerRef.current.scrollTop = + messagesContainerRef.current.scrollHeight; + } + }; + /** Scroll to bottom when message array is updated + * to enable message visibility + */ + useEffect(() => { + scrollToBottom(); + }, [messages, isThinking, showQuestionOptions]); + + /** + * Function handling when user clicks on a question in + * the dropdown creating a user and corresponding bot message + * and adding it in the chat + * @param {} question : Question object from questions array + */ + const handleQuestionClick = (question) => { + if (!canSelectQuestion) return; + + setShowQuestionOptions(false); + + // Track question before adding to messages + if (onQuestionAsked && question.originalIndex !== undefined) { + onQuestionAsked(question.originalIndex); + } + + // Add user message after small delay + setTimeout(() => { + const userMsg = { + sender: AvatarType.User, + text: question.text, + id: `user-${question.id}-${Date.now()}`, + timestamp: new Date(), + }; + // Add new user message to message history + setMessages((prev) => [...prev, userMsg]); + + // Find the corresponding AI response by the matching ID + const botObj = fixedAIResponse.find((resp) => resp.id === question.id); + + // Store the answer data and notify parent + const answerData = { + biasType: botObj?.biasType, + biasDefinition: botObj?.biasDefinition, + explanation: botObj?.explanation, + aiResponseText: botObj?.text, + }; + + // Calculate delay based on response length + const delay = Math.ceil((botObj?.text.length || 100) / 100) * 500 + 500; + + setTimeout(() => { + const botMsg = { + sender: "bot", + text: botObj ? botObj.text : "No response found.", + id: `bot-${question.id}-${Date.now()}`, + timestamp: new Date(), + confidence: botObj?.confidence, + isPhase4: showConfidenceScore || showCitations || disclaimerMessage, + isNew: true, + }; + + // Add bot response to message history + setMessages((prev) => [...prev, botMsg]); + setIsThinking(false); + setIsTyping(true); + + if (onAnswerDataChange) { + onAnswerDataChange(answerData); + } + }, delay); + }, 300); // delay for question fade + }; + + /** + * Decide which animation the blob should use based + * on the AI state + * @returns {string} "pulsing" | "spinning" | "static" + */ + const getBlobMode = () => { + if (isThinking) return "pulsing"; + if (isTyping) return "spinning"; + return "static"; + }; + + return ( +
+ {/* Scrollable container that displays the chat messages */} +
+ {/* Container for indvidual messages */} + {messages && messages.length > 0 + ? messages.map((msg, index) => ( +
+ {/* Avatar circle */} + + {/* Message box adjacent to user message */} +
+
+ {/* User messages - just display text */} + {msg.sender === AvatarType.User ? ( + msg.text + ) : ( + // Bot messages - use renderAIMessage for additional features + <> + {index === messages.length - 1 && isTyping ? ( + // Typing animation for latest message + { + setMessages((prev) => + prev.map((m) => + m.id === msg.id ? { ...m, isNew: false } : m, + ), + ); + setIsTyping(false); + }} + /> + ) : ( + // Display message text + msg.text + )} + + {/* Show confidence, disclaimer, citations AFTER typing completes */} + {(!isTyping || index !== messages.length - 1) && + msg.confidence && + msg.isPhase4 && ( +
+ {showConfidenceScore && ( +
+ Confidence Score: + {msg.confidence} +
+ )} + + {disclaimerMessage && ( +
+ Disclaimer: + {disclaimerMessage} +
+ )} + + {showCitations && ( +
+ Source: +
+

ALLpedia

+ Hyper Link Image +
+
+ )} +
+ )} + + )} +
+ {/* Show blob for most recent AI messages while its typing */} + {msg.sender === "bot" && + index === messages.length - 1 && + isTyping && ( +
+ +
+ )} +
+
+ )) + : null} + + {/* Question options after greeting */} + {showQuestionOptions && messages.length > 0 && !isTyping && ( +
+ + +
+
+ {userQuestions.map((question, index) => ( + + + {index < userQuestions.length - 1 && ( +
+ )} + + ))} +
+
+
+ )} + + {/* Show thinking blob when AI is thinking (before message appears) */} + {isThinking && ( +
+ +
+ +
+
+ )} +
+ + {/* Message fade in keyframe animation */} + +
+ ); +}; +AIChatBot.propTypes = { + userQuestions: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + text: PropTypes.string.isRequired, + }), + ), + fixedAIResponse: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + text: PropTypes.string.isRequired, + biasType: PropTypes.string, + biasDefinition: PropTypes.object, + explanation: PropTypes.string, + }), + ), + onAnswerDataChange: PropTypes.func, + onTypingChange: PropTypes.func, + onThinkingChange: PropTypes.func, + messages: PropTypes.array.isRequired, + setMessages: PropTypes.func.isRequired, + canSelectQuestion: PropTypes.bool, + showConfidenceScore: PropTypes.bool, + showCitations: PropTypes.bool, + disclaimerMessage: PropTypes.string, + onCitationClick: PropTypes.func, + onQuestionAsked: PropTypes.func, +}; + +export default AIChatBot; diff --git a/client/src/components/exercise/lab13/components/AIPanel/AIChatBotTab.js b/client/src/components/exercise/lab13/components/AIPanel/AIChatBotTab.js new file mode 100644 index 000000000..f35d29e3e --- /dev/null +++ b/client/src/components/exercise/lab13/components/AIPanel/AIChatBotTab.js @@ -0,0 +1,284 @@ +import { + BIAS_DEFINITIONS, + BIAS_POSITION_MAP, + BIAS_TYPES, +} from "src/constants/lab13/BiasQuestionsConfig"; +import AIChatBot from "../AIChatBot"; +import { Tab } from "../Tab/Tab"; +import { useContext, useMemo } from "react"; +import ExerciseStateContext from "../../Lab13Context"; +import PropTypes from "prop-types"; + +const AIChatBotTab = ({ + currentTopic, + topicData, + setSelectedBiasData, + setShowRatingModal, + setCurrentAnswerData, + setQuestionAnswered, + showRatingModal, + showBiasExplanation, + clickedReviewButtonThisPhase, + showWikipediaTab, + currentDisplayTime, + currentAnswerData, + questionAnswered, + isBotThinking, + isBotTyping, + requireWikipedia, + activeTopic, + setClickedReviewButtonThisPhase, + setIsBotThinking, + setIsBotTyping, + setCurrentQuestion, + setActiveTab, +}) => { + const { + chatMessages, + setChatMessages, + hasVisitedWikipedia, + currentPhase, + showConfidenceScore, + showCitations, + disclaimerMessage, + askedQuestions, + setAskedQuestions, + topicIndex, + } = useContext(ExerciseStateContext); + + const activeBias = + BIAS_POSITION_MAP[currentTopic?.biasPosition] || BIAS_TYPES.HALO_EFFECT; + const handleAnswerSelected = ( + biasType, + biasDefinition, + explanation, + aiResponseText, + ) => { + setSelectedBiasData({ + biasType, + biasDefinition, + explanation, + aiResponseText, + }); + setShowRatingModal(true); + }; + + const handleAnswerDataChange = (data) => { + setCurrentAnswerData(data); + setQuestionAnswered(true); + }; + + const getCurrentInstruction = () => { + if (!currentTopic) return null; + + // hide instructions when modals are open or when review button was clicked + if ( + showRatingModal || + showBiasExplanation || + clickedReviewButtonThisPhase + ) { + return null; + } + + if (topicIndex === 0) { + if (!canReviewResponse) { + return `You're now on your moderately knowledgeable topic, ${currentTopic.title}. Select a prompt to interact with ALL-IE.`; + } + return null; + } + + if (topicIndex === 1) { + if (questionAnswered && showWikipediaTab && !hasVisitedWikipedia) { + return `ALLpedia is now available. Click the ALLpedia tab above if you'd like to fact-check ALL-IE's response on ${currentTopic.title} before reviewing it.`; + } + if (!questionAnswered) { + return `You're now on your most knowledgeable topic, ${currentTopic.title}. Select a prompt to interact with ALL-IE.`; + } + return null; + } + if (topicIndex === 2 && currentPhase < 4) { + if (!questionAnswered || !currentAnswerData) { + return `You're now on your least knowledgeable topic, ${currentTopic.title}. Select a prompt to interact with ALL-IE.`; + } + if (questionAnswered && !canReviewResponse) { + const timeLeft = 15 - currentDisplayTime; + if (!hasVisitedWikipedia) { + return `Before reviewing ALL-IE's response on ${currentTopic.title}, click the ALLpedia tab above and stay on the page for at least 15 seconds.`; + } + return `Please spend ${timeLeft} more second${timeLeft !== 1 ? "s" : ""} on ALLpedia`; + } + return null; + } + + if (currentPhase === 4) { + if (!questionAnswered) { + return `Great! Now that you've implemented your IDE fixes, interact with ALL-IE again on your least knowledgeable topic, ${currentTopic.title}.`; + } + if (questionAnswered && !canReviewResponse) { + const timeLeft = 15 - currentDisplayTime; + if (!hasVisitedWikipedia) { + return `Before reviewing ALL-IE's response on ${currentTopic.title}, click the ALLpedia tab above and stay on the page for at least 15 seconds.`; + } + return `Please spend ${timeLeft} more second${timeLeft !== 1 ? "s" : ""} on ALLpedia`; + } + return null; + } + + return null; + }; + + // Check if ai review button should be enabled + const canReviewResponse = useMemo(() => { + if (!currentAnswerData || isBotTyping || isBotThinking || showRatingModal) { + return false; + } + + // Phase 3 and 4 require Wikipedia visit for 15 or more seconds + if (requireWikipedia) { + return hasVisitedWikipedia && currentDisplayTime >= 15; + } + + return true; + }, [ + currentAnswerData, + isBotTyping, + isBotThinking, + showRatingModal, + requireWikipedia, + hasVisitedWikipedia, + currentDisplayTime, + ]); + + // Filter questions for round 4 to only show unasked questions + const getAvailableQuestions = useMemo(() => { + if (!topicData?.questions) return []; + + if (currentPhase === 4) { + // Phase 4, show only unasked questions from current topic + + const available = topicData.questions + .map((q, index) => ({ ...q, originalIndex: index })) + .filter((q) => { + const questionKey = `${activeTopic}-${q.originalIndex}`; + return !askedQuestions.includes(questionKey); + }) + .slice(0, 2); // Only first 2 unasked + + return available; + } + + // All other phases, show all questions with originalIndex + return topicData.questions.map((q, index) => ({ + ...q, + originalIndex: index, + })); + }, [currentPhase, topicData, askedQuestions, activeTopic]); + + // Switch to Wikipedia tab on citation click + const handleCitationClick = () => { + setActiveTab("ALLpedia"); + }; + + // Track when a question is asked + const handleQuestionAsked = (questionIndex) => { + const questionKey = `${activeTopic}-${questionIndex}`; + setCurrentQuestion(questionIndex); + + setAskedQuestions((prev) => { + if (!prev.includes(questionKey)) { + return [...prev, questionKey]; + } + return prev; + }); + }; + return ( + +
+
+ ({ + id: index + 1, + text: q.text, + originalIndex: + q.originalIndex !== undefined ? q.originalIndex : index, + }))} + fixedAIResponse={getAvailableQuestions.map((q, index) => { + return { + id: index + 1, + text: q.answers[activeBias].text, + isCorrect: q.answers[activeBias].isCorrect, + explanation: q.answers[activeBias].explanation, + biasType: activeBias, + biasDefinition: BIAS_DEFINITIONS[activeBias], + confidence: q.answers[activeBias].confidence || 93, + }; + })} + onAnswerDataChange={handleAnswerDataChange} + onTypingChange={setIsBotTyping} + onThinkingChange={setIsBotThinking} + messages={chatMessages} + setMessages={setChatMessages} + canSelectQuestion={!questionAnswered} + showConfidenceScore={currentPhase === 4 && showConfidenceScore} + showCitations={currentPhase === 4 && showCitations} + disclaimerMessage={currentPhase === 4 ? disclaimerMessage : ""} + onCitationClick={handleCitationClick} + onQuestionAsked={handleQuestionAsked} + /> +
+
+ {getCurrentInstruction() && ( +
+ {getCurrentInstruction()} +
+ )} + + {canReviewResponse && ( + + )} +
+
+
+ ); +}; + +AIChatBotTab.propTypes = { + currentTopic: PropTypes.object, + topicData: PropTypes.object, + setSelectedBiasData: PropTypes.func.isRequired, + setShowRatingModal: PropTypes.func.isRequired, + setCurrentAnswerData: PropTypes.func.isRequired, + setQuestionAnswered: PropTypes.func.isRequired, + showRatingModal: PropTypes.bool.isRequired, + showBiasExplanation: PropTypes.bool.isRequired, + clickedReviewButtonThisPhase: PropTypes.bool.isRequired, + showWikipediaTab: PropTypes.bool.isRequired, + currentDisplayTime: PropTypes.number.isRequired, + currentAnswerData: PropTypes.object, + questionAnswered: PropTypes.bool.isRequired, + isBotThinking: PropTypes.bool.isRequired, + isBotTyping: PropTypes.bool.isRequired, + requireWikipedia: PropTypes.bool.isRequired, + activeTopic: PropTypes.string.isRequired, + setClickedReviewButtonThisPhase: PropTypes.func.isRequired, + setIsBotThinking: PropTypes.func.isRequired, + setIsBotTyping: PropTypes.func.isRequired, + setCurrentQuestion: PropTypes.func.isRequired, + setActiveTab: PropTypes.func.isRequired, +}; + +export default AIChatBotTab; diff --git a/client/src/components/exercise/lab13/components/AIPanel/AIPanelRatingModal.js b/client/src/components/exercise/lab13/components/AIPanel/AIPanelRatingModal.js new file mode 100644 index 000000000..2d7473330 --- /dev/null +++ b/client/src/components/exercise/lab13/components/AIPanel/AIPanelRatingModal.js @@ -0,0 +1,86 @@ +import { BIAS_DEFINITIONS } from "src/constants/lab13/BiasQuestionsConfig"; +import RatingModal from "../RatingModal"; +import PropTypes from "prop-types"; +const AIPanelRatingModal = ({ + showRatingModal, + setShowRatingModal, + showBiasExplanation, + setShowBiasExplanation, + selectedBiasData, + handleBiasExplanationClose, + setToneRating, + setConfidenceRating, + toneRating, + confidenceRating, +}) => { + const biasDefinition = selectedBiasData + ? BIAS_DEFINITIONS[selectedBiasData.biasType] + : null; + + const handleRatingSubmit = () => { + setShowRatingModal(false); + setShowBiasExplanation(true); + }; + + return ( + + {biasDefinition.name} +
+ ) : null + } + textModalBody={ + selectedBiasData && biasDefinition ? ( +
+ {selectedBiasData.aiResponseText && ( +
+

+ Given AI Response: + "{selectedBiasData.aiResponseText}" +

+
+ )} +
+

+ {selectedBiasData.explanation} +

+
+
+
+ What is {biasDefinition.name}? +
+

{biasDefinition.definition}

+
+
+ ) : null + } + onCloseTextModal={handleBiasExplanationClose} + /> + ); +}; + +AIPanelRatingModal.propTypes = { + showRatingModal: PropTypes.bool.isRequired, + setShowRatingModal: PropTypes.func.isRequired, + showBiasExplanation: PropTypes.bool.isRequired, + setShowBiasExplanation: PropTypes.func.isRequired, + selectedBiasData: PropTypes.object, + handleBiasExplanationClose: PropTypes.func.isRequired, + setToneRating: PropTypes.func.isRequired, + setConfidenceRating: PropTypes.func.isRequired, + toneRating: PropTypes.string.isRequired, + confidenceRating: PropTypes.string.isRequired, +}; + +export default AIPanelRatingModal; diff --git a/client/src/components/exercise/lab13/components/AIPanel/AllPediaImage.js b/client/src/components/exercise/lab13/components/AIPanel/AllPediaImage.js new file mode 100644 index 000000000..f2a3ec5ca --- /dev/null +++ b/client/src/components/exercise/lab13/components/AIPanel/AllPediaImage.js @@ -0,0 +1,21 @@ +import PropTypes from "prop-types"; + +const AllPediaImage = ({ wikipediaContent }) => { + return ( +
+ {wikipediaContent.title} +
+ ); +}; +AllPediaImage.propTypes = { + wikipediaContent: PropTypes.shape({ + imageUrl: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + }).isRequired, +}; +export default AllPediaImage; diff --git a/client/src/components/exercise/lab13/components/AIPanel/AllPediaTab.js b/client/src/components/exercise/lab13/components/AIPanel/AllPediaTab.js new file mode 100644 index 000000000..0dc580f3d --- /dev/null +++ b/client/src/components/exercise/lab13/components/AIPanel/AllPediaTab.js @@ -0,0 +1,121 @@ +import { Tab } from "../Tab/Tab"; +import ProgressBar from "src/components/all-components/ProgressBar"; +import AllPediaImage from "../AIPanel/AllPediaImage"; +import PropTypes from "prop-types"; +import { renderTextWithHighlight } from "../../functions/RenderTextWithHighlight"; +import { getAnswerDataHighlights } from "../../functions/getAnswerDataHighlights"; + +const AllPediaTab = ({ + wikipediaContent, + requireWikipedia, + currentDisplayTime, + currentAnswerData, + currentQuestion, + activeTopic, +}) => { + return ( + +
+ {/* Header with Title and Timer */} +
+
+ {/* Left: Empty spacer for balance */} +
{/* Empty div for grid balance */}
+ + {/* Title */} +
+

+ {wikipediaContent.title} +

+
+ + {/* Timer */} +
+ {requireWikipedia && ( +
+ {currentDisplayTime >= 15 ? ( +
+ + + Complete + +
+ ) : ( + + )} +
+ )} +
+
+
+ + {/* Text on left and image on right */} +
+
+ {/* Left: Text Content */} +
+
+ {wikipediaContent.text.split("\n\n").map((paragraph, index) => ( +

+ {renderTextWithHighlight( + paragraph, + getAnswerDataHighlights( + currentAnswerData, + currentQuestion, + activeTopic, + ), + )} +

+ ))} +
+
+ +
+ + {/* Sources at bottom */} +
+

+ Sources: +

+
    + {wikipediaContent.sources.map((source, index) => ( +
  • + + {source} + +
  • + ))} +
+
+
+
+
+ ); +}; + +AllPediaTab.propTypes = { + wikipediaContent: PropTypes.shape({ + title: PropTypes.string.isRequired, + text: PropTypes.string.isRequired, + sources: PropTypes.arrayOf(PropTypes.string).isRequired, + }).isRequired, + requireWikipedia: PropTypes.bool.isRequired, + currentDisplayTime: PropTypes.number.isRequired, + currentAnswerData: PropTypes.object.isRequired, + currentQuestion: PropTypes.number.isRequired, + activeTopic: PropTypes.string.isRequired, +}; + +export default AllPediaTab; diff --git a/client/src/components/exercise/lab13/components/Avatar.js b/client/src/components/exercise/lab13/components/Avatar.js new file mode 100644 index 000000000..8abaf492c --- /dev/null +++ b/client/src/components/exercise/lab13/components/Avatar.js @@ -0,0 +1,50 @@ +import React from "react"; +import PropTypes from "prop-types"; +import RobotImage from "../../../../assets/images/lab13/robot.png"; +import { AvatarType } from "../../../../constants/lab13/AvatarType"; +import UserPfp from "src/components/all-components/UserPfp"; + +const Avatar = ({ type, size = 40 }) => { + const isAI = type === AvatarType.AI; + + return ( +
+ {isAI ? ( + AI Avatar + ) : ( + + )} +
+ ); +}; + +Avatar.propTypes = { + type: PropTypes.oneOf([AvatarType.AI, AvatarType.User]).isRequired, + size: PropTypes.number, +}; + +export default Avatar; diff --git a/client/src/components/exercise/lab13/components/BlobLoader.css b/client/src/components/exercise/lab13/components/BlobLoader.css new file mode 100644 index 000000000..5e67983ea --- /dev/null +++ b/client/src/components/exercise/lab13/components/BlobLoader.css @@ -0,0 +1,104 @@ +.blob-1 { + border-radius: 32% 58% 69% 43% / 48% 32% 59% 55%; +} + +.blob-2 { + border-radius: 38% 62% 63% 37% / 41% 44% 56% 59%; +} + +.blob-3 { + border-radius: 31% 45% 74% 35% / 38% 56% 51% 87%; +} + +/* Spinning Animation */ +@keyframes blob-1-spin { + + 0%, + 100% { + border-radius: 32% 58% 69% 43% / 48% 32% 59% 55%; + transform: rotate(0deg) scale(1); + } + + 50% { + transform: rotate(180deg) scale(0.92); + } + + 100% { + transform: rotate(360deg) scale(1); + } +} + +@keyframes blob-2-spin { + + 0%, + 100% { + border-radius: 38% 62% 63% 37% / 41% 44% 56% 59%; + transform: rotate(0deg) scale(1); + } + + 50% { + transform: rotate(180deg) scale(1.08); + } + + 100% { + transform: rotate(360deg) scale(1); + } +} + +@keyframes blob-3-spin { + + 0%, + 100% { + border-radius: 31% 45% 74% 35% / 38% 56% 51% 87%; + transform: rotate(0deg) scale(1); + } + + 50% { + transform: rotate(-180deg) scale(0.95); + } + + 100% { + transform: rotate(-360deg) scale(1); + } +} + +/* Pulsing animations (no rotation, just scale) */ +@keyframes blob-1-pulse { + + 0%, + 100% { + border-radius: 32% 58% 69% 43% / 48% 32% 59% 55%; + transform: scale(0.95); + } + + 50% { + transform: scale(1.05); + } +} + +/* Pulsing Animation */ +@keyframes blob-2-pulse { + + 0%, + 100% { + border-radius: 38% 62% 63% 37% / 41% 44% 56% 59%; + transform: scale(0.95); + } + + 50% { + transform: scale(1.05); + } +} + +@keyframes blob-3-pulse { + + 0%, + 100% { + border-radius: 31% 45% 74% 35% / 38% 56% 51% 87%; + transform: scale(0.95); + } + + 50% { + transform: scale(1.05); + } +} \ No newline at end of file diff --git a/client/src/components/exercise/lab13/components/BlobLoader.jsx b/client/src/components/exercise/lab13/components/BlobLoader.jsx new file mode 100644 index 000000000..a5835c0cc --- /dev/null +++ b/client/src/components/exercise/lab13/components/BlobLoader.jsx @@ -0,0 +1,92 @@ +import React from "react"; +import PropTypes from "prop-types"; +import "./BlobLoader.css"; + +/** + * Animated blob loader that creates three blob + * shapes that animate in different ways and displays + * them. + * @param {string} props.animationMode - Controls the animation behaviour: + * - 'pulsing' : blob becomes larger then smaller + * - 'spinning': blob rotates and becomes larger than smaller + * - 'static' : blob doesn't move + * @returns {JSX.Element} - Blob loader component + */ +export default function BlobLoader({ animationMode = "static" }) { + return ( +
+ {/* Blob 1/3 */} +
+ {/* Blob 2/3 */} +
+ {/* Blob 3/3 */} +
+
+ ); +} + +BlobLoader.propTypes = { + animationMode: PropTypes.oneOf(["pulsing", "spinning", "static"]).isRequired, +}; + +const styles = { + // Blob container + blobContainer: { + position: "relative", + width: "40px", + height: "40px", + display: "flex", + alignItems: "center", + justifyContent: "center", + left: "12px", + top: "20px", + }, + // Base blob styling + blob: { + position: "absolute", + top: 0, + left: 0, + width: "80%", + height: "80%", + border: "1px solid black", + }, +}; diff --git a/client/src/components/exercise/lab13/components/DragAndDropNoCorrectAnswer/DragDropGame.js b/client/src/components/exercise/lab13/components/DragAndDropNoCorrectAnswer/DragDropGame.js new file mode 100644 index 000000000..645819460 --- /dev/null +++ b/client/src/components/exercise/lab13/components/DragAndDropNoCorrectAnswer/DragDropGame.js @@ -0,0 +1,236 @@ +import React, { useState } from "react"; +import { DndContext } from "@dnd-kit/core"; +import DroppableColumn from "./DroppableColumn"; +import DroppableBank from "./DroppableBank"; +import PropTypes from "prop-types"; +import LabButton from "../../../../all-components/LabButton"; +import StatusBanner from "../../../../all-components/StatusBanner"; + +/** + * Example usage: + * const initialColumns = [ + * { id: "column1", title: "Column 1", cards: [] }, + * { id: "column2", title: "Column 2", cards: [] }, + * ]; + * const initialBank = [ + * { id: "card1", content: "Card 1" }, + * { id: "card2", content: "Card 2" }, + * ]; + * const correctAssignments = [ + * { id: "column1", cards: ["card1"] }, + * { id: "column2", cards: ["card2"] }, + * ]; + */ + +const arrayToObject = (array, key) => { + return array.reduce((obj, item) => ({ ...obj, [item[key]]: item }), {}); +}; + +const DragDropGame = ({ + containerStyle, + colStyle, + bankStyle, + colCardStyle, + bankCardStyle, + msgStyle, + cols, + initialBank, + correctAssignments, + success, + setSuccess, + colHeaderStyle, + handleNav, + colContainerStyle, + gameStyle, + cardIcon, +}) => { + const [columns, setColumns] = useState(arrayToObject(cols, "id")); + const [bank, setBank] = useState(initialBank); + const [message, setMessage] = useState(""); + const [correct, setCorrect] = useState(success); + + const correct_assignments = arrayToObject(correctAssignments, "id"); + + const onDragEnd = (event) => { + const { active, over } = event; + if (!over) return; + + const sourceId = active.id; + const destinationId = over.id; + + setColumns((prevColumns) => { + const newColumns = { ...prevColumns }; + let movedCard; + + if (sourceId === destinationId) return prevColumns; + + // Prevent placing more than one card in a column + if ( + destinationId !== "bank" && + newColumns[destinationId].cards.length >= 1 + ) { + const existingCard = newColumns[destinationId].cards[0]; + setBank((prevBank) => + prevBank.some((card) => card.id === existingCard.id) + ? prevBank + : [...prevBank, existingCard], + ); + newColumns[destinationId].cards = []; + } + + // Remove the card from its source + for (const key in newColumns) { + const sourceCards = newColumns[key].cards; + if (sourceCards.some((card) => card.id === sourceId)) { + movedCard = sourceCards.find((card) => card.id === sourceId); + newColumns[key].cards = sourceCards.filter( + (card) => card.id !== sourceId, + ); + break; + } + } + + // If not found in columns, get from bank + if (!movedCard) { + movedCard = bank.find((card) => card.id === sourceId); + if (movedCard) { + setBank((prevBank) => + prevBank.filter((card) => card.id !== sourceId), + ); + } + } + + if (movedCard) { + if (destinationId === "bank") { + setBank((prevBank) => + prevBank.some((card) => card.id === movedCard.id) + ? prevBank + : [...prevBank, movedCard], + ); + } else { + newColumns[destinationId].cards = [movedCard]; + } + } + + return newColumns; + }); + }; + + const verifyPlacement = () => { + if (bank.length !== 0) { + setMessage("Please place all cards before submitting."); + return; + } + + const updatedColumns = { ...columns }; + let incorrectCards = []; + + for (const [columnId, correctCol] of Object.entries(correct_assignments)) { + const placedCards = updatedColumns[columnId].cards; + const correctCards = correctCol.cards; + + const misplaced = placedCards.filter( + (card) => !correctCards.includes(card.id), + ); + + placedCards.forEach((card) => { + card.isCorrect = correctCards.includes(card.id); + }); + + if (misplaced.length > 0) { + incorrectCards = [...incorrectCards, ...misplaced]; + } + } + + if (incorrectCards.length > 0) { + setMessage("Incorrect placement. Try again!"); + setColumns(updatedColumns); + return; + } + + setMessage("Success! All cards are placed."); + setSuccess(true); + setCorrect(true); + setColumns(updatedColumns); + }; + + return ( + +
+
+ +
+ +
+ {Object.keys(columns).map((colId) => ( + + ))} +
+
+ +
+ {message && {message}} + +
+
+ ); +}; + +DragDropGame.propTypes = { + containerStyle: PropTypes.string.isRequired, + colStyle: PropTypes.string.isRequired, + bankStyle: PropTypes.string.isRequired, + colCardStyle: PropTypes.string.isRequired, + bankCardStyle: PropTypes.string.isRequired, + msgStyle: PropTypes.string.isRequired, + cols: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + cards: PropTypes.array.isRequired, + }), + ), + initialBank: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string, + content: PropTypes.string, + }), + ), + correctAssignments: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + cards: PropTypes.array.isRequired, + }), + ), + success: PropTypes.bool, + setSuccess: PropTypes.func, + colHeaderStyle: PropTypes.string, + handleNav: PropTypes.func.isRequired, + colContainerStyle: PropTypes.string, + gameStyle: PropTypes.string, + cardIcon: PropTypes.any, +}; + +export default DragDropGame; diff --git a/client/src/components/exercise/lab13/components/DragAndDropNoCorrectAnswer/DraggableCard.js b/client/src/components/exercise/lab13/components/DragAndDropNoCorrectAnswer/DraggableCard.js new file mode 100644 index 000000000..10fbf007e --- /dev/null +++ b/client/src/components/exercise/lab13/components/DragAndDropNoCorrectAnswer/DraggableCard.js @@ -0,0 +1,69 @@ +import { useDraggable } from "@dnd-kit/core"; +import React, { useState, useEffect } from "react"; +import PropTypes from "prop-types"; +import { twMerge } from "tailwind-merge"; + +const DraggableCard = ({ card, cardStyle, cardIcon }) => { + const [borderColor, setBorderColor] = useState(""); + + const { attributes, listeners, setNodeRef, transform } = useDraggable({ + id: card.id, + }); + + const style = transform + ? { transform: `translate(${transform.x}px, ${transform.y}px)` } + : {}; + + useEffect(() => { + if (!card.isCorrect) { + setBorderColor( + "tw-border-solid !tw-shadow-lg !tw-border-brightRed tw-shadow-brightRed tw-font-bold tw-text-brightRed", + ); + } else { + setBorderColor(""); + } + }, [card.isCorrect]); + + return ( +
+ {card.content} +

+ {" "} + {cardIcon} {card.title} +

+
+        {card.body}
+      
+
+ ); +}; + +DraggableCard.propTypes = { + card: PropTypes.shape({ + id: PropTypes.string.isRequired, + title: PropTypes.string, + content: PropTypes.string, + body: PropTypes.string, + color: PropTypes.string, + isCorrect: PropTypes.bool, + }).isRequired, + cardStyle: PropTypes.string.isRequired, + cardIcon: PropTypes.any, +}; + +export default DraggableCard; diff --git a/client/src/components/exercise/lab13/components/DragAndDropNoCorrectAnswer/DroppableBank.js b/client/src/components/exercise/lab13/components/DragAndDropNoCorrectAnswer/DroppableBank.js new file mode 100644 index 000000000..2bea1ea7b --- /dev/null +++ b/client/src/components/exercise/lab13/components/DragAndDropNoCorrectAnswer/DroppableBank.js @@ -0,0 +1,35 @@ +import { useDroppable } from "@dnd-kit/core"; +import React from "react"; +import DraggableCard from "./DraggableCard"; +import PropTypes from "prop-types"; + +const DroppableBank = ({ bank, bankStyle, cardStyle, cardIcon }) => { + const { setNodeRef } = useDroppable({ id: "bank" }); + + return ( +
+ {bank.map((card) => ( + + ))} +
+ ); +}; + +DroppableBank.propTypes = { + bank: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string, + content: PropTypes.string, + }), + ).isRequired, + bankStyle: PropTypes.string.isRequired, + cardStyle: PropTypes.string.isRequired, + cardIcon: PropTypes.any, +}; + +export default DroppableBank; diff --git a/client/src/components/exercise/lab13/components/DragAndDropNoCorrectAnswer/DroppableColumn.js b/client/src/components/exercise/lab13/components/DragAndDropNoCorrectAnswer/DroppableColumn.js new file mode 100644 index 000000000..5a407445f --- /dev/null +++ b/client/src/components/exercise/lab13/components/DragAndDropNoCorrectAnswer/DroppableColumn.js @@ -0,0 +1,53 @@ +import React from "react"; +import { useDroppable } from "@dnd-kit/core"; +import DraggableCard from "./DraggableCard"; +import PropTypes from "prop-types"; + +const DroppableColumn = ({ + column, + cards, + colStyle, + cardStyle, + colHeaderStyle, + colContainerStyle, + cardIcon, +}) => { + const { setNodeRef } = useDroppable({ id: column.id }); + + return ( +
+

{column.title}

+
+ {cards.map((card) => ( + + ))} +
+
+ ); +}; + +DroppableColumn.propTypes = { + column: PropTypes.shape({ + id: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + }).isRequired, + cards: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + content: PropTypes.string, + isCorrect: PropTypes.bool, + }), + ).isRequired, + colStyle: PropTypes.string.isRequired, + cardStyle: PropTypes.string.isRequired, + colHeaderStyle: PropTypes.string.isRequired, + colContainerStyle: PropTypes.string, + cardIcon: PropTypes.any, +}; + +export default DroppableColumn; diff --git a/client/src/components/exercise/lab13/components/RatingModal.js b/client/src/components/exercise/lab13/components/RatingModal.js new file mode 100644 index 000000000..74420278e --- /dev/null +++ b/client/src/components/exercise/lab13/components/RatingModal.js @@ -0,0 +1,155 @@ +import React, { useRef, useEffect } from "react"; +import PropTypes from "prop-types"; +import ALLModal from "src/components/all-components/ALLModal"; +import Likert from "src/components/all-components/Likert"; +import LabButton from "src/components/all-components/LabButton"; + +const optionsList = ["Very Low", "Low", "Medium", "High", "Very High"]; + +const RatingModal = ({ + show, + setShow, + toneRating, + setToneRating, + confidenceRating, + setConfidenceRating, + onSubmit, + showTextModal, + setShowTextModal, + textModalHeader, + textModalBody, + onCloseTextModal, +}) => { + const firstModalRef = useRef(null); + // Control overlay when modals are shown + useEffect(() => { + if (show || showTextModal) { + document.body.style.overflow = "hidden"; + } else { + document.body.style.overflow = "unset"; + } + return () => { + document.body.style.overflow = "unset"; + }; + }, [show, showTextModal]); + + const handleSubmit = () => { + if (!toneRating || !confidenceRating) { + alert("Please rate both factors before submitting."); + return; + } + onSubmit(); + }; + + return ( + <> +
+
+ +
+ Rate how much each part of the AI response impacted your + trust. +
+ +
+ } + showFooter={false} + customBody={ +
+
+
+

+ How much did the tone of the AI response impact + your trust in it? +

+
+ + setToneRating( + e.target.value.toLowerCase().replace(/ /g, "-"), + ) + } + /> +
+ +
+
+

+ How much did the AI sounding confident impact + your trust in it? +

+
+ + setConfidenceRating( + e.target.value.toLowerCase().replace(/ /g, "-"), + ) + } + /> +
+
+ +
+
+ } + /> + + {textModalHeader} +
+ } + showFooter={false} + customBody={ +
+ {textModalBody} + +
+ } + /> +
+
+ + ); +}; + +RatingModal.propTypes = { + show: PropTypes.bool.isRequired, + setShow: PropTypes.func.isRequired, + toneRating: PropTypes.string.isRequired, + setToneRating: PropTypes.func.isRequired, + confidenceRating: PropTypes.string.isRequired, + setConfidenceRating: PropTypes.func.isRequired, + onSubmit: PropTypes.func.isRequired, + showTextModal: PropTypes.bool.isRequired, + setShowTextModal: PropTypes.func.isRequired, + textModalHeader: PropTypes.node, + textModalBody: PropTypes.node, + onCloseTextModal: PropTypes.func.isRequired, +}; + +export default RatingModal; diff --git a/client/src/components/exercise/lab13/components/Tab/Tab.js b/client/src/components/exercise/lab13/components/Tab/Tab.js new file mode 100644 index 000000000..675a67c8d --- /dev/null +++ b/client/src/components/exercise/lab13/components/Tab/Tab.js @@ -0,0 +1,27 @@ +import { useContext, useEffect, useRef } from "react"; +import { TabsContext } from "./TabsContext"; +import PropTypes from "prop-types"; + +export const Tab = ({ label, children }) => { + const { logTab } = useContext(TabsContext); + + // Use ref to track if this is the first render + const isFirstRender = useRef(true); + const prevChildrenRef = useRef(children); + + useEffect(() => { + // Only log if children actually changed or first render + if (isFirstRender.current || prevChildrenRef.current !== children) { + logTab({ label, content: children }); + prevChildrenRef.current = children; + isFirstRender.current = false; + } + }, [children, label, logTab]); + + return null; +}; + +Tab.propTypes = { + label: PropTypes.string.isRequired, + children: PropTypes.node.isRequired, +}; diff --git a/client/src/components/exercise/lab13/components/Tab/Tabs.css b/client/src/components/exercise/lab13/components/Tab/Tabs.css new file mode 100644 index 000000000..6651b100a --- /dev/null +++ b/client/src/components/exercise/lab13/components/Tab/Tabs.css @@ -0,0 +1,146 @@ +:root { + /* Curve size */ + --tabGirth: 12px; + /* Tabs overlapping value */ + --tabOverlap: -24px; +} + +/* Container holding entire tab */ +.tabs-container { + position: relative; + width: 100%; + height: 100%; + /* background-color: #EEEEEE; */ + border-radius: 8px 8px 0 0; +} + +/* Header container holding all tabs */ +.tabs-header { + display: flex; + align-items: flex-start; + /* Horizontal scroll if tabs exceed width */ + overflow-x: auto; + /* All tabs on one line */ + flex-wrap: nowrap; + /* space for shadows */ + padding: 8px 8px 0 8px; +} + +/* Wrapper for shadow and tab-headers overlapping */ +.tab-shadow-wrapper { + flex: 1 1 0; + position: relative; + display: flex; + max-width: 200px; + /* Overlap the previous tab */ + margin-left: var(--tabOverlap); + filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.4)); + z-index: 1; + overflow: hidden; +} + +.tab-shadow-wrapper.active { + z-index: 100; +} + +/* First button does not overlap with prev */ +.tab-shadow-wrapper:first-child { + margin-left: 0; +} + +/* Tab header buttons */ +.tabs-header button { + flex: 1; + max-width: none; + border: none; + padding: 8px 25px; + cursor: pointer; + /* Color of tabs */ + background-color: #eeeeee; + /* Hide wrapped long */ + white-space: nowrap; + /* Add ellispes for overflowed text */ + text-overflow: ellipsis; + + clip-path: shape(from bottom left, + curve to var(--tabGirth) calc(100% - var(--tabGirth)) with var(--tabGirth) 100%, + vline to calc(var(--tabGirth)), + curve to calc(var(--tabGirth) * 2) 0 with var(--tabGirth) 0, + hline to calc(100% - calc(var(--tabGirth) * 2)), + curve to calc(100% - var(--tabGirth)) var(--tabGirth) with calc(100% - var(--tabGirth)) 0, + vline to calc(100% - var(--tabGirth)), + curve to 100% 100% with calc(100% - var(--tabGirth)) 100%); + + transition: transform 0.1s; + overflow: hidden; +} + +.tabs-header button:not(.active):hover { + transform: translateY(2px); +} + +.tabs-header button:not(.active):active { + transform: translateY(4px); +} + +/* Hover overlay for inactive tabs */ +.tabs-header button:not(.active):before { + content: ""; + position: absolute; + top: 50%; + left: 50%; + width: calc(100% - 48px); + height: calc(100% - 12px); + /* Color of inner rectangle in-active tabs */ + background-color: #dddddd; + transform: translate(-50%, -50%); + opacity: 0; + visibility: hidden; + + transition: + opacity 0.2s ease, + visibility 0.2s ease; + z-index: -1; + border-radius: 8px; +} + +.tabs-header button:not(.active):hover::before { + opacity: 1; + visibility: visible; +} + +.tabs-header button:not(.active) tab-label { + position: relative; + z-index: 1; + color: black; +} + +/* Active tab precise styling */ +.tab-shadow-wrapper.active button { + background-color: white; +} + +.tabs-header button.active { + clip-path: shape(from bottom left, + curve to 12px calc(100% - 12px) with 12px 100%, + vline to 12px, + curve to 24px 0 with 12px 0, + hline to calc(100% - 24px), + curve to calc(100% - 12px) 12px with calc(100% - 12px) 0, + vline to calc(100% - 12px), + curve to 100% 100% with calc(100% - 12px) 100%); +} + +.tab-panels { + position: relative; + background: white; + /* Blend the line between active tab and panel */ + margin-top: -1px; + box-shadow: 0 5px 6px rgba(0, 0, 0, 0.3); + padding: 25px 30px 25px 25px; + height: calc(58vh); + overflow-y: auto; + border-top: 10px solid white; + border: solid white; + border-width: 10px 0; +} \ No newline at end of file diff --git a/client/src/components/exercise/lab13/components/Tab/Tabs.js b/client/src/components/exercise/lab13/components/Tab/Tabs.js new file mode 100644 index 000000000..5dd6c58e1 --- /dev/null +++ b/client/src/components/exercise/lab13/components/Tab/Tabs.js @@ -0,0 +1,78 @@ +import React, { useState, useCallback } from "react"; +import { TabsContext } from "./TabsContext"; +import PropTypes from "prop-types"; +import "./Tabs.css"; + +export const Tabs = ({ + children, + activeTab: externalActiveTab, + onTabChange, +}) => { + // Store tab(s) that log themselves in Tabs parent + const [tabs, setTabs] = useState([]); + + // Use external activeTab or fall back to first tab + const activeTabIndex = tabs.findIndex( + (tab) => tab.label === externalActiveTab, + ); + const currentActiveIndex = activeTabIndex !== -1 ? activeTabIndex : 0; + + // Memoize logTab to prevent infinite loops + const logTab = useCallback((tab) => { + setTabs((prev) => { + const existingIndex = prev.findIndex((t) => t.label === tab.label); + if (existingIndex !== -1) { + const updated = [...prev]; + updated[existingIndex] = tab; + return updated; + } + return [...prev, tab]; + }); + }, []); // Empty dependency array since it only uses setTabs + + const handleTabClick = (index) => { + if (onTabChange) { + onTabChange(tabs[index].label); + } + }; + + return ( + {} }} + > +
+ {/* Populate headers with buttons for logged tabs */} +
+ {tabs.map((tab, index) => ( +
+ +
+ ))} +
+ {/* Populate with tab content */} +
+ {tabs.map((tab, index) => + index === currentActiveIndex ? ( +
{tab.content}
+ ) : null, + )} +
+
+ {children} +
+ ); +}; + +Tabs.propTypes = { + children: PropTypes.node.isRequired, + activeTab: PropTypes.string, + onTabChange: PropTypes.func, +}; diff --git a/client/src/components/exercise/lab13/components/Tab/TabsContext.js b/client/src/components/exercise/lab13/components/Tab/TabsContext.js new file mode 100644 index 000000000..6f184a204 --- /dev/null +++ b/client/src/components/exercise/lab13/components/Tab/TabsContext.js @@ -0,0 +1,12 @@ +import { createContext } from "react"; + +export const TabsContext = createContext({ + /** + * Shares the context and current active tab inbetween all + * Tabs components + */ + // Function tabs will call to log themselves as a new tab + logTab: () => {}, + activeTab: 0, + setActiveTab: () => {}, +}); diff --git a/client/src/components/exercise/lab13/components/TypingMessage.js b/client/src/components/exercise/lab13/components/TypingMessage.js new file mode 100644 index 000000000..6f9d15fff --- /dev/null +++ b/client/src/components/exercise/lab13/components/TypingMessage.js @@ -0,0 +1,45 @@ +import PropTypes from "prop-types"; +import React, { useEffect, useState } from "react"; +/** + * Typewriter animation component effect for bot responses + * that displays text character by character + * @param {*} text : Text string to display with the typing effect + * @param {*} onUpdate : Functon to flag after each character is written + * @returns + */ +const TypingMessage = ({ text, onUpdate, onComplete }) => { + const [displayedText, setDisplayedText] = useState(""); + const [currentIndex, setCurrentIndex] = useState(0); + + /** + * Adds character from currentIndex to displayedText + * one at a time everytime the currentIndex, text, or onUpdate changes + */ + useEffect(() => { + if (currentIndex < text.length) { + const timeout = setTimeout(() => { + setDisplayedText((prev) => prev + text[currentIndex]); + setCurrentIndex((prev) => prev + 1); + + // Scroll to bottom when onUpdate is called by component + if (onUpdate) onUpdate(); + // Typing speed, one character every 15ms + }, 15); + + return () => clearTimeout(timeout); + // onComplete becomes true when finished typing + } else if (currentIndex === text.length && onComplete) { + onComplete(); + } + }, [currentIndex, text, onUpdate, onComplete]); + + return <>{displayedText}; +}; + +TypingMessage.propTypes = { + text: PropTypes.string.isRequired, + onUpdate: PropTypes.func, + onComplete: PropTypes.func, +}; + +export default TypingMessage; diff --git a/client/src/components/exercise/lab13/functions/RenderTextWithHighlight.js b/client/src/components/exercise/lab13/functions/RenderTextWithHighlight.js new file mode 100644 index 000000000..cd565ba27 --- /dev/null +++ b/client/src/components/exercise/lab13/functions/RenderTextWithHighlight.js @@ -0,0 +1,33 @@ +// Helper function to parse text and highlight specific patterns +export const renderTextWithHighlight = (text, highlightPatterns = []) => { + if (!text || !highlightPatterns.length) return text; + + // Combine all patterns into one regex on a single pass + const sortedPatterns = [...highlightPatterns].sort( + (a, b) => b.length - a.length, + ); + const escapedPatterns = sortedPatterns.map((p) => + p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), + ); + const combinedPattern = escapedPatterns.map((p) => `(${p})`).join("|"); + const regex = new RegExp(combinedPattern, "gi"); + + // Split once - capturing groups make matches return at odd indices + const segments = text.split(regex); + let keyCounter = 0; + + return segments.map((segment, i) => { + // Odd indices are matches (from capturing groups) + if (i % 2 === 1 && segment) { + return ( + + {segment} + + ); + } + return segment; + }); +}; diff --git a/client/src/components/exercise/lab13/functions/getAnswerDataHighlights.js b/client/src/components/exercise/lab13/functions/getAnswerDataHighlights.js new file mode 100644 index 000000000..e3d92a39b --- /dev/null +++ b/client/src/components/exercise/lab13/functions/getAnswerDataHighlights.js @@ -0,0 +1,15 @@ +import { HIGHLIGHTS_MAPPING } from "src/constants/lab13/HighlightsMapping"; + +// Get highlights based on current answer data, topic, and question +export const getAnswerDataHighlights = ( + currentAnswerData, + currentQuestion, + activeTopic, +) => { + if (!currentAnswerData || currentQuestion === null) return []; + + const topicKey = activeTopic?.toLowerCase(); + const biasType = currentAnswerData.biasType; + + return HIGHLIGHTS_MAPPING[topicKey]?.[currentQuestion]?.[biasType] || []; +}; diff --git a/client/src/components/exercise/lab13/pages/AIPanel.js b/client/src/components/exercise/lab13/pages/AIPanel.js new file mode 100644 index 000000000..abc3b1ead --- /dev/null +++ b/client/src/components/exercise/lab13/pages/AIPanel.js @@ -0,0 +1,414 @@ +import { React, useContext, useMemo, useState, useEffect, useRef } from "react"; +import { startExercise } from "src/reducers/lab2/actions"; +import { navigate } from "@reach/router"; +import { Tabs } from "../components/Tab/Tabs"; +import ExerciseStateContext from "../Lab13Context"; +import { getTopicById } from "src/constants/lab13/BiasQuestionsConfig"; +import { content } from "src/constants/lab13/WikipediaContent"; +import AIPanelRatingModal from "../components/AIPanel/AIPanelRatingModal"; +import AIChatBotTab from "../components/AIPanel/AIChatBotTab"; +import AllPediaTab from "../components/AIPanel/AllPediaTab"; + +const AIPanel = () => { + const { + rankingColumns, + chatMessages, + setChatMessages, + setHasVisitedWikipedia, + currentPhase, + setCurrentPhase, + topicIndex, + setTopicIndex, + wikipediaAccumulatedTime, + setWikipediaAccumulatedTime, + wikipediaSessionStart, + setWikipediaSessionStart, + } = useContext(ExerciseStateContext); + + const [showRatingModal, setShowRatingModal] = useState(false); + const [showBiasExplanation, setShowBiasExplanation] = useState(false); + const [selectedBiasData, setSelectedBiasData] = useState(null); + const [currentAnswerData, setCurrentAnswerData] = useState(null); + const [isBotTyping, setIsBotTyping] = useState(false); + const [isBotThinking, setIsBotThinking] = useState(false); + const [questionAnswered, setQuestionAnswered] = useState(false); + const [activeTab, setActiveTab] = useState("AIChatBot"); + const [currentDisplayTime, setCurrentDisplayTime] = useState(0); + const [currentQuestion, setCurrentQuestion] = useState(null); + const phase4IntroAddedRef = useRef(false); + const [toneRating, setToneRating] = useState(""); + const [confidenceRating, setConfidenceRating] = useState(""); + const [clickedReviewButtonThisPhase, setClickedReviewButtonThisPhase] = + useState(false); + + // State to track when to show the wikipedia page + const [hasShownWikipediaInPhase, setHasShownWikipediaInPhase] = + useState(false); + + // Get all three topics in order: medium, most, least + const getOrderedTopics = useMemo(() => { + if (!rankingColumns?.length) return []; + + const topics = []; + // Medium knowledgeable (index 1) - Halo Effect + if (rankingColumns[1]?.cards?.length > 0) { + topics.push({ + id: rankingColumns[1].cards[0].id, + biasPosition: 1, + title: rankingColumns[1].cards[0].title, + }); + } + // Most knowledgeable (index 0) - Truth Bias + if (rankingColumns[0]?.cards?.length > 0) { + topics.push({ + id: rankingColumns[0].cards[0].id, + biasPosition: 0, + title: rankingColumns[0].cards[0].title, + }); + } + // Least knowledgeable (index 2) - Dunning-Kruger + if (rankingColumns[2]?.cards?.length > 0) { + topics.push({ + id: rankingColumns[2].cards[0].id, + biasPosition: 2, + title: rankingColumns[2].cards[0].title, + }); + } + + return topics; + }, [rankingColumns]); + + const currentTopic = getOrderedTopics[topicIndex] || null; + const activeTopic = currentTopic?.id || null; + const topicData = getTopicById(activeTopic); + + // Display Wikipedia based on current phase and whether or not the AI has finished typing + const showWikipediaTab = useMemo(() => { + // Phase 1 does not show wiki + if (topicIndex === 0) { + return false; + } + + // Phase 2, 3, 4 shows wiki after the first AI response is completed + if (topicIndex >= 1) { + // keep displaying if the wikishown was already set to true + if (hasShownWikipediaInPhase) { + return true; + } + + if (currentAnswerData && !isBotTyping && !isBotThinking) { + return true; + } + } + + return false; + }, [ + topicIndex, + hasShownWikipediaInPhase, + currentAnswerData, + isBotThinking, + isBotTyping, + ]); + + const requireWikipedia = topicIndex === 2; + + // Wikipedia time tracking with interval + useEffect(() => { + let intervalId; + + if (activeTab === "ALLpedia" && showWikipediaTab) { + // Mark as visited + + setHasVisitedWikipedia(true); + + // Start new session if not already started + if (!wikipediaSessionStart) { + setWikipediaSessionStart(Date.now()); + } + + intervalId = setInterval(() => { + if (wikipediaSessionStart) { + const sessionElapsed = Math.floor( + (Date.now() - wikipediaSessionStart) / 1000, + ); + const total = wikipediaAccumulatedTime + sessionElapsed; + setCurrentDisplayTime(total); + + // Stop at 15 seconds + if (total >= 15) { + clearInterval(intervalId); + } + } + }, 100); + } else { + // Leaving Wikipedia tab ensure you save the time + if (wikipediaSessionStart) { + const sessionElapsed = Math.floor( + (Date.now() - wikipediaSessionStart) / 1000, + ); + setWikipediaAccumulatedTime((prev) => prev + sessionElapsed); + setWikipediaSessionStart(null); + } + // Keep displaying the accumulated time + setCurrentDisplayTime(wikipediaAccumulatedTime); + } + + return () => { + if (intervalId) clearInterval(intervalId); + }; + }, [ + activeTab, + showWikipediaTab, + wikipediaSessionStart, + wikipediaAccumulatedTime, + setHasVisitedWikipedia, + setWikipediaAccumulatedTime, + setWikipediaSessionStart, + ]); + + // Track and respond when Wikipedia becomes visible + useEffect(() => { + if (showWikipediaTab && !hasShownWikipediaInPhase) { + setHasShownWikipediaInPhase(true); + } + }, [showWikipediaTab, hasShownWikipediaInPhase]); + + const wikipediaContent = + content[activeTopic?.toLowerCase()] || content.localization; + + // Initialize first exercise chat with the ALLie greeting + useEffect(() => { + if (chatMessages.length === 0 && currentTopic) { + setChatMessages([ + { + id: "greeting", + sender: "bot", + text: `Hi! I'm ALL-IE the AI. What can I help you with?`, + timestamp: new Date(), + isNew: true, + }, + ]); + setCurrentPhase(1); + } + }, [currentTopic, chatMessages.length, setChatMessages, setCurrentPhase]); + + // Initialize phase 4 after IDE fixes + useEffect(() => { + if (currentPhase === 4 && currentTopic) { + // Reset Wikipedia tracking for Phase 4 + if (!phase4IntroAddedRef.current) { + setHasVisitedWikipedia(false); + setWikipediaAccumulatedTime(0); + setWikipediaSessionStart(null); + setCurrentDisplayTime(0); + setHasShownWikipediaInPhase(false); + } + + // Enusure we're on the least knowledgeable topic + if (currentTopic.biasPosition !== 2) { + console.warn("Phase 4 should be on least topic! Forcing to index 2"); + setTopicIndex(2); + return; + } + + // Add intro message once + if (!phase4IntroAddedRef.current && chatMessages.length > 0) { + const lastMessage = chatMessages[chatMessages.length - 1]; + const phase4IntroText = `Let's continue. Select another prompt.`; + + if ( + lastMessage.text !== phase4IntroText && + !lastMessage.text.includes("implemented your IDE fixes") + ) { + phase4IntroAddedRef.current = true; + + setTimeout(() => { + setQuestionAnswered(false); + setChatMessages((prev) => [ + ...prev, + { + id: "phase4-intro", + sender: "bot", + text: phase4IntroText, + timestamp: new Date(), + isNew: true, + }, + ]); + }, 500); + } + } + } + }, [ + currentPhase, + currentTopic, + topicIndex, + chatMessages.length, + setChatMessages, + setHasVisitedWikipedia, + setWikipediaAccumulatedTime, + setWikipediaSessionStart, + setTopicIndex, + ]); + + // Add transitions with instructional messages from ALL-ie + const handleBiasExplanationClose = () => { + setShowBiasExplanation(false); + setShowRatingModal(false); + + setClickedReviewButtonThisPhase(true); + setCurrentAnswerData(null); + // Keep questionAnswered=true until the transition message is added, + // so the question dropdown doesn't flash up between topics. + setIsBotThinking(false); + setIsBotTyping(false); + setHasShownWikipediaInPhase(false); + + setTimeout(() => { + setSelectedBiasData(null); + setToneRating(""); + setConfidenceRating(""); + setActiveTab("AIChatBot"); + + setTimeout(() => { + setTopicIndex((prevIndex) => { + const nextIndex = prevIndex + 1; + + // Navigate to IDE introduction after Phase 3 + if (nextIndex >= getOrderedTopics.length && currentPhase < 4) { + setTimeout(() => { + setQuestionAnswered(false); + startExercise(); + navigate("/Lab13/Exercise/IDEIntroduction"); + }, 300); + return prevIndex; + } + + if (currentPhase === 4) { + phase4IntroAddedRef.current = false; + setTimeout(() => { + setQuestionAnswered(false); + startExercise(); + navigate("/Lab13/Exercise/Conclusion"); + }, 300); + return prevIndex; + } + + // Reset Wikipedia tracking for new phase + setHasVisitedWikipedia(false); + setWikipediaAccumulatedTime(0); + setWikipediaSessionStart(null); + setCurrentDisplayTime(0); + setClickedReviewButtonThisPhase(false); + + setCurrentPhase(nextIndex + 1); + + const nextTopic = getOrderedTopics[nextIndex]; + if (nextTopic) { + setTimeout(() => { + // Only allow question selection once the transition message is in place + setQuestionAnswered(false); + setChatMessages((prev) => [ + ...prev, + { + id: `transition-${nextIndex}`, + sender: "bot", + text: `Let's continue. Select another prompt.`, + timestamp: new Date(), + isNew: true, + }, + ]); + }, 800); + } + + return nextIndex; + }); + }, 200); + }, 100); + }; + + return ( +
+ {/* Full-screen overlay when modal is shown */} + {(showRatingModal || showBiasExplanation) && ( +
+ )} + + {activeTopic && topicData && ( + <> +
+ + + {/* ALLpedia Tab, from phase 2 onwards */} + {showWikipediaTab && ( + + )} + +
+ + {/* Rating Modal for Phase 4, shows bias explanation after rating submission */} + + + )} + + {!activeTopic && ( +
+

+ Please complete the ranking to see questions. +

+
+ )} +
+ ); +}; + +export default AIPanel; diff --git a/client/src/components/exercise/lab13/pages/Conclusion.js b/client/src/components/exercise/lab13/pages/Conclusion.js new file mode 100644 index 000000000..42ab1aa79 --- /dev/null +++ b/client/src/components/exercise/lab13/pages/Conclusion.js @@ -0,0 +1,68 @@ +import { React } from "react"; +import useMainStateContext from "src/reducers/MainContext"; +import UserLabService from "../../../../services/UserLabService"; +import { EXERCISE_IDLE } from "src/constants/index"; +import { LAB_ID } from "../../../../constants/lab13"; +import { navigate } from "@reach/router"; + +const Conclusion = () => { + const { actions, state } = useMainStateContext(); + + const handleFinish = async () => { + actions.updateUserState(EXERCISE_IDLE); + await navigate("/Lab13/Reinforcement"); + await UserLabService.complete_exercise(LAB_ID); + if (state.main.user?.firstname !== null && state.main.user !== null) { + await UserLabService.user_complete_exercise( + state.main.user.userid, + LAB_ID, + ); + } + }; + return ( +
+

Conclusion

+

+ Throughout this lab, you experienced how tone, formatting, confidence, + and technical language can shape how trustworthy AI feels, even before + verifying the information. +

+ +

You saw how:

+
    +
  • + Familiar phrasing can make incorrect claims feel true. +
  • +
  • + Professional wording can create an illusion of expertise. +
  • +
  • + Technical language can discourage questioning. +
  • +
+ +

+ These reactions reflect natural cognitive patterns that shape how we + interpret information and assess credibility. +

+ +

+ AI can generate fluent and confident responses, but it + does not understand, reason, or take responsibility. That + responsibility lies with the user. The most important safeguard is{" "} + AI literacy, the ability to recognize bias, question + outputs, and engage with AI thoughtfully! +

+
+ +
+
+ ); +}; + +export default Conclusion; diff --git a/client/src/components/exercise/lab13/pages/ConfidenceRanking.js b/client/src/components/exercise/lab13/pages/ConfidenceRanking.js new file mode 100644 index 000000000..4f5d5638a --- /dev/null +++ b/client/src/components/exercise/lab13/pages/ConfidenceRanking.js @@ -0,0 +1,100 @@ +import React, { useState, useEffect, useContext } from "react"; +import { startExercise } from "src/reducers/lab2/actions"; +import { navigate } from "@reach/router"; +import DragDropGame from "../components/DragAndDropNoCorrectAnswer/DragDropGame"; +import ExerciseStateContext from "../Lab13Context"; +import { initialColumns, initialBank } from "src/constants/lab13/RankingConfig"; +import DragIndicatorRoundedIcon from "@mui/icons-material/DragIndicatorRounded"; + +const ConfidenceRanking = () => { + const { + rankingSuccess, + setRankingSuccess, + rankingColumns, + setRankingColumns, + rankingBank, + setRankingBank, + } = useContext(ExerciseStateContext); + + const [cols, setCols] = useState(() => + rankingColumns.length > 0 + ? structuredClone(rankingColumns) + : structuredClone(initialColumns), + ); + + const [bank, setBank] = useState(() => { + const placedIds = new Set( + (rankingColumns.length > 0 ? rankingColumns : initialColumns).flatMap( + (col) => col.cards.map((card) => card.id), + ), + ); + + const sourceBank = rankingBank.length > 0 ? rankingBank : initialBank; + return sourceBank.filter((card) => !placedIds.has(card.id)); + }); + + const [success, setSuccess] = useState(rankingSuccess); + + // Create a permissive correctAssignments that accepts any arrangement + const correctAssignments = initialColumns.map((col) => ({ + id: col.id, + cards: initialBank.map((item) => item.id), + })); + + // Save to context whenever cols or bank changes + useEffect(() => { + setRankingColumns(cols); + setRankingBank(bank); + }, [cols, bank, setRankingColumns, setRankingBank]); + + // Custom validation: check if all items are placed + useEffect(() => { + const allItemsPlaced = + bank.length === 0 && + cols.every((col) => col.cards && col.cards.length > 0); + setSuccess(allItemsPlaced); + setRankingSuccess(allItemsPlaced); + }, [cols, bank, setRankingSuccess]); + + const handleContinue = () => { + if (!success) { + alert( + "Please complete ranking your knowledge of all topics before continuing", + ); + return; + } + startExercise(); + navigate("/Lab13/Exercise/AIPanel"); + }; + + return ( +
+

Confidence Ranking Page

+

+ Drag and Drop each of the three topics, Dyslexia, Color Blindness, and + Localization from your most familiar to least familiar. +

+ } + handleNav={handleContinue} + /> +
+ ); +}; + +export default ConfidenceRanking; diff --git a/client/src/components/exercise/lab13/pages/ExerciseIntroduction.js b/client/src/components/exercise/lab13/pages/ExerciseIntroduction.js new file mode 100644 index 000000000..61070ef92 --- /dev/null +++ b/client/src/components/exercise/lab13/pages/ExerciseIntroduction.js @@ -0,0 +1,54 @@ +import { React, useEffect } from "react"; +import { EXERCISE_PLAYING } from "src/constants/index"; +import useMainStateContext from "src/reducers/MainContext"; +import { navigate } from "@reach/router"; +import { ExerciseService } from "src/services/lab13/ExerciseService"; + +const ExerciseIntroduction = () => { + const { state, actions } = useMainStateContext(); + + useEffect(() => { + actions.updateUserState(EXERCISE_PLAYING); + }, []); + + const startExercise = async () => { + const body = { + userid: state.main.user.userid, + isExerciseComplete: false, + hasViewed: true, + }; + await ExerciseService.submitExercise(body); + }; + + const handleContinue = () => { + startExercise(); + navigate("/Lab13/Exercise/ConfidenceRanking"); + }; + + return ( +
+

Exercise Start

+
+

+ You are a student at ALL university who is doing their psychology + homework. You are given 3 questions to answer and are allowed to use + ALL's new Generative AI tool, AL, to help you answer them. You + want to tackle each question from your least to most knowledgeable. + Let's start with ranking your knowledge about each topic before + you use AL to help you answer. +

+
+ Click the Start button to begin the exercise! +
+
+ +
+ ); +}; + +export default ExerciseIntroduction; diff --git a/client/src/components/exercise/lab13/pages/IDEExercise.js b/client/src/components/exercise/lab13/pages/IDEExercise.js new file mode 100644 index 000000000..db564e225 --- /dev/null +++ b/client/src/components/exercise/lab13/pages/IDEExercise.js @@ -0,0 +1,92 @@ +import React from "react"; +import { useLab13 } from "../Lab13Context"; +import Repair from "src/components/body/Repair/Repair"; +import PropTypes from "prop-types"; +import { startExercise } from "src/reducers/lab2/actions"; +import { navigate } from "@reach/router"; +import IDEExerciseImplementation from "./repairs/IDEExerciseImplementation"; + +const IDEExercise = () => { + const { + exercisePromptsState, + validInputs, + isFirst, + handleUserInputChange, + checkInputValid, + fetchRepair, + postRepair, + setShowConfidenceScore, + setShowCitations, + setDisclaimerMessage, + setCurrentPhase, + setTopicIndex, + } = useLab13(); + + const handleContinue = () => { + if (!checkInputValid()) { + alert("Please complete all fields correctly before continuing."); + return; + } + + // Save IDE settings to context + const disclaimerValue = exercisePromptsState.find( + (i) => i.id === "disclaimer", + ).value; + const confidenceValue = exercisePromptsState.find( + (i) => i.id === "confidence", + ).value; + const citationsValue = exercisePromptsState.find( + (i) => i.id === "citations", + ).value; + + setDisclaimerMessage(disclaimerValue); + setShowConfidenceScore(!!confidenceValue); + setShowCitations(!!citationsValue); + + setCurrentPhase(4); + setTopicIndex(2); + + startExercise(); + navigate("/Lab13/Exercise/AIPanel"); + }; + + const data = { + exercisePromptsState, + validInputs, + isFirst, + }; + + const functions = { + handleUserInputChange, + checkInputValid, + fetchRepair, + postRepair, + }; + + return ( + + ); +}; + +IDEExercise.propTypes = { + exercisePromptsState: PropTypes.object, + validInputs: PropTypes.object, + isFirst: PropTypes.bool, +}; + +export default IDEExercise; diff --git a/client/src/components/exercise/lab13/pages/IDEIntroduction.js b/client/src/components/exercise/lab13/pages/IDEIntroduction.js new file mode 100644 index 000000000..931ec6673 --- /dev/null +++ b/client/src/components/exercise/lab13/pages/IDEIntroduction.js @@ -0,0 +1,82 @@ +import { React } from "react"; +import { navigate } from "@reach/router"; +import LabButton from "src/components/all-components/LabButton"; + +const IDEIntroduction = () => { + const handleContinue = () => { + navigate("/Lab13/Exercise/IDEExercise"); + }; + + return ( +
+

AI Cognitive Bias Repair

+

+ There are quite a few types of biases that can impact how we view our AI + chatbot responses! Let's explore a few common ways to mitigate + these biases through some common additions to AI chatbots that can help + us reduce the issue of experiencing cognitive biases. +

+

+ Some of these additions include making the AI chatbot provide a + confidence score. Confidence scores are the way the + chatbot let's the user know how sure it is that it completed + it's request. Be careful though, this is not the same as an + accuracy score. +

+

+ Another additions would be the disclaimer text that + many AI chatbots include somewhere in their interface. Disclaimers are a + great way to have a constant reminder to their user that AI can be wrong + and should be double-checked. +

+

+ Finally, another common addition is having the AI chatbot provide + citations for where it got its information from. This + is a great way to help users fact-check the information provided by the + AI chatbot and reduce the chances of falling for misinformation. +

+

+ In the real world, AI systems are designed with features that help + reduce overreliance and encourage critical thinking. Some of these + include: +

+ +
+

+ + Confidence Scores: + {" "} + Show how certain the AI is about its response instead of presenting it + as absolute truth. +

+ +

+ + Citations: + {" "} + Provide traceable sources so users can verify claims. +

+ +

+ + Disclaimer Messages: + {" "} + Remind users that AI outputs may be incorrect or incomplete. +

+ +

+ AI doesn't have to feel authoritative to be useful. Now, + it's your turn to implement these safeguards! +

+
+
+

+ Click "Continue to Repair" to proceed! +

+ +
+
+ ); +}; + +export default IDEIntroduction; diff --git a/client/src/components/exercise/lab13/pages/PanelswithIDEFixes.js b/client/src/components/exercise/lab13/pages/PanelswithIDEFixes.js new file mode 100644 index 000000000..743726405 --- /dev/null +++ b/client/src/components/exercise/lab13/pages/PanelswithIDEFixes.js @@ -0,0 +1,19 @@ +import { React } from "react"; +import { startExercise } from "src/reducers/lab2/actions"; +import { navigate } from "@reach/router"; + +const IDEIntroduction = () => { + const handleContinue = () => { + startExercise(); + navigate("/Lab13/Exercise/Conclusion"); + }; + + return ( +
+ AI/Search Panel with IDE Fixes Page + +
+ ); +}; + +export default IDEIntroduction; diff --git a/client/src/components/exercise/lab13/pages/repairs/IDEExerciseImplementation.js b/client/src/components/exercise/lab13/pages/repairs/IDEExerciseImplementation.js new file mode 100644 index 000000000..bca819f5e --- /dev/null +++ b/client/src/components/exercise/lab13/pages/repairs/IDEExerciseImplementation.js @@ -0,0 +1,108 @@ +import React from "react"; +import CodeLine from "../../../../../components/all-components/CodeBlock/Components/CodeLine"; +import CodeBlockInput from "../../../../../components/all-components/CodeBlock/Components/CodeBlockInput"; +import MultiTab from "../../../../../components/all-components/CodeBlock/Components/MultiTab"; +import PropTypes from "prop-types"; +import ReactText from "../../../../../components/all-components/CodeBlock/StyleComponents/ReactText"; +import CommentText from "../../../../../components/all-components/CodeBlock/StyleComponents/CommentText"; +import ErrorText from "../../../../../components/all-components/CodeBlock/StyleComponents/ErrorText"; + +/** + * IDEExerciseImplementation is a component that is responsible for displaying the codeblock contents + * and user inputs where the participant will make their changes to the codebase and complete the IDE exercise section. + * @param {props} inputs contains the data used for answer validation and display of its contents + * @param {props} userInput is a function that takes the user's input in each input and sends the data to the useDataService hook, + * which then sends that to the useLabService hook + * @param {props} validInputs returns an object based on the number of correct and incorrect inputs of the user in the exercise section + * @param {props} isFirst returns a boolean value of whether or not this is the first time a user is viewing this exercise section + * @returns + */ + +const IDEExerciseImplementation = (props = {}) => { + const { inputs, userInput, validInputs, isFirst } = props; + + return ( + <> + + const addDisclaimers = { + + + + {`// Enter a disclaimer of at least 20 characters, including the words "verify" and "output".`} + + + + disclaimerText = + {'"'} + i.id === "disclaimer")?.value || ""} + attributes={{ + type: "text", + onChange: (e) => userInput("disclaimer", e.target.value), + placeholder: "Enter Answer Here", + }} + /> + {'"'} + ; + {validInputs.disclaimer === false && !isFirst && ( + + + Please enter the correct disclaimer. + + )} + + + + {`// Enter 'true' below:`} + + + + showConfidenceScores = + i.id === "confidence")?.value || ""} + attributes={{ + type: "text", + onChange: (e) => userInput("confidence", e.target.value), + placeholder: "Enter Answer Here", + }} + /> + ; + {validInputs.confidence === false && !isFirst && ( + Please enter {'"true"'}. + )} + + + + {`// Enter 'true' below:`} + + + + showCitations = + i.id === "citations")?.value || ""} + attributes={{ + type: "text", + onChange: (e) => userInput("citations", e.target.value), + placeholder: "Enter Answer Here", + }} + /> + ; + {validInputs.citations === false && !isFirst && ( + Please enter {'"true"'}. + )} + + + } + + + ); +}; + +IDEExerciseImplementation.propTypes = { + inputs: PropTypes.array.isRequired, + userInput: PropTypes.func.isRequired, + validInputs: PropTypes.object.isRequired, + isFirst: PropTypes.bool.isRequired, +}; + +export default IDEExerciseImplementation; diff --git a/client/src/components/exercise/lab14/Main.js b/client/src/components/exercise/lab14/Main.js index e1593bcb5..b3e2e4ffe 100644 --- a/client/src/components/exercise/lab14/Main.js +++ b/client/src/components/exercise/lab14/Main.js @@ -41,7 +41,7 @@ const Main = () => { const [rsaShiftValue, setRsaShiftValue] = useState(1024); return ( -
+
{ - if (Math.abs(num) >= 1e6) { + if (Math.abs(num) >= 1e12) { return num.toExponential(2); } return num.toLocaleString(); diff --git a/client/src/components/exercise/lab14/pages/RSADecryption.js b/client/src/components/exercise/lab14/pages/RSADecryption.js index 38b37c16f..f8aae2a47 100644 --- a/client/src/components/exercise/lab14/pages/RSADecryption.js +++ b/client/src/components/exercise/lab14/pages/RSADecryption.js @@ -28,7 +28,7 @@ const RSADecryption = () => { }; const formatNumber = (num) => { - if (Math.abs(num) >= 1e6) { + if (Math.abs(num) >= 1e12) { return num.toExponential(2); } return num.toLocaleString(); diff --git a/client/src/components/exercise/lab3/Main.js b/client/src/components/exercise/lab3/Main.js index 6226f85e7..9014ca6e2 100644 --- a/client/src/components/exercise/lab3/Main.js +++ b/client/src/components/exercise/lab3/Main.js @@ -41,7 +41,7 @@ class Main extends Component { render() { const { actions, state, user } = this.props; return ( -
+
diff --git a/client/src/components/exercise/lab4/Main.js b/client/src/components/exercise/lab4/Main.js index b289ea15f..22bf95796 100644 --- a/client/src/components/exercise/lab4/Main.js +++ b/client/src/components/exercise/lab4/Main.js @@ -18,7 +18,7 @@ import ExerciseStart from "./pages/ExerciseStart"; const Main = () => { return ( -
+
diff --git a/client/src/components/exercise/lab4/pages/CodeChangeBlocks.js b/client/src/components/exercise/lab4/pages/CodeChangeBlocks.js index 39a7fc3ea..520e3c08c 100644 --- a/client/src/components/exercise/lab4/pages/CodeChangeBlocks.js +++ b/client/src/components/exercise/lab4/pages/CodeChangeBlocks.js @@ -140,7 +140,7 @@ const CodeChangeBlocks = () => { useEffect(() => { actions.updateUserState(EXERCISE_PLAYING); Prism.highlightAll(); - if (window.location.state.role !== undefined) { + if (window.location.state?.role !== undefined) { const el0 = document.getElementById("first"); el0.value = window.location.state.role; doEvent(el0, "input"); diff --git a/client/src/components/exercise/lab5/Main.js b/client/src/components/exercise/lab5/Main.js index 220fa9380..a2c06876d 100644 --- a/client/src/components/exercise/lab5/Main.js +++ b/client/src/components/exercise/lab5/Main.js @@ -46,7 +46,7 @@ class Main extends Component { render() { const { actions, state, user } = this.props; return ( -
+
{ return ( -
+
{/* Part 1: Applicant */} diff --git a/client/src/components/exercise/lab7/Main.js b/client/src/components/exercise/lab7/Main.js index f345a5fbc..9c90fe0e4 100644 --- a/client/src/components/exercise/lab7/Main.js +++ b/client/src/components/exercise/lab7/Main.js @@ -22,7 +22,7 @@ import { Lab7ContextProvider } from "src/reducers/lab7/Lab7Context"; const Main = () => { return ( -
+
diff --git a/client/src/components/exercise/lab8/Main.js b/client/src/components/exercise/lab8/Main.js index bb6f3061e..c09a9a0b4 100644 --- a/client/src/components/exercise/lab8/Main.js +++ b/client/src/components/exercise/lab8/Main.js @@ -19,7 +19,7 @@ const Main = () => { ); return ( -
+
{ EXERCISE_STATES.EXERCISE_SELECTION_DEFAULT, ); return ( -
+
diff --git a/client/src/components/footer/LabFooter.js b/client/src/components/footer/LabFooter.js index 58883fed3..41891cdaa 100644 --- a/client/src/components/footer/LabFooter.js +++ b/client/src/components/footer/LabFooter.js @@ -62,7 +62,7 @@ const LabFooter = (props) => { > {body > 0 && ( ) : ( @@ -204,7 +205,11 @@ const Header = ({ isImagine }) => { isOpen={showSignIn} toggle={toggleSignInShown} > - + { + setShowSignIn(false); + }} + />
diff --git a/client/src/components/header/helpers/LoginButton.js b/client/src/components/header/helpers/LoginButton.js index ba9eb4739..976746ca6 100644 --- a/client/src/components/header/helpers/LoginButton.js +++ b/client/src/components/header/helpers/LoginButton.js @@ -2,11 +2,65 @@ import React from "react"; import API from "../../../services/API"; import GoogleLogin from "../../../assets/images/google_buttons/Google_Sign_In.svg"; +import Avatar from "avataaars"; +import { AvatarData } from "../../body/login/AvatarData"; +import useMainStateContext from "../../../reducers/MainContext"; +import PropTypes from "prop-types"; const LoginButton = (props) => { const { enabled } = props; + const { actions } = useMainStateContext(); - if (enabled) { + const isDev = process.env.NODE_ENV === "development"; + + const developmentLogin = (userId) => { + actions.developmentLogin(userId); + props.closeModal(); + }; + + const devLogin = () => { + return ( +
+

Who would you like to log in as?

+
+ {AvatarData.map((data, index) => { + return ( +
developmentLogin(data.id)} + key={index} + className={ + "tw-flex tw-flex-col tw-items-center tw-rounded-full hover:tw-cursor-pointer" + } + > + + {data.name} +
+ ); + })} +
+
+ ); + }; + + const prodLogin = () => { return (
{ ); + }; + + if (enabled) { + return isDev ? devLogin() : prodLogin(); } return
; }; +LoginButton.propTypes = { + closeModal: PropTypes.function, +}; + export default LoginButton; diff --git a/client/src/constants/index.js b/client/src/constants/index.js index f88af72c3..3eab8973f 100644 --- a/client/src/constants/index.js +++ b/client/src/constants/index.js @@ -261,6 +261,25 @@ export const Sections = { name: "Quiz", }, }, + 13: { + fullname: "Lab 13: Human Cognitive Bias & AI Lab", + name: "Lab13", + 0: { + name: "About", + }, + 1: { + name: "Reading", + }, + 2: { + name: "Exercise", + }, + 3: { + name: "Reinforcement", + }, + 4: { + name: "Quiz", + }, + }, 14: { fullname: "Lab 14: Quantum Cryptography", name: "Lab14", diff --git a/client/src/constants/lab13/AvatarType.js b/client/src/constants/lab13/AvatarType.js new file mode 100644 index 000000000..fcdf207f3 --- /dev/null +++ b/client/src/constants/lab13/AvatarType.js @@ -0,0 +1,4 @@ +export const AvatarType = { + User: "user", + AI: "ai", +}; diff --git a/client/src/constants/lab13/BiasQuestionsConfig.js b/client/src/constants/lab13/BiasQuestionsConfig.js new file mode 100644 index 000000000..b545f0efe --- /dev/null +++ b/client/src/constants/lab13/BiasQuestionsConfig.js @@ -0,0 +1,353 @@ +/** + * Bias Questions Configuration for Lab 13 + * + * Structure: + * - Each topic has 9 AI responses (3 per bias) + * - Only ONE bias is activated per topic based on student's knowledge ranking + * - Mapping: Least knowledgeable → Dunning-Kruger + * Medium knowledgeable → Halo Effect (shown first) + * Most knowledgeable → Truth Bias + */ + +export const BIAS_TYPES = { + TRUTH_BIAS: "TRUTH_BIAS", + HALO_EFFECT: "HALO_EFFECT", + DUNNING_KRUGER: "DUNNING_KRUGER", +}; + +export const BIAS_POSITION_MAP = { + 0: BIAS_TYPES.TRUTH_BIAS, // Most knowledgeable + 1: BIAS_TYPES.HALO_EFFECT, // Medium knowledgeable + 2: BIAS_TYPES.DUNNING_KRUGER, // Least knowledgeable +}; + +export const BIAS_DEFINITIONS = { + TRUTH_BIAS: { + name: "Truth Bias", + definition: + "Truth bias is the tendency to believe that statements are true, especially when they are presented in a confident and clear manner. People are more likely to accept information that sounds authoritative without questioning its accuracy.", + }, + HALO_EFFECT: { + name: "Halo Effect", + definition: + "The halo effect is when a single positive characteristic or polished presentation influences your overall perception of something. If information is presented professionally or sounds credible, people tend to trust it more, even if the content itself may not be accurate.", + }, + DUNNING_KRUGER: { + name: "Dunning-Kruger Effect", + definition: + "The Dunning-Kruger effect occurs when complex or technical language makes people less likely to question information. When something sounds academic or expert-like, people assume it must be correct and don't scrutinize it as carefully.", + }, +}; + +export const biasQuestionsData = [ + { + id: "localization", + topicName: "Localization", + topicDefinition: + "Localization is the process of adapting information or communication to align with the cultural, linguistic, and social expectations of a specific audience. Unlike translation, which focuses only on language, localization also adjusts context, examples, and cultural references.", + questions: [ + { + id: "loc_q1", + text: "Is localization the same as translation?", + answers: { + [BIAS_TYPES.TRUTH_BIAS]: { + text: "Yes, localization and translation are essentially the same process. Both involve converting content from one language to another to make it understandable for different audiences. The terms are often used interchangeably in professional settings.", + isCorrect: false, + explanation: + "This is false. The definitive wording makes this claim sound trustworthy, which can encourage truth bias.", + confidence: 95, + }, + [BIAS_TYPES.HALO_EFFECT]: { + text: "Localization and translation are closely related practices within global communication workflows, often used interchangeably across professional contexts.", + isCorrect: false, + explanation: + "This is false. The polished phrasing and professional tone boosts perceived credibility, which can reinforce the halo effect.", + confidence: 93, + }, + [BIAS_TYPES.DUNNING_KRUGER]: { + text: "In localization theory, translation is considered a subordinate linguistic function, meaning it is functionally equivalent to localization in applied contexts.", + isCorrect: false, + explanation: + "This is false. The technical language and theoretical framing can play into the Dunning-Kruger effect by reducing questioning.", + confidence: 98, + }, + }, + }, + { + id: "loc_q2", + text: 'Is the statement, "Changing clothing material to be more breathable for a local climate is not localization," true?', + answers: { + [BIAS_TYPES.TRUTH_BIAS]: { + text: "No, changing clothing materials is a physical product decision, not localization, which focuses on cultural or linguistic adaptation.", + isCorrect: true, + explanation: + "Although this is true, the clear, direct explanation feels dependable, which can reinforce truth bias in a justified way.", + confidence: 86, + }, + [BIAS_TYPES.HALO_EFFECT]: { + text: "It's technically correct, but it's confusing because people mix this up all the time. Changing clothing materials isn't localization, it's product adaptation. Localization focuses on cultural and linguistic elements, not physical manufacturing changes.", + isCorrect: true, + explanation: + "Although this is true, the confident structure and clean distinctions enhance trust through presentation which can play into the halo effect.", + confidence: 90, + }, + [BIAS_TYPES.DUNNING_KRUGER]: { + text: "Localization frameworks distinguish between cultural adaptation and material optimization, placing climate-driven textile changes outside localization scope.", + isCorrect: true, + explanation: + "Although this is true, the academic tone may seem authoritative, limiting scrutiny, it correctly aligns with the definition, reinforcing the careful trust provided by the Dunning-Kruger effect.", + confidence: 94, + }, + }, + }, + { + id: "loc_q3", + text: "If I change the way I speak to my teacher, is that localizing to the school culture?", + answers: { + [BIAS_TYPES.TRUTH_BIAS]: { + text: "Yes, adjusting how you communicate based on social context can be considered a form of cultural localization.", + isCorrect: true, + explanation: + "Although this is true, the simple, confident phrasing makes the answer feel intuitively right, which can prompt truth bias.", + confidence: 82, + }, + [BIAS_TYPES.HALO_EFFECT]: { + text: "Adapting communication style to social norms reflects cultural awareness, which aligns with broader definitions of localization.", + isCorrect: true, + explanation: + 'Although this is true, formal language creates an "expert" impression, which can amplify the halo effect.', + confidence: 88, + }, + [BIAS_TYPES.DUNNING_KRUGER]: { + text: "Sociolinguistic code-switching is not based on localization theory, which is mostly from translation studies (Venuti, 1995), and applies only to cross-cultural content adaptation.", + isCorrect: false, + explanation: + "This is false. The response sounds academic and authoritative, making the claim hard to challenge which can elicit the Dunning-Kruger effect.", + confidence: 92, + }, + }, + }, + ], + }, + { + id: "colorblindness", + topicName: "Color Blindness", + topicDefinition: + "Color blindness is a visual condition that affects color perception, most commonly red and green. The most common type, red-green color blindness, is inherited through the X chromosome. People with red-green color blindness do not see only red and green, but may have difficulty distinguishing between certain shades. Although many cases are inherited, they can also develop later in life due to eye disease, injury, aging, or certain medications.", + questions: [ + { + id: "cb_q1", + text: "Is the color blindness gene carried in the X or Y chromosome?", + answers: { + [BIAS_TYPES.TRUTH_BIAS]: { + text: "The gene responsible for most forms of color blindness is carried on the X chromosome, which is why the condition is more common in males than females.", + isCorrect: true, + explanation: + "Although this is true, the clear, factual delivery encourages truth bias, making the statement easy to accept without hesitation.", + confidence: 91, + }, + [BIAS_TYPES.HALO_EFFECT]: { + text: "Color blindness is typically inherited through the Y chromosome, a well-established genetic pattern observed in vision science.", + isCorrect: false, + explanation: + "This is false. The scientific-sounding language increases credibility through its style which can trigger the halo effect.", + confidence: 89, + }, + [BIAS_TYPES.DUNNING_KRUGER]: { + text: "X-linked recessive inheritance patterns explain the higher prevalence of color blindness in males, as identified in genetic ophthalmology research.", + isCorrect: true, + explanation: + "Although this is true, the technical language signals expertise, which may induce the Dunning-Kruger effect by discouraging doubt.", + confidence: 96, + }, + }, + }, + { + id: "cb_q2", + text: "Do people who have Red-Green color blindness only see red and green?", + answers: { + [BIAS_TYPES.TRUTH_BIAS]: { + text: "Yes, people with red-green color blindness mainly see only red and green, which limits their overall color perception.", + isCorrect: false, + explanation: + "This is false. Familiar wording makes the statement feel accurate, even though it reinforces a common myth, which can reinforce truth bias.", + confidence: 84, + }, + [BIAS_TYPES.HALO_EFFECT]: { + text: "Individuals with red-green color blindness primarily experience a reduced visual palette around red and green hues.", + isCorrect: false, + explanation: + "This is false. The polished wording makes the misconception sound legitimate, which can feed into the halo effect.", + confidence: 89, + }, + [BIAS_TYPES.DUNNING_KRUGER]: { + text: "Red-green color blindness results in selective chromatic restriction, effectively narrowing visual perception to red-green channels.", + isCorrect: false, + explanation: + "This is false. The academic phrasing may trigger the Dunning-Kruger effect, discouraging readers from questioning the claim.", + confidence: 95, + }, + }, + }, + { + id: "cb_q3", + text: "Can people develop color blindness later in life? Or can only people born with color blindness have it?", + answers: { + [BIAS_TYPES.TRUTH_BIAS]: { + text: "Yes, color blindness can develop later in life due to eye disease, injury, aging, or certain medications.", + isCorrect: true, + explanation: + "Although this is true, the specific, concrete explanation feels reliable which can strengthen truth bias.", + confidence: 90, + }, + [BIAS_TYPES.HALO_EFFECT]: { + text: "Although many cases are inherited, medical conditions or neurological damage can also lead to acquired color blindness.", + isCorrect: true, + explanation: + "Although this is true, the calm, professional tone increases trust, which can activate the halo effect.", + confidence: 88, + }, + [BIAS_TYPES.DUNNING_KRUGER]: { + text: "Color blindness is strictly a genetic condition, meaning individuals are born with it and cannot develop it later in life.", + isCorrect: false, + explanation: + "This is false. The definitive tone discourages skepticism which may activate the Dunning-Kruger effect for readers unfamiliar with medical causes.", + confidence: 93, + }, + }, + }, + ], + }, + { + id: "dyslexia", + topicName: "Dyslexia", + topicDefinition: + "Dyslexia is a neurological learning disability that primarily affects reading and language processing. It is not a vision problem and does not affect intelligence. People with dyslexia may have difficulty connecting written letters to spoken sounds, not how letters visually appear. Dyslexia cannot be cured, but it can be effectively supported through early intervention, structured reading instruction, and classroom accommodations.", + questions: [ + { + id: "dys_q1", + text: "Is dyslexia a vision problem?", + answers: { + [BIAS_TYPES.TRUTH_BIAS]: { + text: "Yes, dyslexia is mainly caused by vision problems that make letters difficult to see clearly while reading.", + isCorrect: false, + explanation: + "This is false. The confident delivery makes the claim feel reliable, which can encourage truth bias even when it's wrong.", + confidence: 89, + }, + [BIAS_TYPES.HALO_EFFECT]: { + text: "Dyslexia is often associated with visual processing challenges that interfere with reading accuracy.", + isCorrect: false, + explanation: + "This is false. The polished, professional tone boosts perceived credibility, which can trigger the halo effect.", + confidence: 86, + }, + [BIAS_TYPES.DUNNING_KRUGER]: { + text: "Dyslexia originates from impairments in visual-perceptual pathways involved in orthographic recognition.", + isCorrect: false, + explanation: + "This is false. Technical wording signals expertise, which may activate the Dunning-Kruger effect, by reducing questioning.", + confidence: 97, + }, + }, + }, + { + id: "dys_q2", + text: "Do people who have dyslexia see words and letters backward?", + answers: { + [BIAS_TYPES.TRUTH_BIAS]: { + text: "Yes, people with dyslexia often see letters and words backward, which explains their reading difficulties.", + isCorrect: false, + explanation: + "This is false. A familiar explanation matches common assumptions, which can promote truth bias.", + confidence: 88, + }, + [BIAS_TYPES.HALO_EFFECT]: { + text: "Letter reversals are a characteristic feature of dyslexia and contribute to challenges in reading fluency.", + isCorrect: false, + explanation: + "This is false. The structured, authoritative phrasing makes the false response sound valid, which can amplify the halo effect.", + confidence: 90, + }, + [BIAS_TYPES.DUNNING_KRUGER]: { + text: "Dyslexia manifests through reversed orthographic encoding during early language acquisition.", + isCorrect: false, + explanation: + "This is false. Complex terminology that implies expertise may activate the Dunning-Kruger effect, causing readers to accept the claim without scrutiny.", + confidence: 96, + }, + }, + }, + { + id: "dys_q3", + text: "Can dyslexia be cured?", + answers: { + [BIAS_TYPES.TRUTH_BIAS]: { + text: "Dyslexia cannot be cured, but with consistent practice and proper educational support, individuals can significantly improve their reading and learning skills.", + isCorrect: true, + explanation: + "Although this is true, clear, confident phrasing makes the response easy to accept, which can reinforce truth bias.", + confidence: 87, + }, + [BIAS_TYPES.HALO_EFFECT]: { + text: "Advances in educational interventions have made it possible for many individuals with dyslexia to achieve strong reading and academic outcomes.", + isCorrect: true, + explanation: + "Although this is true, an optimistic professional tone builds trust through presentation which can activate the halo effect.", + confidence: 85, + }, + [BIAS_TYPES.DUNNING_KRUGER]: { + text: "Dyslexia is a lifelong neurological learning difference that cannot be cured, but can be effectively supported through structured instruction and accommodations.", + isCorrect: true, + explanation: + "Although this is true, the formal tone and phrasing may discourage questioning, activating the Dunning-Kruger effect through perceived expertise.", + confidence: 91, + }, + }, + }, + ], + }, +]; + +/** + * Helper function to get questions for a specific bias from a topic + * @param {string} topicId - The ID of the topic (e.g., 'localization', 'colorblindness', 'dyslexia') + * @param {string} biasType - The bias type (TRUTH_BIAS, HALO_EFFECT, DUNNING_KRUGER) + * @returns {Array} Array of questions with only the specified bias answers + */ +export const getQuestionsByBias = (topicId, biasType) => { + const topic = biasQuestionsData.find((t) => t.id === topicId); + if (!topic) return []; + + return topic.questions.map((question) => ({ + id: question.id, + text: question.text, + answer: { + ...question.answers[biasType], + biasType, + }, + })); +}; + +/** + * Helper function to get topic by ID + * @param {string} topicId - The ID of the topic + * @returns {Object} Topic object with definition and questions + */ +export const getTopicById = (topicId) => { + return biasQuestionsData.find((t) => t.id === topicId); +}; + +/** + * Helper function to determine which bias is activated based on ranking position + * @param {number} rankPosition - The position in ranking (0 = most knowledgeable, 1 = medium, 2 = least) + * @returns {string} The bias type for that ranking position + */ +export const getBiasForRankingPosition = (rankPosition) => { + const biasMap = { + 0: BIAS_TYPES.TRUTH_BIAS, // Most knowledgeable + 1: BIAS_TYPES.HALO_EFFECT, // Medium knowledgeable + 2: BIAS_TYPES.DUNNING_KRUGER, // Least knowledgeable + }; + return biasMap[rankPosition] || BIAS_TYPES.HALO_EFFECT; +}; diff --git a/client/src/constants/lab13/HighlightsMapping.js b/client/src/constants/lab13/HighlightsMapping.js new file mode 100644 index 000000000..b0f2e190e --- /dev/null +++ b/client/src/constants/lab13/HighlightsMapping.js @@ -0,0 +1,82 @@ +export const HIGHLIGHTS_MAPPING = { + localization: { + 0: { + TRUTH_BIAS: [ + "Unlike translation, which focuses only on language, localization also adjusts context, examples, and cultural references", + ], + HALO_EFFECT: [ + "adapting information or communication to align with the cultural, linguistic, and social expectations", + ], + DUNNING_KRUGER: ["context", "examples", "cultural references"], + }, + 1: { + TRUTH_BIAS: [ + "does not involve physical changes, such as changing clothing materials for climate", + ], + HALO_EFFECT: ["using local currency", "changing date formats"], + DUNNING_KRUGER: [ + "However, modifying language or tone to fit different social settings can be considered a form of cultural localization", + ], + }, + 2: { + TRUTH_BIAS: [ + "modifying language or tone to fit different social settings can be considered a form of cultural localization", + ], + HALO_EFFECT: [ + "adapting information or communication", + "cultural, linguistic, and social expectations", + ], + DUNNING_KRUGER: ["context", "examples", "cultural references"], + }, + }, + colorblindness: { + 0: { + TRUTH_BIAS: ["inherited through the X chromosome"], + HALO_EFFECT: ["The most common type, red-green color blindness"], + DUNNING_KRUGER: ["inherited through the X chromosome"], + }, + 1: { + TRUTH_BIAS: [ + "People with red-green color blindness do not see only red and green, but may have difficulty distinguishing between certain shades", + ], + HALO_EFFECT: [ + "may have difficulty distinguishing between certain shades", + ], + DUNNING_KRUGER: ["difficulty distinguishing between certain shades"], + }, + 2: { + TRUTH_BIAS: [ + "develop later in life due to eye disease, injury, aging, or certain medications", + ], + HALO_EFFECT: [ + "Although many cases are inherited, they can also develop later in life", + ], + DUNNING_KRUGER: ["eye disease, injury, aging, or certain medications"], + }, + }, + dyslexia: { + 0: { + TRUTH_BIAS: ["It is not a vision problem"], + HALO_EFFECT: ["does not affect intelligence"], + DUNNING_KRUGER: ["neurological learning disability"], + }, + 1: { + TRUTH_BIAS: [ + "not how letters visually appear", + "connecting written letters to spoken sounds", + ], + HALO_EFFECT: [ + "may have difficulty connecting written letters to spoken sounds", + ], + DUNNING_KRUGER: ["People with dyslexia may have difficulty"], + }, + 2: { + TRUTH_BIAS: [ + "cannot be cured", + "early intervention, structured reading instruction, and classroom accommodations", + ], + HALO_EFFECT: ["effectively supported through early intervention"], + DUNNING_KRUGER: ["Dyslexia cannot be cured"], + }, + }, +}; diff --git a/client/src/constants/lab13/RankingConfig.js b/client/src/constants/lab13/RankingConfig.js new file mode 100644 index 000000000..47ea73b51 --- /dev/null +++ b/client/src/constants/lab13/RankingConfig.js @@ -0,0 +1,39 @@ +const initialColumns = [ + { id: "column1", title: "Most Knowledgeable", cards: [] }, + { id: "column2", title: "Moderately Knowledgeable", cards: [] }, + { id: "column3", title: "Least Knowledgeable", cards: [] }, +]; + +const initialBank = [ + { + id: "dyslexia", + title: "Dyslexia", + body: "", + isCorrect: true, + }, + { + id: "colorblindness", + title: "Color Blindness", + body: "", + isCorrect: true, + }, + { + id: "localization", + title: "Localization", + body: "", + isCorrect: true, + }, +]; + +// Since ranking is subjective, we'll consider any complete ranking as correct +const correctAssignments = [ + { id: "column1", cards: [] }, + { id: "column2", cards: [] }, + { id: "column3", cards: [] }, +]; + +module.exports = { + initialColumns, + initialBank, + correctAssignments, +}; diff --git a/client/src/constants/lab13/WikipediaContent.js b/client/src/constants/lab13/WikipediaContent.js new file mode 100644 index 000000000..ad70ace07 --- /dev/null +++ b/client/src/constants/lab13/WikipediaContent.js @@ -0,0 +1,38 @@ +export const content = { + localization: { + title: "Localization", + text: `Localization is the process of adapting information or communication to align with the cultural, linguistic, and social expectations of a specific audience. Unlike translation, which focuses only on language, localization also adjusts context, examples, and cultural references. + +This may include using local currency or changing date formats, but it does not involve physical changes, such as changing clothing materials for climate. However, modifying language or tone to fit different social settings can be considered a form of cultural localization.`, + sources: [ + "https://resources.gala-global.org/accessibility-localization/", + "https://www.vistatec.com/localization-for-all-advancing-accessibility-and-inclusion-in-a-globalized-world/", + ], + imageUrl: + "https://images.unsplash.com/photo-1526628953301-3e589a6a8b74?w=400", // Placeholder + }, + colorblindness: { + title: "Color Blindness", + text: `Color blindness is a visual condition that affects color perception, most commonly red and green. The most common type, red-green color blindness, is inherited through the X chromosome. People with red-green color blindness do not see only red and green, but may have difficulty distinguishing between certain shades. + +Although many cases are inherited, they can also develop later in life due to eye disease, injury, aging, or certain medications. Complete color blindness is rare and should not be assumed.`, + sources: [ + "https://www.colourblindawareness.org/colour-blindness/", + "https://www.nei.nih.gov/eye-health-information/eye-conditions-and-diseases/color-blindness", + ], + imageUrl: + "https://images.unsplash.com/photo-1584036561566-baf8f5f1b144?w=400", // Placeholder + }, + dyslexia: { + title: "Dyslexia", + text: `Dyslexia is a neurological learning disability that primarily affects reading and language processing. It is not a vision problem and does not affect intelligence. People with dyslexia may have difficulty connecting written letters to spoken sounds, not how letters visually appear. + + Dyslexia cannot be cured, but it can be effectively supported through early intervention, structured reading instruction, and classroom accommodations.`, + sources: [ + "https://dyslexiaida.org/definition-of-dyslexia/", + "https://www.losdschools.org/student-services/dyslexia-handbook/definition-of-dyslexia", + ], + imageUrl: + "https://images.unsplash.com/photo-1456513080510-7bf3a84b82f8?w=400", // Placeholder + }, +}; diff --git a/client/src/constants/lab13/index.js b/client/src/constants/lab13/index.js new file mode 100644 index 000000000..334da585b --- /dev/null +++ b/client/src/constants/lab13/index.js @@ -0,0 +1,10 @@ +const LAB_ID = 13; +const EXERCISE_PATH = "/Lab12/Exercise"; + +const EXERCISE_SELECTION_DEFAULT = "default selection"; + +const EXERCISE_STATES = { + EXERCISE_SELECTION_DEFAULT, +}; + +export { LAB_ID, EXERCISE_PATH, EXERCISE_STATES }; diff --git a/client/src/constants/labs/index.js b/client/src/constants/labs/index.js deleted file mode 100644 index 8dfaca771..000000000 --- a/client/src/constants/labs/index.js +++ /dev/null @@ -1,17 +0,0 @@ -const ACCESSIBILITY = "ACCESSIBILITY"; -const ALL_LABS = "ALL_LABS"; -const AI_MACHINE_LEARNING = "AI_MACHINE_LEARNING"; -const DIFFICULTY_1 = "DIFF_1"; -const DIFFICULTY_2 = "DIFF_2"; -const DIFFICULTY_3 = "DIFF_3"; -const TUTORIALS = "TUTORIALS"; - -module.exports = { - ACCESSIBILITY, - ALL_LABS, - AI_MACHINE_LEARNING, - DIFFICULTY_1, - DIFFICULTY_2, - DIFFICULTY_3, - TUTORIALS, -}; diff --git a/client/src/helpers/Redirect.js b/client/src/helpers/Redirect.js index be81dae3d..9fb692109 100644 --- a/client/src/helpers/Redirect.js +++ b/client/src/helpers/Redirect.js @@ -95,6 +95,9 @@ export const stateChange = (actions, pathname) => { case "Lab12": actions.setLab(12); break; + case "Lab13": + actions.setLab(13); + break; case "Lab14": actions.setLab(14); break; diff --git a/client/src/pages/about-us/MemberDisplay.js b/client/src/pages/about-us/MemberDisplay.js index c03581df6..0bb1a505e 100644 --- a/client/src/pages/about-us/MemberDisplay.js +++ b/client/src/pages/about-us/MemberDisplay.js @@ -138,7 +138,7 @@ const MemberDisplay = (props) => { })}
- {currentMember.favoritelab !== null ? ( + {currentMember.favoritelab ? (

Favorite Lab diff --git a/client/src/pages/labspage/LabsPage.js b/client/src/pages/labspage/LabsPage.js index 1b4b7986b..4f9642bc5 100644 --- a/client/src/pages/labspage/LabsPage.js +++ b/client/src/pages/labspage/LabsPage.js @@ -15,16 +15,6 @@ import Student from "../../assets/images/stockImages/LookingAtComputer.png"; import Girl from "../../assets/images/stockImages/Girl1.png"; import LandingSection from "../../components/all-components/LandingSection"; import UserService from "../../services/UserService"; -import { - ACCESSIBILITY, - AI_MACHINE_LEARNING, - ALL_LABS, - QUANTUM, - DIFFICULTY_1, - DIFFICULTY_2, - DIFFICULTY_3, - TUTORIALS, -} from "../../constants/labs"; import { EXPLORE_LABS_BODY, EXPLORE_LABS_TITLE, @@ -102,10 +92,12 @@ const LabsPage = (props) => { }); }, []); - const labsByDifficulty = (labMap, difficulty) => { + const labsByDifficulty = (labMap, difficulties) => { const filteredMap = new Map(); for (const [key, value] of labMap.entries()) { - const filteredArr = value.filter((x) => x.difficulty === difficulty); + const filteredArr = value.filter((x) => + difficulties.includes(x.difficulty), + ); if (filteredArr.length > 0) { filteredMap.set(key, filteredArr); } @@ -133,9 +125,50 @@ const LabsPage = (props) => { }; const [displayedLabs, setDisplayedLabs] = useState(new Map()); - const [selectedSearch, setSelectedSearch] = useState("ALL_LABS"); const [textSearch, setTextSearch] = useState(""); + const [showFilter, setShowFilter] = useState(false); + const [selectedTopics, setSelectedTopics] = useState([]); + const [selectedDifficulties, setSelectedDifficulties] = useState([]); + + useEffect(() => { + applyFilters(selectedTopics, selectedDifficulties, textSearch); + }, [selectedTopics, selectedDifficulties, textSearch]); + + const changeTopic = (value) => { + setSelectedTopics((prev) => + prev.includes(value) + ? prev.filter((topic) => topic !== value) + : [...prev, value], + ); + }; + const changeDifficulty = (value) => { + setSelectedDifficulties((prev) => + prev.includes(value) + ? prev.filter((level) => level !== value) + : [...prev, value], + ); + }; + const applyFilters = ( + topics = selectedTopics, + difficulties = selectedDifficulties, + text = textSearch, + ) => { + let filtered = new Map(labInformation); + if (topics.length > 0) { + filtered = new Map( + Array.from(filtered.entries()).filter(([key]) => topics.includes(key)), + ); + } + if (difficulties.length > 0) { + filtered = labsByDifficulty(filtered, difficulties); + } + if (text.trim() !== "") { + filtered = labsBySearchPhrase(filtered, text); + } + setDisplayedLabs(filtered); + }; + const getMyLabs = async () => { if (loggedIn) { const allLabs = await LabService.getAllLabs(); @@ -157,66 +190,14 @@ const LabsPage = (props) => { }; useEffect(() => { - const tempMap = new Map(); - - switch (selectedSearch) { - case ALL_LABS: - setDisplayedLabs(new Map(labInformation)); - break; - case AI_MACHINE_LEARNING: - if (labInformation.has("AI")) { - tempMap.set("AI", labInformation.get("AI")); - setDisplayedLabs(tempMap); - } - break; - case ACCESSIBILITY: - if (labInformation.has("Accessibility")) { - tempMap.set("Accessibility", labInformation.get("Accessibility")); - setDisplayedLabs(tempMap); - } - break; - case QUANTUM: - if (labInformation.has("Quantum Computing")) { - tempMap.set( - "Quantum Computing", - labInformation.get("Quantum Computing"), - ); - setDisplayedLabs(tempMap); - } - break; - case DIFFICULTY_1: - setDisplayedLabs(labsByDifficulty(labInformation, 1)); - break; - case DIFFICULTY_2: - setDisplayedLabs(labsByDifficulty(labInformation, 2)); - break; - case DIFFICULTY_3: - setDisplayedLabs(labsByDifficulty(labInformation, 3)); - break; - case TUTORIALS: - if (labInformation.has("Tutorials")) { - tempMap.set("Tutorials", labInformation.get("Tutorials")); - setDisplayedLabs(tempMap); - } - break; - default: - setDisplayedLabs(labInformation); - } + setDisplayedLabs(new Map(labInformation)); getMyLabs(); - }, [labInformation, selectedSearch]); - - const handleSearchChange = (search) => { - setSelectedSearch(search); - }; + }, [labInformation]); const handleSearchTextChange = (search) => { setTextSearch(search); }; - const handleSearch = () => { - setDisplayedLabs(labsBySearchPhrase(labInformation, textSearch)); - }; - const loggedIn = state.main.user !== null && state.main.user.firstname !== null; const [signInModalOpen, setSignInModalOpen] = useState(false); @@ -288,136 +269,170 @@ const LabsPage = (props) => {

Labs

-
- { - handleSearchTextChange(e.target.value); - }} - /> +
+
+ { + handleSearchTextChange(e.target.value); + }} + /> +
-
- - - - - - - - -
- -
- {Array.from(displayedLabs.entries()) - .sort(([a], [b]) => a.localeCompare(b)) - .map(([category, labArray]) => ( -
+
+
diff --git a/client/src/pages/landingpage/error.js b/client/src/pages/landingpage/error.js index 2d3a7f597..666ae690a 100644 --- a/client/src/pages/landingpage/error.js +++ b/client/src/pages/landingpage/error.js @@ -1,24 +1,74 @@ -/* eslint-disable react/prop-types */ import React from "react"; -import Redirect from "../../helpers/Redirect"; +import useScroll from "src/use-hooks/useScroll"; +import { navigate } from "@reach/router"; +import ALLButton from "src/components/all-components/ALLButton"; + +const Error = () => { + useScroll(); -const Error = (props) => { - const { actions } = props; return ( -
-
-

Invalid Page

-
-

Please click the button to navigate home

-
- +
+
+ {/* Left Side */} +
+
+
+

404

+
+
+
+
+ + {/* Right Side */} +
+
+
+

+ Invalid Page +

+

+ The URL you entered either does not exist or is not accessible! + Please click the button to navigate home. +

+
+ navigate("/")} /> +
+
+
+
+
-
+
); }; diff --git a/client/src/pages/landingpage/index.js b/client/src/pages/landingpage/index.js index 19fcfb534..0ccf07104 100644 --- a/client/src/pages/landingpage/index.js +++ b/client/src/pages/landingpage/index.js @@ -36,18 +36,18 @@ const Home = () => { const getFeaturedLabs = async () => { const allLabs = await labService.getAllLabs(); - let lab12; + let lab13; let lab14; allLabs.map((lab) => { - if (lab.labShortName == "Identity") { - lab12 = lab; + if (lab.labShortName == "Cognitive Bias") { + lab13 = lab; } else if (lab.labShortName == "Quantum") { lab14 = lab; } }); - setFeaturedLabs([lab14, lab12]); + setFeaturedLabs([lab14, lab13]); }; const endImagine = () => actions.setIsImagine(false); diff --git a/client/src/reducers/MainContext.js b/client/src/reducers/MainContext.js index 5f6d7e4dd..f33a296e9 100644 --- a/client/src/reducers/MainContext.js +++ b/client/src/reducers/MainContext.js @@ -6,6 +6,7 @@ import { } from "./MainReducerForContext"; import { PropTypes } from "prop-types"; import AuthService from "src/services/AuthService"; +import UserService from "../services/UserService"; /** * MainStateContext is a context object created using createContext() function. @@ -78,6 +79,16 @@ export const MainContextProvider = ({ children }) => { console.error(error); } }, + developmentLogin: async (userId) => { + try { + const user = await UserService.developmentLogin(userId); + if (user) { + dispatch({ type: types.UPDATE_USER, payload: { user: user } }); + } + } catch (error) { + console.error(error); + } + }, setLab: (newLab) => dispatch({ type: types.SET_LAB, payload: { lab: newLab } }), updateUser: (newUser) => diff --git a/client/src/reducers/MainReducer.js b/client/src/reducers/MainReducer.js index 20ac72607..e5d01ceab 100644 --- a/client/src/reducers/MainReducer.js +++ b/client/src/reducers/MainReducer.js @@ -6,6 +6,7 @@ export const types = { SET_IS_IMAGINE: "@accessibility-lab/isImagine", SHOW_SNACKBAR: "@accessibility-lab/showSnackbar", HIDE_SNACKBAR: "@accessibility-lab/hideSnackbar", + DEV_LOGIN: "@accessibility-lab/developmentLogin", }; export const initialState = { @@ -67,6 +68,7 @@ export const MainReducer = (state = initialState, action) => { export const actions = { setBody: (body) => ({ type: types.SET_BODY, body }), login: () => ({ type: types.LOGIN }), + developmentLogin: (user) => ({ type: types.LOGIN, user }), setLab: (lab) => ({ type: types.SET_LAB, lab }), updateUser: (user) => ({ type: types.UPDATE_USER, user }), setIsImagine: (isImagine) => ({ type: types.SET_IS_IMAGINE, isImagine }), diff --git a/client/src/reducers/MainReducerForContext.js b/client/src/reducers/MainReducerForContext.js index fa564a6dc..ce4b718ae 100644 --- a/client/src/reducers/MainReducerForContext.js +++ b/client/src/reducers/MainReducerForContext.js @@ -26,6 +26,7 @@ export const types = { SET_IS_IMAGINE: "@accessibility-lab/context/set_is_imagine", SHOW_SNACKBAR: "@accessibility-lab/context/show_snackbar", HIDE_SNACKBAR: "@accessibility-lab/context/hide_snackbar", + DEV_LOGIN: "@accessibility-lab/context/dev_login", }; /** @@ -46,7 +47,7 @@ export const types = { export const initialState = { userState: EXERCISE_IDLE, main: { - user: null, + user: JSON.parse(localStorage.getItem("user")) || null, lab: 99, body: 0, isImagine: false, diff --git a/client/src/services/UserService.js b/client/src/services/UserService.js index 7ead5ef35..3ce8ea6ce 100644 --- a/client/src/services/UserService.js +++ b/client/src/services/UserService.js @@ -26,6 +26,15 @@ const userService = { process.env.REACT_APP_SERVER_URL + `/user/${userID}/assigned`, ).then((response) => response.json()); }, + developmentLogin: (userID) => { + return API.get( + process.env.REACT_APP_SERVER_URL + `/user/${userID}/development`, + ) + .then((response) => response.json()) + .then((data) => { + return data; + }); + }, }; export default userService; diff --git a/client/src/services/lab13/ExerciseService.js b/client/src/services/lab13/ExerciseService.js new file mode 100644 index 000000000..e06712cbb --- /dev/null +++ b/client/src/services/lab13/ExerciseService.js @@ -0,0 +1,43 @@ +import API from "../API"; + +const prefix = { + POST_SUFFIX: "submit", + LAB_PREFIX: `${process.env.REACT_APP_SERVER_URL}/lab13`, +}; + +const resource = { + EXERCISE: `${prefix.LAB_PREFIX}/exercise`, +}; + +const endpoints = { + GET_EXERCISE: resource.EXERCISE, + POST_EXERCISE: `${resource.EXERCISE}/${prefix.POST_SUFFIX}`, +}; + +const ExerciseService = { + fetchExercise: async (data = {}) => { + try { + const getRoute = `${endpoints.GET_EXERCISE}/${data.userid}`; + return API.get(getRoute).then((response) => { + return response.json(); + }); + } catch (error) { + console.error(error); + } + }, + submitExercise: async (data) => { + try { + const body = { + userID: data.userid, + isExerciseComplete: data.isExerciseComplete, + hasViewed: data.hasViewed, + }; + const response = await API.postWithBody(endpoints.POST_EXERCISE, body); + return response.status; + } catch (error) { + console.error(error); + } + }, +}; + +export { ExerciseService }; diff --git a/client/tailwind.config.js b/client/tailwind.config.js index a3886aacc..3bd9653b8 100644 --- a/client/tailwind.config.js +++ b/client/tailwind.config.js @@ -49,6 +49,8 @@ module.exports = { labYellow: "#ffc334", labGreen: "#7B7B7B", labBlue: "#0d28bc", + lightBlue: "#5D90FE", + mediumBlue: "#0143D0", darkGreen: "#0c3515", lightGreen: "#47ff72", brightRed: "#dc2626", @@ -141,6 +143,8 @@ module.exports = { extend: { animation: ["group", "responsive", "hover", "focus", "reduced-bounce"], bg: ["group", "responsive", "hover", "focus"], + translate: ["group-hover"], + transform: ["group-hover"], }, }, plugins: [ diff --git a/configPath.js b/configPath.js index 240aa9d5f..7afd6373d 100644 --- a/configPath.js +++ b/configPath.js @@ -8,7 +8,7 @@ const isWindows = platform === 'win32'; // 'win32' is returned for all windows v There is an open (Windows only) bug in PM2 that makes it find the wrong npm path when running. This fix finds a dev's OS, and get's their path to the npm executable if necessary. */ -const NODE_PATH = isWindows && process.env.Path.split(';').filter( f => f.includes('node') )[0]; +const NODE_PATH = isWindows && process.env.Path.split(';').filter(f => f.includes('node'))[0]; const WINDOWS_PATH = isWindows && path.join(NODE_PATH, 'node_modules', 'npm', 'bin', 'npm-cli.js'); const UNIX_PATH = 'npm'; diff --git a/server/controllers/UserController.js b/server/controllers/UserController.js index 582b21359..62d7fa61c 100644 --- a/server/controllers/UserController.js +++ b/server/controllers/UserController.js @@ -68,6 +68,19 @@ const authenticateCallback = async (req, res) => { } }; +const developmentLogin = async (req, res) => { + try { + await UserService.updateGuestUserId(req.params.userID, 1); + const user = await UserService.getUser(req.params.userID); + req.session.token = 1; + req.session.userID = user.userid; + req.session.save(); + res.json(user); + } catch (e) { + console.error('Development Login failed! ', e); + } +}; + const storeURL = (req, res) => { req.session.url = req.body.url.href; res.sendStatus(200); @@ -75,10 +88,13 @@ const storeURL = (req, res) => { // Logging out will clear sessions const logout = (req, res, next) => { + + const redirect = process.env.ENVIRONMENT === 'dev' ? process.env.CLIENT_URL + '/' : req.session.url; req.logout({keepSessionInfo: true}, (error) => { if (error) next(error); req.session.token = null; - res.redirect(req.session.url); + req.session.userID = null; + res.redirect(redirect); }); }; @@ -94,4 +110,5 @@ module.exports = { getUserInstructingGroups, getUserAssignedLabs, getUserToDoLabs, + developmentLogin, }; diff --git a/server/controllers/lab13/ExerciseController.js b/server/controllers/lab13/ExerciseController.js new file mode 100644 index 000000000..17916bc4e --- /dev/null +++ b/server/controllers/lab13/ExerciseController.js @@ -0,0 +1,43 @@ +const ExerciseService = require('../../services/lab13/ExerciseService'); +/** + * getExercise(): is a function responsible for retrieving the + * user id from the query params from the route to the endpoint. + * this allows for the ability to retrieve the last game state. + * @param {Object} req request object containing userId; + * @param {Object} res response object containing the response; + */ +async function getExercise(req) { + try { + const { userID } = req.params; + return await ExerciseService.getExercise(userID); + } catch (error) { + console.error('Error: Could Not Find Exercise', error); + } +} + +/** + * postExercise(): is a function that is responsible for sending the + * user's state to the database for when they are completed with a section + * of the lab's repair. This function is responsible for getting the contents + * of the body of the request and pass it off to the Exercise service. + * @param {Object} req request object containing user information and payload. + * @param {Object} res response object containing the response; + */ +async function postExercise(req) { + try { + const { userID, hasViewed, isExerciseComplete } = req.body; + const response = await ExerciseService.postExercise({ + userId: userID, + hasViewed: hasViewed, + isExerciseComplete: isExerciseComplete, + }); + return response; + } catch (error) { + console.error(error); + } +} + +module.exports = { + getExercise, + postExercise, +}; diff --git a/server/database/models/lab13/Exercise.js b/server/database/models/lab13/Exercise.js new file mode 100644 index 000000000..e4f471ad6 --- /dev/null +++ b/server/database/models/lab13/Exercise.js @@ -0,0 +1,33 @@ +module.exports = (sequelize, DataTypes) => { + const Exercise = sequelize.define('ExerciseLab13', + { + repairId: { + type: DataTypes.INTEGER, + unique: true, + primaryKey: true, + autoIncrement: true, + }, + userid: { + type: DataTypes.BIGINT, + }, + attemptTime: { + type: DataTypes.DATE, + }, + isExerciseComplete: { + type: DataTypes.BOOLEAN, + }, + hasViewed: { + type: DataTypes.BOOLEAN, + }, + attemptCount: { + type: DataTypes.INTEGER, + }, + }, + { + tableName: 'lab13_exercise', + }, + ); + + Exercise.sync(); + return Exercise; +}; diff --git a/server/database/schema.sql b/server/database/schema.sql index c17e2454a..6d4068590 100644 --- a/server/database/schema.sql +++ b/server/database/schema.sql @@ -388,6 +388,8 @@ create table users UNIQUE (email1), UNIQUE (email2) ); +insert into users (userid, firstname, lastinitial, email1, email2, userpfp) +VALUES (98, 'Ally', 'A', 'allyaccessibility@all.edu', null, null), (99, 'Lily', 'L', 'lilylabs@all.edu', null, null), (100, 'Edna', 'E', 'ednaeducation@all.edu', null, null); create table lab8_exercise ( @@ -700,10 +702,10 @@ VALUES (0, 'How to Build an Accessible Learning Lab', 'How to Build an Accessibl ] ', 3, 'coming soon', 'coming soon', true), (1, 'Accessibility to Sound and Speech', 'Sound & Speech', 'Accessibility', '/ear.jpg', 'Learn about designing the web for the Deaf and Hard-of-Hearing community.', 'This lab explores the Perceivable accessibility principle in regards to sound and speech. This principle states that information and elements of the interface must be presented to users in ways they can perceive without loss of information. The lab demonstrates how having only audio cues for a certain objective makes the software inaccessible for users who are deaf or hard of hearing.', '["LO1: Knowledge of user significance, characteristics, and needs: Recognize the significance of the population that is deaf and hard of hearing and their needs for accessible software (Knowledge)","LO2: Exposure to and analysis of poorly accessible design: Examine a software application that doesn’t properly accommodate accessibility for people with difficulties with sound and speech (Analysis)","LO3: Apply solutions to solve access problems: Use knowledge of accessibility design solutions to construct corrective measures to allow previously inaccessible software to become accessible to appropriate parties (Application)","LO4: Develop further empathy: Relate to individuals who experience difficulties with accessibility to sound and speech (Comprehension)"]', 'Jan Guillermo, Saad Khan, Heather Moses, Manali Chakraborty, Komal Sorte, Sakshi Karnawat', 'https://all.rit.edu/Lab1/', null, e'In this lab, you will learn why it is important to create software - that is accessible to users with hearing impairments. + that is accessible to users with hearing loss. You will learn how organizations like the National Association of the Deaf (NAD) - fought for easier access for hearing impaired individuals, - increase your understanding through an interactive module about hearing impairments, + fought for easier access for deaf/hard of hearing individuals, + increase your understanding through an interactive module about hearing loss, view related media to reinforce the topic, and take a quiz to test your knowledge. Click "Next" to start!', e'{ "piechart": {"header":"Approximate Deaf and Hard of Hearing Population in the United States", @@ -2994,7 +2996,7 @@ to test your knowledge. Click "Next" to start!', e'{ "LO1: Construct a basic neural network using provided components (Synthesis)", "LO2: Simulate neural network training (Comprehension).", "LO3: Demonstrate bias present in a neural network (Application)." -]', 'Jonathan Cruz, Domenic Mangano, Emily Crilley', 'https://ball.rit.edu/Lab10/', null, 'In this lab, you will learn about where bias is present within neural networks and ways to help reduce the biases developed in the algorithms. You will understand how neural networks work and how to build appropriate training data sets to combat development bias, view related media to reinforce the topic, and take a quiz to test your knowledge. Click "Next" to start!', e'{ +]', 'Jonathan Cruz, Domenic Mangano, Emily Crilley', 'https://all.rit.edu/Lab10/', null, 'In this lab, you will learn about where bias is present within neural networks and ways to help reduce the biases developed in the algorithms. You will understand how neural networks work and how to build appropriate training data sets to combat development bias, view related media to reinforce the topic, and take a quiz to test your knowledge. Click "Next" to start!', e'{ "piechart":{ "header":"Company investments towards AI", "caption":["Billions of dollars invested towards AI by major tech companies"], @@ -3460,13 +3462,15 @@ to test your knowledge. Click "Next" to start!', e'{ "multiChoice": true } ] -', 2, 'coming soon', 'coming soon', true), (12, 'Accessibility to Identity', 'Identity', 'Accessibility', '/identity.jpg', 'Learn about developing identity sensitive software.', 'This lab will introduce the idea of gender and identity and the importance of creating software that is accessible to those who conform outside the social/gender norm. Participants will learn how to design and implement gender sensitive terminology in their software. In the exercise portion of the lab they will encounter an interface that is not accessible, and learn how and why to implement an interface that is accessible to gender and identity.', +', 2, 'coming soon', 'coming soon', true), + +(12, 'Accessibility to Identity', 'Identity', 'Accessibility', '/identity.jpg', 'Learn about developing identity sensitive software.', 'This lab will introduce the idea of gender and identity and the importance of creating software that is accessible to those who conform outside the social/gender norm. Participants will learn how to design and implement gender sensitive terminology in their software. In the exercise portion of the lab they will encounter an interface that is not accessible, and learn how and why to implement an interface that is accessible to gender and identity.', e'[ "LO1: Knowledge of user significance, characteristics, and needs: Recognize the significance of the population that identifies outside the gender norm, and their needs for accessible use of software (Knowledge)", "LO2: Exposure to and analysis of poorly accessible design: Examine a software application that doesn\’t properly accommodate accessibility in regards to identity (Analysis)", "LO3: Apply solutions to solve access problems: Use knowledge of accessibility design solutions to construct corrective measures to allow previously inaccessible software to become accessible to appropriate parties (Application)", "LO4: Develop further empathy: Relate to individuals who experience difficulties with their gender (Comprehension)" -]', 'Domenic Mangano, Heather Moses, Owen Luts', 'https://ball.rit.edu/Lab12/', null, 'In this lab, you will learn about the importance of accessibility for users that identify as genders other than male or female. You will learn about the issues related to lack of accessible software for this demographic, increase your understanding through an interactive module on identity accessibility, view related media to reinforce the topic, and take a quiz to test your knowledge! Click +]', 'Domenic Mangano, Heather Moses, Owen Luts', 'https://all.rit.edu/Lab12/', null, 'In this lab, you will learn about the importance of accessibility for users that identify as genders other than male or female. You will learn about the issues related to lack of accessible software for this demographic, increase your understanding through an interactive module on identity accessibility, view related media to reinforce the topic, and take a quiz to test your knowledge! Click “Next” to start!', e'{ "piechart": { "header":"Unbiased Forms", @@ -3690,13 +3694,282 @@ e'[ ], "multiChoice": false } -]', 2, 'coming soon', 'coming soon', true), (14, 'Quantum Cryptography', 'Quantum', 'Quantum Computing', '/quantumcryptography.jpg', 'Learn about quantum computing through the lens of cryptography.', 'This lab introduces the concepts of Quantum Computing through the lens of cryptography. Participants will learn about different ways to encrypt messages, and the algorithms used to decrypt them. Then, the lab explores the difference in time and efficiency between breaking these ciphers with both Classical and Quantum computers.', +]', 2, 'coming soon', 'coming soon', true), + +(13, 'Human Cognitive Bias and Generative AI', 'Cognitive Bias', 'AI', '/cognitivebiasai.jpg', 'Learn about Human Cognitive Bias and how it impacts day to day interactions with Generative Artifical Intelligence (AI).', 'This lab will introduce the idea of Human Cognitive Bias and how it impacts day to day interactions with Generative AI. Human Bias plays a large part into why do individuals trust AI generated responses without questions if the responses they are recieving are accurate. Participants will learn how to their own unconsicous bias play into typical interactions with AI, impacting a users trust in AI generated responses. In the exercise portion of the lab they will encounter an interface.', +e'[ + "LO1: Recognize how cognitive biases such as the Halo Effect, Authority Bias, and Truth Bias influence trust in AI-generated content (Knowledge)", + "LO2: Create design strategies that promote critical thinking, such as certainty indicators and disclaimers about AI limitations (Synthesis)", + "LO3: Identify common hallucination patterns in generative AI, including false citations and misleading self-references (Application)", + "LO4: Experience how confident language, polished grammar, and proximity to credible sources can lead to overestimation of AI accuracy (Comprehension)" +]', 'Emma Schmitt, Jack DeFeo, Kristen Fang, Darlyn Gomez, Christine Espeleta', 'https://all.rit.edu/Lab13/', null, +'In this lab, you will explore what cognitive bias is and +how it influences trust in AI systems. You will evaluate AI +responses, compare them with search results, and reflect on +your own decision-making process. Afterwards, you will spot +bias mitigation features in real-world AI platforms and take +a quiz to test your knowledge. Click "Next" to start!', e'{ + "description": { + "header":"", + "content":"" + }, + "body":[ + { + "header":"Introduction", + "type":"", + "content":["AI systems are built on logic, data, and probability, yet the humans using them are not always logical. In psychology, this gap between logic and decision making is explained by cognitive bias."] + }, + { + "header":"What is Cognitive Bias?", + "type":"", + "content":["Cognitive bias is a systematic pattern that causes people to deviate from fully rational thought, affecting how we process information, perceive others, and make decisions. Instead of carefully analyzing every fact, we rely on what feels familiar or intuitive to save time and effort, but this can lead to errors in reasoning."] + }, + { + "header": "", + "type": "image", + "content" : { + "image":"/what-is-cognitive-bias-image.png", + "alt":"Split brain diagram showing two pathways of thinking: Rational Analysis (left, dark blue) with gears and magnifying glasses representing slow analytical thought, and Mental Shortcuts (right, orange) with lightning bolts and arrows representing fast intuitive thought", + "sub_caption":"", + "caption":"The brain uses two pathways: slow, analytical thinking (left) and fast, intuitive shortcuts (right). Cognitive biases arise when shortcuts lead to errors." + } + }, + { + "header":"When Cognitive Bias Meets Technology", + "type":"", + "content":["As humans interact more with artificial intelligence (AI), cognitive biases are appearing in new forms. Generative AIs like ChatGPT, Gemini, or Claude are built to sound confident, fluent, and human-like, creating an impression of expertise. The polished tone can reduce usual skepticism, making users more likely to accept information without question, even when it may be inaccurate."] + }, + { + "header": "How Often People Trust or Check AI Answers", + "type": "piechart", + "caption": [""], + "content": { + "data": { + "labels": ["Skeptical of AI Outputs", "Always verify AI answers", "Other"], + "datasets": [{ + "label": "AI Trust Behavior", + "borderColor": "black", + "backgroundColor": ["#9587df", "#ffccab", "#b6e8ce"], + "data": [82, 8, 10], + "borderWidth": "1" + }] + } + } + }, + { + "header": "", + "type": "", + "content": ["Most people claim to be skeptical of AI, but only a small number actually fact check what it says. "] + }, + { + "header":"", + "type":"links", + "content":[ + { + "name":"The AI Trust Gap Research Study", + "link":"https://explodingtopics.com/blog/ai-trust-gap-research" + } + ] + }, + { + "header":"Mata V. Avianca", + "type":"", + "content": ["In the case of Mata V. Avianca (2023), experienced lawyers fell victim to cognitive bias when relying on generative AI output. Attorneys Steven A. Schwartz and Peter LoDuca used ChatGPT to help prepare a legal filing for a passenger, Roberto Mata, who claimed injury on an Avianca flight. The AI confidently produced realistic legal citations and reasoning, but every case it cited wasn\'t real. When questioned by Avianca\'s lawyers and the court on the location of the cited legal cases, Mata\'s lawyers continued to defend the fake citations because ChatGPT assured them the cases \'indeed exist\' and \'can be found in reputable legal databases such as LexisNexis and Westlaw.\' The court later sanctioned them making this one of the first high profile examples of cognitive bias in human-AI interactions! This case shows how polished tone, confident phrasing, and the perceived expertise of the AI can create a halo of trust, even making skilled professionals overrely on it and overlook errors."] + }, + { + "header": "What Can We Do About It: AI Literacy", + "type": "", + "content": ["So why does AI seem so convincing, even when it\'s wrong? The reason lies in how AI systems are built: they’re designed to sound smart. Developers craft their tone, structure, and human-like phrasing to make interactions natural and engaging, which can unconsciously influence our perception."] + }, + { + "header": "", + "type": "", + "content": ["This is why AI literacy matters. AI literacy is the skill of using AI thoughtfully by understanding how it works, recognizing its limits, and knowing when you should question its responses instead of accepting output at face value. Here are a few small powerful habits:"] + }, + { + "header": "", + "type": "study__list", + "content": ["Ask for citations", + "Cross-check the answer against trusted sources", + "Remember that AI’s format doesn’t guarantee accuracy"] + }, + { + "header": "", + "type": "", + "content": ["As AI becomes heavily integrated in our daily lives, practicing these habits will help you protect your own reasoning."] + }, + { + "header": "", + "type": "", + "content": [""] + } + ], + "footer":{ + "links":[ + { + "name": "Mata v. Avianca Case", + "link": "https://law.justia.com/cases/federal/district-courts/new-york/nysdce/1:2022cv01461/575368/54/" + }, + { + "name": "Understanding Cognitive Bias", + "link": "https://www.simplypsychology.org/cognitive-bias.html" + }, + { + "name": "Global Views on AI", + "link": "https://www.pewresearch.org/global/2025/10/15/how-people-around-the-world-view-ai/" + }, + { + "name": "Exploding Topics: AI Trust Gap Report", + "link": "https://explodingtopics.com/blog/ai-trust-gap-research" + }, + { + "name": "AI Makes You Smarter But None the Wiser", + "link": "https://www.sciencedirect.com/science/article/abs/pii/S0747563225002262?via%3Dihub" + }, + { + "name": "AI Dependence and Literacy", + "link": "https://www.tandfonline.com/doi/full/10.1080/10447318.2025.2544006" + } + ] + } +}', +'[{"title":"Artifical Intelligence and Dunning Kruger Effect","link": "https://www.youtube.com/embed/dPbGoeW3uVw?si=29cnMsdK_okYF9Ge"},{"title":"Introduction to Halo Effect","link": "https://www.youtube.com/embed/kpjeMaOirvg?si=j383aHRvYakZyNi1"},{"title":"Truth is an Illusion (Truth Bias)","link":"https://www.youtube.com/embed/cebFWOlx848?si=rHm0WHB4a-BMsQtF"}]', '[ + { + "question": "Which cognitive bias best describes this scenario? Bob is a student who used ChatGPT to do his math homework. He argues with his teacher about his homework that he got a 0 on. He believes that his math skills are strong enough to not check the answers of ChatGPT because they \"look right\". His teacher continued to give him a 0.", + "answers": [ + { + "val": 0, + "type": "0", + "content": "Truth Bias" + }, + { + "val": 0, + "type": "1", + "content": "Halo Effect" + }, + { + "val": 0, + "type": "2", + "content": "Authority Bias" + }, + { + "val": 1, + "type": "3", + "content": "Dunning-Kruger Effect", + "explanation": "The Dunning-Kruger Effect is a cognitive bias in which people overestimate their ability in a task. In this case, Bob overestimates his math skills and trusts the AI without verification." + } + ], + "multiChoice": false + }, + { + "question": "Which one describes the Halo Effect?", + "answers": [ + { + "val": 0, + "type": "0", + "content": "Generative AI giving a wrong answer" + }, + { + "val": 1, + "type": "1", + "content": "Generative AI using \"✅\" in its responses", + "explanation": "The Halo Effect is a cognitive bias in which our overall impression of a person, company, brand, or product is influenced by how we feel and think about their character or properties. In this case, the use of \"✅\" gives the impression that the answer is correct, even if it is not." + }, + { + "val": 0, + "type": "2", + "content": "Believing that Generative AI is correct because it is an online resource" + }, + { + "val": 0, + "type": "3", + "content": "A lack of knowledge in the topic leading to believing that Generative AI is correct" + } + ], + "multiChoice": false + }, + { + "question": "True or False: Since Generative AI has access to lots of information and is trained on a vast dataset, it can always be trusted.", + "answers": [ + { + "val": 0, + "type": "0", + "content": "True" + }, + { + "val": 1, + "type": "1", + "content": "False", + "explanation": "Generative AI can produce incorrect or misleading information, so it should not always be trusted without verification." + } + ], + "multiChoice": false + }, + { + "question": "Which of the following can users use to check the validity of Generative AI responses?", + "answers": [ + { + "val": 1, + "type": "0", + "content": "Disclaimers", + "explanation": "Many Generative AI tools include disclaimers that the information provided may not be accurate or up-to-date." + }, + { + "val": 0, + "type": "1", + "content": "The tone of the response" + }, + { + "val": 1, + "type": "2", + "content": "Confidence Scores", + "explanation": "Generative AI tools can provide confidence scores indicating how certain the model is about its response." + }, + { + "val": 1, + "type": "3", + "content": "A separate search of the question", + "explanation": "Users should verify the information provided by Generative AI through independent research or trusted sources." + } + ], + "multiChoice": true + }, + { + "question": "Which of these is cognitive bias NOT based on?", + "answers": [ + { + "val": 0, + "type": "0", + "content": "Intuition" + }, + { + "val": 1, + "type": "1", + "content": "Facts", + "explanation": "Cognitive biases are systematic patterns of deviation from norm or rationality in judgment, often based on intuition rather than objective facts." + }, + { + "val": 0, + "type": "2", + "content": "Familiarity" + }, + { + "val": 0, + "type": "3", + "content": "Quick Conclusions" + } + ], + "multiChoice": false + } +]', 1, 'coming soon', 'coming soon', true), + +(14, 'Quantum Cryptography', 'Quantum', 'Quantum Computing', '/quantumcryptography.jpg', 'Learn about quantum computing through the lens of cryptography.', 'This lab introduces the concepts of Quantum Computing through the lens of cryptography. Participants will learn about different ways to encrypt messages, and the algorithms used to decrypt them. Then, the lab explores the difference in time and efficiency between breaking these ciphers with both Classical and Quantum computers.', e'[ "LO1: Understand role of qubits giving quantum computers greater computational power (Comprehension)", "LO2: Interact with a simple quantum simulation to see how adding qubits improves factoring (Application)", "LO3: Use the simulation to factor a small number and decrypt an encrypted message (Application)", "LO4: Explain why increasing qubit count helps quantum algorithms break encryption faster (Analysis)" - ]', 'Owen Luts, Vivian Hernandez, William Herrick', 'https://ball.rit.edu/Lab14/', null, + ]', 'Owen Luts, Vivian Hernandez, William Herrick', 'https://all.rit.edu/Lab14/', null, -- About Section 'In this lab, you will learn about the fundamentals of quantum computing and how it differs from classical computing. You will practice applying these concepts through interactive exercises, including encrypting and decrypting messages with the Caesar cipher to compare classical and quantum approaches. Click “Next” to start!', e'{ "description":"", diff --git a/server/package-lock.json b/server/package-lock.json index 02f7b4dcf..f055af876 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -3127,9 +3127,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -5343,9 +5343,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -5896,9 +5896,9 @@ } }, "node_modules/sequelize": { - "version": "6.37.7", - "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.7.tgz", - "integrity": "sha512-mCnh83zuz7kQxxJirtFD7q6Huy6liPanI67BSlbzSYgVNl5eXVdE2CN1FuAeZwG1SNpGsNRCV+bJAVVnykZAFA==", + "version": "6.37.8", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.8.tgz", + "integrity": "sha512-HJ0IQFqcTsTiqbEgiuioYFMSD00TP6Cz7zoTti+zVVBwVe9fEhev9cH6WnM3XU31+ABS356durAb99ZuOthnKw==", "funding": [ { "type": "opencollective", @@ -6484,9 +6484,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { diff --git a/server/routes/index.js b/server/routes/index.js index c7411eedb..12a5fbf79 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -46,6 +46,7 @@ const RepairControllerLab9 = require('../controllers/lab9/RepairController'); // LAB 11 Controller const ExerciseControllerLab11 = require('../controllers/lab11/ExerciseController'); const RepairControllerLab11 = require('../controllers/lab11/RepairController'); + // LAB 10 Controller const ExerciseControllerLab10 = require('../controllers/lab10/ExerciseController'); @@ -53,6 +54,8 @@ const ExerciseControllerLab10 = require('../controllers/lab10/ExerciseController const ExerciseControllerLab12 = require('../controllers/lab12/ExerciseController'); const RepairControllerLab12 = require('../controllers/lab12/RepairController'); +// LAB 13 Controller +const ExerciseControllerLab13 = require('../controllers/lab13/ExerciseController'); // LAB 14 Controller const ExerciseControllerLab14 = require('../controllers/lab14/ExerciseController'); @@ -72,6 +75,7 @@ router.get('/auth/google/callback', UserController.authenticateRedirect, UserCon router.get('/logout', UserController.logout); router.get('/user', UserController.main); router.get('/user/:userID', UserController.getUser); +router.get('/user/:userID/development', UserController.developmentLogin); router.get('/user/:userID/enrolled', UserController.getUserEnrolledGroups); router.get('/user/:userID/groups', UserController.getUserInstructingGroups); router.get('/user/:userID/assigned', UserController.getUserAssignedLabs); @@ -196,6 +200,14 @@ router.post('/lab12/repair/submit', async function (req, res) { res.send(id); }); +{/* Lab 13 Exercise and Repair Controller Calls */ } +router.get('/lab13/exercise/:userID', async function (req, res) { + res.json(await ExerciseControllerLab13.getExercise(req)); +}); +router.post('/lab13/exercise/submit', async function (req, res) { + const id = await ExerciseControllerLab13.postExercise(req); + res.send(id); +}); {/* Lab 14 Exercise and Repair Controller Calls */ } router.get('/lab14/exercise/:userID', async function (req, res) { res.json(await ExerciseControllerLab14.getExercise(req)); diff --git a/server/services/UserService.js b/server/services/UserService.js index 45977d771..064352efd 100644 --- a/server/services/UserService.js +++ b/server/services/UserService.js @@ -106,6 +106,7 @@ const getSession = async (token) => { throw error; } }; + const getUserEnrolledGroups = (userid) => { return db.sequelize.query( `SELECT * FROM "enrollment" @@ -170,12 +171,12 @@ const getUser = (userid) => { userid: userid, }, }) - .then((user) => { - return user; - }) - .catch((err) => { - console.log(err); - }); + .then((user) => { + return user; + }) + .catch((err) => { + console.log(err); + }); }; module.exports = { diff --git a/server/services/lab13/ExerciseService.js b/server/services/lab13/ExerciseService.js new file mode 100644 index 000000000..5d5c1151c --- /dev/null +++ b/server/services/lab13/ExerciseService.js @@ -0,0 +1,77 @@ +const db = require('../../database'); +/** + * getExercise(): is a function that is responsible for retrieving + * the last played exercise by a particular user. this function is + * responsible for querying the database and retrieving the correct + * information from the database. + * @param {Object} data Contains information about the user to search + * the database. + */ +async function getExercise(data) { + try { + const exerciseResponse = await db.ExerciseLab13.findOne( + { + order: [['attemptTime', 'DESC']], + where: { + userid: data, + }, + raw: true, + }, + ); + + return exerciseResponse; + } catch (error) { + console.error(error); + } +} + +/** + * postExercise(): is a function that is responsible for storing + * changes to the database. This allows the user's information to + * be properly stored and allow for the ability to be retrieved later + * when needed. + * @param {Object} data Contains information that is intended to be stored + * in the database, + */ +async function postExercise(data) { + try { + const { + userId, + isExerciseComplete, + hasViewed, + } = data; + const getExerciseResponse = await getExercise(userId); + const currentTime = new Date().toISOString(); + const newExercise = { + userid: userId, + isExerciseComplete: false, + attemptTime: currentTime, + attemptCount: 1, + hasViewed: false, + }; + if (!getExerciseResponse) { + // adds in new entry + return await db.ExerciseLab13.create(newExercise).id; + } else { + const convert = parseInt(getExerciseResponse.attemptCount); + const newVal = convert + 1; + const updatedExercise = { + userid: userId, + isExerciseComplete: isExerciseComplete, + attemptTime: currentTime, + attemptCount: newVal, + hasViewed: hasViewed, + }; + await db.ExerciseLab13.create(updatedExercise).id; + return updatedExercise; + } + } catch (error) { + console.error(error); + } +} + + +module.exports = { + getExercise, + postExercise, +};