@schoolio/player 1.2.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -177,11 +177,11 @@ var import_jsx_runtime = require("react/jsx-runtime");
177
177
  var defaultStyles = {
178
178
  container: {
179
179
  fontFamily: "system-ui, -apple-system, sans-serif",
180
- maxWidth: "800px",
181
- margin: "0 auto",
180
+ width: "100%",
182
181
  padding: "20px",
183
182
  backgroundColor: "#ffffff",
184
- borderRadius: "12px"
183
+ borderRadius: "12px",
184
+ boxSizing: "border-box"
185
185
  },
186
186
  header: {
187
187
  marginBottom: "20px",
@@ -211,7 +211,8 @@ var defaultStyles = {
211
211
  transition: "width 0.3s ease"
212
212
  },
213
213
  question: {
214
- marginBottom: "24px"
214
+ marginBottom: "24px",
215
+ minHeight: "280px"
215
216
  },
216
217
  questionText: {
217
218
  fontSize: "18px",
@@ -228,7 +229,9 @@ var defaultStyles = {
228
229
  border: "2px solid #e5e7eb",
229
230
  borderRadius: "8px",
230
231
  cursor: "pointer",
231
- transition: "all 0.2s ease"
232
+ transition: "all 0.2s ease",
233
+ outline: "none",
234
+ backgroundColor: "#ffffff"
232
235
  },
233
236
  optionSelected: {
234
237
  borderColor: "#6721b0",
@@ -247,7 +250,10 @@ var defaultStyles = {
247
250
  padding: "12px 16px",
248
251
  border: "2px solid #e5e7eb",
249
252
  borderRadius: "8px",
250
- fontSize: "16px"
253
+ fontSize: "16px",
254
+ outline: "none",
255
+ boxSizing: "border-box",
256
+ backgroundColor: "#ffffff"
251
257
  },
252
258
  buttons: {
253
259
  display: "flex",
@@ -276,24 +282,90 @@ var defaultStyles = {
276
282
  color: "#9ca3af",
277
283
  cursor: "not-allowed"
278
284
  },
285
+ buttonAddMore: {
286
+ padding: "12px 24px",
287
+ borderRadius: "8px",
288
+ fontSize: "16px",
289
+ fontWeight: "500",
290
+ cursor: "pointer",
291
+ border: "2px solid #6721b0",
292
+ backgroundColor: "transparent",
293
+ color: "#6721b0",
294
+ transition: "all 0.2s ease",
295
+ display: "flex",
296
+ alignItems: "center",
297
+ justifyContent: "center",
298
+ gap: "8px"
299
+ },
300
+ buttonAddMoreDisabled: {
301
+ border: "2px solid #e5e7eb",
302
+ color: "#9ca3af",
303
+ cursor: "not-allowed"
304
+ },
305
+ buttonsColumn: {
306
+ display: "flex",
307
+ flexDirection: "column",
308
+ gap: "12px",
309
+ marginTop: "24px"
310
+ },
279
311
  timer: {
280
312
  fontSize: "14px",
281
313
  color: "#6b7280",
282
314
  marginLeft: "16px"
283
315
  },
284
316
  results: {
317
+ display: "flex",
318
+ flexDirection: "column",
319
+ alignItems: "center",
320
+ justifyContent: "center",
321
+ padding: "48px 24px",
285
322
  textAlign: "center",
286
- padding: "40px 20px"
323
+ minHeight: "400px",
324
+ position: "relative",
325
+ overflow: "hidden"
287
326
  },
288
- resultScore: {
289
- fontSize: "48px",
290
- fontWeight: "700",
291
- color: "#6721b0",
292
- marginBottom: "8px"
327
+ resultsBackground: {
328
+ position: "absolute",
329
+ inset: "0",
330
+ borderRadius: "16px",
331
+ zIndex: 0
293
332
  },
294
- resultLabel: {
333
+ resultsContent: {
334
+ position: "relative",
335
+ zIndex: 1,
336
+ display: "flex",
337
+ flexDirection: "column",
338
+ alignItems: "center"
339
+ },
340
+ resultDetails: {
295
341
  fontSize: "18px",
296
- color: "#6b7280"
342
+ color: "#6b7280",
343
+ marginBottom: "4px"
344
+ },
345
+ resultStars: {
346
+ display: "flex",
347
+ justifyContent: "center",
348
+ gap: "8px",
349
+ marginBottom: "20px"
350
+ },
351
+ resultStar: {
352
+ fontSize: "32px",
353
+ animation: "starPop 0.5s ease-out forwards",
354
+ opacity: 0
355
+ },
356
+ confettiContainer: {
357
+ position: "absolute",
358
+ inset: "0",
359
+ pointerEvents: "none",
360
+ overflow: "hidden",
361
+ zIndex: 0
362
+ },
363
+ confettiPiece: {
364
+ position: "absolute",
365
+ width: "10px",
366
+ height: "10px",
367
+ top: "-10px",
368
+ animation: "confettiFall 3s ease-out forwards"
297
369
  },
298
370
  loading: {
299
371
  textAlign: "center",
@@ -304,6 +376,80 @@ var defaultStyles = {
304
376
  textAlign: "center",
305
377
  padding: "40px",
306
378
  color: "#ef4444"
379
+ },
380
+ intro: {
381
+ display: "flex",
382
+ flexDirection: "column",
383
+ alignItems: "center",
384
+ justifyContent: "center",
385
+ padding: "48px 24px",
386
+ textAlign: "center",
387
+ background: "linear-gradient(135deg, #f3e8ff 0%, #e0e7ff 50%, #fce7f3 100%)",
388
+ borderRadius: "16px",
389
+ minHeight: "320px"
390
+ },
391
+ introTitle: {
392
+ fontSize: "28px",
393
+ fontWeight: "700",
394
+ color: "#4c1d95",
395
+ marginBottom: "12px"
396
+ },
397
+ introSubtitle: {
398
+ fontSize: "16px",
399
+ color: "#6b7280",
400
+ marginBottom: "8px"
401
+ },
402
+ introQuestionCount: {
403
+ fontSize: "14px",
404
+ color: "#8b5cf6",
405
+ marginBottom: "32px",
406
+ fontWeight: "500"
407
+ },
408
+ startButton: {
409
+ padding: "16px 48px",
410
+ fontSize: "18px",
411
+ fontWeight: "600",
412
+ backgroundColor: "#7c3aed",
413
+ color: "#ffffff",
414
+ border: "none",
415
+ borderRadius: "12px",
416
+ cursor: "pointer",
417
+ transition: "all 0.2s ease",
418
+ boxShadow: "0 4px 14px rgba(124, 58, 237, 0.4)"
419
+ },
420
+ feedback: {
421
+ marginTop: "16px",
422
+ padding: "16px",
423
+ borderRadius: "8px",
424
+ backgroundColor: "#f9fafb",
425
+ border: "1px solid #e5e7eb"
426
+ },
427
+ feedbackCorrect: {
428
+ backgroundColor: "#f0fdf4",
429
+ borderColor: "#22c55e"
430
+ },
431
+ feedbackIncorrect: {
432
+ backgroundColor: "#fef2f2",
433
+ borderColor: "#ef4444"
434
+ },
435
+ feedbackTitle: {
436
+ fontSize: "16px",
437
+ fontWeight: "600",
438
+ marginBottom: "8px",
439
+ display: "flex",
440
+ alignItems: "center",
441
+ gap: "8px"
442
+ },
443
+ feedbackTitleCorrect: {
444
+ color: "#16a34a"
445
+ },
446
+ feedbackTitleIncorrect: {
447
+ color: "#dc2626"
448
+ },
449
+ feedbackExplanation: {
450
+ fontSize: "14px",
451
+ color: "#4b5563",
452
+ lineHeight: "1.5"
307
453
  }
308
454
  };
309
455
  function Spinner({ size = 16, color = "#ffffff" }) {
@@ -335,6 +481,7 @@ function QuizPlayer({
335
481
  onComplete,
336
482
  onError,
337
483
  onProgress,
484
+ onGenerateMoreQuestions,
338
485
  className
339
486
  }) {
340
487
  const [quiz, setQuiz] = (0, import_react.useState)(null);
@@ -349,9 +496,15 @@ function QuizPlayer({
349
496
  const [error, setError] = (0, import_react.useState)(null);
350
497
  const [isLoading, setIsLoading] = (0, import_react.useState)(true);
351
498
  const [elapsedSeconds, setElapsedSeconds] = (0, import_react.useState)(0);
499
+ const [showIntro, setShowIntro] = (0, import_react.useState)(true);
500
+ const [timerStarted, setTimerStarted] = (0, import_react.useState)(false);
501
+ const [showFeedback, setShowFeedback] = (0, import_react.useState)(false);
502
+ const [currentAnswerDetail, setCurrentAnswerDetail] = (0, import_react.useState)(null);
503
+ const [extraQuestions, setExtraQuestions] = (0, import_react.useState)([]);
504
+ const [isGeneratingExtra, setIsGeneratingExtra] = (0, import_react.useState)(false);
352
505
  const apiClient = (0, import_react.useRef)(null);
353
506
  const timerRef = (0, import_react.useRef)(null);
354
- const startTimeRef = (0, import_react.useRef)(Date.now());
507
+ const startTimeRef = (0, import_react.useRef)(0);
355
508
  (0, import_react.useEffect)(() => {
356
509
  apiClient.current = new QuizApiClient({ baseUrl: apiBaseUrl, authToken });
357
510
  }, [apiBaseUrl, authToken]);
@@ -403,7 +556,7 @@ function QuizPlayer({
403
556
  initialize();
404
557
  }, [quizId, lessonId, assignLessonId, courseId, childId, parentId, onError]);
405
558
  (0, import_react.useEffect)(() => {
406
- if (!isLoading && !isCompleted && !error) {
559
+ if (timerStarted && !isCompleted && !error) {
407
560
  startTimeRef.current = Date.now();
408
561
  timerRef.current = setInterval(() => {
409
562
  setElapsedSeconds(Math.floor((Date.now() - startTimeRef.current) / 1e3));
@@ -414,27 +567,39 @@ function QuizPlayer({
414
567
  clearInterval(timerRef.current);
415
568
  }
416
569
  };
417
- }, [isLoading, isCompleted, error]);
570
+ }, [timerStarted, isCompleted, error]);
571
+ const handleStart = (0, import_react.useCallback)(() => {
572
+ setShowIntro(false);
573
+ setTimerStarted(true);
574
+ }, []);
575
+ (0, import_react.useEffect)(() => {
576
+ setShowFeedback(false);
577
+ setCurrentAnswerDetail(null);
578
+ }, [currentQuestionIndex]);
579
+ const allQuestions = quiz ? [...quiz.questions, ...extraQuestions] : [];
580
+ const totalQuestions = allQuestions.length;
581
+ const maxQuestions = 50;
418
582
  (0, import_react.useEffect)(() => {
419
583
  if (quiz && onProgress) {
420
584
  onProgress({
421
585
  currentQuestion: currentQuestionIndex + 1,
422
- totalQuestions: quiz.questions.length,
586
+ totalQuestions,
423
587
  answeredQuestions: answers.size
424
588
  });
425
589
  }
426
- }, [currentQuestionIndex, answers.size, quiz, onProgress]);
427
- const currentQuestion = quiz?.questions[currentQuestionIndex];
590
+ }, [currentQuestionIndex, answers.size, quiz, onProgress, totalQuestions]);
591
+ const currentQuestion = allQuestions[currentQuestionIndex];
428
592
  const handleAnswerChange = (0, import_react.useCallback)((value) => {
429
593
  if (!currentQuestion) return;
430
594
  setAnswers((prev) => new Map(prev).set(currentQuestion.id, value));
431
595
  }, [currentQuestion]);
432
- const handleNext = (0, import_react.useCallback)(async () => {
596
+ const handleCheckAnswer = (0, import_react.useCallback)(async () => {
433
597
  if (!quiz || !attempt || !currentQuestion || !apiClient.current) return;
434
598
  const selectedAnswer2 = answers.get(currentQuestion.id);
435
599
  if (selectedAnswer2 === void 0) return;
436
600
  setIsNavigating(true);
437
601
  const answerDetail = createAnswerDetail(currentQuestion, selectedAnswer2);
602
+ setCurrentAnswerDetail(answerDetail);
438
603
  const newAnswersDetail = [...answersDetail];
439
604
  const existingIdx = newAnswersDetail.findIndex((a) => a.questionId === currentQuestion.id);
440
605
  if (existingIdx >= 0) {
@@ -452,15 +617,39 @@ function QuizPlayer({
452
617
  } finally {
453
618
  setIsNavigating(false);
454
619
  }
455
- if (currentQuestionIndex < quiz.questions.length - 1) {
620
+ setShowFeedback(true);
621
+ }, [quiz, attempt, currentQuestion, answers, answersDetail]);
622
+ const handleContinue = (0, import_react.useCallback)(() => {
623
+ if (!quiz) return;
624
+ setShowFeedback(false);
625
+ setCurrentAnswerDetail(null);
626
+ if (currentQuestionIndex < totalQuestions - 1) {
456
627
  setCurrentQuestionIndex((prev) => prev + 1);
457
628
  }
458
- }, [quiz, attempt, currentQuestion, answers, answersDetail, currentQuestionIndex]);
459
- const handlePrevious = (0, import_react.useCallback)(() => {
460
- if (currentQuestionIndex > 0) {
461
- setCurrentQuestionIndex((prev) => prev - 1);
629
+ }, [quiz, currentQuestionIndex, totalQuestions]);
630
+ const handleAddMoreQuestions = (0, import_react.useCallback)(async () => {
631
+ if (!attempt || !onGenerateMoreQuestions || isGeneratingExtra) return;
632
+ if (totalQuestions >= maxQuestions) return;
633
+ setIsGeneratingExtra(true);
634
+ try {
635
+ const result2 = await onGenerateMoreQuestions(attempt.id, totalQuestions);
636
+ if (result2.extraQuestions && result2.extraQuestions.length > 0) {
637
+ const slotsAvailable = maxQuestions - totalQuestions;
638
+ const questionsToAppend = result2.extraQuestions.slice(0, slotsAvailable);
639
+ if (questionsToAppend.length > 0) {
640
+ setExtraQuestions((prev) => [...prev, ...questionsToAppend]);
641
+ setCurrentQuestionIndex(totalQuestions);
642
+ setShowFeedback(false);
643
+ setCurrentAnswerDetail(null);
644
+ }
645
+ }
646
+ } catch (err) {
647
+ console.error("Failed to generate extra questions:", err);
648
+ onError?.(err instanceof Error ? err : new Error("Failed to generate extra questions"));
649
+ } finally {
650
+ setIsGeneratingExtra(false);
462
651
  }
463
- }, [currentQuestionIndex]);
652
+ }, [attempt, onGenerateMoreQuestions, isGeneratingExtra, totalQuestions, maxQuestions, onError]);
464
653
  const handleSubmit = (0, import_react.useCallback)(async () => {
465
654
  if (!quiz || !attempt || !apiClient.current) return;
466
655
  setIsSubmitting(true);
@@ -477,7 +666,7 @@ function QuizPlayer({
477
666
  }
478
667
  }
479
668
  const scoreData = calculateScore(finalAnswersDetail);
480
- const timeSpent = Math.floor((Date.now() - startTimeRef.current) / 1e3);
669
+ const timeSpent = timerStarted && startTimeRef.current > 0 ? Math.floor((Date.now() - startTimeRef.current) / 1e3) : elapsedSeconds;
481
670
  const updatedAttempt = await apiClient.current.updateAttempt(attempt.id, {
482
671
  answers: finalAnswersDetail,
483
672
  status: "completed",
@@ -490,7 +679,7 @@ function QuizPlayer({
490
679
  attemptId: updatedAttempt.id,
491
680
  score: scoreData.score,
492
681
  correctAnswers: scoreData.correctAnswers,
493
- totalQuestions: quiz.questions.length,
682
+ totalQuestions,
494
683
  answers: finalAnswersDetail,
495
684
  timeSpentSeconds: timeSpent
496
685
  };
@@ -506,7 +695,7 @@ function QuizPlayer({
506
695
  } finally {
507
696
  setIsSubmitting(false);
508
697
  }
509
- }, [quiz, attempt, currentQuestion, answers, answersDetail, onComplete, onError]);
698
+ }, [quiz, attempt, currentQuestion, answers, answersDetail, onComplete, onError, totalQuestions, timerStarted, elapsedSeconds]);
510
699
  if (isLoading) {
511
700
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className, style: defaultStyles.container, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: defaultStyles.loading, children: "Loading quiz..." }) });
512
701
  }
@@ -517,67 +706,403 @@ function QuizPlayer({
517
706
  ] }) }) });
518
707
  }
519
708
  if (isCompleted && result) {
520
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className, style: defaultStyles.container, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: defaultStyles.results, children: [
521
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: defaultStyles.resultScore, children: [
522
- result.score,
523
- "%"
524
- ] }),
525
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: defaultStyles.resultLabel, children: [
526
- result.correctAnswers,
527
- " of ",
528
- result.totalQuestions,
529
- " correct"
530
- ] }),
531
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { ...defaultStyles.resultLabel, marginTop: "8px" }, children: [
532
- "Time: ",
533
- formatTime(result.timeSpentSeconds)
709
+ const percentage = Math.round(result.correctAnswers / result.totalQuestions * 100);
710
+ const getScoreTheme = (pct) => {
711
+ if (pct >= 80) {
712
+ return {
713
+ color: "#22c55e",
714
+ bgGradient: "linear-gradient(135deg, #dcfce7 0%, #bbf7d0 50%, #86efac 100%)",
715
+ badge: "Quiz Champion!",
716
+ badgeColor: "#fbbf24",
717
+ badgeBg: "linear-gradient(135deg, #fef3c7 0%, #fde68a 100%)",
718
+ mascotMood: "celebrating",
719
+ stars: 3
720
+ };
721
+ } else if (pct >= 60) {
722
+ return {
723
+ color: "#f59e0b",
724
+ bgGradient: "linear-gradient(135deg, #fef3c7 0%, #fde68a 50%, #fcd34d 100%)",
725
+ badge: "Rising Star!",
726
+ badgeColor: "#f59e0b",
727
+ badgeBg: "linear-gradient(135deg, #fed7aa 0%, #fdba74 100%)",
728
+ mascotMood: "happy",
729
+ stars: 2
730
+ };
731
+ } else if (pct >= 40) {
732
+ return {
733
+ color: "#3b82f6",
734
+ bgGradient: "linear-gradient(135deg, #dbeafe 0%, #bfdbfe 50%, #93c5fd 100%)",
735
+ badge: "Great Learner!",
736
+ badgeColor: "#3b82f6",
737
+ badgeBg: "linear-gradient(135deg, #bfdbfe 0%, #93c5fd 100%)",
738
+ mascotMood: "encouraging",
739
+ stars: 1
740
+ };
741
+ } else {
742
+ return {
743
+ color: "#8b5cf6",
744
+ bgGradient: "linear-gradient(135deg, #f3e8ff 0%, #e9d5ff 50%, #d8b4fe 100%)",
745
+ badge: "Keep Growing!",
746
+ badgeColor: "#8b5cf6",
747
+ badgeBg: "linear-gradient(135deg, #e9d5ff 0%, #d8b4fe 100%)",
748
+ mascotMood: "supportive",
749
+ stars: 0
750
+ };
751
+ }
752
+ };
753
+ const theme = getScoreTheme(percentage);
754
+ const confettiColors = ["#f43f5e", "#ec4899", "#8b5cf6", "#3b82f6", "#22c55e", "#f59e0b", "#ef4444"];
755
+ const confettiPieces = Array.from({ length: 50 }, (_, i) => ({
756
+ id: i,
757
+ left: `${Math.random() * 100}%`,
758
+ delay: `${Math.random() * 2}s`,
759
+ duration: `${2 + Math.random() * 2}s`,
760
+ color: confettiColors[Math.floor(Math.random() * confettiColors.length)],
761
+ rotation: Math.random() * 360,
762
+ size: 6 + Math.random() * 8
763
+ }));
764
+ const StarIcon = ({ filled, delay }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
765
+ "svg",
766
+ {
767
+ width: "36",
768
+ height: "36",
769
+ viewBox: "0 0 24 24",
770
+ style: {
771
+ animation: "starPop 0.5s ease-out forwards",
772
+ animationDelay: `${delay}s`,
773
+ opacity: 0
774
+ },
775
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
776
+ "path",
777
+ {
778
+ d: "M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z",
779
+ fill: filled ? "#fbbf24" : "#e5e7eb",
780
+ stroke: filled ? "#f59e0b" : "#d1d5db",
781
+ strokeWidth: "1"
782
+ }
783
+ )
784
+ }
785
+ );
786
+ const MascotOwl = ({ mood }) => {
787
+ const getEyeExpression = () => {
788
+ switch (mood) {
789
+ case "celebrating":
790
+ return { leftEye: ">", rightEye: "<", pupilY: 42 };
791
+ // Squinting happy
792
+ case "happy":
793
+ return { leftEye: null, rightEye: null, pupilY: 42 };
794
+ // Normal happy
795
+ case "encouraging":
796
+ return { leftEye: null, rightEye: null, pupilY: 44 };
797
+ // Looking down warmly
798
+ default:
799
+ return { leftEye: null, rightEye: null, pupilY: 42 };
800
+ }
801
+ };
802
+ const eyeExpr = getEyeExpression();
803
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
804
+ "svg",
805
+ {
806
+ width: "120",
807
+ height: "120",
808
+ viewBox: "0 0 100 100",
809
+ style: {
810
+ animation: mood === "celebrating" ? "bounce 0.6s ease-in-out infinite" : "gentleBob 2s ease-in-out infinite"
811
+ },
812
+ children: [
813
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ellipse", { cx: "50", cy: "60", rx: "35", ry: "30", fill: "#8b5cf6" }),
814
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ellipse", { cx: "50", cy: "65", rx: "25", ry: "20", fill: "#c4b5fd" }),
815
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "50", cy: "35", r: "28", fill: "#a78bfa" }),
816
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("polygon", { points: "28,15 35,30 22,28", fill: "#7c3aed" }),
817
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("polygon", { points: "72,15 65,30 78,28", fill: "#7c3aed" }),
818
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ellipse", { cx: "38", cy: "38", rx: "10", ry: "12", fill: "white" }),
819
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ellipse", { cx: "62", cy: "38", rx: "10", ry: "12", fill: "white" }),
820
+ eyeExpr.leftEye ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("text", { x: "38", y: "44", textAnchor: "middle", fontSize: "16", fill: "#1f2937", children: eyeExpr.leftEye }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "38", cy: eyeExpr.pupilY, r: "5", fill: "#1f2937" }),
821
+ eyeExpr.rightEye ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("text", { x: "62", y: "44", textAnchor: "middle", fontSize: "16", fill: "#1f2937", children: eyeExpr.rightEye }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "62", cy: eyeExpr.pupilY, r: "5", fill: "#1f2937" }),
822
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("polygon", { points: "50,45 45,52 55,52", fill: "#fbbf24" }),
823
+ (mood === "celebrating" || mood === "happy") && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
824
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ellipse", { cx: "28", cy: "45", rx: "5", ry: "3", fill: "#fda4af", opacity: "0.6" }),
825
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ellipse", { cx: "72", cy: "45", rx: "5", ry: "3", fill: "#fda4af", opacity: "0.6" })
826
+ ] }),
827
+ mood === "celebrating" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
828
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ellipse", { cx: "18", cy: "55", rx: "8", ry: "15", fill: "#7c3aed", transform: "rotate(-30 18 55)" }),
829
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ellipse", { cx: "82", cy: "55", rx: "8", ry: "15", fill: "#7c3aed", transform: "rotate(30 82 55)" })
830
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
831
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ellipse", { cx: "20", cy: "60", rx: "8", ry: "15", fill: "#7c3aed" }),
832
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ellipse", { cx: "80", cy: "60", rx: "8", ry: "15", fill: "#7c3aed" })
833
+ ] }),
834
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ellipse", { cx: "40", cy: "88", rx: "8", ry: "4", fill: "#fbbf24" }),
835
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ellipse", { cx: "60", cy: "88", rx: "8", ry: "4", fill: "#fbbf24" })
836
+ ]
837
+ }
838
+ );
839
+ };
840
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className, style: defaultStyles.container, children: [
841
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: `
842
+ @keyframes confettiFall {
843
+ 0% {
844
+ transform: translateY(-10px) rotate(0deg);
845
+ opacity: 1;
846
+ }
847
+ 100% {
848
+ transform: translateY(500px) rotate(720deg);
849
+ opacity: 0;
850
+ }
851
+ }
852
+ @keyframes starPop {
853
+ 0% {
854
+ transform: scale(0);
855
+ opacity: 0;
856
+ }
857
+ 50% {
858
+ transform: scale(1.3);
859
+ }
860
+ 100% {
861
+ transform: scale(1);
862
+ opacity: 1;
863
+ }
864
+ }
865
+ @keyframes bounce {
866
+ 0%, 100% {
867
+ transform: translateY(0);
868
+ }
869
+ 50% {
870
+ transform: translateY(-10px);
871
+ }
872
+ }
873
+ @keyframes gentleBob {
874
+ 0%, 100% {
875
+ transform: translateY(0);
876
+ }
877
+ 50% {
878
+ transform: translateY(-5px);
879
+ }
880
+ }
881
+ @keyframes badgePop {
882
+ 0% {
883
+ transform: scale(0) rotate(-10deg);
884
+ opacity: 0;
885
+ }
886
+ 60% {
887
+ transform: scale(1.1) rotate(5deg);
888
+ }
889
+ 100% {
890
+ transform: scale(1) rotate(0deg);
891
+ opacity: 1;
892
+ }
893
+ }
894
+ @keyframes scoreSlideIn {
895
+ 0% {
896
+ transform: translateY(20px);
897
+ opacity: 0;
898
+ }
899
+ 100% {
900
+ transform: translateY(0);
901
+ opacity: 1;
902
+ }
903
+ }
904
+ ` }),
905
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: defaultStyles.results, children: [
906
+ percentage >= 60 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: defaultStyles.confettiContainer, children: confettiPieces.map((piece) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
907
+ "div",
908
+ {
909
+ style: {
910
+ ...defaultStyles.confettiPiece,
911
+ left: piece.left,
912
+ width: `${piece.size}px`,
913
+ height: `${piece.size}px`,
914
+ backgroundColor: piece.color,
915
+ borderRadius: Math.random() > 0.5 ? "50%" : "2px",
916
+ animationDelay: piece.delay,
917
+ animationDuration: piece.duration,
918
+ transform: `rotate(${piece.rotation}deg)`
919
+ }
920
+ },
921
+ piece.id
922
+ )) }),
923
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { ...defaultStyles.resultsBackground, background: theme.bgGradient } }),
924
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: defaultStyles.resultsContent, children: [
925
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: defaultStyles.resultStars, children: [
926
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StarIcon, { filled: theme.stars >= 1, delay: 0.3 }),
927
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StarIcon, { filled: theme.stars >= 2, delay: 0.5 }),
928
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StarIcon, { filled: theme.stars >= 3, delay: 0.7 })
929
+ ] }),
930
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { marginBottom: "16px" }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MascotOwl, { mood: theme.mascotMood }) }),
931
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
932
+ "div",
933
+ {
934
+ style: {
935
+ background: theme.badgeBg,
936
+ padding: "12px 28px",
937
+ borderRadius: "50px",
938
+ boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
939
+ marginBottom: "20px",
940
+ animation: "badgePop 0.6s ease-out 0.2s forwards",
941
+ opacity: 0,
942
+ border: `3px solid ${theme.badgeColor}`
943
+ },
944
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
945
+ "span",
946
+ {
947
+ style: {
948
+ fontSize: "22px",
949
+ fontWeight: "700",
950
+ color: "#1f2937",
951
+ textShadow: "0 1px 2px rgba(255,255,255,0.5)"
952
+ },
953
+ children: theme.badge
954
+ }
955
+ )
956
+ }
957
+ ),
958
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
959
+ "div",
960
+ {
961
+ style: {
962
+ animation: "scoreSlideIn 0.5s ease-out 0.4s forwards",
963
+ opacity: 0
964
+ },
965
+ children: [
966
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
967
+ "div",
968
+ {
969
+ style: {
970
+ fontSize: "48px",
971
+ fontWeight: "800",
972
+ color: theme.color,
973
+ lineHeight: "1",
974
+ marginBottom: "4px"
975
+ },
976
+ children: [
977
+ result.correctAnswers,
978
+ " of ",
979
+ result.totalQuestions
980
+ ]
981
+ }
982
+ ),
983
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
984
+ "div",
985
+ {
986
+ style: {
987
+ fontSize: "20px",
988
+ fontWeight: "600",
989
+ color: "#6b7280",
990
+ marginBottom: "12px"
991
+ },
992
+ children: "correct answers"
993
+ }
994
+ )
995
+ ]
996
+ }
997
+ ),
998
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { ...defaultStyles.resultDetails, marginTop: "8px" }, children: [
999
+ "Time: ",
1000
+ formatTime(result.timeSpentSeconds)
1001
+ ] })
1002
+ ] })
534
1003
  ] })
