diff --git a/backend/src/router.ts b/backend/src/router.ts index dd214134a..ec059ef46 100644 --- a/backend/src/router.ts +++ b/backend/src/router.ts @@ -249,14 +249,6 @@ router.post( chatMessageType: CHAT_MESSAGE_TYPE.BOT_BLOCKED, infoMessage: chatResponse.defenceInfo.blockedReason, }); - - // enable next level when user wins current level - if (chatResponse.wonLevel) { - console.debug("Win conditon met for level: ", currentLevel); - numLevelsCompleted = Math.max(numLevelsCompleted, currentLevel + 1); - req.session.numLevelsCompleted = numLevelsCompleted; - chatResponse.numLevelsCompleted = numLevelsCompleted; - } } } else { handleChatError(res, chatResponse, true, "Missing message"); @@ -266,6 +258,15 @@ router.post( handleChatError(res, chatResponse, false, "Failed to get chatGPT reply"); return; } + + // enable next level when user wins current level + if (chatResponse.wonLevel) { + console.debug("Win conditon met for level: ", currentLevel); + numLevelsCompleted = Math.max(numLevelsCompleted, currentLevel + 1); + req.session.numLevelsCompleted = numLevelsCompleted; + chatResponse.numLevelsCompleted = numLevelsCompleted; + } + // log and send the reply with defence info console.log(chatResponse); res.send(chatResponse); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3eaa2d981..4c1501a75 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -42,7 +42,7 @@ function App({ isNewUser }: { isNewUser: boolean }) { function loadCurrentLevel() { // get current level from local storage const currentLevelStr = localStorage.getItem("currentLevel"); - if (currentLevelStr) { + if (currentLevelStr && !isNewUser) { // start the user from where they last left off return parseInt(currentLevelStr); } else { @@ -80,6 +80,10 @@ function App({ isNewUser }: { isNewUser: boolean }) { } } + function openWelcomeOverlay() { + setOverlayType(OVERLAY_TYPE.WELCOME); + } + function openHandbook() { setOverlayType(OVERLAY_TYPE.HANDBOOK); } @@ -116,6 +120,13 @@ function App({ isNewUser }: { isNewUser: boolean }) { setDefencesToShow(defences); } + // set the start level for a user who clicks beginner/expert + async function setStartLevel(startLevel: LEVEL_NAMES) { + console.log(`setting start level to ${startLevel}`); + await setNewLevel(startLevel); + closeOverlay(); + } + // for going switching level without clearing progress async function setNewLevel(newLevel: LEVEL_NAMES) { console.log(`changing level from ${currentLevel} to ${newLevel}`); @@ -228,6 +239,7 @@ function App({ isNewUser }: { isNewUser: boolean }) { void setStartLevel(level)} closeOverlay={closeOverlay} /> )} @@ -257,6 +269,7 @@ function App({ isNewUser }: { isNewUser: boolean }) { setDefenceConfiguration={setDefenceConfiguration} setEmails={setEmails} setNumCompletedLevels={setNumCompletedLevels} + openWelcomeOverlay={openWelcomeOverlay} /> diff --git a/frontend/src/components/ChatBox/ChatBox.tsx b/frontend/src/components/ChatBox/ChatBox.tsx index 88aea43e5..1fe3f6e74 100644 --- a/frontend/src/components/ChatBox/ChatBox.tsx +++ b/frontend/src/components/ChatBox/ChatBox.tsx @@ -88,7 +88,7 @@ function ChatBox({ function inputKeyUp(event: KeyboardEvent) { // shift+enter shouldn't send message - if (event.key === "Enter" && !event.shiftKey) { + if (event.key === "Enter" && !event.shiftKey && !isSendingMessage) { // asynchronously send the message void sendChatMessage(); } diff --git a/frontend/src/components/DefenceBox/DefenceMechanism.tsx b/frontend/src/components/DefenceBox/DefenceMechanism.tsx index 6549b70ae..2edcaf281 100644 --- a/frontend/src/components/DefenceBox/DefenceMechanism.tsx +++ b/frontend/src/components/DefenceBox/DefenceMechanism.tsx @@ -75,7 +75,7 @@ function DefenceMechanism({ onChange={toggleDefence} // set checked if defence is active checked={defenceDetail.isActive} - aria-label="toggle defence" + aria-label={defenceDetail.name} /> diff --git a/frontend/src/components/LevelSelectionBox/LevelSelectionBox.test.tsx b/frontend/src/components/LevelSelectionBox/LevelSelectionBox.test.tsx index f27066493..ee4074677 100644 --- a/frontend/src/components/LevelSelectionBox/LevelSelectionBox.test.tsx +++ b/frontend/src/components/LevelSelectionBox/LevelSelectionBox.test.tsx @@ -20,13 +20,15 @@ function isSandbox(name: string) { return /Sandbox/i.test(name); } +const storyLevels = LEVELS.filter(({ name }) => !isSandbox(name)); + describe("LevelSelectionBox component tests", () => { - test("renders one button per level", () => { + test("renders one button per level, when not in sandbox", () => { renderComponent(); const levelButtons = screen.getAllByRole("button"); - expect(levelButtons).toHaveLength(LEVELS.length); - LEVELS.forEach(({ name }) => { + expect(levelButtons).toHaveLength(storyLevels.length); + storyLevels.forEach(({ name }) => { expect(screen.getByRole("button", { name })).toBeInTheDocument(); }); }); @@ -43,15 +45,15 @@ describe("LevelSelectionBox component tests", () => { expect(selectedButtons[0]).toHaveAccessibleName(currentLevel.name); }); - test("renders buttons ahead of current level disabled, except sandbox", () => { + test("renders buttons ahead of current level disabled", () => { const numCompletedLevels = 1; const currentLevel = LEVELS[numCompletedLevels]; renderComponent({ ...defaultProps, numCompletedLevels }); - LEVELS.forEach(({ id, name }) => { + storyLevels.forEach(({ id, name }) => { const button = screen.getByRole("button", { name }); - if (id <= currentLevel.id || isSandbox(name)) { + if (id <= currentLevel.id) { expect(button).toBeEnabled(); } else { expect(button).toBeDisabled(); @@ -59,7 +61,7 @@ describe("LevelSelectionBox component tests", () => { }); }); - test.each(LEVELS)( + test.each(storyLevels)( `fires callback on click, unless current level [$name] clicked`, async (level) => { const currentLevel = LEVELS[0]; diff --git a/frontend/src/components/LevelSelectionBox/LevelSelectionBox.tsx b/frontend/src/components/LevelSelectionBox/LevelSelectionBox.tsx index 42d7d7c68..5684699f4 100644 --- a/frontend/src/components/LevelSelectionBox/LevelSelectionBox.tsx +++ b/frontend/src/components/LevelSelectionBox/LevelSelectionBox.tsx @@ -14,24 +14,26 @@ function LevelSelectionBox({ numCompletedLevels, setNewLevel, }: LevelSelectionBoxProps) { + // display levels 1-3 + const displayLevels = LEVELS.filter( + (level) => level.id !== LEVEL_NAMES.SANDBOX + ); + function handleLevelChange(newLevel: LEVEL_NAMES) { if (newLevel !== currentLevel) { setNewLevel(newLevel); } } - return (
- {LEVELS.map((level: Level, index: number) => { + {displayLevels.map((level: Level, index: number) => { return ( { handleLevelChange(level.id); }} - isDisabled={ - index > numCompletedLevels && level.id !== LEVEL_NAMES.SANDBOX - } + isDisabled={index > numCompletedLevels} isSelected={level.id === currentLevel} > {level.name} diff --git a/frontend/src/components/MainComponent/MainBody.tsx b/frontend/src/components/MainComponent/MainBody.tsx index eddc58d47..5cff5335c 100644 --- a/frontend/src/components/MainComponent/MainBody.tsx +++ b/frontend/src/components/MainComponent/MainBody.tsx @@ -11,6 +11,7 @@ import EmailBox from "../EmailBox/EmailBox"; import { EmailInfo } from "../../models/email"; import { useState } from "react"; import ControlPanel from "../ControlPanel/ControlPanel"; +import SwitchModeButton from "../ThemedButtons/SwitchModeButton"; function MainBody({ currentLevel, @@ -24,6 +25,7 @@ function MainBody({ setDefenceConfiguration, setEmails, setNumCompletedLevels, + openWelcomeOverlay, }: { currentLevel: LEVEL_NAMES; defences: DefenceInfo[]; @@ -39,6 +41,7 @@ function MainBody({ ) => Promise; setEmails: (emails: EmailInfo[]) => void; setNumCompletedLevels: (numCompletedLevels: number) => void; + openWelcomeOverlay: () => void; }) { const [completedLevels, setCompletedLevels] = useState>( new Set() @@ -71,6 +74,12 @@ function MainBody({ />
+ { + openWelcomeOverlay(); + }} + /> {getLevelName(currentLevel)} - - - + {currentLevel !== LEVEL_NAMES.SANDBOX && ( + + + + )} + + ))} + + ); +} + +export default StartLevelButtons; diff --git a/frontend/src/components/ThemedButtons/SwitchModeButton.tsx b/frontend/src/components/ThemedButtons/SwitchModeButton.tsx new file mode 100644 index 000000000..0b44bd476 --- /dev/null +++ b/frontend/src/components/ThemedButtons/SwitchModeButton.tsx @@ -0,0 +1,19 @@ +import { LEVEL_NAMES } from "../../models/level"; + +function SwitchModeButton({ + currentLevel, + onClick, +}: { + currentLevel: LEVEL_NAMES; + onClick: () => void; +}) { + return ( + + ); +} + +export default SwitchModeButton; diff --git a/frontend/src/components/ThemedButtons/ThemedButton.css b/frontend/src/components/ThemedButtons/ThemedButton.css index e5d80e3b3..fb31f92cc 100644 --- a/frontend/src/components/ThemedButtons/ThemedButton.css +++ b/frontend/src/components/ThemedButtons/ThemedButton.css @@ -28,7 +28,7 @@ color: var(--main-button-active-hover-text-colour); } -.themed-button[disabled] { +.themed-button.appearsDifferentWhenDisabled[disabled] { background-color: var(--main-button-disabled-background-colour); color: var(--main-button-disabled-text-colour); cursor: default; diff --git a/frontend/src/components/ThemedButtons/ThemedButton.tsx b/frontend/src/components/ThemedButtons/ThemedButton.tsx index 74b79d2c0..c1007c454 100644 --- a/frontend/src/components/ThemedButtons/ThemedButton.tsx +++ b/frontend/src/components/ThemedButtons/ThemedButton.tsx @@ -7,6 +7,7 @@ export interface ThemedButtonProps { isDisabled?: boolean; isSelected?: boolean; onClick: () => void; + appearsDifferentWhenDisabled?: boolean; } function ThemedButton({ @@ -14,8 +15,12 @@ function ThemedButton({ onClick, isDisabled = false, isSelected = false, + appearsDifferentWhenDisabled = true, }: ThemedButtonProps) { - const buttonClass = clsx("themed-button", { selected: isSelected }); + const buttonClass = clsx("themed-button", { + selected: isSelected, + appearsDifferentWhenDisabled: appearsDifferentWhenDisabled, + }); return (