diff --git a/src/components/GenerateCourse/AICourseLesson.tsx b/src/components/GenerateCourse/AICourseLesson.tsx index a1aa1e746..2b0312c6f 100644 --- a/src/components/GenerateCourse/AICourseLesson.tsx +++ b/src/components/GenerateCourse/AICourseLesson.tsx @@ -27,6 +27,7 @@ import { queryClient } from '../../stores/query-client'; import { AICourseFollowUp } from './AICourseFollowUp'; import './AICourseFollowUp.css'; import { RegenerateLesson } from './RegenerateLesson'; +import { TestMyKnowledgeAction } from './TestMyKnowledgeAction'; type AICourseLessonProps = { courseSlug: string; @@ -203,7 +204,9 @@ export function AICourseLesson(props: AICourseLessonProps) { isLoading; const cantGoBack = - (activeModuleIndex === 0 && activeLessonIndex === 0) || isGenerating || isLoading; + (activeModuleIndex === 0 && activeLessonIndex === 0) || + isGenerating || + isLoading; return (
@@ -319,6 +322,14 @@ export function AICourseLesson(props: AICourseLessonProps) {
)} + {!isLoading && !isGenerating && !error && ( + + )} +
+
+ )} + + {error && ( +
+ + {error} +
+ )} + + {!error && isKnowledgeTestOpen && ( + + )} + + ); +} + +type ListQuestionsProps = { + isLoading: boolean; + isGenerating: boolean; + questions: Question[]; +}; + +export function ListQuestions(props: ListQuestionsProps) { + const { isLoading, isGenerating, questions } = props; + + const [selectedAnswers, setSelectedAnswers] = useState< + Record + >({}); + const [submitted, setSubmitted] = useState(false); + const [activeQuestionIndex, setActiveQuestionIndex] = useState(0); + + const activeQuestion = questions[activeQuestionIndex]; + + const handleOptionSelectChange = function ( + questionId: string, + optionId: string, + ) { + setSelectedAnswers((prev) => { + const newAnswers = { ...prev }; + + const canMultiSelect = + activeQuestion.options.filter((option) => option.isCorrect).length > 1; + const isAlreadySelected = selectedAnswers[questionId]?.includes(optionId); + + if (isAlreadySelected) { + newAnswers[questionId] = newAnswers[questionId].filter( + (id) => id !== optionId, + ); + } else { + if (canMultiSelect) { + newAnswers[questionId] = [ + ...(newAnswers[questionId] || []), + optionId, + ]; + } else { + newAnswers[questionId] = [optionId]; + } + } + + return newAnswers; + }); + }; + + const handleNext = useCallback(() => { + const isLastQuestion = activeQuestionIndex === questions.length - 1; + if (isLastQuestion) { + setSubmitted(true); + setActiveQuestionIndex(0); + return; + } + + setActiveQuestionIndex(activeQuestionIndex + 1); + }, [activeQuestionIndex, questions, submitted]); + + const handlePrevious = useCallback(() => { + setActiveQuestionIndex((prev) => Math.max(prev - 1, 0)); + }, [questions]); + + const handleTryAgain = useCallback(() => { + setSelectedAnswers({}); + setSubmitted(false); + setActiveQuestionIndex(0); + }, []); + + const correctAnswerCount = useMemo(() => { + if (!submitted) { + return 0; + } + + return questions.filter((question) => { + const selectedOptionIds = selectedAnswers[question.id]; + const correctAnswerIds = question.options + .filter((option) => option.isCorrect) + .map((option) => option.id); + + return ( + correctAnswerIds.length === selectedOptionIds?.length && + correctAnswerIds.every((correctAnswerId) => + selectedOptionIds?.includes(correctAnswerId), + ) + ); + }).length; + }, [questions, selectedAnswers, submitted]); + + if (isLoading || !questions.length) { + return ( +
+ +
+ ); + } + + return ( + + ); +} + +type QuizItemProps = { + totalQuestions: number; + correctAnswerCount: number; + counter: number; + + question: Question; + onOptionSelectChange?: (id: string, optionId: string) => void; + selectedOptionIds?: string[]; + submitted?: boolean; + + isLoading: boolean; + + onNext?: () => void; + onPrevious?: () => void; + onTryAgain?: () => void; +}; + +export function QuizItem(props: QuizItemProps) { + const { + totalQuestions, + correctAnswerCount, + counter, + isLoading, + question, + onOptionSelectChange, + selectedOptionIds, + submitted = false, + onNext, + onPrevious, + onTryAgain, + } = props; + const { id: questionId, title, options } = question; + + const canMultiSelect = + options.filter((option) => option.isCorrect).length > 1; + + const correctAnswerIds = options + .filter((option) => option.isCorrect) + .map((option) => option.id); + + const isAllCorrectAnswer = + correctAnswerIds.length === selectedOptionIds?.length && + correctAnswerIds.every((correctAnswerId) => + selectedOptionIds?.includes(correctAnswerId), + ); + const hasWrongAnswer = submitted && !isAllCorrectAnswer; + const hasCorrectAnswer = submitted && isAllCorrectAnswer; + + return ( +
+
+ + Question {counter} of {totalQuestions} + +
+
+
+ {submitted && ( + + {hasWrongAnswer ? 'Wrong' : 'Correct'} + + )} +
+ +
+

+ {title} {canMultiSelect ? '(Select Multiple)' : ''} +

+ +
+ {options.map((option, index) => { + let status: QuizOptionStatus = 'default'; + if (submitted) { + if (option.isCorrect) { + status = 'correct'; + } else if (selectedOptionIds?.includes(option.id)) { + status = 'wrong'; + } + } else { + if (selectedOptionIds?.includes(option.id)) { + status = 'selected'; + } + } + + return ( + onOptionSelectChange?.(questionId, option.id)} + submitted={submitted} + /> + ); + })} +
+ +
+
+ {submitted ? ( + + You got {correctAnswerCount} out of {totalQuestions} correct. + + + ) : ( + Answer all questions to submit + )} +
+ +
+ + +
+
+
+ + {isLoading && ( +
+ +
+ )} +
+ ); +} + +type QuizOptionStatus = 'default' | 'selected' | 'wrong' | 'correct'; + +type QuizOptionProps = { + title: string; + status?: QuizOptionStatus; + onSelect: () => void; + submitted?: boolean; +}; + +export function QuizOption(props: QuizOptionProps) { + const { title, status = 'default', onSelect, submitted = false } = props; + + return ( + + ); +} diff --git a/src/lib/ai.ts b/src/lib/ai.ts index d401da377..de9b59157 100644 --- a/src/lib/ai.ts +++ b/src/lib/ai.ts @@ -211,6 +211,62 @@ export async function readStream( reader.releaseLock(); } +export type Question = { + id: string; + title: string; + options: { + id: string; + title: string; + isCorrect: boolean; + }[]; +}; + +export function generateAiCourseLessonQuestions( + questionData: string, +): Question[] { + const questions: Question[] = []; + + const lines = questionData.split('\n'); + let currentQuestion: Question | null = null; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + if (line.startsWith('#')) { + if (currentQuestion) { + questions.push(currentQuestion); + currentQuestion = null; + } + + const title = line.replace('#', '').trim(); + currentQuestion = { + id: nanoid(), + title, + options: [], + }; + } else if (line.startsWith('-')) { + if (!currentQuestion) { + continue; + } + + let title = line.replace('-', '').trim(); + const isCorrect = title.startsWith('*'); + title = title.replace('*', '').trim(); + + currentQuestion.options.push({ + id: nanoid(), + title, + isCorrect, + }); + } + } + + if (currentQuestion) { + questions.push(currentQuestion); + } + + return questions; +} + export type SubTopic = { id: string; type: 'subtopic';