1004
+ ] });
1005
+ }
1006
+ if (quiz && showIntro) {
1007
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className, style: defaultStyles.container, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: defaultStyles.intro, children: [
1008
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: defaultStyles.introTitle, children: quiz.title }),
1009
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: defaultStyles.introSubtitle, children: "Ready to test your knowledge?" }),
1010
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: defaultStyles.introQuestionCount, children: [
1011
+ quiz.questions.length,
1012
+ " question",
1013
+ quiz.questions.length !== 1 ? "s" : "",
1014
+ " to answer"
1015
+ ] }),
1016
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1017
+ "button",
1018
+ {
1019
+ style: defaultStyles.startButton,
1020
+ onClick: handleStart,
1021
+ onMouseOver: (e) => {
1022
+ e.currentTarget.style.transform = "translateY(-2px)";
1023
+ e.currentTarget.style.boxShadow = "0 6px 20px rgba(124, 58, 237, 0.5)";
1024
+ },
1025
+ onMouseOut: (e) => {
1026
+ e.currentTarget.style.transform = "translateY(0)";
1027
+ e.currentTarget.style.boxShadow = "0 4px 14px rgba(124, 58, 237, 0.4)";
1028
+ },
1029
+ "data-testid": "button-start-quiz",
1030
+ children: "Let's Start!"
1031
+ }
1032
+ )
535
1033
  ] }) });
