Skip to content

Commit

Permalink
Add complex hints system workflow
Browse files Browse the repository at this point in the history
  • Loading branch information
SebastienTainon committed Jul 21, 2023
1 parent b117215 commit 7429488
Show file tree
Hide file tree
Showing 8 changed files with 287 additions and 23 deletions.
1 change: 1 addition & 0 deletions frontend/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ setAutoFreeze(true);
log.setDefaultLevel('trace');
log.getLogger('blockly_runner').setDefaultLevel('info');
log.getLogger('editor').setDefaultLevel('info');
log.getLogger('hints').setDefaultLevel('info');
log.getLogger('layout').setDefaultLevel('info');
log.getLogger('libraries').setDefaultLevel('info');
log.getLogger('performance').setDefaultLevel('info');
Expand Down
2 changes: 2 additions & 0 deletions frontend/lang/en-US.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ module.exports = {
NONE: 'None',
CANCEL: 'Cancel',
SELECT: 'Select',
YES: 'Yes',
NO: 'No',
FULLSCREEN: "fullscreen",
EXIT_FULLSCREEN: "exit fullscreen",
SAVE_RECORDING: "Save recording",
Expand Down
2 changes: 2 additions & 0 deletions frontend/lang/fr-FR.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ module.exports = {
NONE: 'Aucun',
CANCEL: 'Annuler',
SELECT: 'Choisir',
YES: 'Oui',
NO: 'Non',
FULLSCREEN: "plein écran",
EXIT_FULLSCREEN: "sortie de plein-écran",
SAVE_RECORDING: "Sauvegarder l'enregistrement",
Expand Down
63 changes: 63 additions & 0 deletions frontend/task/hints/TaskHint.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React from "react";
import {useAppSelector} from "../../hooks";
import {toHtml} from "../../utils/sanitize";
import {TaskHint} from "./hints_slice";
import {formatTaskInstructions} from '../utils';
import {getMessage} from '../../lang';

export interface TaskHintProps {
hint?: TaskHint,
askHintClassName?: string,
goToHintId: (hintId: string) => void,
}

export function TaskHint(props: TaskHintProps) {
const taskLevel = useAppSelector(state => state.task.currentLevel);
const platform = useAppSelector(state => state.options.platform);
const hint = props.hint;

const answerYes = () => {
goToHint(props.hint.yesHintId);
};

const answerNo = () => {
goToHint(props.hint.noHintId);
};

const goToHint = (hintId: string) => {
props.goToHintId(hintId);
}

if (hint.question) {
return (
<div
className="hint-carousel-item"
>
<div className="hint-question">
{hint.question}
</div>

<div className="hint-buttons">
<div className={`hint-button ${props.askHintClassName}`} onClick={answerYes}>
{getMessage('YES')}
</div>

<div className={`hint-button ${props.askHintClassName}`} onClick={answerNo}>
{getMessage('NO')}
</div>
</div>
</div>
)
}

const instructionsJQuery = formatTaskInstructions(hint.content, platform, taskLevel);

return (
<div
className="hint-carousel-item"
dangerouslySetInnerHTML={toHtml(instructionsJQuery.html())}
>

</div>
);
}
87 changes: 70 additions & 17 deletions frontend/task/hints/TaskHints.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import React from "react";
import React, {useCallback, useState} from "react";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faChevronLeft, faChevronRight} from "@fortawesome/free-solid-svg-icons";
import {Carousel} from 'react-bootstrap';
import {useDispatch} from "react-redux";
import {useAppSelector} from "../../hooks";
import {toHtml} from "../../utils/sanitize";
import {hintUnlocked, selectAvailableHints} from "./hints_slice";
import {getMessage} from '../../lang';
import {formatTaskInstructions} from '../utils';
import {TaskHint} from './TaskHint';
import log from 'loglevel';

export interface TaskHintProps {
askHintClassName?: string
Expand All @@ -16,26 +16,75 @@ export interface TaskHintProps {
export function TaskHints(props: TaskHintProps) {
const availableHints = useAppSelector(selectAvailableHints);
const unlockedHintIds = useAppSelector(state => state.hints.unlockedHintIds);
const taskLevel = useAppSelector(state => state.task.currentLevel);
const platform = useAppSelector(state => state.options.platform);
const [displayedHintId, setDisplayedHintId] = useState(unlockedHintIds.length ? unlockedHintIds[0] : null);
const displayedHintIndex = null === displayedHintId ? unlockedHintIds.length : unlockedHintIds.indexOf(displayedHintId);
const displayedHint = availableHints.find(hint => displayedHintId === hint.id);
const nextAvailableHint = availableHints.find(hint => -1 === unlockedHintIds.indexOf(hint.id));
const canAskMoreHints = undefined !== nextAvailableHint && !displayedHint?.question && !displayedHint?.disableNext;

const carouselElements = unlockedHintIds.map(unlockedHintId => {
const instructionsJQuery = formatTaskInstructions(availableHints[unlockedHintId].content, platform, taskLevel);
const dispatch = useDispatch();

return (
<div key={unlockedHintId} className="hint-carousel-item" dangerouslySetInnerHTML={toHtml(instructionsJQuery.html())}></div>
)
});
const unlockNextHint = () => {
dispatch(hintUnlocked(nextAvailableHint.id));
setDisplayedHintId(nextAvailableHint.id);
};

const nextAvailableHint = [...availableHints.keys()].find(key => -1 === unlockedHintIds.indexOf(key));
const goToHintId = useCallback((hintId: string) => {
if (-1 === unlockedHintIds.indexOf(hintId)) {
dispatch(hintUnlocked(hintId));
}
setDisplayedHintId(hintId);
}, []);

const dispatch = useDispatch();

const unlockNextHint = () => {
dispatch(hintUnlocked(nextAvailableHint));
let currentHintPreviousId = null;
let currentHintNextId = null;
if (displayedHint) {
if (!displayedHint.disableNext && !displayedHint.question) {
if (displayedHint.nextHintId) {
currentHintNextId = displayedHint.nextHintId;
}
}

if (!displayedHint.disablePrevious) {
if (displayedHint.previousHintId) {
currentHintPreviousId = displayedHint.previousHintId;
} else {
const previousHintIndex = displayedHintIndex - 1;
if (unlockedHintIds[previousHintIndex]) {
currentHintPreviousId = unlockedHintIds[previousHintIndex];
}
}
}
}

const handleSelect = (selectedIndex) => {
if (selectedIndex === displayedHintIndex - 1) {
// Back
if (currentHintPreviousId) {
goToHintId(currentHintPreviousId);
}
} else if (selectedIndex === displayedHintIndex + 1) {
// Next
if (currentHintNextId) {
goToHintId(currentHintNextId);
} else if (canAskMoreHints) {
setDisplayedHintId(null);
}
}
};

if (undefined !== nextAvailableHint) {

const carouselElements = unlockedHintIds.map(unlockedHintId => {
return <TaskHint
key={unlockedHintId}
hint={availableHints.find(hint => unlockedHintId === hint.id)}
askHintClassName={props.askHintClassName}
goToHintId={goToHintId}
/>;
});

if (canAskMoreHints) {
carouselElements.push(
<div className="hint-carousel-item hint-unlock">
<div className={`hint-button ${props.askHintClassName}`} onClick={unlockNextHint}>
Expand All @@ -45,10 +94,14 @@ export function TaskHints(props: TaskHintProps) {
);
}

log.getLogger('hints').debug('current hint id', {displayedHint, displayedHintId, displayedHintIndex, unlockedHintIds, currentHintPreviousId, currentHintNextId, canAskMoreHints})

return (
<div className="hints-container">
<div className="hints-content">
<div className={`hints-content ${null === currentHintPreviousId ? 'has-no-previous' : ''} ${null === currentHintNextId && !canAskMoreHints ? 'has-no-next' : ''}`}>
<Carousel
activeIndex={displayedHintIndex}
onSelect={handleSelect}
interval={null}
wrap={false}
prevIcon={<FontAwesomeIcon icon={faChevronLeft} size="lg"/>}
Expand Down
24 changes: 24 additions & 0 deletions frontend/task/hints/hints.scss
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,30 @@
line-height: 20px;
}

.hint-question {
font-weight: bold;
}

.hint-buttons {
display: flex;
align-items: center;
gap: 20px;
margin-top: 1rem;

.hint-button {
font-size: 0.85em;
height: 40px;
}
}

.hints-content.has-no-previous .carousel-control-prev {
display: none;
}

.hints-content.has-no-next .carousel-control-next {
display: none;
}

// Carousel
.carousel-container {
position: relative;
Expand Down
60 changes: 56 additions & 4 deletions frontend/task/hints/hints_slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,22 @@ import {useDispatch} from 'react-redux';
import {taskLevelsList} from '../platform/platform_slice';

export interface TaskHint {
content: string,
content?: string,
minScore?: number, // Between 0 and 1
id?: string,
locked?: boolean,
previousHintId?: string,
nextHintId?: string,
question?: string,
yesHintId?: string,
noHintId?: string,
disableNext?: boolean,
disablePrevious?: boolean,
}

export interface HintsState {
availableHints: TaskHint[],
unlockedHintIds: number[],
unlockedHintIds: string[],
}

export const hintsInitialState = {
Expand All @@ -34,11 +43,54 @@ export const hintsSlice = createSlice({
initialState: hintsInitialState,
reducers: {
hintsLoaded(state, action: PayloadAction<TaskHint[]>) {
// Add id to hints
state.availableHints = action.payload;
let currentId = 0;
const hintsById: {[hintId: string]: TaskHint} = {};
for (let hint of state.availableHints) {
if (!hint.id) {
hint.id = `hint:${currentId++}`;
}
hintsById[hint.id] = hint;
}
// Add previous links to hints
for (let [hintId, hint] of Object.entries(hintsById)) {
if (hint.yesHintId) {
if (!hintsById[hint.yesHintId]) {
throw "This hint id does not exist: " + hint.yesHintId;
}
if (!hintsById[hint.yesHintId].previousHintId) {
hintsById[hint.yesHintId].previousHintId = hintId;
}
}
if (hint.noHintId) {
if (!hintsById[hint.noHintId]) {
throw "This hint id does not exist: " + hint.noHintId;
}
if (!hintsById[hint.noHintId].previousHintId) {
hintsById[hint.noHintId].previousHintId = hintId;
}
}
if (hint.nextHintId) {
if (!hintsById[hint.nextHintId]) {
throw "This hint id does not exist: " + hint.nextHintId;
}
if (!hintsById[hint.nextHintId].previousHintId) {
hintsById[hint.nextHintId].previousHintId = hintId;
}
}
}
},
hintUnlocked(state, action: PayloadAction<number>) {
hintUnlocked(state, action: PayloadAction<string>) {
if (-1 === state.unlockedHintIds.indexOf(action.payload)) {
state.unlockedHintIds.push(action.payload);
const newUnlockedHintIds = [...state.unlockedHintIds, action.payload];
// Re-order unlocked hint ids
state.unlockedHintIds = [];
for (let hint of state.availableHints) {
if (-1 !== newUnlockedHintIds.indexOf(hint.id)) {
state.unlockedHintIds.push(hint.id);
}
}
}
},
},
Expand Down
Loading

0 comments on commit 7429488

Please sign in to comment.