Finalized the Level Generation

This commit is contained in:
Tiago Ribeiro
2024-07-26 14:09:08 +01:00
parent 6a803fe137
commit 8f6639b7fc
4 changed files with 318 additions and 434 deletions

View File

@@ -18,8 +18,6 @@ function Question({
const renderPrompt = (prompt: string) => { const renderPrompt = (prompt: string) => {
return reactStringReplace(prompt, /((<u>)\w+(<\/u>))/g, (match) => { return reactStringReplace(prompt, /((<u>)\w+(<\/u>))/g, (match) => {
const word = match.replaceAll("<u>", "").replaceAll("</u>", ""); const word = match.replaceAll("<u>", "").replaceAll("</u>", "");
console.log(word);
return word.length > 0 ? <u>{word}</u> : null; return word.length > 0 ? <u>{word}</u> : null;
}); });
}; };

View File

@@ -46,28 +46,17 @@ interface NavProps {
badge?: number; badge?: number;
} }
const Nav = ({ const Nav = ({Icon, label, path, keyPath, disabled = false, isMinimized = false, badge}: NavProps) => {
Icon,
label,
path,
keyPath,
disabled = false,
isMinimized = false,
badge,
}: NavProps) => {
return ( return (
<Link <Link
href={!disabled ? keyPath : ""} href={!disabled ? keyPath : ""}
className={clsx( className={clsx(
"flex items-center gap-4 rounded-full p-4 text-gray-500 hover:text-white", "flex items-center gap-4 rounded-full p-4 text-gray-500 hover:text-white",
"transition-all duration-300 ease-in-out relative", "transition-all duration-300 ease-in-out relative",
disabled disabled ? "hover:bg-mti-gray-dim cursor-not-allowed" : "hover:bg-mti-purple-light cursor-pointer",
? "hover:bg-mti-gray-dim cursor-not-allowed"
: "hover:bg-mti-purple-light cursor-pointer",
path === keyPath && "bg-mti-purple-light text-white", path === keyPath && "bg-mti-purple-light text-white",
isMinimized ? "w-fit" : "w-full min-w-[200px] px-8 2xl:min-w-[220px]" isMinimized ? "w-fit" : "w-full min-w-[200px] px-8 2xl:min-w-[220px]",
)} )}>
>
<Icon size={24} /> <Icon size={24} />
{!isMinimized && <span className="text-lg font-semibold">{label}</span>} {!isMinimized && <span className="text-lg font-semibold">{label}</span>}
{!!badge && badge > 0 && ( {!!badge && badge > 0 && (
@@ -75,9 +64,8 @@ const Nav = ({
className={clsx( className={clsx(
"bg-mti-purple-light h-5 w-5 text-xs rounded-full flex items-center justify-center text-white", "bg-mti-purple-light h-5 w-5 text-xs rounded-full flex items-center justify-center text-white",
"transition ease-in-out duration-300", "transition ease-in-out duration-300",
isMinimized && "absolute right-0 top-0" isMinimized && "absolute right-0 top-0",
)} )}>
>
{badge} {badge}
</div> </div>
)} )}
@@ -85,20 +73,10 @@ const Nav = ({
); );
}; };
export default function Sidebar({ export default function Sidebar({path, navDisabled = false, focusMode = false, user, onFocusLayerMouseEnter, className}: Props) {
path,
navDisabled = false,
focusMode = false,
user,
onFocusLayerMouseEnter,
className,
}: Props) {
const router = useRouter(); const router = useRouter();
const [isMinimized, toggleMinimize] = usePreferencesStore((state) => [ const [isMinimized, toggleMinimize] = usePreferencesStore((state) => [state.isSidebarMinimized, state.toggleSidebarMinimized]);
state.isSidebarMinimized,
state.toggleSidebarMinimized,
]);
const {totalAssignedTickets} = useTicketsListener(user.id); const {totalAssignedTickets} = useTicketsListener(user.id);
@@ -115,71 +93,23 @@ export default function Sidebar({
className={clsx( className={clsx(
"relative flex h-full flex-col justify-between bg-transparent px-4 py-4 pb-8", "relative flex h-full flex-col justify-between bg-transparent px-4 py-4 pb-8",
isMinimized ? "w-fit" : "-xl:w-fit w-1/6", isMinimized ? "w-fit" : "-xl:w-fit w-1/6",
className className,
)} )}>
>
<div className="-xl:hidden flex-col gap-3 xl:flex"> <div className="-xl:hidden flex-col gap-3 xl:flex">
<Nav <Nav disabled={disableNavigation} Icon={MdSpaceDashboard} label="Dashboard" path={path} keyPath="/" isMinimized={isMinimized} />
disabled={disableNavigation} {checkAccess(user, ["student", "teacher", "developer"], "viewExams") && (
Icon={MdSpaceDashboard} <Nav disabled={disableNavigation} Icon={BsFileEarmarkText} label="Exams" path={path} keyPath="/exam" isMinimized={isMinimized} />
label="Dashboard"
path={path}
keyPath="/"
isMinimized={isMinimized}
/>
{checkAccess(
user,
["student", "teacher", "developer"],
"viewExams"
) && (
<Nav
disabled={disableNavigation}
Icon={BsFileEarmarkText}
label="Exams"
path={path}
keyPath="/exam"
isMinimized={isMinimized}
/>
)} )}
{checkAccess( {checkAccess(user, ["student", "teacher", "developer"], "viewExercises") && (
user, <Nav disabled={disableNavigation} Icon={BsPencil} label="Exercises" path={path} keyPath="/exercises" isMinimized={isMinimized} />
["student", "teacher", "developer"],
"viewExercises"
) && (
<Nav
disabled={disableNavigation}
Icon={BsPencil}
label="Exercises"
path={path}
keyPath="/exercises"
isMinimized={isMinimized}
/>
)} )}
{checkAccess(user, getTypesOfUser(["agent"]), "viewStats") && ( {checkAccess(user, getTypesOfUser(["agent"]), "viewStats") && (
<Nav <Nav disabled={disableNavigation} Icon={BsGraphUp} label="Stats" path={path} keyPath="/stats" isMinimized={isMinimized} />
disabled={disableNavigation}
Icon={BsGraphUp}
label="Stats"
path={path}
keyPath="/stats"
isMinimized={isMinimized}
/>
)} )}
{checkAccess(user, getTypesOfUser(["agent"]), "viewRecords") && ( {checkAccess(user, getTypesOfUser(["agent"]), "viewRecords") && (
<Nav <Nav disabled={disableNavigation} Icon={BsClockHistory} label="Record" path={path} keyPath="/record" isMinimized={isMinimized} />
disabled={disableNavigation}
Icon={BsClockHistory}
label="Record"
path={path}
keyPath="/record"
isMinimized={isMinimized}
/>
)} )}
{checkAccess( {checkAccess(user, ["admin", "developer", "agent", "corporate", "mastercorporate"], "viewPaymentRecords") && (
user,
["admin", "developer", "agent", "corporate", "mastercorporate"],
"viewPaymentRecords"
) && (
<Nav <Nav
disabled={disableNavigation} disabled={disableNavigation}
Icon={BsCurrencyDollar} Icon={BsCurrencyDollar}
@@ -189,13 +119,7 @@ export default function Sidebar({
isMinimized={isMinimized} isMinimized={isMinimized}
/> />
)} )}
{checkAccess(user, [ {checkAccess(user, ["admin", "developer", "corporate", "teacher", "mastercorporate"]) && (
"admin",
"developer",
"corporate",
"teacher",
"mastercorporate",
]) && (
<Nav <Nav
disabled={disableNavigation} disabled={disableNavigation}
Icon={BsShieldFill} Icon={BsShieldFill}
@@ -205,22 +129,6 @@ export default function Sidebar({
isMinimized={isMinimized} isMinimized={isMinimized}
/> />
)} )}
{checkAccess(user, [
"admin",
"developer",
"corporate",
"teacher",
"mastercorporate",
]) && (
<Nav
disabled={disableNavigation}
Icon={BsShieldFill}
label="Permissions"
path={path}
keyPath="/permissions"
isMinimized={isMinimized}
/>
)}
{checkAccess(user, ["admin", "developer", "agent"], "viewTickets") && ( {checkAccess(user, ["admin", "developer", "agent"], "viewTickets") && (
<Nav <Nav
disabled={disableNavigation} disabled={disableNavigation}
@@ -232,7 +140,7 @@ export default function Sidebar({
badge={totalAssignedTickets} badge={totalAssignedTickets}
/> />
)} )}
{checkAccess(user, ["developer"]) && ( {checkAccess(user, ["developer", "admin"]) && (
<> <>
<Nav <Nav
disabled={disableNavigation} disabled={disableNavigation}
@@ -254,69 +162,20 @@ export default function Sidebar({
)} )}
</div> </div>
<div className="-xl:flex flex-col gap-3 xl:hidden"> <div className="-xl:flex flex-col gap-3 xl:hidden">
<Nav <Nav disabled={disableNavigation} Icon={MdSpaceDashboard} label="Dashboard" path={path} keyPath="/" isMinimized={true} />
disabled={disableNavigation} <Nav disabled={disableNavigation} Icon={BsFileEarmarkText} label="Exams" path={path} keyPath="/exam" isMinimized={true} />
Icon={MdSpaceDashboard} <Nav disabled={disableNavigation} Icon={BsPencil} label="Exercises" path={path} keyPath="/exercises" isMinimized={true} />
label="Dashboard"
path={path}
keyPath="/"
isMinimized={true}
/>
<Nav
disabled={disableNavigation}
Icon={BsFileEarmarkText}
label="Exams"
path={path}
keyPath="/exam"
isMinimized={true}
/>
<Nav
disabled={disableNavigation}
Icon={BsPencil}
label="Exercises"
path={path}
keyPath="/exercises"
isMinimized={true}
/>
{checkAccess(user, getTypesOfUser(["agent"]), "viewStats") && ( {checkAccess(user, getTypesOfUser(["agent"]), "viewStats") && (
<Nav <Nav disabled={disableNavigation} Icon={BsGraphUp} label="Stats" path={path} keyPath="/stats" isMinimized={true} />
disabled={disableNavigation}
Icon={BsGraphUp}
label="Stats"
path={path}
keyPath="/stats"
isMinimized={true}
/>
)} )}
{checkAccess(user, getTypesOfUser(["agent"]), "viewRecords") && ( {checkAccess(user, getTypesOfUser(["agent"]), "viewRecords") && (
<Nav <Nav disabled={disableNavigation} Icon={BsClockHistory} label="Record" path={path} keyPath="/record" isMinimized={true} />
disabled={disableNavigation}
Icon={BsClockHistory}
label="Record"
path={path}
keyPath="/record"
isMinimized={true}
/>
)} )}
{checkAccess(user, getTypesOfUser(["student"])) && ( {checkAccess(user, getTypesOfUser(["student"])) && (
<Nav <Nav disabled={disableNavigation} Icon={BsShieldFill} label="Settings" path={path} keyPath="/settings" isMinimized={true} />
disabled={disableNavigation}
Icon={BsShieldFill}
label="Settings"
path={path}
keyPath="/settings"
isMinimized={true}
/>
)} )}
{checkAccess(user, getTypesOfUser(["student"])) && ( {checkAccess(user, getTypesOfUser(["student"])) && (
<Nav <Nav disabled={disableNavigation} Icon={BsShieldFill} label="Permissions" path={path} keyPath="/permissions" isMinimized={true} />
disabled={disableNavigation}
Icon={BsShieldFill}
label="Permissions"
path={path}
keyPath="/permissions"
isMinimized={true}
/>
)} )}
{checkAccess(user, ["developer"]) && ( {checkAccess(user, ["developer"]) && (
<> <>
@@ -347,17 +206,10 @@ export default function Sidebar({
onClick={toggleMinimize} onClick={toggleMinimize}
className={clsx( className={clsx(
"hover:text-mti-rose -xl:hidden flex cursor-pointer items-center gap-4 rounded-full p-4 text-black transition duration-300 ease-in-out", "hover:text-mti-rose -xl:hidden flex cursor-pointer items-center gap-4 rounded-full p-4 text-black transition duration-300 ease-in-out",
isMinimized ? "w-fit" : "w-full min-w-[250px] px-8" isMinimized ? "w-fit" : "w-full min-w-[250px] px-8",
)} )}>
> {isMinimized ? <BsChevronBarRight size={24} /> : <BsChevronBarLeft size={24} />}
{isMinimized ? ( {!isMinimized && <span className="text-lg font-medium">Minimize</span>}
<BsChevronBarRight size={24} />
) : (
<BsChevronBarLeft size={24} />
)}
{!isMinimized && (
<span className="text-lg font-medium">Minimize</span>
)}
</div> </div>
<div <div
role="button" role="button"
@@ -365,18 +217,13 @@ export default function Sidebar({
onClick={focusMode ? () => {} : logout} onClick={focusMode ? () => {} : logout}
className={clsx( className={clsx(
"hover:text-mti-rose flex cursor-pointer items-center gap-4 rounded-full p-4 text-black transition duration-300 ease-in-out", "hover:text-mti-rose flex cursor-pointer items-center gap-4 rounded-full p-4 text-black transition duration-300 ease-in-out",
isMinimized ? "w-fit" : "w-full min-w-[250px] px-8" isMinimized ? "w-fit" : "w-full min-w-[250px] px-8",
)} )}>
>
<RiLogoutBoxFill size={24} /> <RiLogoutBoxFill size={24} />
{!isMinimized && ( {!isMinimized && <span className="-xl:hidden text-lg font-medium">Log Out</span>}
<span className="-xl:hidden text-lg font-medium">Log Out</span>
)}
</div> </div>
</div> </div>
{focusMode && ( {focusMode && <FocusLayer onFocusLayerMouseEnter={onFocusLayerMouseEnter} />}
<FocusLayer onFocusLayerMouseEnter={onFocusLayerMouseEnter} />
)}
</section> </section>
); );
} }

View File

@@ -18,8 +18,6 @@ function Question({
const renderPrompt = (prompt: string) => { const renderPrompt = (prompt: string) => {
return reactStringReplace(prompt, /((<u>)\w+(<\/u>))/g, (match) => { return reactStringReplace(prompt, /((<u>)\w+(<\/u>))/g, (match) => {
const word = match.replaceAll("<u>", "").replaceAll("</u>", ""); const word = match.replaceAll("<u>", "").replaceAll("</u>", "");
console.log(word);
return word.length > 0 ? <u>{word}</u> : null; return word.length > 0 ? <u>{word}</u> : null;
}); });
}; };

View File

@@ -1,3 +1,5 @@
import FillBlanksEdit from "@/components/Generation/fill.blanks.edit";
import WriteBlankEdits from "@/components/Generation/write.blanks.edit";
import Input from "@/components/Low/Input"; import Input from "@/components/Low/Input";
import Select from "@/components/Low/Select"; import Select from "@/components/Low/Select";
import { import {
@@ -8,6 +10,7 @@ import {
LevelPart, LevelPart,
FillBlanksExercise, FillBlanksExercise,
WriteBlanksExercise, WriteBlanksExercise,
Exercise,
} from "@/interfaces/exam"; } from "@/interfaces/exam";
import useExamStore from "@/stores/examStore"; import useExamStore from "@/stores/examStore";
import {getExamById} from "@/utils/exams"; import {getExamById} from "@/utils/exams";
@@ -42,8 +45,6 @@ const QuestionDisplay = ({question, onUpdate}: {question: MultipleChoiceQuestion
const renderPrompt = (prompt: string) => { const renderPrompt = (prompt: string) => {
return reactStringReplace(prompt, /((<u>)\w+(<\/u>))/g, (match) => { return reactStringReplace(prompt, /((<u>)\w+(<\/u>))/g, (match) => {
const word = match.replaceAll("<u>", "").replaceAll("</u>", ""); const word = match.replaceAll("<u>", "").replaceAll("</u>", "");
console.log(word);
return word.length > 0 ? <u>{word}</u> : null; return word.length > 0 ? <u>{word}</u> : null;
}); });
}; };
@@ -118,10 +119,67 @@ const TaskTab = ({section, setSection}: {section: LevelSection; setSection: (sec
questions: (x as MultipleChoiceExercise).questions.map((q) => (q.id === question.id ? question : q)), questions: (x as MultipleChoiceExercise).questions.map((q) => (q.id === question.id ? question : q)),
})), })),
}; };
console.log(updatedExam);
setSection(updatedExam as any); setSection(updatedExam as any);
}; };
const renderExercise = (exercise: Exercise) => {
if (exercise.type === "multipleChoice")
return (
<div key={exercise.id} className="w-full h-full flex flex-col gap-2">
<div className="flex gap-2">
<span className="text-xl font-semibold">Multiple Choice</span>
<span className="rounded-xl bg-white border border-ielts-level p-1 px-4 w-fit">{exercise.questions.length} questions</span>
</div>
<span>{exercise.prompt}</span>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{exercise.questions.map((question) => (
<QuestionDisplay question={question} onUpdate={onUpdate} key={question.id} />
))}
</div>
</div>
);
if (exercise.type === "fillBlanks")
return (
<div key={exercise.id} className="w-full h-full flex flex-col gap-2">
<div className="flex gap-2">
<span className="text-xl font-semibold">Fill Blanks</span>
</div>
<span>{exercise.prompt}</span>
<FillBlanksEdit
exercise={exercise}
key={exercise.id}
updateExercise={(data: any) =>
setSection({
...section,
part: {...section.part!, exercises: section.part!.exercises.map((x) => (x.id === exercise.id ? {...x, ...data} : x))},
})
}
/>
</div>
);
if (exercise.type === "writeBlanks")
return (
<div key={exercise.id} className="w-full h-full flex flex-col gap-2">
<div className="flex gap-2">
<span className="text-xl font-semibold">Write Blanks</span>
</div>
<span>{exercise.prompt}</span>
<WriteBlankEdits
exercise={exercise}
key={exercise.id}
updateExercise={(data: any) =>
setSection({
...section,
part: {...section.part!, exercises: section.part!.exercises.map((x) => (x.id === exercise.id ? {...x, ...data} : x))},
})
}
/>
</div>
);
};
return ( return (
<Tab.Panel className="w-full bg-ielts-level/20 min-h-[600px] h-full rounded-xl p-6 flex flex-col gap-4"> <Tab.Panel className="w-full bg-ielts-level/20 min-h-[600px] h-full rounded-xl p-6 flex flex-col gap-4">
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
@@ -159,28 +217,8 @@ const TaskTab = ({section, setSection}: {section: LevelSection; setSection: (sec
)} )}
{section?.part && ( {section?.part && (
<div className="flex flex-col gap-2 w-full overflow-y-scroll scrollbar-hide h-full"> <div className="flex flex-col gap-2 w-full overflow-y-scroll scrollbar-hide h-full">
{section.part.exercises {section.part.context && <div>{section.part.context}</div>}
.filter((x) => x.type === "multipleChoice") {section.part.exercises.map(renderExercise)}
.map((ex) => {
const exercise = ex as MultipleChoiceExercise;
return (
<div key={ex.id} className="w-full h-full flex flex-col gap-2">
<div className="flex gap-2">
<span className="text-xl font-semibold">Multiple Choice</span>
<span className="rounded-xl bg-white border border-ielts-level p-1 px-4 w-fit">
{exercise.questions.length} questions
</span>
</div>
<span>{exercise.prompt}</span>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{exercise.questions.map((question) => (
<QuestionDisplay question={question} onUpdate={onUpdate} key={question.id} />
))}
</div>
</div>
);
})}
</div> </div>
)} )}
</Tab.Panel> </Tab.Panel>
@@ -237,6 +275,8 @@ const LevelGeneration = () => {
} }
}); });
let newParts = [...parts];
axios axios
.post<{exercises: {[key: string]: any}}>("/api/exam/level/generate/level", {nr_exercises: numberOfParts, ...body}) .post<{exercises: {[key: string]: any}}>("/api/exam/level/generate/level", {nr_exercises: numberOfParts, ...body})
.then((result) => { .then((result) => {
@@ -270,22 +310,20 @@ const LevelGeneration = () => {
userSolutions: [], userSolutions: [],
}; };
setParts((prev) => const item = {
prev.map((p, i) => exercises: [exercise],
};
newParts = newParts.map((p, i) =>
i === index i === index
? { ? {
...p, ...p,
part: { part: item,
exercises: [exercise],
},
} }
: p, : p,
),
); );
return { return item;
exercises: [exercise],
};
} }
if (part.type === "blank_space_text") { if (part.type === "blank_space_text") {
@@ -300,22 +338,20 @@ const LevelGeneration = () => {
userSolutions: [], userSolutions: [],
}; };
setParts((prev) => const item = {
prev.map((p, i) => exercises: [exercise],
};
newParts = newParts.map((p, i) =>
i === index i === index
? { ? {
...p, ...p,
part: { part: item,
exercises: [exercise],
},
} }
: p, : p,
),
); );
return { return item;
exercises: [exercise],
};
} }
const mcExercise: MultipleChoiceExercise = { const mcExercise: MultipleChoiceExercise = {
@@ -339,29 +375,26 @@ const LevelGeneration = () => {
userSolutions: [], userSolutions: [],
}; };
setParts((prev) => const item = {
prev.map((p, i) =>
i === index
? {
...p,
part: {
context: currentExercise.text.content,
exercises: [mcExercise, wbExercise],
},
}
: p,
),
);
return {
context: currentExercise.text.content, context: currentExercise.text.content,
exercises: [mcExercise, wbExercise], exercises: [mcExercise, wbExercise],
}; };
newParts = newParts.map((p, i) =>
i === index
? {
...p,
part: item,
}
: p,
);
return item;
}) })
.filter((x) => !!x) as LevelPart[], .filter((x) => !!x) as LevelPart[],
}; };
console.log(exam); setParts(newParts);
setGeneratedExam(exam); setGeneratedExam(exam);
}) })
.finally(() => setIsLoading(false)); .finally(() => setIsLoading(false));
@@ -375,8 +408,13 @@ const LevelGeneration = () => {
setIsLoading(true); setIsLoading(true);
const exam = {
...generatedExam,
parts: generatedExam.parts.map((p, i) => ({...p, exercises: parts[i].part!.exercises})),
};
axios axios
.post(`/api/exam/level`, generatedExam) .post(`/api/exam/level`, exam)
.then((result) => { .then((result) => {
playSound("sent"); playSound("sent");
console.log(`Generated Exam ID: ${result.data.id}`); console.log(`Generated Exam ID: ${result.data.id}`);
@@ -437,7 +475,10 @@ const LevelGeneration = () => {
<TaskTab <TaskTab
key={index} key={index}
section={parts[index]} section={parts[index]}
setSection={(part) => setParts((prev) => prev.map((x, i) => (i === index ? part : x)))} setSection={(part) => {
console.log(part);
setParts((prev) => prev.map((x, i) => (i === index ? part : x)));
}}
/> />
))} ))}
</Tab.Panels> </Tab.Panels>