536
1034
  }
537
1035
  if (!quiz || !currentQuestion) {
538
1036
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className, style: defaultStyles.container, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: defaultStyles.error, children: "No quiz data available" }) });
539
1037
  }
540
1038
  const selectedAnswer = answers.get(currentQuestion.id);
541
- const isLastQuestion = currentQuestionIndex === quiz.questions.length - 1;
542
- const progressPercent = (currentQuestionIndex + 1) / quiz.questions.length * 100;
1039
+ const isLastQuestion = currentQuestionIndex === totalQuestions - 1;
1040
+ const progressPercent = (currentQuestionIndex + 1) / totalQuestions * 100;
1041
+ const remainingSlots = maxQuestions - totalQuestions;
1042
+ const questionsToAdd = Math.min(5, remainingSlots);
1043
+ const canAddMore = onGenerateMoreQuestions && remainingSlots > 0;
543
1044
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className, style: defaultStyles.container, children: [
544
1045
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: defaultStyles.header, children: [
545
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center" }, children: [
546
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: defaultStyles.title, children: quiz.title }),
547
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: defaultStyles.timer, children: formatTime(elapsedSeconds) })
548
- ] }),
1046
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: defaultStyles.title, children: quiz.title }),
549
1047
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: defaultStyles.progress, children: [
550
1048
  "Question ",
551
1049
  currentQuestionIndex + 1,
552
1050
  " of ",
553
- quiz.questions.length
1051
+ totalQuestions
554
1052
  ] }),
