diff --git a/docs/concepts/items.mdx b/docs/concepts/items.mdx index fb2cd787..c4f2371b 100644 --- a/docs/concepts/items.mdx +++ b/docs/concepts/items.mdx @@ -17,6 +17,11 @@ Every piece of workspace content is a workspace item. Items can be opened in tab A container for organizing related items. Folders are part of the workspace item tree and can be opened like any other workspace location. + + A study set with multiple-choice or short-answer questions, saved answer history, feedback, and explanations. Quiz creation asks the AI to emphasize application and reasoning, use plausible distractors based on misconceptions, and keep options comparable in wording and length. Correct multiple-choice answers are shuffled into a stable display order when questions are created. + + Choose Short answer when creating a quiz to write responses and receive AI feedback against a model answer and grading criteria. Each successful grading request uses an AI message from your plan. Answers lock after grading; reset quiz progress for a fresh attempt. If grading fails, the response stays in the current tab so it can be retried. Editing a short-answer question invalidates its previous grade. + ## Supported Uploads diff --git a/src/features/workspaces/components/quizzes/QuizViewer.tsx b/src/features/workspaces/components/quizzes/QuizViewer.tsx index 3ac924e8..4fe47636 100644 --- a/src/features/workspaces/components/quizzes/QuizViewer.tsx +++ b/src/features/workspaces/components/quizzes/QuizViewer.tsx @@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { Button } from "#/components/ui/button"; import { Skeleton } from "#/components/ui/skeleton"; +import { Textarea } from "#/components/ui/textarea"; import { sendComposerPrompt } from "#/features/workspaces/composer/workspace-composer-actions"; import { StudyProgressStrip } from "#/features/workspaces/components/study/StudyProgressStrip"; import { StudyRichText } from "#/features/workspaces/components/study/StudyRichText"; @@ -21,6 +22,7 @@ import type { QuizQuestion } from "#/features/workspaces/quizzes/quiz-content"; import { quizViewerQueryOptions, useRecordQuizAnswer, + useRecordQuizShortAnswer, useResetQuizStudyProgress, } from "#/features/workspaces/quizzes/quiz-queries"; import { @@ -30,6 +32,7 @@ import { } from "#/features/workspaces/quizzes/quiz-study-session"; import { getQuizAnswer, + isQuizAnswerCorrect, summarizeQuizStudyProgress, type QuizStudyState, } from "#/features/workspaces/quizzes/quiz-study-state"; @@ -137,6 +140,8 @@ function QuizStudySession({ workspaceId: item.workspaceId, }; const recordAnswer = useRecordQuizAnswer(queryInput); + const recordShortAnswer = useRecordQuizShortAnswer(queryInput); + const [responses, setResponses] = useState>({}); const { isPending: isResetting, mutate: resetProgress } = useResetQuizStudyProgress(queryInput); const startSession = useCallback( (nextMode: QuizStudyMode, nextShuffled: boolean) => { @@ -196,7 +201,7 @@ function QuizStudySession({ itemId: item.id, ...getQuizStudyViewState({ answered: answer !== undefined, - ...(answer ? { correct: answer.selectedOptionId === currentQuestion.correctOptionId } : {}), + ...(answer ? { correct: isQuizAnswerCorrect(currentQuestion, answer) } : {}), mode, progress: quizProgress, sessionPosition: currentIndex + 1, @@ -286,18 +291,21 @@ function QuizStudySession({ if (!currentQuestion) return null; const graded = answer !== undefined; - const isCorrect = answer?.selectedOptionId === currentQuestion.correctOptionId; - const selectedOptionId = graded ? answer.selectedOptionId : pendingOptionId; + const isCorrect = isQuizAnswerCorrect(currentQuestion, answer); + const selectedOptionId = + answer && "selectedOptionId" in answer ? answer.selectedOptionId : pendingOptionId; + const options = currentQuestion.kind === "multiple_choice" ? currentQuestion.options : []; + const correctOptionId = + currentQuestion.kind === "multiple_choice" ? currentQuestion.correctOptionId : null; + const response = responses[currentQuestion.id] ?? ""; const isLastQuestion = currentIndex >= studyQuestions.length - 1; // Khan-style: the pending pick, and the correct option once graded, get an // outline. Wrong picks keep only the filled X — no row chrome — so they // stay in the hairline list. const outlinedOptionIds = new Set( - [!graded ? selectedOptionId : null, graded ? currentQuestion.correctOptionId : null].filter( - Boolean, - ), + [!graded ? selectedOptionId : null, graded ? correctOptionId : null].filter(Boolean), ); - const lastOption = currentQuestion.options.at(-1); + const lastOption = options.at(-1); return (
-
- {currentQuestion.options.map((option, optionIndex) => { - const isSelected = option.id === selectedOptionId; - const isCorrectOption = option.id === currentQuestion.correctOptionId; - const previousOption = currentQuestion.options[optionIndex - 1]; - return ( -
- -