aki-008 commited on
Commit
feafade
·
1 Parent(s): 48f5789

chore: quiz ui update

Browse files
Frontend/src/components/quize/mcq.tsx CHANGED
@@ -1,5 +1,11 @@
1
  import React, { useCallback, useEffect, useMemo, useState } from "react";
2
- import { ChevronRight, CheckCircle, XCircle, ArrowLeft } from "lucide-react";
 
 
 
 
 
 
3
 
4
  // 1. Define the structure of the incoming API data
5
  interface BackendQuestion {
@@ -13,7 +19,6 @@ interface BackendQuestion {
13
  interface MCQQuizPageProps {
14
  onBack: () => void;
15
  totalTimeSeconds?: number;
16
- // 2. Add the data prop
17
  data?: {
18
  quiz: BackendQuestion[];
19
  } | null;
@@ -25,9 +30,8 @@ interface QuestionItem {
25
  id: number;
26
  question: string;
27
  options: string[];
28
- answer: string; // We will convert this to the actual option text
29
- explanation?: string; // Add explanation field
30
- // runtime fields
31
  selected?: string | null;
32
  status?: QStatus;
33
  }
@@ -50,33 +54,29 @@ const formatTime = (seconds: number) => {
50
  const MCQQuizPage: React.FC<MCQQuizPageProps> = ({
51
  onBack,
52
  totalTimeSeconds = 15 * 60,
53
- data, // Receive data
54
  }) => {
55
- // 3. Initialize state with Real Data if available, else Mock
56
  const [questions, setQuestions] = useState<QuestionItem[]>(() => {
57
  if (data && data.quiz && data.quiz.length > 0) {
58
  return data.quiz.map((q, idx) => ({
59
  id: idx + 1,
60
  question: q.question,
61
  options: q.options,
62
- // Convert "a" -> "Option Text"
63
  answer: mapBackendAnswerToText(q.answer, q.options),
64
  explanation: q.explanation,
65
  selected: null,
66
  status: idx === 0 ? "visited" : "notVisited",
67
  }));
68
  }
69
- // Fallback if no data
70
  return [];
71
  });
72
 
73
  const [currentIndex, setCurrentIndex] = useState(0);
74
- const [isAnswered, setIsAnswered] = useState(false);
75
  const [showScore, setShowScore] = useState(false);
76
  const [timeLeft, setTimeLeft] = useState(totalTimeSeconds);
77
 
78
  const totalQuestions = questions.length;
79
- // Safety check if data is empty
80
  const currentQuestion = questions[currentIndex] || {
81
  id: 0,
82
  question: "Loading...",
@@ -85,10 +85,6 @@ const MCQQuizPage: React.FC<MCQQuizPageProps> = ({
85
  status: "notVisited",
86
  };
87
 
88
- useEffect(() => {
89
- setIsAnswered(Boolean(currentQuestion?.selected));
90
- }, [currentQuestion]);
91
-
92
  // Timer
93
  useEffect(() => {
94
  if (showScore) return;
@@ -100,36 +96,41 @@ const MCQQuizPage: React.FC<MCQQuizPageProps> = ({
100
  return () => clearInterval(t);
101
  }, [timeLeft, showScore]);
102
 
 
103
  const handleAnswerClick = useCallback(
104
  (option: string) => {
105
  setQuestions((prev) =>
106
  prev.map((q, idx) =>
107
  idx === currentIndex
108
- ? { ...q, selected: option, status: "answered" }
109
  : q
110
  )
111
  );
112
- setIsAnswered(true);
113
  },
114
  [currentIndex]
115
  );
116
 
117
- const goToQuestion = useCallback((index: number) => {
118
- setQuestions((prev) =>
119
- prev.map((q, idx) => {
120
- if (idx === index) {
121
- return {
122
- ...q,
123
- status: q.status === "notVisited" ? "visited" : q.status,
124
- };
125
- }
126
- return q;
127
- })
128
- );
129
- setCurrentIndex(index);
130
- }, []);
 
 
 
 
131
 
132
  const handleSaveAndNext = useCallback(() => {
 
133
  setQuestions((prev) =>
134
  prev.map((q, idx) => {
135
  if (idx === currentIndex) {
@@ -151,9 +152,10 @@ const MCQQuizPage: React.FC<MCQQuizPageProps> = ({
151
  if (next < totalQuestions) {
152
  goToQuestion(next);
153
  } else {
 
154
  handleSubmit();
155
  }
156
- }, [currentIndex, goToQuestion, totalQuestions]);
157
 
158
  const handleMarkForReview = useCallback(() => {
159
  setQuestions((prev) =>
@@ -171,7 +173,6 @@ const MCQQuizPage: React.FC<MCQQuizPageProps> = ({
171
  idx === currentIndex ? { ...q, selected: null, status: "visited" } : q
172
  )
173
  );
174
- setIsAnswered(false);
175
  }, [currentIndex]);
176
 
177
  const computeScore = useCallback(() => {
@@ -190,19 +191,33 @@ const MCQQuizPage: React.FC<MCQQuizPageProps> = ({
190
  setShowScore(true);
191
  }, []);
192
 
 
193
  const statusClassForPalette = (q: QuestionItem, idx: number) => {
194
- if (idx === currentIndex)
195
- return "bg-blue-500 text-white ring-2 ring-blue-300";
 
 
 
 
 
 
 
 
196
  switch (q.status) {
197
  case "answered":
198
- return "bg-green-500 text-white";
 
199
  case "markedForReview":
200
- return "bg-yellow-400 text-black";
 
201
  case "visited":
202
- return "bg-red-500 text-white";
 
203
  default:
204
- return "bg-slate-300 text-slate-700";
205
  }
 
 
206
  };
207
 
208
  const score = useMemo(() => computeScore(), [questions, computeScore]);
@@ -210,24 +225,38 @@ const MCQQuizPage: React.FC<MCQQuizPageProps> = ({
210
  // --- RENDER SCORE ---
211
  if (showScore) {
212
  return (
213
- <div className="min-h-screen bg-gray-100 flex items-center justify-center p-6">
214
- <div className="w-full max-w-3xl p-8 bg-white rounded-xl shadow-lg text-center">
215
- <CheckCircle className="w-16 h-16 text-green-500 mx-auto mb-4" />
216
- <h2 className="text-3xl font-bold mb-2">Test Complete</h2>
217
- <p className="text-gray-600 mb-6">Your results are below.</p>
218
-
219
- <div className="text-center mb-6">
220
- <div className="text-xl text-gray-500">Score</div>
221
- <div className="text-5xl font-extrabold text-blue-600">{score}</div>
222
- <div className="text-sm text-gray-500">out of {totalQuestions}</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  </div>
224
 
225
  <div className="flex justify-center gap-3">
226
  <button
227
  onClick={onBack}
228
- className="px-6 py-3 rounded-lg border border-slate-300 text-slate-700 bg-white hover:bg-slate-50"
229
  >
230
- Back to Generator
231
  </button>
232
  </div>
233
  </div>
@@ -235,12 +264,14 @@ const MCQQuizPage: React.FC<MCQQuizPageProps> = ({
235
  );
236
  }
237
 
238
- // Handle Loading/Empty State
239
  if (questions.length === 0) {
240
  return (
241
- <div className="min-h-screen flex items-center justify-center bg-gray-50 text-gray-500">
242
  No questions available. Please try generating again.
243
- <button onClick={onBack} className="ml-4 text-blue-500 underline">
 
 
 
244
  Go Back
245
  </button>
246
  </div>
@@ -249,154 +280,167 @@ const MCQQuizPage: React.FC<MCQQuizPageProps> = ({
249
 
250
  // --- MAIN QUIZ UI ---
251
  return (
252
- <div className="min-h-screen bg-gray-50 p-6 lg:p-12">
253
- <div className="max-w-7xl mx-auto grid grid-cols-12 gap-6">
254
- {/* Left: Main Question area */}
255
- <div className="col-span-12 lg:col-span-8">
256
- <div className="mb-4 flex items-center justify-between">
257
- <div>
258
- <button
259
- onClick={onBack}
260
- className="flex items-center text-blue-600 hover:text-blue-500"
261
- >
262
- <ArrowLeft className="w-4 h-4 mr-2" />
263
- Back to Generator
264
- </button>
265
- <h1 className="text-2xl lg:text-3xl font-bold mt-3">
266
- Generated MCQ Quiz
267
- </h1>
268
- </div>
269
 
270
- <div className="hidden md:flex flex-col items-end text-sm text-gray-600">
271
- <div className="text-xl font-bold text-blue-600">
272
- {currentIndex + 1} / {totalQuestions}
273
- </div>
 
 
 
 
 
 
 
 
 
274
  </div>
275
  </div>
276
 
277
- <div className="bg-white rounded-xl shadow p-6 mb-6 border border-slate-200">
278
- <div className="mb-6">
279
- <h2 className="text-xl lg:text-2xl font-semibold text-slate-800">
280
  {currentQuestion.question}
281
  </h2>
282
  </div>
283
 
284
- <div className="grid gap-4">
285
  {currentQuestion.options.map((opt, i) => {
286
  const isSelected = currentQuestion.selected === opt;
287
- // Determine styling logic
 
288
  let optionClass =
289
- "bg-white hover:bg-slate-50 border border-slate-200 text-slate-800";
290
-
291
- if (isAnswered) {
292
- if (opt === currentQuestion.answer) {
293
- optionClass =
294
- "bg-green-600/80 text-white border-green-500 shadow";
295
- } else if (isSelected) {
296
- optionClass =
297
- "bg-red-600/80 text-white border-red-500 shadow";
298
- } else {
299
- optionClass =
300
- "bg-slate-50 border-slate-200 text-slate-700 opacity-70";
301
- }
302
- } else if (isSelected) {
303
- optionClass = "bg-blue-600/80 text-white";
304
  }
305
 
306
  return (
307
  <button
308
  key={i}
309
  onClick={() => handleAnswerClick(opt)}
310
- disabled={isAnswered}
311
- className={`p-4 rounded-lg text-left border transition flex justify-between items-center ${optionClass}`}
312
  >
313
- <span>{opt}</span>
314
- {isAnswered && opt === currentQuestion.answer && (
315
- <CheckCircle className="w-5 h-5 text-green-100" />
316
- )}
317
- {isAnswered &&
318
- isSelected &&
319
- opt !== currentQuestion.answer && (
320
- <XCircle className="w-5 h-5 text-red-200" />
 
 
 
 
 
321
  )}
 
322
  </button>
323
  );
324
  })}
325
  </div>
326
 
327
- {/* Explanation Section (Only visible after answering) */}
328
- {isAnswered && currentQuestion.explanation && (
329
- <div className="mt-6 p-4 bg-blue-50 border border-blue-100 rounded-lg text-blue-800 text-sm">
330
- <strong>Explanation:</strong> {currentQuestion.explanation}
331
- </div>
332
- )}
333
-
334
  {/* Bottom actions */}
335
- <div className="mt-6 flex flex-col md:flex-row gap-3 md:gap-4 items-center justify-between">
336
- <div className="flex gap-2 flex-wrap">
337
  <button
338
  onClick={handleMarkForReview}
339
- className="px-4 py-2 rounded-md bg-yellow-400 text-black font-medium"
340
  >
341
  Mark for Review
342
  </button>
343
  <button
344
  onClick={handleClearResponse}
345
- disabled={isAnswered}
346
- className="px-4 py-2 rounded-md border border-slate-300 text-slate-700 bg-white disabled:opacity-50"
347
- >
348
- Clear Response
349
- </button>
350
- <button
351
- onClick={handleSaveAndNext}
352
- className="px-4 py-2 rounded-md bg-gradient-to-r from-blue-500 to-blue-700 text-white font-medium flex items-center gap-2"
353
  >
354
- Save & Next <ChevronRight className="w-4 h-4" />
355
  </button>
356
  </div>
 
 
 
 
 
 
 
 
 
 
357
  </div>
358
  </div>
359
  </div>
360
 
361
  {/* Right: Sidebar */}
362
  <aside className="col-span-12 lg:col-span-4">
363
- <div className="sticky top-6 space-y-4">
364
- {/* Timer */}
365
- <div className="bg-white rounded-xl p-4 shadow border border-slate-200 text-center">
366
- <div className="text-sm text-gray-500">Time Left</div>
367
- <div className="mt-2 text-3xl font-bold text-red-600">
368
- {formatTime(Math.max(0, timeLeft))}
 
 
 
 
369
  </div>
370
  </div>
371
 
372
- {/* Palette */}
373
- <div className="bg-white rounded-xl p-4 shadow border border-slate-200">
374
- <div className="flex items-center justify-between mb-3">
375
- <h3 className="text-sm font-semibold">Question Palette</h3>
376
- </div>
377
- <div className="grid grid-cols-5 gap-2">
 
378
  {questions.map((q, idx) => (
379
  <button
380
  key={q.id}
381
  onClick={() => goToQuestion(idx)}
382
- className={`w-full aspect-square rounded-md flex items-center justify-center font-medium ${statusClassForPalette(
383
- q,
384
- idx
385
- )}`}
386
  >
387
  {idx + 1}
388
  </button>
389
  ))}
390
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
391
  </div>
392
 
 
393
  <button
394
  onClick={() => {
395
- if (window.confirm("Submit test now?")) handleSubmit();
 
396
  }}
397
- className="w-full px-4 py-2 rounded-md bg-red-600 text-white font-semibold"
398
  >
399
- Submit Test
400
  </button>
401
  </div>
402
  </aside>
 
1
  import React, { useCallback, useEffect, useMemo, useState } from "react";
2
+ import {
3
+ ChevronRight,
4
+ CheckCircle,
5
+ ArrowLeft,
6
+ Clock,
7
+ AlertCircle,
8
+ } from "lucide-react";
9
 
10
  // 1. Define the structure of the incoming API data
11
  interface BackendQuestion {
 
19
  interface MCQQuizPageProps {
20
  onBack: () => void;
21
  totalTimeSeconds?: number;
 
22
  data?: {
23
  quiz: BackendQuestion[];
24
  } | null;
 
30
  id: number;
31
  question: string;
32
  options: string[];
33
+ answer: string;
34
+ explanation?: string;
 
35
  selected?: string | null;
36
  status?: QStatus;
37
  }
 
54
  const MCQQuizPage: React.FC<MCQQuizPageProps> = ({
55
  onBack,
56
  totalTimeSeconds = 15 * 60,
57
+ data,
58
  }) => {
59
+ // 3. Initialize state
60
  const [questions, setQuestions] = useState<QuestionItem[]>(() => {
61
  if (data && data.quiz && data.quiz.length > 0) {
62
  return data.quiz.map((q, idx) => ({
63
  id: idx + 1,
64
  question: q.question,
65
  options: q.options,
 
66
  answer: mapBackendAnswerToText(q.answer, q.options),
67
  explanation: q.explanation,
68
  selected: null,
69
  status: idx === 0 ? "visited" : "notVisited",
70
  }));
71
  }
 
72
  return [];
73
  });
74
 
75
  const [currentIndex, setCurrentIndex] = useState(0);
 
76
  const [showScore, setShowScore] = useState(false);
77
  const [timeLeft, setTimeLeft] = useState(totalTimeSeconds);
78
 
79
  const totalQuestions = questions.length;
 
80
  const currentQuestion = questions[currentIndex] || {
81
  id: 0,
82
  question: "Loading...",
 
85
  status: "notVisited",
86
  };
87
 
 
 
 
 
88
  // Timer
89
  useEffect(() => {
90
  if (showScore) return;
 
96
  return () => clearInterval(t);
97
  }, [timeLeft, showScore]);
98
 
99
+ // Handle Selection (Just select, don't validate yet)
100
  const handleAnswerClick = useCallback(
101
  (option: string) => {
102
  setQuestions((prev) =>
103
  prev.map((q, idx) =>
104
  idx === currentIndex
105
+ ? { ...q, selected: option, status: "answered" } // Mark as answered locally
106
  : q
107
  )
108
  );
 
109
  },
110
  [currentIndex]
111
  );
112
 
113
+ const goToQuestion = useCallback(
114
+ (index: number) => {
115
+ setQuestions((prev) =>
116
+ prev.map((q, idx) => {
117
+ // If leaving a question that wasn't answered and wasn't marked for review, mark as visited (red)
118
+ if (idx === currentIndex) {
119
+ // Keep existing status if answered or marked
120
+ if (q.status === "answered" || q.status === "markedForReview")
121
+ return q;
122
+ return { ...q, status: "visited" };
123
+ }
124
+ return q;
125
+ })
126
+ );
127
+ setCurrentIndex(index);
128
+ },
129
+ [currentIndex]
130
+ ); // Added currentIndex dependency
131
 
132
  const handleSaveAndNext = useCallback(() => {
133
+ // Logic handled in goToQuestion essentially, but we ensure status update
134
  setQuestions((prev) =>
135
  prev.map((q, idx) => {
136
  if (idx === currentIndex) {
 
152
  if (next < totalQuestions) {
153
  goToQuestion(next);
154
  } else {
155
+ // Last question
156
  handleSubmit();
157
  }
158
+ }, [currentIndex, goToQuestion, totalQuestions]); // Added dependencies
159
 
160
  const handleMarkForReview = useCallback(() => {
161
  setQuestions((prev) =>
 
173
  idx === currentIndex ? { ...q, selected: null, status: "visited" } : q
174
  )
175
  );
 
176
  }, [currentIndex]);
177
 
178
  const computeScore = useCallback(() => {
 
191
  setShowScore(true);
192
  }, []);
193
 
194
+ // Palette Status Colors
195
  const statusClassForPalette = (q: QuestionItem, idx: number) => {
196
+ const isActive = idx === currentIndex;
197
+ const baseClass =
198
+ "w-full aspect-square rounded-md flex items-center justify-center font-bold text-sm transition-all border-2";
199
+
200
+ // Active Border Highlight (Yellow)
201
+ const borderClass = isActive
202
+ ? "border-[#F7E396] shadow-[0_0_10px_#F7E396]"
203
+ : "border-transparent";
204
+
205
+ let bgClass = "";
206
  switch (q.status) {
207
  case "answered":
208
+ bgClass = "bg-green-500 text-white";
209
+ break;
210
  case "markedForReview":
211
+ bgClass = "bg-yellow-400 text-[#434E78]";
212
+ break;
213
  case "visited":
214
+ bgClass = "bg-red-500 text-white"; // Visited but skipped
215
+ break;
216
  default:
217
+ bgClass = "bg-[#434E78]/50 text-gray-400"; // Not visited
218
  }
219
+
220
+ return `${baseClass} ${bgClass} ${borderClass}`;
221
  };
222
 
223
  const score = useMemo(() => computeScore(), [questions, computeScore]);
 
225
  // --- RENDER SCORE ---
226
  if (showScore) {
227
  return (
228
+ <div className="min-h-screen bg-[#434E78] flex items-center justify-center p-6 relative overflow-hidden">
229
+ {/* Background Blobs */}
230
+ <div className="absolute top-[-10%] right-[-5%] w-96 h-96 bg-[#F7E396] rounded-full mix-blend-overlay filter blur-3xl opacity-10 animate-blob pointer-events-none"></div>
231
+ <div className="absolute bottom-[-10%] left-[-10%] w-96 h-96 bg-[#607B8F] rounded-full mix-blend-overlay filter blur-3xl opacity-10 animate-blob animation-delay-2000 pointer-events-none"></div>
232
+
233
+ <div className="w-full max-w-2xl p-10 bg-[#607B8F] rounded-2xl shadow-2xl text-center border border-white/10 relative z-10">
234
+ <CheckCircle className="w-20 h-20 text-[#F7E396] mx-auto mb-6" />
235
+ <h2 className="text-4xl font-bold mb-2 text-white font-handwriting">
236
+ Test Complete
237
+ </h2>
238
+ <p className="text-gray-200 mb-8">
239
+ You have successfully submitted the quiz.
240
+ </p>
241
+
242
+ <div className="text-center mb-10 p-6 bg-[#434E78]/50 rounded-xl border border-white/10">
243
+ <div className="text-sm text-gray-300 uppercase tracking-widest mb-2">
244
+ Your Score
245
+ </div>
246
+ <div className="text-6xl font-extrabold text-[#F7E396] drop-shadow-md">
247
+ {score}
248
+ </div>
249
+ <div className="text-lg text-gray-300 mt-2">
250
+ out of {totalQuestions}
251
+ </div>
252
  </div>
253
 
254
  <div className="flex justify-center gap-3">
255
  <button
256
  onClick={onBack}
257
+ className="px-8 py-4 rounded-xl bg-[#F7E396] text-[#434E78] font-bold hover:bg-[#E97F4A] hover:text-white transition shadow-lg flex items-center gap-2"
258
  >
259
+ <ArrowLeft className="w-5 h-5" /> Back to Generator
260
  </button>
261
  </div>
262
  </div>
 
264
  );
265
  }
266
 
 
267
  if (questions.length === 0) {
268
  return (
269
+ <div className="min-h-screen flex items-center justify-center bg-[#434E78] text-gray-300">
270
  No questions available. Please try generating again.
271
+ <button
272
+ onClick={onBack}
273
+ className="ml-4 text-[#F7E396] underline hover:text-[#E97F4A]"
274
+ >
275
  Go Back
276
  </button>
277
  </div>
 
280
 
281
  // --- MAIN QUIZ UI ---
282
  return (
283
+ <div className="min-h-screen bg-[#434E78] text-white p-4 lg:p-8 font-sans relative overflow-hidden">
284
+ {/* Background Blobs */}
285
+ <div className="absolute top-[-10%] right-[-5%] w-96 h-96 bg-[#F7E396] rounded-full mix-blend-overlay filter blur-3xl opacity-10 animate-blob pointer-events-none"></div>
286
+ <div className="absolute bottom-[-10%] left-[-10%] w-96 h-96 bg-[#607B8F] rounded-full mix-blend-overlay filter blur-3xl opacity-10 animate-blob animation-delay-2000 pointer-events-none"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
287
 
288
+ <div className="max-w-7xl mx-auto grid grid-cols-1 lg:grid-cols-12 gap-6 relative z-10 h-full">
289
+ {/* Left: Main Question area */}
290
+ <div className="col-span-12 lg:col-span-8 flex flex-col">
291
+ <div className="mb-6 flex items-center justify-between">
292
+ <button
293
+ onClick={onBack}
294
+ className="flex items-center text-gray-300 hover:text-[#F7E396] transition"
295
+ >
296
+ <ArrowLeft className="w-5 h-5 mr-2" />
297
+ Exit Quiz
298
+ </button>
299
+ <div className="hidden md:block text-[#F7E396] font-bold text-lg bg-[#607B8F] px-4 py-1 rounded-full border border-white/10 shadow-sm">
300
+ Question {currentIndex + 1} / {totalQuestions}
301
  </div>
302
  </div>
303
 
304
+ <div className="bg-[#607B8F] rounded-2xl shadow-xl p-8 mb-6 border border-white/10 flex-1 flex flex-col">
305
+ <div className="mb-8">
306
+ <h2 className="text-xl lg:text-2xl font-bold text-white leading-relaxed">
307
  {currentQuestion.question}
308
  </h2>
309
  </div>
310
 
311
+ <div className="grid gap-4 flex-1 content-start">
312
  {currentQuestion.options.map((opt, i) => {
313
  const isSelected = currentQuestion.selected === opt;
314
+
315
+ // Styling: Only show selection state, NOT correctness
316
  let optionClass =
317
+ "bg-[#434E78]/40 border border-white/10 text-gray-200 hover:bg-[#434E78]/60";
318
+
319
+ if (isSelected) {
320
+ // Active selection styling
321
+ optionClass =
322
+ "bg-[#F7E396] text-[#434E78] border-[#F7E396] font-bold shadow-md transform scale-[1.01]";
 
 
 
 
 
 
 
 
 
323
  }
324
 
325
  return (
326
  <button
327
  key={i}
328
  onClick={() => handleAnswerClick(opt)}
329
+ className={`p-5 rounded-xl text-left transition-all duration-200 flex justify-between items-center group ${optionClass}`}
 
330
  >
331
+ <span className="text-lg">{opt}</span>
332
+ {/* Circle Indicator */}
333
+ <div
334
+ className={`w-6 h-6 rounded-full border-2 flex items-center justify-center
335
+ ${
336
+ isSelected
337
+ ? "border-[#434E78]"
338
+ : "border-gray-400 group-hover:border-[#F7E396]"
339
+ }
340
+ `}
341
+ >
342
+ {isSelected && (
343
+ <div className="w-3 h-3 rounded-full bg-[#434E78]"></div>
344
  )}
345
+ </div>
346
  </button>
347
  );
348
  })}
349
  </div>
350
 
 
 
 
 
 
 
 
351
  {/* Bottom actions */}
352
+ <div className="mt-10 pt-6 border-t border-white/10 flex flex-col md:flex-row gap-4 items-center justify-between">
353
+ <div className="flex gap-3 w-full md:w-auto">
354
  <button
355
  onClick={handleMarkForReview}
356
+ className="flex-1 md:flex-none px-6 py-3 rounded-lg bg-yellow-400 text-[#434E78] font-bold hover:bg-yellow-300 transition shadow-md"
357
  >
358
  Mark for Review
359
  </button>
360
  <button
361
  onClick={handleClearResponse}
362
+ disabled={!currentQuestion.selected}
363
+ className="flex-1 md:flex-none px-6 py-3 rounded-lg border border-gray-400 text-gray-300 hover:bg-white/10 hover:text-white disabled:opacity-50 disabled:cursor-not-allowed transition"
 
 
 
 
 
 
364
  >
365
+ Clear Selection
366
  </button>
367
  </div>
368
+
369
+ <button
370
+ onClick={handleSaveAndNext}
371
+ className="w-full md:w-auto px-8 py-3 rounded-lg bg-[#F7E396] text-[#434E78] font-bold hover:bg-[#E97F4A] hover:text-white transition shadow-lg flex items-center justify-center gap-2"
372
+ >
373
+ {currentIndex === totalQuestions - 1
374
+ ? "Submit Quiz"
375
+ : "Save & Next"}
376
+ <ChevronRight className="w-5 h-5" />
377
+ </button>
378
  </div>
379
  </div>
380
  </div>
381
 
382
  {/* Right: Sidebar */}
383
  <aside className="col-span-12 lg:col-span-4">
384
+ <div className="sticky top-6 space-y-6">
385
+ {/* Timer Card */}
386
+ <div className="bg-[#607B8F] rounded-2xl p-6 shadow-xl border border-white/10 text-center relative overflow-hidden">
387
+ <div className="relative z-10">
388
+ <div className="flex items-center justify-center gap-2 text-gray-300 mb-2">
389
+ <Clock className="w-4 h-4" /> Time Remaining
390
+ </div>
391
+ <div className="text-5xl font-mono font-bold text-[#F7E396] tracking-wider">
392
+ {formatTime(Math.max(0, timeLeft))}
393
+ </div>
394
  </div>
395
  </div>
396
 
397
+ {/* Palette Card */}
398
+ <div className="bg-[#607B8F] rounded-2xl p-6 shadow-xl border border-white/10">
399
+ <h3 className="text-lg font-bold text-white mb-4 flex items-center gap-2 border-b border-white/10 pb-3">
400
+ Question Palette
401
+ </h3>
402
+
403
+ <div className="grid grid-cols-5 gap-3">
404
  {questions.map((q, idx) => (
405
  <button
406
  key={q.id}
407
  onClick={() => goToQuestion(idx)}
408
+ className={statusClassForPalette(q, idx)}
 
 
 
409
  >
410
  {idx + 1}
411
  </button>
412
  ))}
413
  </div>
414
+
415
+ {/* Legend */}
416
+ <div className="mt-6 grid grid-cols-2 gap-y-3 gap-x-2 text-xs text-gray-300">
417
+ <div className="flex items-center gap-2">
418
+ <div className="w-3 h-3 bg-green-500 rounded-sm"></div>{" "}
419
+ Answered
420
+ </div>
421
+ <div className="flex items-center gap-2">
422
+ <div className="w-3 h-3 bg-red-500 rounded-sm"></div> Skipped
423
+ </div>
424
+ <div className="flex items-center gap-2">
425
+ <div className="w-3 h-3 bg-yellow-400 rounded-sm"></div>{" "}
426
+ Review
427
+ </div>
428
+ <div className="flex items-center gap-2">
429
+ <div className="w-3 h-3 bg-[#434E78]/50 rounded-sm border border-gray-500"></div>{" "}
430
+ Not Visited
431
+ </div>
432
+ </div>
433
  </div>
434
 
435
+ {/* Submit Button */}
436
  <button
437
  onClick={() => {
438
+ if (window.confirm("Are you sure you want to submit the test?"))
439
+ handleSubmit();
440
  }}
441
+ className="w-full px-6 py-4 rounded-xl bg-red-500 text-white font-bold hover:bg-red-600 transition shadow-lg flex items-center justify-center gap-2"
442
  >
443
+ <AlertCircle className="w-5 h-5" /> Submit Test
444
  </button>
445
  </div>
446
  </aside>
Frontend/src/pages/quize.tsx CHANGED
@@ -1,35 +1,25 @@
1
- import React, { useState, useRef, useEffect } from "react";
2
  import {
3
  FileSpreadsheet,
4
- Code2,
5
  ListChecks,
6
  Upload,
7
  X,
8
  Loader2,
9
  Clock,
 
 
 
10
  } from "lucide-react";
11
  import MCQQuizPage from "../components/quize/mcq";
12
- import API from "../api/api"; // Import your Axios instance
13
  import * as pdfjsLib from "pdfjs-dist";
14
 
15
  // Initialize PDF worker
16
  pdfjsLib.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjsLib.version}/build/pdf.worker.min.mjs`;
17
 
18
- const CodingQuizPage = ({ onBack }: any) => (
19
- <div className="min-h-screen bg-black text-white flex flex-col items-center justify-center">
20
- <h1 className="text-4xl font-bold mb-6">Coding Quiz Coming Soon...</h1>
21
- <button
22
- onClick={onBack}
23
- className="px-6 py-3 bg-blue-600 rounded-lg text-white"
24
- >
25
- ← Back
26
- </button>
27
- </div>
28
- );
29
-
30
  const ResumeGeneratedQuize: React.FC = () => {
31
  const [showQuiz, setShowQuiz] = useState(false);
32
- const [quizType, setQuizType] = useState<"mcq" | "coding" | null>(null);
33
  const [uploadType, setUploadType] = useState<"resume" | "notes" | null>(null);
34
  const [uploadedFile, setUploadedFile] = useState<string | null>(null);
35
  const [fileError, setFileError] = useState<string | null>(null);
@@ -38,8 +28,7 @@ const ResumeGeneratedQuize: React.FC = () => {
38
 
39
  // Configuration State
40
  const [customPrompt, setCustomPrompt] = useState("");
41
- const [duration, setDuration] = useState(15); // Default 15 minutes
42
-
43
  const [quizData, setQuizData] = useState(null);
44
 
45
  const resumeInputRef = useRef<HTMLInputElement>(null);
@@ -84,31 +73,26 @@ const ResumeGeneratedQuize: React.FC = () => {
84
  setFileObject(null);
85
  };
86
 
87
- // --- PDF TEXT EXTRACTION ---
88
  const extractTextFromPDF = async (file: File): Promise<string> => {
89
  const arrayBuffer = await file.arrayBuffer();
90
  const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
91
  let fullText = "";
92
-
93
  for (let i = 1; i <= pdf.numPages; i++) {
94
  const page = await pdf.getPage(i);
95
  const textContent = await page.getTextContent();
96
  const pageText = textContent.items.map((item: any) => item.str).join(" ");
97
  fullText += pageText + "\n";
98
  }
99
-
100
  return fullText;
101
  };
102
 
103
- // ---------- GENERATE QUIZ ----------
104
  const generateQuiz = async () => {
105
- if (!fileObject || !quizType || !uploadType) return; // Added uploadType check
106
 
107
  setIsProcessing(true);
108
  setFileError(null);
109
 
110
  try {
111
- // 1. Extract text on the client side
112
  console.log("Extracting text from PDF...");
113
  const extractedText = await extractTextFromPDF(fileObject);
114
 
@@ -118,30 +102,22 @@ const ResumeGeneratedQuize: React.FC = () => {
118
  );
119
  }
120
 
121
- // 2. Prepare Payload
122
- // Both /resume and /notes endpoints accept 'parsed_doc' and 'user_prompt'
123
  const payload = {
124
  parsed_doc: extractedText.trim(),
125
  user_prompt:
126
  customPrompt.trim() || "Generate a quiz based on this content.",
127
  };
128
 
129
- // 3. Determine Endpoint based on Upload Type
130
- // If uploadType is "notes", use /quiz/notes (which ingests data)
131
- // If uploadType is "resume", use /quiz/resume (transient)
132
  const endpoint = uploadType === "notes" ? "/quiz/notes" : "/quiz/resume";
133
 
134
  console.log(`Sending to ${endpoint}...`);
135
 
136
- // 4. Send to Backend
137
  const response = await API.post(endpoint, payload);
138
-
139
  setQuizData(response.data);
140
  setShowQuiz(true);
141
  } catch (error: any) {
142
  console.error("Quiz Generation Error:", error);
143
  let errorMessage = "Failed to generate quiz. Check login status.";
144
-
145
  if (error.response?.data?.detail) {
146
  if (typeof error.response.data.detail === "string") {
147
  errorMessage = error.response.data.detail;
@@ -157,229 +133,271 @@ const ResumeGeneratedQuize: React.FC = () => {
157
  } else if (error.code === "ERR_BAD_REQUEST") {
158
  errorMessage = "Server rejected the data. Are you logged in?";
159
  }
160
-
161
  setFileError(errorMessage);
162
  } finally {
163
  setIsProcessing(false);
164
  }
165
  };
166
 
167
- // Load quiz UI
168
  if (showQuiz) {
169
- if (quizType === "mcq")
170
- return (
171
- <MCQQuizPage
172
- data={quizData}
173
- onBack={() => setShowQuiz(false)}
174
- totalTimeSeconds={duration * 60} // Pass user selected time
175
- />
176
- );
177
- if (quizType === "coding")
178
- return (
179
- <CodingQuizPage data={quizData} onBack={() => setShowQuiz(false)} />
180
- );
181
  }
182
 
183
  // ---- UI COMPONENTS ----
184
- const buttonClass =
185
- "w-full py-3 rounded-lg bg-gradient-to-r from-blue-500 to-blue-700 hover:from-blue-400 hover:to-blue-600 font-medium transition shadow-lg shadow-blue-500/30 text-white mt-4 flex justify-center items-center gap-2";
186
 
187
  const OutputTypeOption = ({ icon, title, desc, value }: any) => (
188
  <div
189
  onClick={() => setQuizType(value)}
190
- className={`p-4 rounded-xl border transition cursor-pointer
191
  ${
192
  quizType === value
193
- ? "bg-blue-600/30 border-blue-500 shadow-lg shadow-blue-500/30"
194
- : "bg-slate-900/50 border-slate-700 hover:bg-slate-800/60"
195
  }
196
  `}
197
  >
198
- <div className="flex items-center gap-3 mb-2">
199
- {icon}
200
- <h4 className="text-xl font-semibold text-gray-50">{title}</h4>
201
- </div>
202
- <p className="text-gray-400 text-sm">{desc}</p>
203
- </div>
204
- );
205
-
206
- const SourceCard = () => (
207
- <div className="p-6 bg-slate-900/60 rounded-2xl shadow-xl border border-slate-700 hover:bg-slate-800/70 transition flex flex-col h-full">
208
- <div className="flex items-start gap-4 mb-4">
209
- <FileSpreadsheet className="w-8 h-8 text-cyan-400" />
210
- <div>
211
- <h2 className="text-2xl font-bold">Upload Materials</h2>
212
- <p className="text-gray-400 mt-1 text-sm">
213
- Upload your resume or notes (Max 10MB)
214
- </p>
215
- </div>
216
- </div>
217
-
218
- <input
219
- ref={resumeInputRef}
220
- type="file"
221
- accept=".pdf"
222
- onChange={(e) => handleFileUpload(e, "resume")}
223
- className="hidden"
224
- />
225
- <input
226
- ref={notesInputRef}
227
- type="file"
228
- accept=".pdf"
229
- onChange={(e) => handleFileUpload(e, "notes")}
230
- className="hidden"
231
- />
232
-
233
- <div className="flex gap-3 mt-4">
234
- <button
235
- onClick={handleResumeClick}
236
- className="flex-1 py-2 px-4 bg-cyan-600 hover:bg-cyan-700 rounded-lg text-white font-medium transition flex items-center justify-center gap-2"
237
  >
238
- <Upload className="w-4 h-4" />
239
- Resume
240
- </button>
241
- <button
242
- onClick={handleNotesClick}
243
- className="flex-1 py-2 px-4 bg-purple-600 hover:bg-purple-700 rounded-lg text-white font-medium transition flex items-center justify-center gap-2"
 
 
244
  >
245
- <Upload className="w-4 h-4" />
246
- Notes
247
- </button>
248
  </div>
249
-
250
- {fileError && (
251
- <div className="mt-4 p-3 bg-red-500/20 border border-red-500/50 rounded-lg">
252
- <p className="text-sm text-red-300">{fileError}</p>
253
- </div>
254
- )}
255
-
256
- {uploadedFile && (
257
- <div className="mt-4 p-3 bg-green-500/20 border border-green-500/50 rounded-lg flex items-center justify-between">
258
- <div>
259
- <p className="text-sm text-green-300 font-medium">
260
- File uploaded successfully
261
- </p>
262
- <p className="text-xs text-green-200 mt-1">{uploadedFile}</p>
263
- <p className="text-xs text-green-200">
264
- {uploadType === "resume" ? "Resume" : "Notes"}
265
- </p>
266
- </div>
267
- <button
268
- onClick={clearUpload}
269
- className="text-green-300 hover:text-green-200"
270
- >
271
- <X className="w-4 h-4" />
272
- </button>
273
- </div>
274
- )}
275
  </div>
276
  );
277
 
278
  return (
279
- <div className="min-h-screen bg-black text-white py-20 px-6 lg:px-12">
280
- <h1 className="text-4xl font-bold mb-16 text-center">
281
- Smart AI-Powered Quiz Generator
282
- </h1>
283
-
284
- <div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
285
- <div>
286
- <h3 className="text-3xl font-bold mb-6 text-gray-100 border-b border-blue-700 pb-2">
287
- 1. Select Quiz Source
288
- </h3>
289
- <div className="space-y-4">
290
- <SourceCard />
 
 
 
 
 
 
 
 
291
  </div>
292
- </div>
293
 
294
- <div className="lg:sticky lg:top-8 self-start">
295
- <h3 className="text-3xl font-bold mb-6 text-gray-100 border-b border-blue-700 pb-2">
296
- 2. Configure & Generate
297
- </h3>
298
-
299
- <div className="bg-slate-900/60 rounded-2xl shadow-2xl p-6 border border-slate-800 mb-8 space-y-6">
300
- {/* Custom Prompt Input */}
301
- <div>
302
- <label className="text-lg font-semibold block mb-3 text-gray-200">
303
- Custom Prompt/Instructions (Optional)
304
- </label>
305
- <textarea
306
- rows={3}
307
- value={customPrompt}
308
- onChange={(e) => setCustomPrompt(e.target.value)}
309
- placeholder="e.g., 'Focus on Python only'"
310
- className="w-full p-3 rounded-lg bg-black/40 border border-slate-700 text-gray-100 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-600 transition"
311
- ></textarea>
312
- <p className="text-gray-500 text-sm mt-2">
313
- This prompt influences the quiz generation.
314
- </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
315
  </div>
316
 
317
- {/* NEW: Duration Slider */}
318
- <div>
319
- <label className="text-lg font-semibold block mb-3 text-gray-200 flex items-center justify-between">
320
- <span className="flex items-center gap-2">
321
- <Clock className="w-5 h-5 text-blue-400" /> Quiz Duration
322
- </span>
323
- <span className="text-blue-400 font-bold">{duration} min</span>
324
- </label>
325
- <input
326
- type="range"
327
- min="1"
328
- max="60"
329
- step="5"
330
- value={duration}
331
- onChange={(e) => setDuration(parseInt(e.target.value))}
332
- className="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer accent-blue-500"
333
- />
334
- <div className="flex justify-between text-xs text-gray-500 mt-2">
335
- <span>5 min</span>
336
- <span>30 min</span>
337
- <span>60 min</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
338
  </div>
339
  </div>
340
  </div>
341
  </div>
342
  </div>
343
-
344
- <div className="lg:sticky lg:top-8 self-start mt-8">
345
- <h4 className="text-2xl font-semibold mb-4 text-gray-200">
346
- Choose Output Type:
347
- </h4>
348
-
349
- <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
350
- <OutputTypeOption
351
- value="mcq"
352
- icon={<ListChecks className="w-6 h-6 text-blue-400" />}
353
- title="Multiple Choice Quiz (MCQ)"
354
- desc="Ideal for quick assessment."
355
- />
356
- <OutputTypeOption
357
- value="coding"
358
- icon={<Code2 className="w-6 h-6 text-blue-400" />}
359
- title="Coding Challenge Quiz"
360
- desc="Generate code-based evaluation tasks."
361
- />
362
- </div>
363
-
364
- <button
365
- className={
366
- buttonClass +
367
- (!quizType || !fileObject || isProcessing
368
- ? " opacity-50 cursor-not-allowed"
369
- : " opacity-100 cursor-pointer")
370
- }
371
- disabled={!quizType || !fileObject || isProcessing}
372
- onClick={generateQuiz}
373
- >
374
- {isProcessing ? (
375
- <>
376
- <Loader2 className="animate-spin w-5 h-5" /> Generating...
377
- </>
378
- ) : (
379
- "Generate Quiz Now"
380
- )}
381
- </button>
382
- </div>
383
  </div>
384
  );
385
  };
 
1
+ import React, { useState, useRef } from "react";
2
  import {
3
  FileSpreadsheet,
 
4
  ListChecks,
5
  Upload,
6
  X,
7
  Loader2,
8
  Clock,
9
+ FileText,
10
+ ChevronRight,
11
+ Brain,
12
  } from "lucide-react";
13
  import MCQQuizPage from "../components/quize/mcq";
14
+ import API from "../api/api";
15
  import * as pdfjsLib from "pdfjs-dist";
16
 
17
  // Initialize PDF worker
18
  pdfjsLib.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjsLib.version}/build/pdf.worker.min.mjs`;
19
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  const ResumeGeneratedQuize: React.FC = () => {
21
  const [showQuiz, setShowQuiz] = useState(false);
22
+ const [quizType, setQuizType] = useState<"mcq" | null>("mcq");
23
  const [uploadType, setUploadType] = useState<"resume" | "notes" | null>(null);
24
  const [uploadedFile, setUploadedFile] = useState<string | null>(null);
25
  const [fileError, setFileError] = useState<string | null>(null);
 
28
 
29
  // Configuration State
30
  const [customPrompt, setCustomPrompt] = useState("");
31
+ const [duration, setDuration] = useState(15);
 
32
  const [quizData, setQuizData] = useState(null);
33
 
34
  const resumeInputRef = useRef<HTMLInputElement>(null);
 
73
  setFileObject(null);
74
  };
75
 
 
76
  const extractTextFromPDF = async (file: File): Promise<string> => {
77
  const arrayBuffer = await file.arrayBuffer();
78
  const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
79
  let fullText = "";
 
80
  for (let i = 1; i <= pdf.numPages; i++) {
81
  const page = await pdf.getPage(i);
82
  const textContent = await page.getTextContent();
83
  const pageText = textContent.items.map((item: any) => item.str).join(" ");
84
  fullText += pageText + "\n";
85
  }
 
86
  return fullText;
87
  };
88
 
 
89
  const generateQuiz = async () => {
90
+ if (!fileObject || !quizType || !uploadType) return;
91
 
92
  setIsProcessing(true);
93
  setFileError(null);
94
 
95
  try {
 
96
  console.log("Extracting text from PDF...");
97
  const extractedText = await extractTextFromPDF(fileObject);
98
 
 
102
  );
103
  }
104
 
 
 
105
  const payload = {
106
  parsed_doc: extractedText.trim(),
107
  user_prompt:
108
  customPrompt.trim() || "Generate a quiz based on this content.",
109
  };
110
 
 
 
 
111
  const endpoint = uploadType === "notes" ? "/quiz/notes" : "/quiz/resume";
112
 
113
  console.log(`Sending to ${endpoint}...`);
114
 
 
115
  const response = await API.post(endpoint, payload);
 
116
  setQuizData(response.data);
117
  setShowQuiz(true);
118
  } catch (error: any) {
119
  console.error("Quiz Generation Error:", error);
120
  let errorMessage = "Failed to generate quiz. Check login status.";
 
121
  if (error.response?.data?.detail) {
122
  if (typeof error.response.data.detail === "string") {
123
  errorMessage = error.response.data.detail;
 
133
  } else if (error.code === "ERR_BAD_REQUEST") {
134
  errorMessage = "Server rejected the data. Are you logged in?";
135
  }
 
136
  setFileError(errorMessage);
137
  } finally {
138
  setIsProcessing(false);
139
  }
140
  };
141
 
 
142
  if (showQuiz) {
143
+ return (
144
+ <MCQQuizPage
145
+ data={quizData}
146
+ onBack={() => setShowQuiz(false)}
147
+ totalTimeSeconds={duration * 60}
148
+ />
149
+ );
 
 
 
 
 
150
  }
151
 
152
  // ---- UI COMPONENTS ----
 
 
153
 
154
  const OutputTypeOption = ({ icon, title, desc, value }: any) => (
155
  <div
156
  onClick={() => setQuizType(value)}
157
+ className={`p-3 rounded-xl border-2 transition-all cursor-pointer flex flex-col gap-2 group h-full
158
  ${
159
  quizType === value
160
+ ? "bg-[#607B8F] border-[#F7E396] shadow-lg transform scale-[1.01]"
161
+ : "bg-[#607B8F]/50 border-transparent hover:bg-[#607B8F] hover:border-[#E97F4A]/50"
162
  }
163
  `}
164
  >
165
+ <div className="flex items-center gap-3">
166
+ <div
167
+ className={`p-1.5 rounded-lg ${
168
+ quizType === value
169
+ ? "bg-[#F7E396] text-[#434E78]"
170
+ : "bg-[#434E78]/50 text-[#F7E396]"
171
+ }`}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  >
173
+ {icon}
174
+ </div>
175
+ <h4
176
+ className={`text-sm font-bold ${
177
+ quizType === value
178
+ ? "text-[#F7E396]"
179
+ : "text-white group-hover:text-[#F7E396]"
180
+ }`}
181
  >
182
+ {title}
183
+ </h4>
 
184
  </div>
185
+ <p className="text-gray-300 text-xs leading-relaxed">{desc}</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  </div>
187
  );
188
 
189
  return (
190
+ // FIXED: h-screen + overflow-hidden to fix double scrollbars.
191
+ <div className="w-full h-screen bg-[#434E78] text-white relative font-sans overflow-hidden flex flex-col">
192
+ {/* Background Blobs */}
193
+ <div className="absolute top-[-10%] right-[-5%] w-96 h-96 bg-[#F7E396] rounded-full mix-blend-overlay filter blur-3xl opacity-10 animate-blob pointer-events-none"></div>
194
+ <div className="absolute bottom-[-10%] left-[-10%] w-96 h-96 bg-[#607B8F] rounded-full mix-blend-overlay filter blur-3xl opacity-10 animate-blob animation-delay-2000 pointer-events-none"></div>
195
+
196
+ {/* Main Content Wrapper - Added 'pt-6' to fix header cut-off */}
197
+ <div className="flex-1 overflow-y-auto p-4 lg:p-6 pt-8 lg:pt-12 w-full z-10 flex flex-col items-center">
198
+ <div className="max-w-6xl w-full flex flex-col h-full">
199
+ {/* Header - Compacted margins */}
200
+ <div className="text-center mb-6 shrink-0 mt-2">
201
+ <h1 className="text-3xl font-bold mb-2 font-handwriting drop-shadow-md">
202
+ Smart AI-Powered Quiz Generator
203
+ </h1>
204
+ <div className="flex items-center justify-center gap-2 text-[#F7E396] text-sm">
205
+ <Brain className="w-5 h-5" />
206
+ <span className="font-semibold tracking-wide">
207
+ Generate Quizzes from Resumes or Notes
208
+ </span>
209
+ </div>
210
  </div>
 
211
 
212
+ {/* Content Grid - Compacted spacing */}
213
+ <div className="grid grid-cols-1 lg:grid-cols-12 gap-5 items-start flex-1">
214
+ {/* LEFT COLUMN */}
215
+ <div className="lg:col-span-7 flex flex-col gap-5">
216
+ {/* 1. Source Card - Compact Padding (p-5) */}
217
+ <div className="bg-[#607B8F] rounded-2xl shadow-xl border border-white/10 p-5">
218
+ <div className="flex items-center gap-3 mb-3 border-b border-white/10 pb-3">
219
+ <FileSpreadsheet className="w-5 h-5 text-[#F7E396]" />
220
+ <h3 className="text-lg font-bold text-white">
221
+ 1. Select Quiz Source
222
+ </h3>
223
+ </div>
224
+
225
+ <div className="space-y-3">
226
+ <p className="text-gray-200 text-xs">
227
+ Upload your material (Max 10MB PDF).
228
+ </p>
229
+
230
+ <div className="flex gap-3">
231
+ <button
232
+ onClick={handleResumeClick}
233
+ className={`flex-1 py-2.5 px-4 rounded-xl font-bold transition flex items-center justify-center gap-2 shadow-md text-sm
234
+ ${
235
+ uploadType === "resume"
236
+ ? "bg-[#F7E396] text-[#434E78] ring-2 ring-white"
237
+ : "bg-[#434E78] text-white hover:bg-[#E97F4A]"
238
+ }`}
239
+ >
240
+ <Upload className="w-4 h-4" />
241
+ Resume
242
+ </button>
243
+
244
+ <button
245
+ onClick={handleNotesClick}
246
+ className={`flex-1 py-2.5 px-4 rounded-xl font-bold transition flex items-center justify-center gap-2 shadow-md text-sm
247
+ ${
248
+ uploadType === "notes"
249
+ ? "bg-[#F7E396] text-[#434E78] ring-2 ring-white"
250
+ : "bg-[#434E78] text-white hover:bg-[#E97F4A]"
251
+ }`}
252
+ >
253
+ <Upload className="w-4 h-4" />
254
+ Notes
255
+ </button>
256
+ </div>
257
+
258
+ <input
259
+ ref={resumeInputRef}
260
+ type="file"
261
+ accept=".pdf"
262
+ onChange={(e) => handleFileUpload(e, "resume")}
263
+ className="hidden"
264
+ />
265
+ <input
266
+ ref={notesInputRef}
267
+ type="file"
268
+ accept=".pdf"
269
+ onChange={(e) => handleFileUpload(e, "notes")}
270
+ className="hidden"
271
+ />
272
+
273
+ {fileError && (
274
+ <div className="p-3 bg-red-500/20 border border-red-500/50 rounded-lg text-red-200 text-xs">
275
+ {fileError}
276
+ </div>
277
+ )}
278
+
279
+ {uploadedFile && (
280
+ <div className="p-3 bg-[#434E78]/50 border border-[#F7E396]/50 rounded-lg flex items-center justify-between">
281
+ <div className="overflow-hidden">
282
+ <p className="text-[#F7E396] font-semibold text-xs">
283
+ File Uploaded
284
+ </p>
285
+ <div className="flex items-center gap-2 mt-1">
286
+ <FileText className="w-3 h-3 text-gray-300" />
287
+ <span className="text-white text-xs truncate block">
288
+ {uploadedFile}
289
+ </span>
290
+ </div>
291
+ </div>
292
+ <button
293
+ onClick={clearUpload}
294
+ className="text-gray-400 hover:text-white p-1 shrink-0"
295
+ >
296
+ <X className="w-4 h-4" />
297
+ </button>
298
+ </div>
299
+ )}
300
+ </div>
301
+ </div>
302
+
303
+ {/* 2. Output Type Selection */}
304
+ <div className="bg-[#607B8F] rounded-2xl shadow-xl border border-white/10 p-5">
305
+ <div className="flex items-center gap-3 mb-3 border-b border-white/10 pb-3">
306
+ <ListChecks className="w-5 h-5 text-[#F7E396]" />
307
+ <h3 className="text-lg font-bold text-white">
308
+ 2. Choose Output Type
309
+ </h3>
310
+ </div>
311
+
312
+ <div className="grid grid-cols-1">
313
+ <OutputTypeOption
314
+ value="mcq"
315
+ icon={<ListChecks className="w-5 h-5" />}
316
+ title="Multiple Choice Quiz (MCQ)"
317
+ desc="Generate a standardized multiple-choice assessment."
318
+ />
319
+ </div>
320
+ </div>
321
  </div>
322
 
323
+ {/* RIGHT COLUMN: Configuration */}
324
+ <div className="lg:col-span-5 flex flex-col h-full">
325
+ <div className="bg-[#607B8F] rounded-2xl shadow-xl border border-white/10 p-5 flex flex-col h-full">
326
+ <div className="flex items-center gap-3 mb-3 border-b border-white/10 pb-3 shrink-0">
327
+ <Clock className="w-5 h-5 text-[#F7E396]" />
328
+ <h3 className="text-lg font-bold text-white">3. Configure</h3>
329
+ </div>
330
+
331
+ <div className="flex-1 flex flex-col gap-4">
332
+ {/* Custom Prompt - Reduced height */}
333
+ <div className="flex-1 flex flex-col min-h-0">
334
+ <label className="text-[#F7E396] font-bold block mb-2 text-xs tracking-wide">
335
+ Custom Instructions (Optional)
336
+ </label>
337
+ <textarea
338
+ value={customPrompt}
339
+ onChange={(e) => setCustomPrompt(e.target.value)}
340
+ placeholder="e.g., 'Focus on React hooks...'"
341
+ // Reduced min-height to 100px for compactness
342
+ className="w-full flex-1 p-3 rounded-lg bg-[#434E78]/50 border-2 border-transparent focus:border-[#F7E396] focus:ring-0 outline-none transition text-white placeholder-gray-400 resize-none shadow-inner text-sm min-h-[100px]"
343
+ ></textarea>
344
+ </div>
345
+
346
+ {/* Duration Slider */}
347
+ <div className="shrink-0 pt-2">
348
+ <div className="flex justify-between items-center mb-2">
349
+ <label className="text-[#F7E396] font-bold text-xs tracking-wide">
350
+ Quiz Duration
351
+ </label>
352
+ <span className="bg-[#434E78] px-2 py-1 rounded-full text-white text-xs font-bold border border-white/10">
353
+ {duration} min
354
+ </span>
355
+ </div>
356
+ <input
357
+ type="range"
358
+ min="1"
359
+ max="60"
360
+ step="5"
361
+ value={duration}
362
+ onChange={(e) => setDuration(parseInt(e.target.value))}
363
+ className="w-full h-1.5 bg-[#434E78] rounded-lg appearance-none cursor-pointer accent-[#F7E396]"
364
+ />
365
+ <div className="flex justify-between text-xs text-gray-300 mt-1 font-medium">
366
+ <span>5m</span>
367
+ <span>30m</span>
368
+ <span>60m</span>
369
+ </div>
370
+ </div>
371
+
372
+ {/* Action Button - Reduced height */}
373
+ <button
374
+ onClick={generateQuiz}
375
+ disabled={!quizType || !fileObject || isProcessing}
376
+ className={`w-full py-3.5 rounded-xl font-bold text-base flex items-center justify-center gap-3 transition-all transform hover:-translate-y-1 shadow-lg shrink-0 mt-2
377
+ ${
378
+ !quizType || !fileObject || isProcessing
379
+ ? "bg-gray-500 text-gray-300 cursor-not-allowed opacity-50"
380
+ : "bg-[#F7E396] text-[#434E78] hover:bg-[#E97F4A] hover:text-white"
381
+ }
382
+ `}
383
+ >
384
+ {isProcessing ? (
385
+ <>
386
+ <Loader2 className="animate-spin w-5 h-5" />{" "}
387
+ Generating...
388
+ </>
389
+ ) : (
390
+ <>
391
+ Generate Quiz <ChevronRight className="w-5 h-5" />
392
+ </>
393
+ )}
394
+ </button>
395
+ </div>
396
  </div>
397
  </div>
398
  </div>
399
  </div>
400
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
401
  </div>
402
  );
403
  };