555
1053
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: defaultStyles.progressBar, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { ...defaultStyles.progressFill, width: `${progressPercent}%` } }) })
556
1054
  ] }),
557
1055
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: defaultStyles.question, children: [
558
1056
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: defaultStyles.questionText, children: currentQuestion.question }),
559
- (currentQuestion.type === "single" || currentQuestion.type === "true-false") && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: defaultStyles.options, children: currentQuestion.options?.map((option, idx) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
560
- "div",
561
- {
562
- style: {
563
- ...defaultStyles.option,
564
- ...selectedAnswer === option ? defaultStyles.optionSelected : {}
1057
+ (currentQuestion.type === "single" || currentQuestion.type === "true-false") && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: defaultStyles.options, children: currentQuestion.options?.map((option, idx) => {
1058
+ const isSelected = selectedAnswer === option;
1059
+ const isCorrectOption = currentQuestion.correctAnswer === option;
1060
+ let optionStyle = { ...defaultStyles.option };
1061
+ if (showFeedback) {
1062
+ if (isCorrectOption) {
1063
+ optionStyle = { ...optionStyle, ...defaultStyles.optionCorrect };
1064
+ } else if (isSelected && !isCorrectOption) {
1065
+ optionStyle = { ...optionStyle, ...defaultStyles.optionIncorrect };
1066
+ }
1067
+ } else if (isSelected) {
1068
+ optionStyle = { ...optionStyle, ...defaultStyles.optionSelected };
1069
+ }
1070
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1071
+ "div",
1072
+ {
1073
+ style: {
1074
+ ...optionStyle,
1075
+ cursor: showFeedback ? "default" : "pointer"
1076
+ },
1077
+ onClick: () => !showFeedback && handleAnswerChange(option),
1078
+ children: option
565
1079
  },
566
- onClick: () => handleAnswerChange(option),
567
- children: option
568
- },
569
- idx
570
- )) }),
1080
+ idx
1081
+ );
1082
+ }) }),
571
1083
  currentQuestion.type === "multiple" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: defaultStyles.options, children: currentQuestion.options?.map((option, idx) => {
572
1084
  const selected = Array.isArray(selectedAnswer) && selectedAnswer.includes(option);
1085
+ const correctAnswers = Array.isArray(currentQuestion.correctAnswer) ? currentQuestion.correctAnswer : currentQuestion.correctAnswer ? [currentQuestion.correctAnswer] : [];
1086
+ const isCorrectOption = correctAnswers.includes(option);
1087
+ let optionStyle = { ...defaultStyles.option };
1088
+ if (showFeedback) {
1089
+ if (isCorrectOption) {
1090
+ optionStyle = { ...optionStyle, ...defaultStyles.optionCorrect };
1091
+ } else if (selected && !isCorrectOption) {
1092
+ optionStyle = { ...optionStyle, ...defaultStyles.optionIncorrect };
1093
+ }
1094
+ } else if (selected) {
1095
+ optionStyle = { ...optionStyle, ...defaultStyles.optionSelected };
1096
+ }
573
1097
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
574
1098
  "div",
575
1099
  {
576
1100
  style: {
577
- ...defaultStyles.option,
578
- ...selected ? defaultStyles.optionSelected : {}
1101
+ ...optionStyle,
1102
+ cursor: showFeedback ? "default" : "pointer"
579
1103
  },
580
1104
  onClick: () => {
1105
+ if (showFeedback) return;
581
1106
  const current = Array.isArray(selectedAnswer) ? selectedAnswer : [];
582
1107
  if (selected) {
583
1108
  handleAnswerChange(current.filter((o) => o !== option));
@@ -596,7 +1121,8 @@ function QuizPlayer({
596
1121
  style: { ...defaultStyles.input, minHeight: currentQuestion.type === "essay" ? "150px" : "60px" },
597
1122
  value: selectedAnswer || "",
598
1123
  onChange: (e) => handleAnswerChange(e.target.value),
599
- placeholder: "Type your answer here..."
1124
+ placeholder: "Type your answer here...",
1125
+ disabled: showFeedback
600
1126
  }
601
1127
  ),
602
1128
  currentQuestion.type === "fill" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: defaultStyles.options, children: currentQuestion.blanks?.map((_, idx) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
@@ -609,47 +1135,86 @@ function QuizPlayer({
609
1135
  current[idx] = e.target.value;
610
1136
  handleAnswerChange(current);
611
1137
  },
612
- placeholder: `Blank ${idx + 1}`
1138
+ placeholder: `Blank ${idx + 1}`,
1139
+ disabled: showFeedback
613
1140
  },
614
1141
  idx
615
- )) })
1142
+ )) }),
1143
+ showFeedback && currentAnswerDetail && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: {
1144
+ ...defaultStyles.feedback,
1145
+ ...currentAnswerDetail.isCorrect ? defaultStyles.feedbackCorrect : defaultStyles.feedbackIncorrect
1146
+ }, children: [
1147
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: {
1148
+ ...defaultStyles.feedbackTitle,
1149
+ ...currentAnswerDetail.isCorrect ? defaultStyles.feedbackTitleCorrect : defaultStyles.feedbackTitleIncorrect
1150
+ }, children: currentAnswerDetail.isCorrect ? "\u2713 Correct!" : "\u2717 Incorrect" }),
1151
+ currentQuestion.explanation && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: defaultStyles.feedbackExplanation, children: currentQuestion.explanation })
1152
+ ] })
616
1153
  ] }),
