Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 33 additions & 2 deletions packages/alea-frontend/components/ProblemList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { getExamsForCourse } from '@alea/spec';
import { ExamSelect } from '@alea/stex-react-renderer';
import { useCourseProblemCounts } from '../hooks/useCourseProblemCount';
import shadows from '../theme/shadows';
import { getQuizzesForCourse } from '@alea/spec';

interface TitleMetadata {
uri?: string;
Expand Down Expand Up @@ -72,6 +73,9 @@ const sortExamsByDateDesc = (exams: ExamInfo[]): ExamInfo[] => {
const ProblemList: FC<ProblemListProps> = ({ courseSections, courseId }) => {
const [exams, setExams] = useState<ExamInfo[]>([]);
const [selectedExam, setSelectedExam] = useState('');

const [quizzes, setQuizzes] = useState<ExamInfo[]>([]);
const [selectedQuiz, setSelectedQuiz] = useState('');
const router = useRouter();
const { practiceProblems: t, peerGrading: g } = getLocaleObject(router);
const theme = useTheme();
Expand All @@ -87,6 +91,18 @@ const ProblemList: FC<ProblemListProps> = ({ courseSections, courseId }) => {
.catch(console.error);
}, [courseId]);

useEffect(() => {
if (!courseId) return;

getQuizzesForCourse(courseId)
.then((data) => {
console.log("QUIZ DATA:", data);
const sorted = sortExamsByDateDesc(data);
setQuizzes(sorted);
})
.catch(console.error);
}, [courseId]);

const { data: problemCounts = {}, isLoading } = useCourseProblemCounts(courseId);

if (isLoading) {
Expand Down Expand Up @@ -166,7 +182,7 @@ const ProblemList: FC<ProblemListProps> = ({ courseSections, courseId }) => {
bgcolor: 'bacground.paper',
borderRadius: '12px',
border: '1px solid ',
borderColor:'divider',
borderColor: 'divider',
boxShadow: shadows[2],
}}
>
Expand Down Expand Up @@ -200,6 +216,21 @@ const ProblemList: FC<ProblemListProps> = ({ courseSections, courseId }) => {
}}
label="Select Exam"
/>

<ExamSelect
exams={quizzes}
courseId={courseId}
value={selectedQuiz}
onChange={(quizUri) => {
setSelectedQuiz(quizUri);

router.push({
pathname: '/quiz-problems',
query: { quizUri, courseId },
});
}}
label="Select Quiz"
/>
</Box>
</Box>

Expand Down Expand Up @@ -320,4 +351,4 @@ const ProblemList: FC<ProblemListProps> = ({ courseSections, courseId }) => {
);
};

export default ProblemList;
export default ProblemList;
29 changes: 27 additions & 2 deletions packages/alea-frontend/pages/api/get-problems-per-section.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { getAllCoursesFromDb } from './get-all-courses';
import { getCategorizedProblems } from './get-categorized-problem';
import { getExamsForCourse, getProblemsForExam } from '@alea/spec';
import {
getExamsForCourse,
getProblemsForExam,
getProblemsForQuiz,
getQuizzesForCourse,
} from '@alea/spec';
import { Language } from '@alea/utils';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
Expand Down Expand Up @@ -30,11 +35,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)

const sectionProblemSet = new Set(practiceProblems.map((p) => p.problemId));

const quizzes = await getQuizzesForCourse(courseId);
const quizProblemMap = new Map<string, { quizUri: string; quizLabel: string }[]>();

