ENCOA-182, ENCOA-185, ENCOA-177, ENCOA-168, ENCOA-186, ENCOA-176, ENCOA-189, ENCOA-167

This commit is contained in:
Carlos Mesquita
2024-09-06 09:39:38 +01:00
parent b6b5f3a9f1
commit 77ac15c2bb
11 changed files with 577 additions and 212 deletions

View File

@@ -3,16 +3,32 @@ import { useEffect, useRef, useState } from "react";
interface Props {
part: LevelPart,
contextWord: string | undefined,
setContextWordLine: React.Dispatch<React.SetStateAction<number | undefined>>
contextWords: { match: string, originalLine: string }[] | undefined,
setContextWordLines: React.Dispatch<React.SetStateAction<number[] | undefined>>
setTotalLines: React.Dispatch<React.SetStateAction<number>>
}
const TextComponent: React.FC<Props> = ({ part, contextWord, setContextWordLine }) => {
const TextComponent: React.FC<Props> = ({ part, contextWords, setContextWordLines, setTotalLines }) => {
const textRef = useRef<HTMLDivElement>(null);
const [lineNumbers, setLineNumbers] = useState<number[]>([]);
const [lineHeight, setLineHeight] = useState<number>(0);
const [addBreaksTo, setAddBreaksTo] = useState<number[]>([]);
const getBoldTag = (context: string) => {
const regex = /<b\s+class=['"]([^'"]+)['"]>(\d+)<\/b>/;
const match = context.match(regex);
if (match) {
return {
className: match[1],
number: match[2],
fullTag: match[0]
};
}
return null;
};
const bTag = getBoldTag(part.context!);
const calculateLineNumbers = () => {
if (textRef.current) {
const computedStyle = window.getComputedStyle(textRef.current);
@@ -39,7 +55,6 @@ const TextComponent: React.FC<Props> = ({ part, contextWord, setContextWordLine
const lines = paragraphs.map((line, lineIndex) => {
const paragraphWords = line.split(/(\s+)/);
return paragraphWords.map((word, wordIndex) => {
if (lineIndex !== 0 && wordIndex == 0 && lineIndex < paragraphs.length) {
betweenParagraphs[lineIndex - 1][1] = word;
}
@@ -47,9 +62,17 @@ const TextComponent: React.FC<Props> = ({ part, contextWord, setContextWordLine
if (wordIndex == paragraphWords.length - 1 && lineIndex < paragraphs.length) {
betweenParagraphs[lineIndex][0] = word;
}
const span = document.createElement('span');
span.textContent = word;
if (wordIndex === 0 && bTag) {
const b = document.createElement('b');
b.classList.add(bTag.className);
b.textContent = `${lineIndex + 1}`;
span.appendChild(b);
span.appendChild(document.createTextNode(word.substring(1)));
}else {
span.appendChild(document.createTextNode(word));
}
return span;
})
}
@@ -67,8 +90,11 @@ const TextComponent: React.FC<Props> = ({ part, contextWord, setContextWordLine
const processedLines: string[][] = [[]];
let currentLine = 1;
let currentLineTop: number | undefined;
let contextWordLine: number | null = null;
let contextWordLines: number[] = [];
if (contextWords) {
contextWordLines = Array(contextWords.length).fill(-1);
}
const firstChild = offscreenElement.firstChild as HTMLElement;
if (firstChild) {
currentLineTop = firstChild.getBoundingClientRect().top;
@@ -78,7 +104,7 @@ const TextComponent: React.FC<Props> = ({ part, contextWord, setContextWordLine
let betweenIndex = 0;
const addBreaksTo: number[] = [];
spans.forEach((span, index)=> {
spans.forEach((span, index) => {
const rect = span.getBoundingClientRect();
const top = rect.top;
@@ -98,17 +124,22 @@ const TextComponent: React.FC<Props> = ({ part, contextWord, setContextWordLine
}
processedLines[processedLines.length - 1].push(span.textContent?.trim() || '');
if (contextWord && contextWordLine === null && span.textContent?.includes(contextWord)) {
contextWordLine = currentLine;
if (contextWords && contextWordLines.some(element => element === -1)) {
contextWords.forEach((w, index) => {
if (span.textContent?.includes(w.match) && contextWordLines[index] == -1) {
contextWordLines[index] = currentLine;
}
})
}
});
setAddBreaksTo(addBreaksTo);
setLineNumbers(processedLines.map((_, index) => index + 1));
if (contextWordLine) {
setContextWordLine(contextWordLine);
setTotalLines(currentLine);
if (contextWordLines.length > 0) {
setContextWordLines(contextWordLines);
}
document.body.removeChild(offscreenElement);
@@ -135,18 +166,18 @@ const TextComponent: React.FC<Props> = ({ part, contextWord, setContextWordLine
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [part.context, contextWord]);
}, [part.context, contextWords]);
return (
<div className="flex mt-2">
<div className="flex-shrink-0 w-8 pr-2">
{lineNumbers.map(num => (
<>
<div key={num} className="text-gray-400 flex justify-end" style={{ lineHeight: `${lineHeight}px` }}>
{num}
</div>
{/* Do not delete the space between the span or else the lines get messed up */}
{addBreaksTo.includes(num) && <span className={`h-[${lineHeight}px] whitespace-pre-wrap`}> </span>}
<div key={num} className="text-gray-400 flex justify-end" style={{ lineHeight: `${lineHeight}px` }}>
{num}
</div>
{/* Do not delete the space between the span or else the lines get messed up */}
{addBreaksTo.includes(num) && <span className={`h-[${lineHeight}px] whitespace-pre-wrap`}> </span>}
</>
))}
</div>

View File

@@ -85,8 +85,9 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0));
const [contextWord, setContextWord] = useState<string | undefined>(undefined);
const [contextWordLine, setContextWordLine] = useState<number | undefined>(undefined);
const [contextWords, setContextWords] = useState<{ match: string, originalLine: string }[] | undefined>(undefined);
const [contextWordLines, setContextWordLines] = useState<number[] | undefined>(undefined);
const [totalLines, setTotalLines] = useState<number>(0);
const [showSolutionsSave, setShowSolutionsSave] = useState(showSolutions ? userSolutions.filter((x) => x.module === "level") : undefined)
@@ -206,6 +207,14 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
if (questionIndex == 0) {
setPartIndex(partIndex - 1);
if (!seenParts.has(partIndex - 1)) {
setBgColor(levelBgColor);
setShowPartDivider(true);
setQuestionIndex(0);
setSeenParts(prev => new Set(prev).add(partIndex - 1));
return;
}
const lastExerciseIndex = exam.parts[partIndex - 1].exercises.length - 1;
const lastExercise = exam.parts[partIndex - 1].exercises[lastExerciseIndex];
setExerciseIndex(lastExerciseIndex);
@@ -260,8 +269,9 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
{exam.parts[partIndex].context &&
<TextComponent
part={exam.parts[partIndex]}
contextWord={contextWord}
setContextWordLine={setContextWordLine}
contextWords={contextWords}
setContextWordLines={setContextWordLines}
setTotalLines={setTotalLines}
/>}
</div>
</>
@@ -321,35 +331,59 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
useEffect(() => {
const regex = /.*?['"](.*?)['"] in line (\d+)\?$/;
const findMatch = (index: number) => {
if (currentExercise && currentExercise.type === "multipleChoice" && currentExercise!.questions[index]) {
const match = currentExercise!.questions[index].prompt.match(regex);
if (match) {
return { match: match[1], originalLine: match[2] }
}
}
return;
}
// if the client for some whatever random reason decides
// to add more questions update this
const numberOfQuestions = 2;
if (exam.parts[partIndex].context) {
const hits = Array.from({ length: numberOfQuestions }).reduce<{ match: string, originalLine: string }[]>((acc, _, i) => {
const result = findMatch(questionIndex + i);
if (!!result) {
acc.push(result);
}
return acc;
}, []);
if (hits.length > 0) {
setContextWords(hits)
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentExercise, questionIndex, totalLines]);
useEffect(() => {
if (
exerciseIndex !== -1 && currentExercise &&
currentExercise.type === "multipleChoice" &&
currentExercise.questions[questionIndex] &&
currentExercise.questions[questionIndex].prompt &&
exam.parts[partIndex].context
exam.parts[partIndex].context && contextWordLines
) {
const match = currentExercise.questions[questionIndex].prompt.match(regex);
if (match) {
const word = match[1];
const originalLineNumber = match[2];
if (word !== contextWord) {
setContextWord(word);
}
const updatedPrompt = currentExercise.questions[questionIndex].prompt.replace(
`in line ${originalLineNumber}`,
`in line ${contextWordLine || originalLineNumber}`
);
currentExercise.questions[questionIndex].prompt = updatedPrompt;
if (contextWordLines.length > 0) {
contextWordLines.forEach((n, i) => {
if (contextWords && contextWords[i] && n !== -1) {
const updatedPrompt = currentExercise!.questions[questionIndex + i].prompt.replace(
`in line ${contextWords[i].originalLine}`,
`in line ${n}`
);
currentExercise!.questions[questionIndex + i].prompt = updatedPrompt;
}
})
setChangedPrompt(true);
} else {
setContextWord(undefined);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentExercise, questionIndex, contextWordLine]);
}, [contextWordLines]);
useEffect(() => {
if (continueAnyways) {
@@ -419,7 +453,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
<Button color="purple" onClick={() => setShowSubmissionModal(false)} variant="outline" className="max-w-[200px] self-end w-full !text-xl">
Cancel
</Button>
<Button color="rose" onClick={() => { setShowSubmissionModal(false); setContinueAnyways(true)}} className="max-w-[200px] self-end w-full !text-xl">
<Button color="rose" onClick={() => { setShowSubmissionModal(false); setContinueAnyways(true) }} className="max-w-[200px] self-end w-full !text-xl">
Confirm
</Button>
</div>
@@ -469,6 +503,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
</div>
)}
<ModuleTitle
examLabel={exam.label}
partLabel={partLabel()}
minTimer={exam.minTimer}
exerciseIndex={calculateExerciseIndex()}