617
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: defaultStyles.buttons, children: [
618
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1154
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: defaultStyles.buttonsColumn, children: [
1155
+ showFeedback && isLastQuestion && canAddMore && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
619
1156
  "button",
620
1157
  {
621
1158
  style: {
622
- ...defaultStyles.button,
623
- ...currentQuestionIndex > 0 ? defaultStyles.buttonSecondary : defaultStyles.buttonDisabled
1159
+ ...defaultStyles.buttonAddMore,
1160
+ ...isGeneratingExtra ? defaultStyles.buttonAddMoreDisabled : {}
624
1161
  },
625
- onClick: handlePrevious,
626
- disabled: currentQuestionIndex === 0,
627
- children: "Previous"
1162
+ onClick: handleAddMoreQuestions,
1163
+ disabled: isGeneratingExtra,
1164
+ "data-testid": "button-add-more-questions",
1165
+ children: isGeneratingExtra ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1166
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, { size: 16, color: "#9ca3af" }),
1167
+ "Generating Questions..."
1168
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1169
+ "+ Add ",
1170
+ questionsToAdd,
1171
+ " More Question",
1172
+ questionsToAdd !== 1 ? "s" : ""
1173
+ ] })
628
1174
  }
629
1175
  ),
630
- isLastQuestion ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
631
- "button",
632
- {
633
- style: {
634
- ...defaultStyles.button,
635
- ...isSubmitting ? defaultStyles.buttonDisabled : defaultStyles.buttonPrimary
636
- },
637
- onClick: handleSubmit,
638
- disabled: isSubmitting || selectedAnswer === void 0,
639
- children: isSubmitting ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, { size: 16, color: "#9ca3af" }) : "Submit Quiz"
640
- }
641
- ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
642
- "button",
643
- {
644
- style: {
645
- ...defaultStyles.button,
646
- ...isNavigating || selectedAnswer === void 0 ? defaultStyles.buttonDisabled : defaultStyles.buttonPrimary
647
- },
648
- onClick: handleNext,
649
- disabled: isNavigating || selectedAnswer === void 0,
650
- children: isNavigating ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, { size: 16, color: "#9ca3af" }) : "Next"
651
- }
652
- )
1176
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { ...defaultStyles.buttons, justifyContent: "flex-end" }, children: showFeedback ? (
1177
+ // After viewing feedback
1178
+ isLastQuestion ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1179
+ "button",
1180
+ {
1181
+ style: {
1182
+ ...defaultStyles.button,
1183
+ ...isSubmitting || isGeneratingExtra ? defaultStyles.buttonDisabled : defaultStyles.buttonPrimary
1184
+ },
1185
+ onClick: handleSubmit,
1186
+ disabled: isSubmitting || isGeneratingExtra,
1187
+ "data-testid": "button-submit-quiz",
1188
+ children: isSubmitting ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, { size: 16, color: "#9ca3af" }) : "Submit Quiz"
1189
+ }
1190
+ ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1191
+ "button",
1192
+ {
1193
+ style: {
1194
+ ...defaultStyles.button,
1195
+ ...defaultStyles.buttonPrimary
1196
+ },
1197
+ onClick: handleContinue,
1198
+ "data-testid": "button-continue",
1199
+ children: "Continue"
1200
+ }
1201
+ )
1202
+ ) : (
1203
+ // Before checking answer
1204
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1205
+ "button",
1206
+ {
1207
+ style: {
1208
+ ...defaultStyles.button,
1209
+ ...isNavigating || selectedAnswer === void 0 ? defaultStyles.buttonDisabled : defaultStyles.buttonPrimary
1210
+ },
1211
+ onClick: handleCheckAnswer,
1212
+ disabled: isNavigating || selectedAnswer === void 0,
1213
+ "data-testid": "button-check-answer",
1214
+ children: isNavigating ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, { size: 16, color: "#9ca3af" }) : "Check Answer"
1215
+ }
1216
+ )
1217
+ ) })
653
1218
  ] })
654
1219
  ] });
655
1220
  }