for (const exam of exams) {
const examProblems = await getProblemsForExam(exam.uri);

for (const problemUri of examProblems) {

if (!sectionProblemSet.has(problemUri)) continue;

examOnlyProblemSet.add(problemUri);
Expand All @@ -49,6 +56,23 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
}

for (const quiz of quizzes) {
const quizProblems = await getProblemsForQuiz(quiz.uri);

for (const problemUri of quizProblems) {
if (!sectionProblemSet.has(problemUri)) continue;

const existing = quizProblemMap.get(problemUri) ?? [];

existing.push({
quizUri: quiz.uri,
quizLabel: quiz.number ? `Quiz ${quiz.number}` : 'Quiz',
});

quizProblemMap.set(problemUri, existing);
}
}

const practiceProblemSet = new Set(practiceProblems.map((p) => p.problemId));

const examOnlyProblems = Array.from(examOnlyProblemSet)
Expand All @@ -64,6 +88,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const enrichedProblems = allProblems.map((p) => ({
...p,
examRefs: examProblemMap.get(p.problemId) ?? [],
quizRefs: quizProblemMap.get(p.problemId) ?? [],
}));

return res.status(200).json(enrichedProblems);
Expand Down
166 changes: 166 additions & 0 deletions packages/alea-frontend/pages/quiz-problems.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { useEffect, useMemo, useState } from 'react';
import { useRouter } from 'next/router';
import { Box, Chip, CircularProgress, Tooltip, Typography } from '@mui/material';

import {
FTMLProblemWithSolution,
getProblemsForQuiz,
formatQuizLabelShortFromUri,
formatQuizLabelFullFromUri,
getQuizMetadataByUri,
} from '@alea/spec';

import {
AnswerContext,
GradingContext,
QuizDisplay,
ShowGradingFor,
} from '@alea/stex-react-renderer';

import MainLayout from '../layouts/MainLayout';
import { contentFragment } from '@flexiformal/ftml-backend';

async function buildFTMLProblem(problemUri: string): Promise<FTMLProblemWithSolution> {
const fragmentResponse: any[] = await contentFragment({ uri: problemUri });
return {
problem: {
uri: problemUri,
html: fragmentResponse[2],
title_html: '',
},
answerClasses: [],
};
}

async function buildQuizProblems(
problemUris: string[]
): Promise<Record<string, FTMLProblemWithSolution>> {
const result: Record<string, FTMLProblemWithSolution> = {};
await Promise.all(
problemUris.map(async (uri) => {
result[uri] = await buildFTMLProblem(uri);
})
);
return result;
}

const QuizProblemsPage = () => {
const router = useRouter();
const quizUri = router.query.quizUri as string | undefined;
const targetProblemId = router.query.problemId as string | undefined;

const [quizMeta, setQuizMeta] = useState<any>(null);
const [problems, setProblems] = useState<Record<string, FTMLProblemWithSolution>>({});
const [loading, setLoading] = useState(true);
const [initialIndex, setInitialIndex] = useState<number>(0);

useEffect(() => {
if (!quizUri) return;

const fetchData = async () => {
setLoading(true);
try {
const decodedUri = decodeURIComponent(quizUri);

const meta = await getQuizMetadataByUri(decodedUri);
setQuizMeta(meta);

const uris = await getProblemsForQuiz(decodedUri);

if (targetProblemId) {
const idx = uris.indexOf(decodeURIComponent(targetProblemId));
if (idx !== -1) setInitialIndex(idx);
}

const quizProblems = await buildQuizProblems(uris);
setProblems(quizProblems);
} catch (error) {
console.error('Error loading quiz data:', error);
} finally {
setLoading(false);
}
};

fetchData();
}, [quizUri, targetProblemId]);

const quizLabelShort = useMemo(() => {
if (!quizUri || !quizMeta) return '';
return formatQuizLabelShortFromUri(quizUri, quizMeta);
}, [quizUri, quizMeta]);

const quizLabelFull = useMemo(() => {
if (!quizUri || !quizMeta) return '';
return formatQuizLabelFullFromUri(quizUri, quizMeta);
}, [quizUri, quizMeta]);

if (loading) {
return (
<MainLayout title="Quiz">
<Box display="flex" justifyContent="center" alignItems="center" height="80vh">
<CircularProgress />
</Box>
</MainLayout>
);
}

return (
<MainLayout title={`Review: ${quizLabelFull}`}>
<Box sx={{ px: 2, pt: 2 }}>
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 1 }}>
{quizLabelShort && (
<Tooltip
title={
<Box>
<Typography variant="subtitle2" fontWeight="bold">
{quizLabelFull}
</Typography>
<Typography variant="caption" color="inherit">
This problem belongs to this quiz
</Typography>
</Box>
}
placement="left"
arrow
>
<Chip
label={quizLabelShort}
color="primary"
sx={{
fontWeight: 600,
px: 1.5,
borderRadius: '8px',
boxShadow: '0px 3px 10px rgba(25,118,210,0.3)',
background: 'linear-gradient(90deg, #1976d2 0%, #42a5f5 100%)',
}}
/>
</Tooltip>
)}
</Box>

<GradingContext.Provider
value={{
showGradingFor: ShowGradingFor.INSTRUCTOR,
isGrading: false,
showGrading: false,
gradingInfo: undefined,
studentId: undefined,
}}
>
<AnswerContext.Provider value={{}}>
<QuizDisplay
problems={problems}
existingResponses={{}}
isFrozen={false}
showPerProblemTime={false}
isExamProblem={false}
initialProblemIdx={initialIndex}
/>
</AnswerContext.Provider>
</GradingContext.Provider>
</Box>
</MainLayout>
);
};

export default QuizProblemsPage;
4 changes: 1 addition & 3 deletions packages/stex-react-renderer/src/lib/ExamSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ export function ExamSelect({
label = 'Appeared in exams',
size = 'small',
}: ExamSelectProps) {
if (!exams.length) return null;

return (
<FormControl size={size} sx={{ minWidth: 180 }}>
<InputLabel sx={{ fontSize: '0.85rem' }}>{label}</InputLabel>
Expand All @@ -45,7 +43,7 @@ export function ExamSelect({
}}
>
<MenuItem disabled value="">
<em>Select exam</em>
<em>{exams.length ? 'Select' : 'No items available'}</em>
</MenuItem>

{exams.map((exam) => {
Expand Down
Loading
Loading