Spaces:
Sleeping
Sleeping
Dip commited on
Commit ·
834ac4e
1
Parent(s): 2810594
Ab ek or kaam karo ki agar user sub or topic mei kise ek ko choose karne
Browse files- src/ai/flows/generate-mock-exam-questions.ts +15 -13
- src/components/result-page.tsx +1 -3
- src/components/test-interface.tsx +26 -17
- src/components/test-setup-form.tsx +134 -35
- src/lib/types.ts +3 -2
src/ai/flows/generate-mock-exam-questions.ts
CHANGED
|
@@ -16,8 +16,8 @@ import {z} from 'genkit';
|
|
| 16 |
// Input Schema
|
| 17 |
const GenerateMockExamQuestionsInputSchema = z.object({
|
| 18 |
exam: z.string().describe('The name of the Indian Government Exam.'),
|
| 19 |
-
|
| 20 |
-
|
| 21 |
difficulty: z.enum(['Easy', 'Medium', 'Hard', 'Mixed']).describe('The difficulty level of the questions.'),
|
| 22 |
numberOfQuestions: z.number().int().min(1).max(100).describe('The number of questions to generate.'),
|
| 23 |
language: z.enum(['English', 'Hindi', 'Bilingual']).describe('The language for the questions and options.'),
|
|
@@ -32,6 +32,8 @@ const QuestionOutputSchema = z.object({
|
|
| 32 |
options: z.array(z.string()).length(4).describe('An array containing the four answer options (A, B, C, D).'),
|
| 33 |
correctAnswerIndex: z.number().int().min(0).max(3).describe('The 0-indexed position of the correct answer within the options array.'),
|
| 34 |
explanation: z.string().describe('A detailed explanation for the correct answer.'),
|
|
|
|
|
|
|
| 35 |
});
|
| 36 |
|
| 37 |
const GenerateMockExamQuestionsOutputSchema = z.array(QuestionOutputSchema).describe('An array of generated multiple-choice questions.');
|
|
@@ -44,11 +46,11 @@ Your task is to generate {{numberOfQuestions}} unique multiple-choice questions
|
|
| 44 |
|
| 45 |
The difficulty level should be "{{difficulty}}".
|
| 46 |
|
| 47 |
-
{{#if
|
| 48 |
-
Focus on the
|
| 49 |
{{/if}}
|
| 50 |
-
{{#if
|
| 51 |
-
|
| 52 |
{{/if}}
|
| 53 |
|
| 54 |
{{#if suggestion}}
|
|
@@ -57,26 +59,26 @@ Specifically, cover the topic: "{{topic}}".
|
|
| 57 |
|
| 58 |
**VERY IMPORTANT INSTRUCTIONS FOR ACCURACY:**
|
| 59 |
|
| 60 |
-
1. **Chain of Thought Process:** Before creating the options, first, think step-by-step to formulate the question text and
|
| 61 |
|
| 62 |
-
2. **Subject-Specific Rules:**
|
| 63 |
-
|
| 64 |
-
When the subject is "Quantitative Aptitude" or "Mathematics":
|
| 65 |
- **CRITICAL FOR RENDERING:** Wrap all mathematical formulas and expressions in single dollar signs for inline math (e.g., $x^2 + y^2 = r^2$) and double dollar signs for block-level math. For example: $$ \\frac{a}{b} $$. This is absolutely essential for the formulas to display correctly.
|
| 66 |
- Solve the question yourself step-by-step to ensure the correct answer is generated accurately before creating the options.
|
| 67 |
|
| 68 |
-
When
|
| 69 |
- Strictly adhere to established grammar rules, like those found in Wren & Martin. Avoid using ambiguous synonyms or idioms in a way that could have multiple interpretations.
|
| 70 |
|
| 71 |
-
When
|
| 72 |
- Double-check the logic of any patterns (e.g., in number or letter series). The logic must be consistent and not open to interpretation. Verify your own logic before finalizing the question.
|
| 73 |
-
{{/if}}
|
| 74 |
|
| 75 |
Your final output **MUST** be a JSON array of question objects. Each object must have:
|
| 76 |
- 'questionText': The question itself. Ensure all math expressions are wrapped in dollar signs ($...$ or $$...$$).
|
| 77 |
- 'options': An array of exactly four distinct answer choices.
|
| 78 |
- 'correctAnswerIndex': The 0-indexed number (0, 1, 2, or 3) corresponding to the correct answer in the 'options' array.
|
| 79 |
- 'explanation': Provide a detailed, step-by-step explanation in simple, easy-to-understand language. First, explain the conventional method. Then, if a faster method exists, add a section titled "**Short Trick:**" and explain the shortcut. **Ensure all mathematical expressions are in LaTeX format and correctly wrapped in dollar signs ($...$ for inline and $$...$$ for block) for proper rendering.**
|
|
|
|
|
|
|
| 80 |
|
| 81 |
Ensure the questions are relevant, clear, and unambiguous.
|
| 82 |
`;
|
|
|
|
| 16 |
// Input Schema
|
| 17 |
const GenerateMockExamQuestionsInputSchema = z.object({
|
| 18 |
exam: z.string().describe('The name of the Indian Government Exam.'),
|
| 19 |
+
subjects: z.array(z.string()).optional().describe('A list of subjects for the questions (optional).'),
|
| 20 |
+
topics: z.array(z.string()).optional().describe('A list of specific topics within the subjects (optional).'),
|
| 21 |
difficulty: z.enum(['Easy', 'Medium', 'Hard', 'Mixed']).describe('The difficulty level of the questions.'),
|
| 22 |
numberOfQuestions: z.number().int().min(1).max(100).describe('The number of questions to generate.'),
|
| 23 |
language: z.enum(['English', 'Hindi', 'Bilingual']).describe('The language for the questions and options.'),
|
|
|
|
| 32 |
options: z.array(z.string()).length(4).describe('An array containing the four answer options (A, B, C, D).'),
|
| 33 |
correctAnswerIndex: z.number().int().min(0).max(3).describe('The 0-indexed position of the correct answer within the options array.'),
|
| 34 |
explanation: z.string().describe('A detailed explanation for the correct answer.'),
|
| 35 |
+
subject: z.string().describe('The subject this question belongs to.'),
|
| 36 |
+
topic: z.string().describe('The topic this question belongs to.'),
|
| 37 |
});
|
| 38 |
|
| 39 |
const GenerateMockExamQuestionsOutputSchema = z.array(QuestionOutputSchema).describe('An array of generated multiple-choice questions.');
|
|
|
|
| 46 |
|
| 47 |
The difficulty level should be "{{difficulty}}".
|
| 48 |
|
| 49 |
+
{{#if subjects}}
|
| 50 |
+
Focus on the following subjects: {{#each subjects}}"{{this}}"{{#unless @last}}, {{/unless}}{{/each}}.
|
| 51 |
{{/if}}
|
| 52 |
+
{{#if topics}}
|
| 53 |
+
Within those subjects, specifically cover these topics: {{#each topics}}"{{this}}"{{#unless @last}}, {{/unless}}{{/each}}.
|
| 54 |
{{/if}}
|
| 55 |
|
| 56 |
{{#if suggestion}}
|
|
|
|
| 59 |
|
| 60 |
**VERY IMPORTANT INSTRUCTIONS FOR ACCURACY:**
|
| 61 |
|
| 62 |
+
1. **Chain of Thought Process:** Before creating the options, first, think step-by-step to formulate the question text, determine its subject and topic, find the single, unambiguously correct answer, and create a detailed explanation. Only after this internal verification should you create the multiple-choice options. Ensure one option is 100% correct and the others are plausible but incorrect distractors.
|
| 63 |
|
| 64 |
+
2. **Subject-Specific Rules:** For each question you generate, apply the following rules based on its subject:
|
| 65 |
+
When a question's subject is "Quantitative Aptitude" or "Mathematics":
|
|
|
|
| 66 |
- **CRITICAL FOR RENDERING:** Wrap all mathematical formulas and expressions in single dollar signs for inline math (e.g., $x^2 + y^2 = r^2$) and double dollar signs for block-level math. For example: $$ \\frac{a}{b} $$. This is absolutely essential for the formulas to display correctly.
|
| 67 |
- Solve the question yourself step-by-step to ensure the correct answer is generated accurately before creating the options.
|
| 68 |
|
| 69 |
+
When a question's subject is "English Language & Comprehension" or "English":
|
| 70 |
- Strictly adhere to established grammar rules, like those found in Wren & Martin. Avoid using ambiguous synonyms or idioms in a way that could have multiple interpretations.
|
| 71 |
|
| 72 |
+
When a question's subject is "General Intelligence & Reasoning" or "Reasoning":
|
| 73 |
- Double-check the logic of any patterns (e.g., in number or letter series). The logic must be consistent and not open to interpretation. Verify your own logic before finalizing the question.
|
|
|
|
| 74 |
|
| 75 |
Your final output **MUST** be a JSON array of question objects. Each object must have:
|
| 76 |
- 'questionText': The question itself. Ensure all math expressions are wrapped in dollar signs ($...$ or $$...$$).
|
| 77 |
- 'options': An array of exactly four distinct answer choices.
|
| 78 |
- 'correctAnswerIndex': The 0-indexed number (0, 1, 2, or 3) corresponding to the correct answer in the 'options' array.
|
| 79 |
- 'explanation': Provide a detailed, step-by-step explanation in simple, easy-to-understand language. First, explain the conventional method. Then, if a faster method exists, add a section titled "**Short Trick:**" and explain the shortcut. **Ensure all mathematical expressions are in LaTeX format and correctly wrapped in dollar signs ($...$ for inline and $$...$$ for block) for proper rendering.**
|
| 80 |
+
- 'subject': The subject this question belongs to from the provided list of subjects.
|
| 81 |
+
- 'topic': The specific topic this question belongs to.
|
| 82 |
|
| 83 |
Ensure the questions are relevant, clear, and unambiguous.
|
| 84 |
`;
|
src/components/result-page.tsx
CHANGED
|
@@ -16,7 +16,6 @@ import rehypeKatex from 'rehype-katex';
|
|
| 16 |
|
| 17 |
import type { TestResult, TestSession } from '@/lib/types';
|
| 18 |
import { offerPersonalizedStudySuggestions } from '@/ai/flows/offer-personalized-study-suggestions';
|
| 19 |
-
import { useToast } from '@/hooks/use-toast';
|
| 20 |
|
| 21 |
const COLORS = {
|
| 22 |
correct: 'hsl(var(--chart-2))',
|
|
@@ -28,7 +27,6 @@ export function ResultPageClient({ resultId }: { resultId: string }) {
|
|
| 28 |
const [suggestions, setSuggestions] = useState<string[]>([]);
|
| 29 |
const { user, isUserLoading } = useUser();
|
| 30 |
const firestore = useFirestore();
|
| 31 |
-
const { toast } = useToast();
|
| 32 |
|
| 33 |
const resultRef = useMemoFirebase(() => {
|
| 34 |
if (!user || !firestore) return null;
|
|
@@ -56,7 +54,7 @@ export function ResultPageClient({ resultId }: { resultId: string }) {
|
|
| 56 |
skippedQuestions: result.skipped,
|
| 57 |
overallAccuracy: result.accuracy,
|
| 58 |
performanceBreakdown: result.performanceBreakdown.map(p => ({
|
| 59 |
-
categoryType:
|
| 60 |
categoryName: p.category,
|
| 61 |
correctCount: p.correctCount,
|
| 62 |
incorrectCount: p.incorrectCount,
|
|
|
|
| 16 |
|
| 17 |
import type { TestResult, TestSession } from '@/lib/types';
|
| 18 |
import { offerPersonalizedStudySuggestions } from '@/ai/flows/offer-personalized-study-suggestions';
|
|
|
|
| 19 |
|
| 20 |
const COLORS = {
|
| 21 |
correct: 'hsl(var(--chart-2))',
|
|
|
|
| 27 |
const [suggestions, setSuggestions] = useState<string[]>([]);
|
| 28 |
const { user, isUserLoading } = useUser();
|
| 29 |
const firestore = useFirestore();
|
|
|
|
| 30 |
|
| 31 |
const resultRef = useMemoFirebase(() => {
|
| 32 |
if (!user || !firestore) return null;
|
|
|
|
| 54 |
skippedQuestions: result.skipped,
|
| 55 |
overallAccuracy: result.accuracy,
|
| 56 |
performanceBreakdown: result.performanceBreakdown.map(p => ({
|
| 57 |
+
categoryType: p.categoryType,
|
| 58 |
categoryName: p.category,
|
| 59 |
correctCount: p.correctCount,
|
| 60 |
incorrectCount: p.incorrectCount,
|
src/components/test-interface.tsx
CHANGED
|
@@ -130,14 +130,27 @@ export function TestInterface({ testId }: { testId: string }) {
|
|
| 130 |
let incorrectAnswers = 0;
|
| 131 |
let skipped = 0;
|
| 132 |
|
|
|
|
|
|
|
| 133 |
session.questions.forEach((q, i) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
const userAnswer = session.userAnswers[i];
|
| 135 |
if (userAnswer.status === 'not-visited' || userAnswer.status === 'not-answered' || userAnswer.status === 'marked-for-review') {
|
| 136 |
skipped++;
|
|
|
|
| 137 |
} else if (userAnswer.selectedOption === q.correctAnswerIndex) {
|
| 138 |
correctAnswers++;
|
|
|
|
| 139 |
} else {
|
| 140 |
incorrectAnswers++;
|
|
|
|
| 141 |
}
|
| 142 |
});
|
| 143 |
|
|
@@ -149,22 +162,18 @@ export function TestInterface({ testId }: { testId: string }) {
|
|
| 149 |
score -= incorrectAnswers * session.config.negativeMarkingRatio;
|
| 150 |
}
|
| 151 |
|
| 152 |
-
const performanceBreakdown: import('@/lib/types').PerformanceBreakdown[] = []
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
skippedCount: skipped,
|
| 165 |
-
totalCount: session.questions.length,
|
| 166 |
-
});
|
| 167 |
-
}
|
| 168 |
|
| 169 |
const result: TestResult = {
|
| 170 |
id: session.id,
|
|
@@ -181,7 +190,7 @@ export function TestInterface({ testId }: { testId: string }) {
|
|
| 181 |
performanceBreakdown,
|
| 182 |
};
|
| 183 |
|
| 184 |
-
const finalSession: TestSession = { ...session, userId: user.uid, endTime: endTime };
|
| 185 |
|
| 186 |
const batch = writeBatch(firestore);
|
| 187 |
|
|
|
|
| 130 |
let incorrectAnswers = 0;
|
| 131 |
let skipped = 0;
|
| 132 |
|
| 133 |
+
const performanceByCategory: { [key: string]: { correct: number; incorrect: number; skipped: number; total: number; type: 'subject' | 'topic' } } = {};
|
| 134 |
+
|
| 135 |
session.questions.forEach((q, i) => {
|
| 136 |
+
const categoryName = q.topic || q.subject;
|
| 137 |
+
const categoryType = q.topic ? 'topic' as const : 'subject' as const;
|
| 138 |
+
|
| 139 |
+
if (!performanceByCategory[categoryName]) {
|
| 140 |
+
performanceByCategory[categoryName] = { correct: 0, incorrect: 0, skipped: 0, total: 0, type: categoryType };
|
| 141 |
+
}
|
| 142 |
+
performanceByCategory[categoryName].total++;
|
| 143 |
+
|
| 144 |
const userAnswer = session.userAnswers[i];
|
| 145 |
if (userAnswer.status === 'not-visited' || userAnswer.status === 'not-answered' || userAnswer.status === 'marked-for-review') {
|
| 146 |
skipped++;
|
| 147 |
+
performanceByCategory[categoryName].skipped++;
|
| 148 |
} else if (userAnswer.selectedOption === q.correctAnswerIndex) {
|
| 149 |
correctAnswers++;
|
| 150 |
+
performanceByCategory[categoryName].correct++;
|
| 151 |
} else {
|
| 152 |
incorrectAnswers++;
|
| 153 |
+
performanceByCategory[categoryName].incorrect++;
|
| 154 |
}
|
| 155 |
});
|
| 156 |
|
|
|
|
| 162 |
score -= incorrectAnswers * session.config.negativeMarkingRatio;
|
| 163 |
}
|
| 164 |
|
| 165 |
+
const performanceBreakdown: import('@/lib/types').PerformanceBreakdown[] = Object.entries(performanceByCategory).map(([category, stats]) => {
|
| 166 |
+
const attemptedInCategory = stats.correct + stats.incorrect;
|
| 167 |
+
return {
|
| 168 |
+
category: category,
|
| 169 |
+
categoryType: stats.type,
|
| 170 |
+
accuracy: attemptedInCategory > 0 ? (stats.correct / attemptedInCategory) * 100 : 0,
|
| 171 |
+
correctCount: stats.correct,
|
| 172 |
+
incorrectCount: stats.incorrect,
|
| 173 |
+
skippedCount: stats.skipped,
|
| 174 |
+
totalCount: stats.total,
|
| 175 |
+
};
|
| 176 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
|
| 178 |
const result: TestResult = {
|
| 179 |
id: session.id,
|
|
|
|
| 190 |
performanceBreakdown,
|
| 191 |
};
|
| 192 |
|
| 193 |
+
const finalSession: TestSession = { ...session, userId: user.uid, endTime: endTime, userAnswers: session.userAnswers.map(ua => ({...ua, selectedOption: ua.selectedOption === undefined ? null : ua.selectedOption})) };
|
| 194 |
|
| 195 |
const batch = writeBatch(firestore);
|
| 196 |
|
src/components/test-setup-form.tsx
CHANGED
|
@@ -24,6 +24,15 @@ import {
|
|
| 24 |
SelectTrigger,
|
| 25 |
SelectValue,
|
| 26 |
} from "@/components/ui/select";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
import { Input } from "@/components/ui/input";
|
| 28 |
import { Switch } from "@/components/ui/switch";
|
| 29 |
import { Slider } from "@/components/ui/slider";
|
|
@@ -35,11 +44,13 @@ import type { TestSession, UserConfiguration } from "@/lib/types";
|
|
| 35 |
import { useDoc, useUser, useFirestore, useMemoFirebase } from "@/firebase";
|
| 36 |
import { doc } from "firebase/firestore";
|
| 37 |
import { Textarea } from "./ui/textarea";
|
|
|
|
|
|
|
| 38 |
|
| 39 |
const formSchema = z.object({
|
| 40 |
exam: z.string({ required_error: "Please select an exam." }),
|
| 41 |
-
|
| 42 |
-
|
| 43 |
numberOfQuestions: z.number().min(5).max(100),
|
| 44 |
difficulty: z.enum(DIFFICULTIES),
|
| 45 |
language: z.enum(LANGUAGES),
|
|
@@ -72,6 +83,8 @@ export function TestSetupForm() {
|
|
| 72 |
const form = useForm<z.infer<typeof formSchema>>({
|
| 73 |
resolver: zodResolver(formSchema),
|
| 74 |
defaultValues: {
|
|
|
|
|
|
|
| 75 |
numberOfQuestions: 10,
|
| 76 |
difficulty: "Medium",
|
| 77 |
language: "English",
|
|
@@ -80,38 +93,41 @@ export function TestSetupForm() {
|
|
| 80 |
});
|
| 81 |
|
| 82 |
const examValue = form.watch("exam");
|
| 83 |
-
const
|
| 84 |
const negativeMarkingValue = form.watch("negativeMarking");
|
| 85 |
|
| 86 |
useEffect(() => {
|
| 87 |
if (examValue) {
|
| 88 |
const availableSubjects = SUBJECTS[examValue] || [];
|
| 89 |
setSubjects(availableSubjects);
|
| 90 |
-
const
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
|
|
|
| 94 |
}
|
| 95 |
} else {
|
| 96 |
setSubjects([]);
|
| 97 |
-
form.
|
| 98 |
-
form.resetField("topic", { defaultValue: undefined });
|
| 99 |
}
|
| 100 |
-
|
|
|
|
| 101 |
|
| 102 |
useEffect(() => {
|
| 103 |
-
if (examValue &&
|
| 104 |
-
const availableTopics = TOPICS[examValue]?.[
|
| 105 |
setTopics(availableTopics);
|
| 106 |
-
const
|
| 107 |
-
|
| 108 |
-
|
|
|
|
| 109 |
}
|
| 110 |
} else {
|
| 111 |
setTopics([]);
|
| 112 |
-
form.
|
| 113 |
}
|
| 114 |
-
|
|
|
|
| 115 |
|
| 116 |
|
| 117 |
function onSubmit(values: z.infer<typeof formSchema>) {
|
|
@@ -129,8 +145,8 @@ export function TestSetupForm() {
|
|
| 129 |
try {
|
| 130 |
const questions = await generateMockExamQuestions({
|
| 131 |
exam: values.exam,
|
| 132 |
-
|
| 133 |
-
|
| 134 |
numberOfQuestions: values.numberOfQuestions,
|
| 135 |
difficulty: values.difficulty,
|
| 136 |
language: values.language,
|
|
@@ -200,32 +216,115 @@ export function TestSetupForm() {
|
|
| 200 |
/>
|
| 201 |
<FormField
|
| 202 |
control={form.control}
|
| 203 |
-
name="
|
| 204 |
render={({ field }) => (
|
| 205 |
<FormItem>
|
| 206 |
-
<FormLabel>
|
| 207 |
-
|
| 208 |
-
<
|
| 209 |
-
<
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
<FormMessage />
|
| 214 |
</FormItem>
|
| 215 |
)}
|
| 216 |
/>
|
| 217 |
<FormField
|
| 218 |
control={form.control}
|
| 219 |
-
name="
|
| 220 |
render={({ field }) => (
|
| 221 |
<FormItem>
|
| 222 |
-
<FormLabel>
|
| 223 |
-
<
|
| 224 |
-
<
|
| 225 |
-
<
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 229 |
<FormMessage />
|
| 230 |
</FormItem>
|
| 231 |
)}
|
|
|
|
| 24 |
SelectTrigger,
|
| 25 |
SelectValue,
|
| 26 |
} from "@/components/ui/select";
|
| 27 |
+
import {
|
| 28 |
+
DropdownMenu,
|
| 29 |
+
DropdownMenuCheckboxItem,
|
| 30 |
+
DropdownMenuContent,
|
| 31 |
+
DropdownMenuLabel,
|
| 32 |
+
DropdownMenuSeparator,
|
| 33 |
+
DropdownMenuTrigger,
|
| 34 |
+
} from "@/components/ui/dropdown-menu";
|
| 35 |
+
|
| 36 |
import { Input } from "@/components/ui/input";
|
| 37 |
import { Switch } from "@/components/ui/switch";
|
| 38 |
import { Slider } from "@/components/ui/slider";
|
|
|
|
| 44 |
import { useDoc, useUser, useFirestore, useMemoFirebase } from "@/firebase";
|
| 45 |
import { doc } from "firebase/firestore";
|
| 46 |
import { Textarea } from "./ui/textarea";
|
| 47 |
+
import { ScrollArea } from "./ui/scroll-area";
|
| 48 |
+
import { cn } from "@/lib/utils";
|
| 49 |
|
| 50 |
const formSchema = z.object({
|
| 51 |
exam: z.string({ required_error: "Please select an exam." }),
|
| 52 |
+
subjects: z.array(z.string()).optional(),
|
| 53 |
+
topics: z.array(z.string()).optional(),
|
| 54 |
numberOfQuestions: z.number().min(5).max(100),
|
| 55 |
difficulty: z.enum(DIFFICULTIES),
|
| 56 |
language: z.enum(LANGUAGES),
|
|
|
|
| 83 |
const form = useForm<z.infer<typeof formSchema>>({
|
| 84 |
resolver: zodResolver(formSchema),
|
| 85 |
defaultValues: {
|
| 86 |
+
subjects: [],
|
| 87 |
+
topics: [],
|
| 88 |
numberOfQuestions: 10,
|
| 89 |
difficulty: "Medium",
|
| 90 |
language: "English",
|
|
|
|
| 93 |
});
|
| 94 |
|
| 95 |
const examValue = form.watch("exam");
|
| 96 |
+
const subjectsValue = form.watch("subjects");
|
| 97 |
const negativeMarkingValue = form.watch("negativeMarking");
|
| 98 |
|
| 99 |
useEffect(() => {
|
| 100 |
if (examValue) {
|
| 101 |
const availableSubjects = SUBJECTS[examValue] || [];
|
| 102 |
setSubjects(availableSubjects);
|
| 103 |
+
const currentSubjects = form.getValues('subjects') || [];
|
| 104 |
+
const validSubjects = currentSubjects.filter(s => availableSubjects.includes(s));
|
| 105 |
+
|
| 106 |
+
if (validSubjects.length !== currentSubjects.length) {
|
| 107 |
+
form.setValue("subjects", validSubjects);
|
| 108 |
}
|
| 109 |
} else {
|
| 110 |
setSubjects([]);
|
| 111 |
+
form.setValue("subjects", []);
|
|
|
|
| 112 |
}
|
| 113 |
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
| 114 |
+
}, [examValue]);
|
| 115 |
|
| 116 |
useEffect(() => {
|
| 117 |
+
if (examValue && subjectsValue && subjectsValue.length === 1) {
|
| 118 |
+
const availableTopics = TOPICS[examValue]?.[subjectsValue[0]] || [];
|
| 119 |
setTopics(availableTopics);
|
| 120 |
+
const currentTopics = form.getValues('topics') || [];
|
| 121 |
+
const validTopics = currentTopics.filter(t => availableTopics.includes(t));
|
| 122 |
+
if(validTopics.length !== currentTopics.length) {
|
| 123 |
+
form.setValue('topics', validTopics);
|
| 124 |
}
|
| 125 |
} else {
|
| 126 |
setTopics([]);
|
| 127 |
+
form.setValue('topics', []);
|
| 128 |
}
|
| 129 |
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
| 130 |
+
}, [examValue, subjectsValue]);
|
| 131 |
|
| 132 |
|
| 133 |
function onSubmit(values: z.infer<typeof formSchema>) {
|
|
|
|
| 145 |
try {
|
| 146 |
const questions = await generateMockExamQuestions({
|
| 147 |
exam: values.exam,
|
| 148 |
+
subjects: values.subjects && values.subjects.length > 0 ? values.subjects : undefined,
|
| 149 |
+
topics: values.topics && values.topics.length > 0 ? values.topics : undefined,
|
| 150 |
numberOfQuestions: values.numberOfQuestions,
|
| 151 |
difficulty: values.difficulty,
|
| 152 |
language: values.language,
|
|
|
|
| 216 |
/>
|
| 217 |
<FormField
|
| 218 |
control={form.control}
|
| 219 |
+
name="subjects"
|
| 220 |
render={({ field }) => (
|
| 221 |
<FormItem>
|
| 222 |
+
<FormLabel>Subjects</FormLabel>
|
| 223 |
+
<DropdownMenu>
|
| 224 |
+
<DropdownMenuTrigger asChild>
|
| 225 |
+
<FormControl>
|
| 226 |
+
<Button
|
| 227 |
+
variant="outline"
|
| 228 |
+
className={cn(
|
| 229 |
+
"w-full justify-start text-left font-normal h-auto min-h-10 py-2",
|
| 230 |
+
!field.value?.length && "text-muted-foreground"
|
| 231 |
+
)}
|
| 232 |
+
disabled={subjects.length === 0}
|
| 233 |
+
>
|
| 234 |
+
{field.value?.length > 0 ? (
|
| 235 |
+
<div className="flex gap-2 flex-wrap">
|
| 236 |
+
{field.value.map(val => (
|
| 237 |
+
<div key={val} className="truncate rounded-md bg-secondary text-secondary-foreground px-2 py-1 text-xs">
|
| 238 |
+
{val}
|
| 239 |
+
</div>
|
| 240 |
+
))}
|
| 241 |
+
</div>
|
| 242 |
+
) : (
|
| 243 |
+
"Select subjects"
|
| 244 |
+
)}
|
| 245 |
+
</Button>
|
| 246 |
+
</FormControl>
|
| 247 |
+
</DropdownMenuTrigger>
|
| 248 |
+
<DropdownMenuContent className="w-[var(--radix-dropdown-menu-trigger-width)]" align="start">
|
| 249 |
+
<DropdownMenuLabel>Available Subjects</DropdownMenuLabel>
|
| 250 |
+
<DropdownMenuSeparator />
|
| 251 |
+
<ScrollArea className="h-48">
|
| 252 |
+
{subjects.map(subject => (
|
| 253 |
+
<DropdownMenuCheckboxItem
|
| 254 |
+
key={subject}
|
| 255 |
+
checked={field.value?.includes(subject)}
|
| 256 |
+
onCheckedChange={(checked) => {
|
| 257 |
+
const selected = field.value || [];
|
| 258 |
+
return checked
|
| 259 |
+
? field.onChange([...selected, subject])
|
| 260 |
+
: field.onChange(selected.filter(s => s !== subject));
|
| 261 |
+
}}
|
| 262 |
+
>
|
| 263 |
+
{subject}
|
| 264 |
+
</DropdownMenuCheckboxItem>
|
| 265 |
+
))}
|
| 266 |
+
</ScrollArea>
|
| 267 |
+
</DropdownMenuContent>
|
| 268 |
+
</DropdownMenu>
|
| 269 |
<FormMessage />
|
| 270 |
</FormItem>
|
| 271 |
)}
|
| 272 |
/>
|
| 273 |
<FormField
|
| 274 |
control={form.control}
|
| 275 |
+
name="topics"
|
| 276 |
render={({ field }) => (
|
| 277 |
<FormItem>
|
| 278 |
+
<FormLabel>Topics</FormLabel>
|
| 279 |
+
<DropdownMenu>
|
| 280 |
+
<DropdownMenuTrigger asChild>
|
| 281 |
+
<FormControl>
|
| 282 |
+
<Button
|
| 283 |
+
variant="outline"
|
| 284 |
+
className={cn(
|
| 285 |
+
"w-full justify-start text-left font-normal h-auto min-h-10 py-2",
|
| 286 |
+
!field.value?.length && "text-muted-foreground"
|
| 287 |
+
)}
|
| 288 |
+
disabled={topics.length === 0}
|
| 289 |
+
>
|
| 290 |
+
{field.value?.length > 0 ? (
|
| 291 |
+
<div className="flex gap-2 flex-wrap">
|
| 292 |
+
{field.value.map(val => (
|
| 293 |
+
<div key={val} className="truncate rounded-md bg-secondary text-secondary-foreground px-2 py-1 text-xs">
|
| 294 |
+
{val}
|
| 295 |
+
</div>
|
| 296 |
+
))}
|
| 297 |
+
</div>
|
| 298 |
+
) : (
|
| 299 |
+
"Select topics"
|
| 300 |
+
)}
|
| 301 |
+
</Button>
|
| 302 |
+
</FormControl>
|
| 303 |
+
</DropdownMenuTrigger>
|
| 304 |
+
<DropdownMenuContent className="w-[var(--radix-dropdown-menu-trigger-width)]" align="start">
|
| 305 |
+
<DropdownMenuLabel>Available Topics</DropdownMenuLabel>
|
| 306 |
+
<DropdownMenuSeparator />
|
| 307 |
+
<ScrollArea className="h-48">
|
| 308 |
+
{topics.map(topic => (
|
| 309 |
+
<DropdownMenuCheckboxItem
|
| 310 |
+
key={topic}
|
| 311 |
+
checked={field.value?.includes(topic)}
|
| 312 |
+
onCheckedChange={(checked) => {
|
| 313 |
+
const selected = field.value || [];
|
| 314 |
+
return checked
|
| 315 |
+
? field.onChange([...selected, topic])
|
| 316 |
+
: field.onChange(selected.filter(s => s !== topic));
|
| 317 |
+
}}
|
| 318 |
+
>
|
| 319 |
+
{topic}
|
| 320 |
+
</DropdownMenuCheckboxItem>
|
| 321 |
+
))}
|
| 322 |
+
</ScrollArea>
|
| 323 |
+
</DropdownMenuContent>
|
| 324 |
+
</DropdownMenu>
|
| 325 |
+
<FormDescription>
|
| 326 |
+
Topics are only available when one subject is selected.
|
| 327 |
+
</FormDescription>
|
| 328 |
<FormMessage />
|
| 329 |
</FormItem>
|
| 330 |
)}
|
src/lib/types.ts
CHANGED
|
@@ -10,8 +10,8 @@ export type AIProviderSettings = {
|
|
| 10 |
|
| 11 |
export type ExamConfig = {
|
| 12 |
exam: string;
|
| 13 |
-
|
| 14 |
-
|
| 15 |
numberOfQuestions: number;
|
| 16 |
difficulty: 'Easy' | 'Medium' | 'Hard' | 'Mixed';
|
| 17 |
language: 'English' | 'Hindi' | 'Bilingual';
|
|
@@ -45,6 +45,7 @@ export type TestSession = {
|
|
| 45 |
|
| 46 |
export type PerformanceBreakdown = {
|
| 47 |
category: string;
|
|
|
|
| 48 |
accuracy: number;
|
| 49 |
correctCount: number;
|
| 50 |
incorrectCount: number;
|
|
|
|
| 10 |
|
| 11 |
export type ExamConfig = {
|
| 12 |
exam: string;
|
| 13 |
+
subjects?: string[];
|
| 14 |
+
topics?: string[];
|
| 15 |
numberOfQuestions: number;
|
| 16 |
difficulty: 'Easy' | 'Medium' | 'Hard' | 'Mixed';
|
| 17 |
language: 'English' | 'Hindi' | 'Bilingual';
|
|
|
|
| 45 |
|
| 46 |
export type PerformanceBreakdown = {
|
| 47 |
category: string;
|
| 48 |
+
categoryType: 'subject' | 'topic';
|
| 49 |
accuracy: number;
|
| 50 |
correctCount: number;
|
| 51 |
incorrectCount: number;
|