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 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
- subject: z.string().optional().describe('The subject for the questions (optional).'),
20
- topic: z.string().optional().describe('The specific topic within the subject (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,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 subject}}
48
- Focus on the subject: "{{subject}}".
49
  {{/if}}
50
- {{#if topic}}
51
- Specifically, cover the topic: "{{topic}}".
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 determine the single, unambiguously correct answer and 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.
61
 
62
- 2. **Subject-Specific Rules:**
63
- {{#if subject}}
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 the subject is "English Language & Comprehension" or "English":
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 the subject is "General Intelligence & Reasoning" or "Reasoning":
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: result.config.topic ? 'topic' as const : 'subject' as const,
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
- if (session.config.topic || session.config.subject) {
155
- const categoryName = session.config.topic || session.config.subject!;
156
- const attemptedInCategory = correctAnswers + incorrectAnswers;
157
- const accuracyInCategory = attemptedInCategory > 0 ? (correctAnswers / attemptedInCategory) * 100 : 0;
158
-
159
- performanceBreakdown.push({
160
- category: categoryName,
161
- accuracy: accuracyInCategory,
162
- correctCount: correctAnswers,
163
- incorrectCount: incorrectAnswers,
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
- subject: z.string().optional(),
42
- topic: z.string().optional(),
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 subjectValue = form.watch("subject");
84
  const negativeMarkingValue = form.watch("negativeMarking");
85
 
86
  useEffect(() => {
87
  if (examValue) {
88
  const availableSubjects = SUBJECTS[examValue] || [];
89
  setSubjects(availableSubjects);
90
- const currentSubject = form.getValues('subject');
91
- if(currentSubject && currentSubject !== 'all' && !availableSubjects.includes(currentSubject)) {
92
- form.resetField("subject", { defaultValue: undefined });
93
- form.resetField("topic", { defaultValue: undefined });
 
94
  }
95
  } else {
96
  setSubjects([]);
97
- form.resetField("subject", { defaultValue: undefined });
98
- form.resetField("topic", { defaultValue: undefined });
99
  }
100
- }, [examValue, form]);
 
101
 
102
  useEffect(() => {
103
- if (examValue && subjectValue) {
104
- const availableTopics = TOPICS[examValue]?.[subjectValue] || [];
105
  setTopics(availableTopics);
106
- const currentTopic = form.getValues('topic');
107
- if(currentTopic && currentTopic !== 'all' && !availableTopics.includes(currentTopic)) {
108
- form.resetField("topic", { defaultValue: undefined });
 
109
  }
110
  } else {
111
  setTopics([]);
112
- form.resetField("topic", { defaultValue: undefined });
113
  }
114
- }, [examValue, subjectValue, form]);
 
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
- subject: values.subject === 'all' ? undefined : values.subject,
133
- topic: values.topic === 'all' ? undefined : values.topic,
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="subject"
204
  render={({ field }) => (
205
  <FormItem>
206
- <FormLabel>Subject (Optional)</FormLabel>
207
- <Select onValueChange={field.onChange} value={field.value ?? 'all'} disabled={subjects.length === 0}>
208
- <FormControl>
209
- <SelectTrigger><SelectValue placeholder="Select a subject" /></SelectTrigger>
210
- </FormControl>
211
- <SelectContent><SelectItem value="all">All Subjects</SelectItem>{subjects.map(s => <SelectItem key={s} value={s}>{s}</SelectItem>)}</SelectContent>
212
- </Select>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
  <FormMessage />
214
  </FormItem>
215
  )}
216
  />
217
  <FormField
218
  control={form.control}
219
- name="topic"
220
  render={({ field }) => (
221
  <FormItem>
222
- <FormLabel>Topic (Optional)</FormLabel>
223
- <Select onValueChange={field.onChange} value={field.value ?? 'all'} disabled={topics.length === 0}>
224
- <FormControl>
225
- <SelectTrigger><SelectValue placeholder="Select a topic" /></SelectTrigger>
226
- </FormControl>
227
- <SelectContent><SelectItem value="all">All Topics</SelectItem>{topics.map(t => <SelectItem key={t} value={t}>{t}</SelectItem>)}</SelectContent>
228
- </Select>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- subject?: string;
14
- topic?: string;
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;