@schoolio/player 1.4.0 → 1.4.2

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.mjs CHANGED
@@ -80,6 +80,29 @@ var QuizApiClient = class {
80
80
  `/api/external/question-chat/${questionId}/${childId}`
81
81
  );
82
82
  }
83
+ async getChatsByAttempt(attemptId) {
84
+ return this.request(
85
+ "GET",
86
+ `/api/external/quiz-attempts/${attemptId}/chats`
87
+ );
88
+ }
89
+ async getTextToSpeech(text, voice = "nova") {
90
+ const headers = {
91
+ "Content-Type": "application/json"
92
+ };
93
+ if (this.authToken) {
94
+ headers["Authorization"] = `Bearer ${this.authToken}`;
95
+ }
96
+ const response = await fetch(`${this.baseUrl}/api/external/tts`, {
97
+ method: "POST",
98
+ headers,
99
+ body: JSON.stringify({ text, voice })
100
+ });
101
+ if (!response.ok) {
102
+ throw new Error(`TTS request failed: HTTP ${response.status}`);
103
+ }
104
+ return response.blob();
105
+ }
83
106
  };
84
107
 
85
108
  // src/utils.ts
@@ -482,9 +505,13 @@ var panelStyles = {
482
505
  messageRow: {
483
506
  display: "flex"
484
507
  },
508
+ messageContent: {
509
+ display: "flex",
510
+ alignItems: "flex-end",
511
+ gap: "4px",
512
+ maxWidth: "85%"
513
+ },
485
514
  userMessage: {
486
- maxWidth: "85%",
487
- marginLeft: "auto",
488
515
  padding: "10px 14px",
489
516
  borderRadius: "16px 16px 4px 16px",
490
517
  backgroundColor: "#6721b0",
@@ -493,8 +520,6 @@ var panelStyles = {
493
520
  lineHeight: 1.4
494
521
  },
495
522
  assistantMessage: {
496
- maxWidth: "85%",
497
- marginRight: "auto",
498
523
  padding: "10px 14px",
499
524
  borderRadius: "16px 16px 16px 4px",
500
525
  backgroundColor: "#ffffff",
@@ -508,7 +533,8 @@ var panelStyles = {
508
533
  borderTop: "1px solid #e2e8f0",
509
534
  backgroundColor: "#ffffff",
510
535
  display: "flex",
511
- gap: "8px"
536
+ gap: "8px",
537
+ alignItems: "center"
512
538
  },
513
539
  input: {
514
540
  flex: 1,
@@ -518,13 +544,11 @@ var panelStyles = {
518
544
  fontSize: "14px",
519
545
  outline: "none"
520
546
  },
521
- sendButton: {
547
+ buttonBase: {
522
548
  width: "40px",
523
549
  height: "40px",
524
550
  borderRadius: "50%",
525
551
  border: "none",
526
- backgroundColor: "#6721b0",
527
- color: "#ffffff",
528
552
  cursor: "pointer",
529
553
  display: "flex",
530
554
  alignItems: "center",
@@ -532,10 +556,47 @@ var panelStyles = {
532
556
  fontSize: "16px",
533
557
  transition: "all 0.2s ease"
534
558
  },
559
+ sendButton: {
560
+ backgroundColor: "#6721b0",
561
+ color: "#ffffff"
562
+ },
535
563
  sendButtonDisabled: {
536
564
  backgroundColor: "#d1d5db",
537
565
  cursor: "not-allowed"
538
566
  },
567
+ micButton: {
568
+ backgroundColor: "#ffffff",
569
+ border: "1px solid #e2e8f0",
570
+ color: "#374151"
571
+ },
572
+ micButtonActive: {
573
+ backgroundColor: "#ef4444",
574
+ color: "#ffffff",
575
+ border: "none"
576
+ },
577
+ speakButton: {
578
+ width: "28px",
579
+ height: "28px",
580
+ borderRadius: "50%",
581
+ border: "none",
582
+ backgroundColor: "transparent",
583
+ color: "#9ca3af",
584
+ cursor: "pointer",
585
+ display: "flex",
586
+ alignItems: "center",
587
+ justifyContent: "center",
588
+ transition: "all 0.2s ease",
589
+ flexShrink: 0
590
+ },
591
+ speakButtonReady: {
592
+ color: "#22c55e"
593
+ },
594
+ speakButtonPlaying: {
595
+ color: "#6721b0"
596
+ },
597
+ speakButtonLoading: {
598
+ color: "#f97316"
599
+ },
539
600
  loadingDots: {
540
601
  display: "flex",
541
602
  alignItems: "center",
@@ -573,6 +634,33 @@ var STARTER_PROMPTS = [
573
634
  { id: "word_help", label: "I don't understand a word", message: "I don't understand a word in this question. Can you help?" },
574
635
  { id: "explain_again", label: "Explain this again", message: "Can you explain this question to me again in a different way?" }
575
636
  ];
637
+ var MicIcon = () => /* @__PURE__ */ jsxs2("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
638
+ /* @__PURE__ */ jsx2("path", { d: "M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" }),
639
+ /* @__PURE__ */ jsx2("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }),
640
+ /* @__PURE__ */ jsx2("line", { x1: "12", x2: "12", y1: "19", y2: "22" })
641
+ ] });
642
+ var MicOffIcon = () => /* @__PURE__ */ jsxs2("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
643
+ /* @__PURE__ */ jsx2("line", { x1: "2", x2: "22", y1: "2", y2: "22" }),
644
+ /* @__PURE__ */ jsx2("path", { d: "M18.89 13.23A7.12 7.12 0 0 0 19 12v-2" }),
645
+ /* @__PURE__ */ jsx2("path", { d: "M5 10v2a7 7 0 0 0 12 5" }),
646
+ /* @__PURE__ */ jsx2("path", { d: "M15 9.34V5a3 3 0 0 0-5.68-1.33" }),
647
+ /* @__PURE__ */ jsx2("path", { d: "M9 9v3a3 3 0 0 0 5.12 2.12" }),
648
+ /* @__PURE__ */ jsx2("line", { x1: "12", x2: "12", y1: "19", y2: "22" })
649
+ ] });
650
+ var VolumeIcon2 = () => /* @__PURE__ */ jsxs2("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
651
+ /* @__PURE__ */ jsx2("polygon", { points: "11 5 6 9 2 9 2 15 6 15 11 19 11 5" }),
652
+ /* @__PURE__ */ jsx2("path", { d: "M15.54 8.46a5 5 0 0 1 0 7.07" }),
653
+ /* @__PURE__ */ jsx2("path", { d: "M19.07 4.93a10 10 0 0 1 0 14.14" })
654
+ ] });
655
+ var SendIcon = () => /* @__PURE__ */ jsxs2("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
656
+ /* @__PURE__ */ jsx2("line", { x1: "22", y1: "2", x2: "11", y2: "13" }),
657
+ /* @__PURE__ */ jsx2("polygon", { points: "22 2 15 22 11 13 2 9 22 2" })
658
+ ] });
659
+ var HelpIcon = () => /* @__PURE__ */ jsxs2("svg", { width: "48", height: "48", viewBox: "0 0 24 24", fill: "none", stroke: "#6721b0", strokeWidth: "2", children: [
660
+ /* @__PURE__ */ jsx2("circle", { cx: "12", cy: "12", r: "10" }),
661
+ /* @__PURE__ */ jsx2("path", { d: "M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" }),
662
+ /* @__PURE__ */ jsx2("line", { x1: "12", y1: "17", x2: "12.01", y2: "17" })
663
+ ] });
576
664
  function QuestionChatPanel({
577
665
  apiClient,
578
666
  question,
@@ -580,20 +668,130 @@ function QuestionChatPanel({
580
668
  childId,
581
669
  parentId,
582
670
  lessonId,
583
- courseId
671
+ courseId,
672
+ answerResult
584
673
  }) {
585
674
  const [messages, setMessages] = useState2([]);
586
675
  const [inputValue, setInputValue] = useState2("");
587
676
  const [isLoading, setIsLoading] = useState2(false);
588
677
  const [chatId, setChatId] = useState2(null);
589
678
  const [hoveredButton, setHoveredButton] = useState2(null);
679
+ const [isListening, setIsListening] = useState2(false);
680
+ const [speakingIndex, setSpeakingIndex] = useState2(null);
681
+ const [audioReadyMap, setAudioReadyMap] = useState2(/* @__PURE__ */ new Map());
682
+ const [hasOfferedHelp, setHasOfferedHelp] = useState2(false);
590
683
  const messagesContainerRef = useRef2(null);
591
684
  const messagesEndRef = useRef2(null);
685
+ const recognitionRef = useRef2(null);
686
+ const audioRef = useRef2(null);
687
+ const audioCacheRef = useRef2(/* @__PURE__ */ new Map());
688
+ const isSpeechSupported = typeof window !== "undefined" && (window.SpeechRecognition || window.webkitSpeechRecognition);
592
689
  const scrollToBottom = useCallback(() => {
593
690
  if (messagesContainerRef.current) {
594
691
  messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight;
595
692
  }
596
693
  }, []);
694
+ const preCacheAudio = useCallback(async (text) => {
695
+ if (audioCacheRef.current.has(text)) {
696
+ setAudioReadyMap((prev) => new Map(prev).set(text, true));
697
+ return;
698
+ }
699
+ setAudioReadyMap((prev) => new Map(prev).set(text, false));
700
+ try {
701
+ const audioBlob = await apiClient.getTextToSpeech(text, "nova");
702
+ audioCacheRef.current.set(text, audioBlob);
703
+ setAudioReadyMap((prev) => new Map(prev).set(text, true));
704
+ } catch (error) {
705
+ console.error("Pre-cache TTS error:", error);
706
+ }
707
+ }, [apiClient]);
708
+ const startListening = useCallback(() => {
709
+ const SpeechRecognitionAPI = window.SpeechRecognition || window.webkitSpeechRecognition;
710
+ if (!SpeechRecognitionAPI) {
711
+ console.log("Speech recognition not supported");
712
+ return;
713
+ }
714
+ const recognition = new SpeechRecognitionAPI();
715
+ recognition.continuous = true;
716
+ recognition.interimResults = true;
717
+ recognition.lang = "en-US";
718
+ recognition.onstart = () => {
719
+ console.log("Speech recognition started");
720
+ setIsListening(true);
721
+ };
722
+ recognition.onresult = (event) => {
723
+ let finalTranscript = "";
724
+ for (let i = 0; i < Object.keys(event.results).length; i++) {
725
+ const result = event.results[i];
726
+ if (result && result[0]) {
727
+ finalTranscript += result[0].transcript;
728
+ }
729
+ }
730
+ if (finalTranscript) {
731
+ setInputValue(finalTranscript);
732
+ }
733
+ };
734
+ recognition.onerror = (event) => {
735
+ console.log("Speech recognition error:", event.error);
736
+ if (event.error === "not-allowed") {
737
+ alert("Microphone access was denied. Please allow microphone access and try again.");
738
+ }
739
+ setIsListening(false);
740
+ };
741
+ recognition.onend = () => {
742
+ console.log("Speech recognition ended");
743
+ setIsListening(false);
744
+ };
745
+ recognitionRef.current = recognition;
746
+ try {
747
+ recognition.start();
748
+ } catch (e) {
749
+ console.log("Failed to start recognition:", e);
750
+ setIsListening(false);
751
+ }
752
+ }, []);
753
+ const stopListening = useCallback(() => {
754
+ if (recognitionRef.current) {
755
+ recognitionRef.current.stop();
756
+ setIsListening(false);
757
+ }
758
+ }, []);
759
+ const speakMessage = useCallback(async (text, index) => {
760
+ if (audioRef.current) {
761
+ audioRef.current.pause();
762
+ audioRef.current = null;
763
+ }
764
+ if (speakingIndex === index) {
765
+ setSpeakingIndex(null);
766
+ return;
767
+ }
768
+ setSpeakingIndex(index);
769
+ try {
770
+ let audioBlob;
771
+ const cachedBlob = audioCacheRef.current.get(text);
772
+ if (cachedBlob) {
773
+ audioBlob = cachedBlob;
774
+ } else {
775
+ audioBlob = await apiClient.getTextToSpeech(text, "nova");
776
+ audioCacheRef.current.set(text, audioBlob);
777
+ }
778
+ const audioUrl = URL.createObjectURL(audioBlob);
779
+ const audio = new Audio(audioUrl);
780
+ audioRef.current = audio;
781
+ audio.onended = () => {
782
+ setSpeakingIndex(null);
783
+ URL.revokeObjectURL(audioUrl);
784
+ };
785
+ audio.onerror = () => {
786
+ setSpeakingIndex(null);
787
+ URL.revokeObjectURL(audioUrl);
788
+ };
789
+ await audio.play();
790
+ } catch (error) {
791
+ console.error("TTS error:", error);
792
+ setSpeakingIndex(null);
793
+ }
794
+ }, [speakingIndex, apiClient]);
597
795
  useEffect2(() => {
598
796
  scrollToBottom();
599
797
  }, [messages, scrollToBottom]);
@@ -601,19 +799,35 @@ function QuestionChatPanel({
601
799
  setMessages([]);
602
800
  setChatId(null);
603
801
  setInputValue("");
802
+ setHasOfferedHelp(false);
604
803
  const loadHistory = async () => {
605
804
  try {
606
805
  const history = await apiClient.getChatHistory(question.id, childId);
607
806
  if (history.chatId && history.messages.length > 0) {
608
807
  setChatId(history.chatId);
609
808
  setMessages(history.messages);
809
+ history.messages.filter((m) => m.role === "assistant").forEach((m) => preCacheAudio(m.content));
610
810
  }
611
811
  } catch (err) {
612
812
  console.error("Failed to load chat history:", err);
613
813
  }
614
814
  };
615
815
  loadHistory();
616
- }, [question.id, childId, apiClient]);
816
+ }, [question.id, childId, apiClient, preCacheAudio]);
817
+ useEffect2(() => {
818
+ if (answerResult?.wasIncorrect && !hasOfferedHelp) {
819
+ setHasOfferedHelp(true);
820
+ const selectedAnswerText = answerResult.selectedAnswer || "that answer";
821
+ const helpMessage = `Looks like you chose "${selectedAnswerText}" which was incorrect. Would you like me to help explain the correct answer?`;
822
+ const assistantMessage = {
823
+ role: "assistant",
824
+ content: helpMessage,
825
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
826
+ };
827
+ setMessages((prev) => [...prev, assistantMessage]);
828
+ preCacheAudio(helpMessage);
829
+ }
830
+ }, [answerResult, hasOfferedHelp, preCacheAudio]);
617
831
  const initializeChat = async () => {
618
832
  if (chatId) return chatId;
619
833
  try {
@@ -635,6 +849,9 @@ function QuestionChatPanel({
635
849
  };
636
850
  const sendMessage = async (messageText) => {
637
851
  if (!messageText.trim() || isLoading) return;
852
+ if (isListening) {
853
+ stopListening();
854
+ }
638
855
  setIsLoading(true);
639
856
  const userMsg = {
640
857
  role: "user",
@@ -654,15 +871,23 @@ function QuestionChatPanel({
654
871
  questionContext: question,
655
872
  childId
656
873
  });
657
- setMessages((prev) => [...prev, response.assistantMessage]);
874
+ const assistantMessage = response.assistantMessage || {
875
+ role: "assistant",
876
+ content: "I'm here to help!",
877
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
878
+ };
879
+ setMessages((prev) => [...prev, assistantMessage]);
880
+ preCacheAudio(assistantMessage.content);
658
881
  } catch (err) {
659
882
  console.error("Failed to send message:", err);
883
+ const content = "Sorry, I'm having trouble right now. Please try again!";
660
884
  const errorMsg = {
661
885
  role: "assistant",
662
- content: "Sorry, I'm having trouble right now. Please try again!",
886
+ content,
663
887
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
664
888
  };
665
889
  setMessages((prev) => [...prev, errorMsg]);
890
+ preCacheAudio(content);
666
891
  } finally {
667
892
  setIsLoading(false);
668
893
  }
@@ -679,14 +904,14 @@ function QuestionChatPanel({
679
904
  0%, 60%, 100% { transform: translateY(0); }
680
905
  30% { transform: translateY(-4px); }
681
906
  }
907
+ @keyframes pulse {
908
+ 0%, 100% { opacity: 1; }
909
+ 50% { opacity: 0.5; }
910
+ }
682
911
  ` }),
683
912
  /* @__PURE__ */ jsx2("div", { style: panelStyles.header, children: /* @__PURE__ */ jsx2("span", { children: "Need Help?" }) }),
684
913
  /* @__PURE__ */ jsx2("div", { ref: messagesContainerRef, style: panelStyles.messagesContainer, children: messages.length === 0 ? /* @__PURE__ */ jsxs2("div", { style: panelStyles.emptyState, children: [
685
- /* @__PURE__ */ jsx2("div", { style: panelStyles.helperIcon, children: /* @__PURE__ */ jsxs2("svg", { width: "48", height: "48", viewBox: "0 0 24 24", fill: "none", stroke: "#6721b0", strokeWidth: "2", children: [
686
- /* @__PURE__ */ jsx2("circle", { cx: "12", cy: "12", r: "10" }),
687
- /* @__PURE__ */ jsx2("path", { d: "M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" }),
688
- /* @__PURE__ */ jsx2("line", { x1: "12", y1: "17", x2: "12.01", y2: "17" })
689
- ] }) }),
914
+ /* @__PURE__ */ jsx2("div", { style: panelStyles.helperIcon, children: /* @__PURE__ */ jsx2(HelpIcon, {}) }),
690
915
  /* @__PURE__ */ jsx2("div", { style: { fontSize: "14px", fontWeight: "500", marginBottom: "8px" }, children: "Hi! I'm your question helper" }),
691
916
  /* @__PURE__ */ jsx2("div", { style: { fontSize: "13px", color: "#9ca3af" }, children: "Ask me if you need help understanding this question" }),
692
917
  /* @__PURE__ */ jsx2("div", { style: { ...panelStyles.starterPrompts, marginTop: "16px" }, children: STARTER_PROMPTS.map((prompt) => /* @__PURE__ */ jsx2(
@@ -713,7 +938,26 @@ function QuestionChatPanel({
713
938
  ...panelStyles.messageRow,
714
939
  justifyContent: msg.role === "user" ? "flex-end" : "flex-start"
715
940
  },
716
- children: /* @__PURE__ */ jsx2("div", { style: msg.role === "user" ? panelStyles.userMessage : panelStyles.assistantMessage, children: msg.content })
941
+ children: /* @__PURE__ */ jsxs2("div", { style: {
942
+ ...panelStyles.messageContent,
943
+ flexDirection: msg.role === "user" ? "row-reverse" : "row"
944
+ }, children: [
945
+ /* @__PURE__ */ jsx2("div", { style: msg.role === "user" ? panelStyles.userMessage : panelStyles.assistantMessage, children: msg.content }),
946
+ msg.role === "assistant" && /* @__PURE__ */ jsx2(
947
+ "button",
948
+ {
949
+ style: {
950
+ ...panelStyles.speakButton,
951
+ ...speakingIndex === idx ? panelStyles.speakButtonPlaying : audioReadyMap.get(msg.content) === true ? panelStyles.speakButtonReady : audioReadyMap.get(msg.content) === false ? panelStyles.speakButtonLoading : {},
952
+ ...audioReadyMap.get(msg.content) === false ? { animation: "pulse 1.5s ease-in-out infinite" } : {}
953
+ },
954
+ onClick: () => speakMessage(msg.content, idx),
955
+ "data-testid": `button-speak-${idx}`,
956
+ title: "Listen to this message",
957
+ children: /* @__PURE__ */ jsx2(VolumeIcon2, {})
958
+ }
959
+ )
960
+ ] })
717
961
  },
718
962
  idx
719
963
  )),
@@ -732,26 +976,38 @@ function QuestionChatPanel({
732
976
  value: inputValue,
733
977
  onChange: (e) => setInputValue(e.target.value),
734
978
  onKeyPress: handleKeyPress,
735
- placeholder: "Ask about this question...",
979
+ placeholder: isListening ? "Listening..." : "Ask about this question...",
736
980
  style: panelStyles.input,
737
- disabled: isLoading,
981
+ disabled: isLoading || isListening,
738
982
  "data-testid": "input-chat-message"
739
983
  }
740
984
  ),
985
+ isSpeechSupported && /* @__PURE__ */ jsx2(
986
+ "button",
987
+ {
988
+ onClick: isListening ? stopListening : startListening,
989
+ disabled: isLoading,
990
+ style: {
991
+ ...panelStyles.buttonBase,
992
+ ...isListening ? panelStyles.micButtonActive : panelStyles.micButton
993
+ },
994
+ "data-testid": "button-voice-input",
995
+ title: isListening ? "Stop listening" : "Speak your question",
996
+ children: isListening ? /* @__PURE__ */ jsx2(MicOffIcon, {}) : /* @__PURE__ */ jsx2(MicIcon, {})
997
+ }
998
+ ),
741
999
  /* @__PURE__ */ jsx2(
742
1000
  "button",
743
1001
  {
744
1002
  onClick: () => sendMessage(inputValue),
745
1003
  disabled: isLoading || !inputValue.trim(),
746
1004
  style: {
1005
+ ...panelStyles.buttonBase,
747
1006
  ...panelStyles.sendButton,
748
1007
  ...isLoading || !inputValue.trim() ? panelStyles.sendButtonDisabled : {}
749
1008
  },
750
1009
  "data-testid": "button-send-chat",
751
- children: /* @__PURE__ */ jsxs2("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
752
- /* @__PURE__ */ jsx2("line", { x1: "22", y1: "2", x2: "11", y2: "13" }),
753
- /* @__PURE__ */ jsx2("polygon", { points: "22 2 15 22 11 13 2 9 22 2" })
754
- ] })
1010
+ children: /* @__PURE__ */ jsx2(SendIcon, {})
755
1011
  }
756
1012
  )
757
1013
  ] })
@@ -764,15 +1020,28 @@ var defaultStyles = {
764
1020
  container: {
765
1021
  fontFamily: "system-ui, -apple-system, sans-serif",
766
1022
  width: "100%",
1023
+ height: "100%",
767
1024
  padding: "20px",
768
1025
  backgroundColor: "#ffffff",
769
1026
  borderRadius: "12px",
770
- boxSizing: "border-box"
1027
+ boxSizing: "border-box",
1028
+ display: "flex",
1029
+ flexDirection: "column"
771
1030
  },
772
1031
  header: {
773
1032
  marginBottom: "20px",
774
1033
  borderBottom: "1px solid #e5e7eb",
775
- paddingBottom: "16px"
1034
+ paddingBottom: "16px",
1035
+ display: "flex",
1036
+ flexDirection: "column"
1037
+ },
1038
+ headerTop: {
1039
+ display: "flex",
1040
+ justifyContent: "space-between",
1041
+ alignItems: "flex-start"
1042
+ },
1043
+ headerLeft: {
1044
+ flex: 1
776
1045
  },
777
1046
  title: {
778
1047
  fontSize: "24px",
@@ -784,7 +1053,8 @@ var defaultStyles = {
784
1053
  color: "#6b7280"
785
1054
  },
786
1055
  progressBar: {
787
- width: "100%",
1056
+ width: "50%",
1057
+ maxWidth: "300px",
788
1058
  height: "8px",
789
1059
  backgroundColor: "#e5e7eb",
790
1060
  borderRadius: "4px",
@@ -899,16 +1169,21 @@ var defaultStyles = {
899
1169
  },
900
1170
  mainLayout: {
901
1171
  display: "flex",
902
- gap: "24px"
1172
+ gap: "24px",
1173
+ flex: 1,
1174
+ minHeight: 0,
1175
+ alignItems: "stretch"
903
1176
  },
904
1177
  quizContent: {
905
1178
  flex: 1,
906
- minWidth: 0
1179
+ minWidth: 0,
1180
+ overflow: "auto"
907
1181
  },
908
1182
  chatPanel: {
909
1183
  width: "320px",
910
1184
  flexShrink: 0,
911
- height: "460px"
1185
+ display: "flex",
1186
+ flexDirection: "column"
912
1187
  },
913
1188
  timer: {
914
1189
  fontSize: "14px",
@@ -1116,6 +1391,16 @@ function QuizPlayer({
1116
1391
  const apiClient = useRef3(null);
1117
1392
  const timerRef = useRef3(null);
1118
1393
  const startTimeRef = useRef3(0);
1394
+ const onCompleteRef = useRef3(onComplete);
1395
+ const onErrorRef = useRef3(onError);
1396
+ const onProgressRef = useRef3(onProgress);
1397
+ const onGenerateMoreQuestionsRef = useRef3(onGenerateMoreQuestions);
1398
+ useEffect3(() => {
1399
+ onCompleteRef.current = onComplete;
1400
+ onErrorRef.current = onError;
1401
+ onProgressRef.current = onProgress;
1402
+ onGenerateMoreQuestionsRef.current = onGenerateMoreQuestions;
1403
+ });
1119
1404
  useEffect3(() => {
1120
1405
  apiClient.current = new QuizApiClient({ baseUrl: apiBaseUrl, authToken });
1121
1406
  }, [apiBaseUrl, authToken]);
@@ -1162,11 +1447,11 @@ function QuizPlayer({
1162
1447
  const message = err instanceof Error ? err.message : "Failed to load quiz";
1163
1448
  setError(message);
1164
1449
  setIsLoading(false);
1165
- onError?.(err instanceof Error ? err : new Error(message));
1450
+ onErrorRef.current?.(err instanceof Error ? err : new Error(message));
1166
1451
  }
1167
1452
  }
1168
1453
  initialize();
1169
- }, [quizId, lessonId, assignLessonId, courseId, childId, parentId, forceNewAttempt, onError]);
1454
+ }, [quizId, lessonId, assignLessonId, courseId, childId, parentId, forceNewAttempt]);
1170
1455
  useEffect3(() => {
1171
1456
  if (timerStarted && !isCompleted && !error) {
1172
1457
  startTimeRef.current = Date.now();
@@ -1192,14 +1477,14 @@ function QuizPlayer({
1192
1477
  const totalQuestions = allQuestions.length;
1193
1478
  const maxQuestions = 50;
1194
1479
  useEffect3(() => {
1195
- if (quiz && onProgress) {
1196
- onProgress({
1480
+ if (quiz && onProgressRef.current) {
1481
+ onProgressRef.current({
1197
1482
  currentQuestion: currentQuestionIndex + 1,
1198
1483
  totalQuestions,
1199
1484
  answeredQuestions: answers.size
1200
1485
  });
1201
1486
  }
1202
- }, [currentQuestionIndex, answers.size, quiz, onProgress, totalQuestions]);
1487
+ }, [currentQuestionIndex, answers.size, quiz, totalQuestions]);
1203
1488
  const currentQuestion = allQuestions[currentQuestionIndex];
1204
1489
  const handleAnswerChange = useCallback2((value) => {
1205
1490
  if (!currentQuestion) return;
@@ -1244,7 +1529,7 @@ function QuizPlayer({
1244
1529
  if (totalQuestions >= maxQuestions) return;
1245
1530
  setIsGeneratingExtra(true);
1246
1531
  try {
1247
- const result2 = await onGenerateMoreQuestions(attempt.id, totalQuestions);
1532
+ const result2 = await onGenerateMoreQuestionsRef.current(attempt.id, totalQuestions);
1248
1533
  if (result2.extraQuestions && result2.extraQuestions.length > 0) {
1249
1534
  const slotsAvailable = maxQuestions - totalQuestions;
1250
1535
  const questionsToAppend = result2.extraQuestions.slice(0, slotsAvailable);
@@ -1257,11 +1542,11 @@ function QuizPlayer({
1257
1542
  }
1258
1543
  } catch (err) {
1259
1544
  console.error("Failed to generate extra questions:", err);
1260
- onError?.(err instanceof Error ? err : new Error("Failed to generate extra questions"));
1545
+ onErrorRef.current?.(err instanceof Error ? err : new Error("Failed to generate extra questions"));
1261
1546
  } finally {
1262
1547
  setIsGeneratingExtra(false);
1263
1548
  }
1264
- }, [attempt, onGenerateMoreQuestions, isGeneratingExtra, totalQuestions, maxQuestions, onError]);
1549
+ }, [attempt, isGeneratingExtra, totalQuestions, maxQuestions]);
1265
1550
  const handleSubmit = useCallback2(async () => {
1266
1551
  if (!quiz || !attempt || !apiClient.current) return;
1267
1552
  setIsSubmitting(true);
@@ -1300,15 +1585,15 @@ function QuizPlayer({
1300
1585
  if (timerRef.current) {
1301
1586
  clearInterval(timerRef.current);
1302
1587
  }
1303
- onComplete?.(quizResult);
1588
+ onCompleteRef.current?.(quizResult);
1304
1589
  } catch (err) {
1305
1590
  const message = err instanceof Error ? err.message : "Failed to submit quiz";
1306
1591
  setError(message);
1307
- onError?.(err instanceof Error ? err : new Error(message));
1592
+ onErrorRef.current?.(err instanceof Error ? err : new Error(message));
1308
1593
  } finally {
1309
1594
  setIsSubmitting(false);
1310
1595
  }
1311
- }, [quiz, attempt, currentQuestion, answers, answersDetail, onComplete, onError, totalQuestions, timerStarted, elapsedSeconds]);
1596
+ }, [quiz, attempt, currentQuestion, answers, answersDetail, totalQuestions, timerStarted, elapsedSeconds]);
1312
1597
  const isExtraQuestion = currentQuestion && extraQuestions.some((q) => q.id === currentQuestion.id);
1313
1598
  const handleSkipQuestion = useCallback2(async (reason, comment) => {
1314
1599
  if (!currentQuestion || !apiClient.current || !attempt) return;
@@ -1344,7 +1629,7 @@ function QuizPlayer({
1344
1629
  timeSpentSeconds: elapsedSeconds
1345
1630
  };
1346
1631
  setResult(quizResult);
1347
- onComplete?.(quizResult);
1632
+ onCompleteRef.current?.(quizResult);
1348
1633
  } else if (currentQuestionIndex >= newTotalQuestions) {
1349
1634
  setCurrentQuestionIndex(newTotalQuestions - 1);
1350
1635
  }
@@ -1356,7 +1641,7 @@ function QuizPlayer({
1356
1641
  } finally {
1357
1642
  setIsSkipping(false);
1358
1643
  }
1359
- }, [currentQuestion, apiClient, attempt, quiz, childId, parentId, lessonId, courseId, assignLessonId, skippedQuestionIds, extraQuestions, currentQuestionIndex, elapsedSeconds, onComplete]);
1644
+ }, [currentQuestion, apiClient, attempt, quiz, childId, parentId, lessonId, courseId, assignLessonId, skippedQuestionIds, extraQuestions, currentQuestionIndex, elapsedSeconds]);
1360
1645
  const handleReportQuestion = useCallback2(async (comment) => {
1361
1646
  if (!currentQuestion || !apiClient.current || !attempt || !comment.trim()) return;
1362
1647
  setIsReporting(true);
@@ -1726,533 +2011,537 @@ function QuizPlayer({
1726
2011
  const remainingSlots = maxQuestions - totalQuestions;
1727
2012
  const questionsToAdd = Math.min(5, remainingSlots);
1728
2013
  const canAddMore = onGenerateMoreQuestions && remainingSlots > 0;
1729
- return /* @__PURE__ */ jsxs3("div", { className, style: defaultStyles.container, children: [
1730
- /* @__PURE__ */ jsxs3("div", { style: defaultStyles.header, children: [
1731
- /* @__PURE__ */ jsx3("div", { style: defaultStyles.title, children: quiz.title }),
1732
- /* @__PURE__ */ jsxs3("div", { style: defaultStyles.progress, children: [
1733
- "Question ",
1734
- currentQuestionIndex + 1,
1735
- " of ",
1736
- totalQuestions
2014
+ return /* @__PURE__ */ jsx3("div", { className, style: defaultStyles.container, children: /* @__PURE__ */ jsxs3("div", { style: defaultStyles.mainLayout, children: [
2015
+ /* @__PURE__ */ jsxs3("div", { style: defaultStyles.quizContent, children: [
2016
+ /* @__PURE__ */ jsxs3("div", { style: defaultStyles.header, children: [
2017
+ /* @__PURE__ */ jsx3("div", { style: defaultStyles.title, children: quiz.title }),
2018
+ /* @__PURE__ */ jsxs3("div", { style: defaultStyles.progress, children: [
2019
+ "Question ",
2020
+ currentQuestionIndex + 1,
2021
+ " of ",
2022
+ totalQuestions
2023
+ ] }),
2024
+ /* @__PURE__ */ jsx3("div", { style: defaultStyles.progressBar, children: /* @__PURE__ */ jsx3("div", { style: { ...defaultStyles.progressFill, width: `${progressPercent}%` } }) })
1737
2025
  ] }),
1738
- /* @__PURE__ */ jsx3("div", { style: defaultStyles.progressBar, children: /* @__PURE__ */ jsx3("div", { style: { ...defaultStyles.progressFill, width: `${progressPercent}%` } }) })
1739
- ] }),
1740
- /* @__PURE__ */ jsxs3("div", { style: defaultStyles.mainLayout, children: [
1741
- /* @__PURE__ */ jsxs3("div", { style: defaultStyles.quizContent, children: [
1742
- /* @__PURE__ */ jsxs3("div", { style: { ...defaultStyles.question, position: "relative", paddingBottom: "40px" }, children: [
1743
- /* @__PURE__ */ jsx3("div", { style: defaultStyles.questionText, children: /* @__PURE__ */ jsx3(TextToSpeech, { text: currentQuestion.question, inline: true, size: "md" }) }),
1744
- isExtraQuestion && !showFeedback && /* @__PURE__ */ jsxs3(
1745
- "button",
2026
+ /* @__PURE__ */ jsxs3("div", { style: { ...defaultStyles.question, position: "relative", paddingBottom: "40px" }, children: [
2027
+ /* @__PURE__ */ jsx3("div", { style: defaultStyles.questionText, children: /* @__PURE__ */ jsx3(TextToSpeech, { text: currentQuestion.question, inline: true, size: "md" }) }),
2028
+ isExtraQuestion && /* @__PURE__ */ jsxs3(
2029
+ "button",
2030
+ {
2031
+ onClick: () => setShowSkipModal(true),
2032
+ title: "Skip question",
2033
+ style: {
2034
+ position: "absolute",
2035
+ bottom: "8px",
2036
+ left: "0",
2037
+ background: "transparent",
2038
+ border: "none",
2039
+ cursor: "pointer",
2040
+ padding: "6px 10px",
2041
+ borderRadius: "6px",
2042
+ color: "#9ca3af",
2043
+ display: "flex",
2044
+ alignItems: "center",
2045
+ justifyContent: "center",
2046
+ gap: "4px",
2047
+ fontSize: "12px",
2048
+ opacity: 0.6,
2049
+ transition: "opacity 0.2s, color 0.2s"
2050
+ },
2051
+ onMouseEnter: (e) => {
2052
+ e.currentTarget.style.opacity = "1";
2053
+ e.currentTarget.style.color = "#6b7280";
2054
+ },
2055
+ onMouseLeave: (e) => {
2056
+ e.currentTarget.style.opacity = "0.6";
2057
+ e.currentTarget.style.color = "#9ca3af";
2058
+ },
2059
+ "data-testid": "button-skip-question",
2060
+ children: [
2061
+ /* @__PURE__ */ jsxs3("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
2062
+ /* @__PURE__ */ jsx3("polygon", { points: "5 4 15 12 5 20 5 4" }),
2063
+ /* @__PURE__ */ jsx3("line", { x1: "19", y1: "5", x2: "19", y2: "19" })
2064
+ ] }),
2065
+ /* @__PURE__ */ jsx3("span", { children: "Skip" })
2066
+ ]
2067
+ }
2068
+ ),
2069
+ !isExtraQuestion && /* @__PURE__ */ jsxs3(
2070
+ "button",
2071
+ {
2072
+ onClick: () => setShowReportModal(true),
2073
+ title: "Report an issue with this question",
2074
+ style: {
2075
+ position: "absolute",
2076
+ bottom: "8px",
2077
+ left: "0",
2078
+ background: "transparent",
2079
+ border: "none",
2080
+ cursor: "pointer",
2081
+ padding: "6px 10px",
2082
+ borderRadius: "6px",
2083
+ color: "#9ca3af",
2084
+ display: "flex",
2085
+ alignItems: "center",
2086
+ justifyContent: "center",
2087
+ gap: "4px",
2088
+ fontSize: "12px",
2089
+ opacity: 0.6,
2090
+ transition: "opacity 0.2s, color 0.2s"
2091
+ },
2092
+ onMouseEnter: (e) => {
2093
+ e.currentTarget.style.opacity = "1";
2094
+ e.currentTarget.style.color = "#ef4444";
2095
+ },
2096
+ onMouseLeave: (e) => {
2097
+ e.currentTarget.style.opacity = "0.6";
2098
+ e.currentTarget.style.color = "#9ca3af";
2099
+ },
2100
+ "data-testid": "button-report-question",
2101
+ children: [
2102
+ /* @__PURE__ */ jsxs3("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
2103
+ /* @__PURE__ */ jsx3("path", { d: "M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z" }),
2104
+ /* @__PURE__ */ jsx3("line", { x1: "4", y1: "22", x2: "4", y2: "15" })
2105
+ ] }),
2106
+ /* @__PURE__ */ jsx3("span", { children: "Report" })
2107
+ ]
2108
+ }
2109
+ ),
2110
+ (currentQuestion.type === "single" || currentQuestion.type === "true-false") && /* @__PURE__ */ jsx3("div", { style: defaultStyles.options, children: currentQuestion.options?.map((option, idx) => {
2111
+ const isSelected = selectedAnswer === option;
2112
+ const isCorrectOption = currentQuestion.correctAnswer === option;
2113
+ let optionStyle = { ...defaultStyles.option };
2114
+ if (showFeedback) {
2115
+ if (isCorrectOption) {
2116
+ optionStyle = { ...optionStyle, ...defaultStyles.optionCorrect };
2117
+ } else if (isSelected && !isCorrectOption) {
2118
+ optionStyle = { ...optionStyle, ...defaultStyles.optionIncorrect };
2119
+ }
2120
+ } else if (isSelected) {
2121
+ optionStyle = { ...optionStyle, ...defaultStyles.optionSelected };
2122
+ }
2123
+ return /* @__PURE__ */ jsxs3(
2124
+ "div",
1746
2125
  {
1747
- onClick: () => setShowSkipModal(true),
1748
- title: "Skip question",
1749
2126
  style: {
1750
- position: "absolute",
1751
- bottom: "8px",
1752
- left: "0",
1753
- background: "transparent",
1754
- border: "none",
1755
- cursor: "pointer",
1756
- padding: "6px 10px",
1757
- borderRadius: "6px",
1758
- color: "#9ca3af",
2127
+ ...optionStyle,
2128
+ cursor: showFeedback ? "default" : "pointer",
1759
2129
  display: "flex",
1760
2130
  alignItems: "center",
1761
- justifyContent: "center",
1762
- gap: "4px",
1763
- fontSize: "12px",
1764
- opacity: 0.6,
1765
- transition: "opacity 0.2s, color 0.2s"
1766
- },
1767
- onMouseEnter: (e) => {
1768
- e.currentTarget.style.opacity = "1";
1769
- e.currentTarget.style.color = "#6b7280";
2131
+ gap: "8px"
1770
2132
  },
1771
- onMouseLeave: (e) => {
1772
- e.currentTarget.style.opacity = "0.6";
1773
- e.currentTarget.style.color = "#9ca3af";
1774
- },
1775
- "data-testid": "button-skip-question",
2133
+ onClick: () => !showFeedback && handleAnswerChange(option),
1776
2134
  children: [
1777
- /* @__PURE__ */ jsxs3("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1778
- /* @__PURE__ */ jsx3("polygon", { points: "5 4 15 12 5 20 5 4" }),
1779
- /* @__PURE__ */ jsx3("line", { x1: "19", y1: "5", x2: "19", y2: "19" })
1780
- ] }),
1781
- /* @__PURE__ */ jsx3("span", { children: "Skip" })
2135
+ /* @__PURE__ */ jsx3("span", { onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsx3(TextToSpeech, { text: option, size: "sm" }) }),
2136
+ /* @__PURE__ */ jsx3("span", { style: { flex: 1 }, children: option })
1782
2137
  ]
2138
+ },
2139
+ idx
2140
+ );
2141
+ }) }),
2142
+ currentQuestion.type === "multiple" && /* @__PURE__ */ jsx3("div", { style: defaultStyles.options, children: currentQuestion.options?.map((option, idx) => {
2143
+ const selected = Array.isArray(selectedAnswer) && selectedAnswer.includes(option);
2144
+ const correctAnswers = Array.isArray(currentQuestion.correctAnswer) ? currentQuestion.correctAnswer : currentQuestion.correctAnswer ? [currentQuestion.correctAnswer] : [];
2145
+ const isCorrectOption = correctAnswers.includes(option);
2146
+ let optionStyle = { ...defaultStyles.option };
2147
+ if (showFeedback) {
2148
+ if (isCorrectOption) {
2149
+ optionStyle = { ...optionStyle, ...defaultStyles.optionCorrect };
2150
+ } else if (selected && !isCorrectOption) {
2151
+ optionStyle = { ...optionStyle, ...defaultStyles.optionIncorrect };
1783
2152
  }
1784
- ),
1785
- !isExtraQuestion && !showFeedback && /* @__PURE__ */ jsxs3(
1786
- "button",
2153
+ } else if (selected) {
2154
+ optionStyle = { ...optionStyle, ...defaultStyles.optionSelected };
2155
+ }
2156
+ return /* @__PURE__ */ jsxs3(
2157
+ "div",
1787
2158
  {
1788
- onClick: () => setShowReportModal(true),
1789
- title: "Report an issue with this question",
1790
2159
  style: {
1791
- position: "absolute",
1792
- bottom: "8px",
1793
- left: "0",
1794
- background: "transparent",
1795
- border: "none",
1796
- cursor: "pointer",
1797
- padding: "6px 10px",
1798
- borderRadius: "6px",
1799
- color: "#9ca3af",
2160
+ ...optionStyle,
2161
+ cursor: showFeedback ? "default" : "pointer",
1800
2162
  display: "flex",
1801
2163
  alignItems: "center",
1802
- justifyContent: "center",
1803
- gap: "4px",
1804
- fontSize: "12px",
1805
- opacity: 0.6,
1806
- transition: "opacity 0.2s, color 0.2s"
1807
- },
1808
- onMouseEnter: (e) => {
1809
- e.currentTarget.style.opacity = "1";
1810
- e.currentTarget.style.color = "#ef4444";
2164
+ gap: "8px"
1811
2165
  },
1812
- onMouseLeave: (e) => {
1813
- e.currentTarget.style.opacity = "0.6";
1814
- e.currentTarget.style.color = "#9ca3af";
2166
+ onClick: () => {
2167
+ if (showFeedback) return;
2168
+ const current = Array.isArray(selectedAnswer) ? selectedAnswer : [];
2169
+ if (selected) {
2170
+ handleAnswerChange(current.filter((o) => o !== option));
2171
+ } else {
2172
+ handleAnswerChange([...current, option]);
2173
+ }
1815
2174
  },
1816
- "data-testid": "button-report-question",
1817
2175
  children: [
1818
- /* @__PURE__ */ jsxs3("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1819
- /* @__PURE__ */ jsx3("path", { d: "M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z" }),
1820
- /* @__PURE__ */ jsx3("line", { x1: "4", y1: "22", x2: "4", y2: "15" })
1821
- ] }),
1822
- /* @__PURE__ */ jsx3("span", { children: "Report" })
2176
+ /* @__PURE__ */ jsx3("span", { onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsx3(TextToSpeech, { text: option, size: "sm" }) }),
2177
+ /* @__PURE__ */ jsx3("span", { style: { flex: 1 }, children: option })
1823
2178
  ]
2179
+ },
2180
+ idx
2181
+ );
2182
+ }) }),
2183
+ (currentQuestion.type === "free" || currentQuestion.type === "essay") && /* @__PURE__ */ jsx3(
2184
+ "textarea",
2185
+ {
2186
+ style: { ...defaultStyles.input, minHeight: currentQuestion.type === "essay" ? "150px" : "60px" },
2187
+ value: selectedAnswer || "",
2188
+ onChange: (e) => handleAnswerChange(e.target.value),
2189
+ placeholder: "Type your answer here...",
2190
+ disabled: showFeedback
2191
+ }
2192
+ ),
2193
+ currentQuestion.type === "fill" && /* @__PURE__ */ jsx3("div", { style: defaultStyles.options, children: currentQuestion.blanks?.map((_, idx) => /* @__PURE__ */ jsx3(
2194
+ "input",
2195
+ {
2196
+ style: defaultStyles.input,
2197
+ value: (Array.isArray(selectedAnswer) ? selectedAnswer[idx] : "") || "",
2198
+ onChange: (e) => {
2199
+ const current = Array.isArray(selectedAnswer) ? [...selectedAnswer] : [];
2200
+ current[idx] = e.target.value;
2201
+ handleAnswerChange(current);
2202
+ },
2203
+ placeholder: `Blank ${idx + 1}`,
2204
+ disabled: showFeedback
2205
+ },
2206
+ idx
2207
+ )) }),
2208
+ showFeedback && currentAnswerDetail && /* @__PURE__ */ jsxs3("div", { style: {
2209
+ ...defaultStyles.feedback,
2210
+ ...currentAnswerDetail.isCorrect ? defaultStyles.feedbackCorrect : defaultStyles.feedbackIncorrect
2211
+ }, children: [
2212
+ /* @__PURE__ */ jsx3("div", { style: {
2213
+ ...defaultStyles.feedbackTitle,
2214
+ ...currentAnswerDetail.isCorrect ? defaultStyles.feedbackTitleCorrect : defaultStyles.feedbackTitleIncorrect
2215
+ }, children: currentAnswerDetail.isCorrect ? "\u2713 Correct!" : "\u2717 Incorrect" }),
2216
+ currentQuestion.explanation && /* @__PURE__ */ jsx3("div", { style: defaultStyles.feedbackExplanation, children: currentQuestion.explanation })
2217
+ ] })
2218
+ ] }),
2219
+ showSkipModal && /* @__PURE__ */ jsx3("div", { style: {
2220
+ position: "fixed",
2221
+ top: 0,
2222
+ left: 0,
2223
+ right: 0,
2224
+ bottom: 0,
2225
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
2226
+ display: "flex",
2227
+ alignItems: "center",
2228
+ justifyContent: "center",
2229
+ zIndex: 1e3
2230
+ }, children: /* @__PURE__ */ jsxs3("div", { style: {
2231
+ backgroundColor: "#ffffff",
2232
+ borderRadius: "12px",
2233
+ padding: "24px",
2234
+ maxWidth: "400px",
2235
+ width: "90%",
2236
+ boxShadow: "0 20px 40px rgba(0, 0, 0, 0.2)"
2237
+ }, children: [
2238
+ /* @__PURE__ */ jsx3("h3", { style: { margin: "0 0 8px 0", fontSize: "18px", fontWeight: "600", color: "#1f2937" }, children: "Skip Question" }),
2239
+ /* @__PURE__ */ jsx3("p", { style: { margin: "0 0 16px 0", fontSize: "14px", color: "#6b7280" }, children: "Help us improve by telling us why you're skipping this question." }),
2240
+ /* @__PURE__ */ jsxs3("div", { style: { display: "flex", flexDirection: "column", gap: "8px", marginBottom: "16px" }, children: [
2241
+ /* @__PURE__ */ jsx3(
2242
+ "button",
2243
+ {
2244
+ onClick: () => setSelectedSkipReason("question_issue"),
2245
+ disabled: isSkipping,
2246
+ style: {
2247
+ padding: "12px 16px",
2248
+ borderRadius: "8px",
2249
+ border: selectedSkipReason === "question_issue" ? "2px solid #8b5cf6" : "1px solid #e5e7eb",
2250
+ backgroundColor: selectedSkipReason === "question_issue" ? "#f5f3ff" : "#f9fafb",
2251
+ cursor: isSkipping ? "not-allowed" : "pointer",
2252
+ fontSize: "14px",
2253
+ fontWeight: "500",
2254
+ color: "#374151",
2255
+ textAlign: "left",
2256
+ opacity: isSkipping ? 0.6 : 1
2257
+ },
2258
+ "data-testid": "button-skip-reason-issue",
2259
+ children: "Question has an issue"
1824
2260
  }
1825
2261
  ),
1826
- (currentQuestion.type === "single" || currentQuestion.type === "true-false") && /* @__PURE__ */ jsx3("div", { style: defaultStyles.options, children: currentQuestion.options?.map((option, idx) => {
1827
- const isSelected = selectedAnswer === option;
1828
- const isCorrectOption = currentQuestion.correctAnswer === option;
1829
- let optionStyle = { ...defaultStyles.option };
1830
- if (showFeedback) {
1831
- if (isCorrectOption) {
1832
- optionStyle = { ...optionStyle, ...defaultStyles.optionCorrect };
1833
- } else if (isSelected && !isCorrectOption) {
1834
- optionStyle = { ...optionStyle, ...defaultStyles.optionIncorrect };
1835
- }
1836
- } else if (isSelected) {
1837
- optionStyle = { ...optionStyle, ...defaultStyles.optionSelected };
1838
- }
1839
- return /* @__PURE__ */ jsxs3(
1840
- "div",
1841
- {
1842
- style: {
1843
- ...optionStyle,
1844
- cursor: showFeedback ? "default" : "pointer",
1845
- display: "flex",
1846
- alignItems: "center",
1847
- gap: "8px"
1848
- },
1849
- onClick: () => !showFeedback && handleAnswerChange(option),
1850
- children: [
1851
- /* @__PURE__ */ jsx3("span", { onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsx3(TextToSpeech, { text: option, size: "sm" }) }),
1852
- /* @__PURE__ */ jsx3("span", { style: { flex: 1 }, children: option })
1853
- ]
2262
+ /* @__PURE__ */ jsx3(
2263
+ "button",
2264
+ {
2265
+ onClick: () => setSelectedSkipReason("dont_know"),
2266
+ disabled: isSkipping,
2267
+ style: {
2268
+ padding: "12px 16px",
2269
+ borderRadius: "8px",
2270
+ border: selectedSkipReason === "dont_know" ? "2px solid #8b5cf6" : "1px solid #e5e7eb",
2271
+ backgroundColor: selectedSkipReason === "dont_know" ? "#f5f3ff" : "#f9fafb",
2272
+ cursor: isSkipping ? "not-allowed" : "pointer",
2273
+ fontSize: "14px",
2274
+ fontWeight: "500",
2275
+ color: "#374151",
2276
+ textAlign: "left",
2277
+ opacity: isSkipping ? 0.6 : 1
1854
2278
  },
1855
- idx
1856
- );
1857
- }) }),
1858
- currentQuestion.type === "multiple" && /* @__PURE__ */ jsx3("div", { style: defaultStyles.options, children: currentQuestion.options?.map((option, idx) => {
1859
- const selected = Array.isArray(selectedAnswer) && selectedAnswer.includes(option);
1860
- const correctAnswers = Array.isArray(currentQuestion.correctAnswer) ? currentQuestion.correctAnswer : currentQuestion.correctAnswer ? [currentQuestion.correctAnswer] : [];
1861
- const isCorrectOption = correctAnswers.includes(option);
1862
- let optionStyle = { ...defaultStyles.option };
1863
- if (showFeedback) {
1864
- if (isCorrectOption) {
1865
- optionStyle = { ...optionStyle, ...defaultStyles.optionCorrect };
1866
- } else if (selected && !isCorrectOption) {
1867
- optionStyle = { ...optionStyle, ...defaultStyles.optionIncorrect };
1868
- }
1869
- } else if (selected) {
1870
- optionStyle = { ...optionStyle, ...defaultStyles.optionSelected };
2279
+ "data-testid": "button-skip-reason-dont-know",
2280
+ children: "I don't know the answer"
1871
2281
  }
1872
- return /* @__PURE__ */ jsxs3(
1873
- "div",
1874
- {
1875
- style: {
1876
- ...optionStyle,
1877
- cursor: showFeedback ? "default" : "pointer",
1878
- display: "flex",
1879
- alignItems: "center",
1880
- gap: "8px"
1881
- },
1882
- onClick: () => {
1883
- if (showFeedback) return;
1884
- const current = Array.isArray(selectedAnswer) ? selectedAnswer : [];
1885
- if (selected) {
1886
- handleAnswerChange(current.filter((o) => o !== option));
1887
- } else {
1888
- handleAnswerChange([...current, option]);
1889
- }
1890
- },
1891
- children: [
1892
- /* @__PURE__ */ jsx3("span", { onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsx3(TextToSpeech, { text: option, size: "sm" }) }),
1893
- /* @__PURE__ */ jsx3("span", { style: { flex: 1 }, children: option })
1894
- ]
1895
- },
1896
- idx
1897
- );
1898
- }) }),
1899
- (currentQuestion.type === "free" || currentQuestion.type === "essay") && /* @__PURE__ */ jsx3(
2282
+ )
2283
+ ] }),
2284
+ /* @__PURE__ */ jsxs3("div", { style: { marginBottom: "16px" }, children: [
2285
+ /* @__PURE__ */ jsx3("label", { style: { display: "block", fontSize: "13px", fontWeight: "500", color: "#374151", marginBottom: "6px" }, children: "Additional details (optional)" }),
2286
+ /* @__PURE__ */ jsx3(
1900
2287
  "textarea",
1901
2288
  {
1902
- style: { ...defaultStyles.input, minHeight: currentQuestion.type === "essay" ? "150px" : "60px" },
1903
- value: selectedAnswer || "",
1904
- onChange: (e) => handleAnswerChange(e.target.value),
1905
- placeholder: "Type your answer here...",
1906
- disabled: showFeedback
2289
+ value: skipComment,
2290
+ onChange: (e) => setSkipComment(e.target.value.slice(0, 200)),
2291
+ placeholder: "Tell us more about the issue...",
2292
+ disabled: isSkipping,
2293
+ style: {
2294
+ width: "100%",
2295
+ minHeight: "80px",
2296
+ padding: "10px 12px",
2297
+ borderRadius: "8px",
2298
+ border: "1px solid #e5e7eb",
2299
+ fontSize: "14px",
2300
+ resize: "vertical",
2301
+ fontFamily: "inherit",
2302
+ boxSizing: "border-box"
2303
+ },
2304
+ "data-testid": "input-skip-comment"
1907
2305
  }
1908
2306
  ),
1909
- currentQuestion.type === "fill" && /* @__PURE__ */ jsx3("div", { style: defaultStyles.options, children: currentQuestion.blanks?.map((_, idx) => /* @__PURE__ */ jsx3(
1910
- "input",
2307
+ /* @__PURE__ */ jsxs3("div", { style: { fontSize: "12px", color: "#9ca3af", marginTop: "4px", textAlign: "right" }, children: [
2308
+ skipComment.length,
2309
+ "/200"
2310
+ ] })
2311
+ ] }),
2312
+ /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "10px" }, children: [
2313
+ /* @__PURE__ */ jsx3(
2314
+ "button",
1911
2315
  {
1912
- style: defaultStyles.input,
1913
- value: (Array.isArray(selectedAnswer) ? selectedAnswer[idx] : "") || "",
1914
- onChange: (e) => {
1915
- const current = Array.isArray(selectedAnswer) ? [...selectedAnswer] : [];
1916
- current[idx] = e.target.value;
1917
- handleAnswerChange(current);
2316
+ onClick: () => {
2317
+ setShowSkipModal(false);
2318
+ setSkipComment("");
2319
+ setSelectedSkipReason(null);
1918
2320
  },
1919
- placeholder: `Blank ${idx + 1}`,
1920
- disabled: showFeedback
1921
- },
1922
- idx
1923
- )) }),
1924
- showFeedback && currentAnswerDetail && /* @__PURE__ */ jsxs3("div", { style: {
1925
- ...defaultStyles.feedback,
1926
- ...currentAnswerDetail.isCorrect ? defaultStyles.feedbackCorrect : defaultStyles.feedbackIncorrect
1927
- }, children: [
1928
- /* @__PURE__ */ jsx3("div", { style: {
1929
- ...defaultStyles.feedbackTitle,
1930
- ...currentAnswerDetail.isCorrect ? defaultStyles.feedbackTitleCorrect : defaultStyles.feedbackTitleIncorrect
1931
- }, children: currentAnswerDetail.isCorrect ? "\u2713 Correct!" : "\u2717 Incorrect" }),
1932
- currentQuestion.explanation && /* @__PURE__ */ jsx3("div", { style: defaultStyles.feedbackExplanation, children: currentQuestion.explanation })
2321
+ style: {
2322
+ flex: 1,
2323
+ padding: "10px 16px",
2324
+ borderRadius: "8px",
2325
+ border: "1px solid #e5e7eb",
2326
+ backgroundColor: "#ffffff",
2327
+ cursor: "pointer",
2328
+ fontSize: "14px",
2329
+ fontWeight: "500",
2330
+ color: "#6b7280"
2331
+ },
2332
+ "data-testid": "button-skip-cancel",
2333
+ children: "Cancel"
2334
+ }
2335
+ ),
2336
+ /* @__PURE__ */ jsx3(
2337
+ "button",
2338
+ {
2339
+ onClick: () => selectedSkipReason && handleSkipQuestion(selectedSkipReason, skipComment),
2340
+ disabled: isSkipping || !selectedSkipReason,
2341
+ style: {
2342
+ flex: 1,
2343
+ padding: "10px 16px",
2344
+ borderRadius: "8px",
2345
+ border: "none",
2346
+ backgroundColor: selectedSkipReason ? "#8b5cf6" : "#d1d5db",
2347
+ cursor: isSkipping || !selectedSkipReason ? "not-allowed" : "pointer",
2348
+ fontSize: "14px",
2349
+ fontWeight: "500",
2350
+ color: "#ffffff",
2351
+ opacity: isSkipping ? 0.6 : 1
2352
+ },
2353
+ "data-testid": "button-skip-submit",
2354
+ children: isSkipping ? "Skipping..." : "Skip Question"
2355
+ }
2356
+ )
2357
+ ] })
2358
+ ] }) }),
2359
+ showReportModal && /* @__PURE__ */ jsx3("div", { style: {
2360
+ position: "fixed",
2361
+ top: 0,
2362
+ left: 0,
2363
+ right: 0,
2364
+ bottom: 0,
2365
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
2366
+ display: "flex",
2367
+ alignItems: "center",
2368
+ justifyContent: "center",
2369
+ zIndex: 1e3
2370
+ }, children: /* @__PURE__ */ jsxs3("div", { style: {
2371
+ backgroundColor: "#ffffff",
2372
+ borderRadius: "12px",
2373
+ padding: "24px",
2374
+ maxWidth: "400px",
2375
+ width: "90%",
2376
+ boxShadow: "0 20px 40px rgba(0, 0, 0, 0.2)"
2377
+ }, children: [
2378
+ /* @__PURE__ */ jsx3("h3", { style: { margin: "0 0 8px 0", fontSize: "18px", fontWeight: "600", color: "#1f2937" }, children: "Report Question" }),
2379
+ /* @__PURE__ */ jsx3("p", { style: { margin: "0 0 16px 0", fontSize: "14px", color: "#6b7280" }, children: "What's wrong with this question?" }),
2380
+ /* @__PURE__ */ jsxs3("div", { style: { marginBottom: "16px" }, children: [
2381
+ /* @__PURE__ */ jsx3(
2382
+ "textarea",
2383
+ {
2384
+ value: reportComment,
2385
+ onChange: (e) => setReportComment(e.target.value.slice(0, 300)),
2386
+ placeholder: "Describe the issue with this question...",
2387
+ disabled: isReporting,
2388
+ style: {
2389
+ width: "100%",
2390
+ minHeight: "120px",
2391
+ padding: "10px 12px",
2392
+ borderRadius: "8px",
2393
+ border: "1px solid #e5e7eb",
2394
+ fontSize: "14px",
2395
+ resize: "vertical",
2396
+ fontFamily: "inherit",
2397
+ boxSizing: "border-box"
2398
+ },
2399
+ "data-testid": "input-report-comment"
2400
+ }
2401
+ ),
2402
+ /* @__PURE__ */ jsxs3("div", { style: { fontSize: "12px", color: "#9ca3af", marginTop: "4px", textAlign: "right" }, children: [
2403
+ reportComment.length,
2404
+ "/300"
1933
2405
  ] })
1934
2406
  ] }),
1935
- showSkipModal && /* @__PURE__ */ jsx3("div", { style: {
1936
- position: "fixed",
1937
- top: 0,
1938
- left: 0,
1939
- right: 0,
1940
- bottom: 0,
1941
- backgroundColor: "rgba(0, 0, 0, 0.5)",
1942
- display: "flex",
1943
- alignItems: "center",
1944
- justifyContent: "center",
1945
- zIndex: 1e3
1946
- }, children: /* @__PURE__ */ jsxs3("div", { style: {
1947
- backgroundColor: "#ffffff",
1948
- borderRadius: "12px",
1949
- padding: "24px",
1950
- maxWidth: "400px",
1951
- width: "90%",
1952
- boxShadow: "0 20px 40px rgba(0, 0, 0, 0.2)"
1953
- }, children: [
1954
- /* @__PURE__ */ jsx3("h3", { style: { margin: "0 0 8px 0", fontSize: "18px", fontWeight: "600", color: "#1f2937" }, children: "Skip Question" }),
1955
- /* @__PURE__ */ jsx3("p", { style: { margin: "0 0 16px 0", fontSize: "14px", color: "#6b7280" }, children: "Help us improve by telling us why you're skipping this question." }),
1956
- /* @__PURE__ */ jsxs3("div", { style: { display: "flex", flexDirection: "column", gap: "8px", marginBottom: "16px" }, children: [
1957
- /* @__PURE__ */ jsx3(
1958
- "button",
1959
- {
1960
- onClick: () => setSelectedSkipReason("question_issue"),
1961
- disabled: isSkipping,
1962
- style: {
1963
- padding: "12px 16px",
1964
- borderRadius: "8px",
1965
- border: selectedSkipReason === "question_issue" ? "2px solid #8b5cf6" : "1px solid #e5e7eb",
1966
- backgroundColor: selectedSkipReason === "question_issue" ? "#f5f3ff" : "#f9fafb",
1967
- cursor: isSkipping ? "not-allowed" : "pointer",
1968
- fontSize: "14px",
1969
- fontWeight: "500",
1970
- color: "#374151",
1971
- textAlign: "left",
1972
- opacity: isSkipping ? 0.6 : 1
1973
- },
1974
- "data-testid": "button-skip-reason-issue",
1975
- children: "Question has an issue"
1976
- }
1977
- ),
1978
- /* @__PURE__ */ jsx3(
1979
- "button",
1980
- {
1981
- onClick: () => setSelectedSkipReason("dont_know"),
1982
- disabled: isSkipping,
1983
- style: {
1984
- padding: "12px 16px",
1985
- borderRadius: "8px",
1986
- border: selectedSkipReason === "dont_know" ? "2px solid #8b5cf6" : "1px solid #e5e7eb",
1987
- backgroundColor: selectedSkipReason === "dont_know" ? "#f5f3ff" : "#f9fafb",
1988
- cursor: isSkipping ? "not-allowed" : "pointer",
1989
- fontSize: "14px",
1990
- fontWeight: "500",
1991
- color: "#374151",
1992
- textAlign: "left",
1993
- opacity: isSkipping ? 0.6 : 1
1994
- },
1995
- "data-testid": "button-skip-reason-dont-know",
1996
- children: "I don't know the answer"
1997
- }
1998
- )
1999
- ] }),
2000
- /* @__PURE__ */ jsxs3("div", { style: { marginBottom: "16px" }, children: [
2001
- /* @__PURE__ */ jsx3("label", { style: { display: "block", fontSize: "13px", fontWeight: "500", color: "#374151", marginBottom: "6px" }, children: "Additional details (optional)" }),
2002
- /* @__PURE__ */ jsx3(
2003
- "textarea",
2004
- {
2005
- value: skipComment,
2006
- onChange: (e) => setSkipComment(e.target.value.slice(0, 200)),
2007
- placeholder: "Tell us more about the issue...",
2008
- disabled: isSkipping,
2009
- style: {
2010
- width: "100%",
2011
- minHeight: "80px",
2012
- padding: "10px 12px",
2013
- borderRadius: "8px",
2014
- border: "1px solid #e5e7eb",
2015
- fontSize: "14px",
2016
- resize: "vertical",
2017
- fontFamily: "inherit",
2018
- boxSizing: "border-box"
2019
- },
2020
- "data-testid": "input-skip-comment"
2021
- }
2022
- ),
2023
- /* @__PURE__ */ jsxs3("div", { style: { fontSize: "12px", color: "#9ca3af", marginTop: "4px", textAlign: "right" }, children: [
2024
- skipComment.length,
2025
- "/200"
2026
- ] })
2027
- ] }),
2028
- /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "10px" }, children: [
2029
- /* @__PURE__ */ jsx3(
2030
- "button",
2031
- {
2032
- onClick: () => {
2033
- setShowSkipModal(false);
2034
- setSkipComment("");
2035
- setSelectedSkipReason(null);
2036
- },
2037
- style: {
2038
- flex: 1,
2039
- padding: "10px 16px",
2040
- borderRadius: "8px",
2041
- border: "1px solid #e5e7eb",
2042
- backgroundColor: "#ffffff",
2043
- cursor: "pointer",
2044
- fontSize: "14px",
2045
- fontWeight: "500",
2046
- color: "#6b7280"
2047
- },
2048
- "data-testid": "button-skip-cancel",
2049
- children: "Cancel"
2050
- }
2051
- ),
2052
- /* @__PURE__ */ jsx3(
2053
- "button",
2054
- {
2055
- onClick: () => selectedSkipReason && handleSkipQuestion(selectedSkipReason, skipComment),
2056
- disabled: isSkipping || !selectedSkipReason,
2057
- style: {
2058
- flex: 1,
2059
- padding: "10px 16px",
2060
- borderRadius: "8px",
2061
- border: "none",
2062
- backgroundColor: selectedSkipReason ? "#8b5cf6" : "#d1d5db",
2063
- cursor: isSkipping || !selectedSkipReason ? "not-allowed" : "pointer",
2064
- fontSize: "14px",
2065
- fontWeight: "500",
2066
- color: "#ffffff",
2067
- opacity: isSkipping ? 0.6 : 1
2068
- },
2069
- "data-testid": "button-skip-submit",
2070
- children: isSkipping ? "Skipping..." : "Skip Question"
2071
- }
2072
- )
2073
- ] })
2074
- ] }) }),
2075
- showReportModal && /* @__PURE__ */ jsx3("div", { style: {
2076
- position: "fixed",
2077
- top: 0,
2078
- left: 0,
2079
- right: 0,
2080
- bottom: 0,
2081
- backgroundColor: "rgba(0, 0, 0, 0.5)",
2082
- display: "flex",
2083
- alignItems: "center",
2084
- justifyContent: "center",
2085
- zIndex: 1e3
2086
- }, children: /* @__PURE__ */ jsxs3("div", { style: {
2087
- backgroundColor: "#ffffff",
2088
- borderRadius: "12px",
2089
- padding: "24px",
2090
- maxWidth: "400px",
2091
- width: "90%",
2092
- boxShadow: "0 20px 40px rgba(0, 0, 0, 0.2)"
2093
- }, children: [
2094
- /* @__PURE__ */ jsx3("h3", { style: { margin: "0 0 8px 0", fontSize: "18px", fontWeight: "600", color: "#1f2937" }, children: "Report Question" }),
2095
- /* @__PURE__ */ jsx3("p", { style: { margin: "0 0 16px 0", fontSize: "14px", color: "#6b7280" }, children: "What's wrong with this question?" }),
2096
- /* @__PURE__ */ jsxs3("div", { style: { marginBottom: "16px" }, children: [
2097
- /* @__PURE__ */ jsx3(
2098
- "textarea",
2099
- {
2100
- value: reportComment,
2101
- onChange: (e) => setReportComment(e.target.value.slice(0, 300)),
2102
- placeholder: "Describe the issue with this question...",
2103
- disabled: isReporting,
2104
- style: {
2105
- width: "100%",
2106
- minHeight: "120px",
2107
- padding: "10px 12px",
2108
- borderRadius: "8px",
2109
- border: "1px solid #e5e7eb",
2110
- fontSize: "14px",
2111
- resize: "vertical",
2112
- fontFamily: "inherit",
2113
- boxSizing: "border-box"
2114
- },
2115
- "data-testid": "input-report-comment"
2116
- }
2117
- ),
2118
- /* @__PURE__ */ jsxs3("div", { style: { fontSize: "12px", color: "#9ca3af", marginTop: "4px", textAlign: "right" }, children: [
2119
- reportComment.length,
2120
- "/300"
2121
- ] })
2122
- ] }),
2123
- /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "10px" }, children: [
2124
- /* @__PURE__ */ jsx3(
2125
- "button",
2126
- {
2127
- onClick: () => {
2128
- setShowReportModal(false);
2129
- setReportComment("");
2130
- },
2131
- style: {
2132
- flex: 1,
2133
- padding: "10px 16px",
2134
- borderRadius: "8px",
2135
- border: "1px solid #e5e7eb",
2136
- backgroundColor: "#ffffff",
2137
- cursor: "pointer",
2138
- fontSize: "14px",
2139
- fontWeight: "500",
2140
- color: "#6b7280"
2141
- },
2142
- "data-testid": "button-report-cancel",
2143
- children: "Cancel"
2144
- }
2145
- ),
2146
- /* @__PURE__ */ jsx3(
2147
- "button",
2148
- {
2149
- onClick: () => handleReportQuestion(reportComment),
2150
- disabled: isReporting || !reportComment.trim(),
2151
- style: {
2152
- flex: 1,
2153
- padding: "10px 16px",
2154
- borderRadius: "8px",
2155
- border: "none",
2156
- backgroundColor: reportComment.trim() ? "#ef4444" : "#d1d5db",
2157
- cursor: isReporting || !reportComment.trim() ? "not-allowed" : "pointer",
2158
- fontSize: "14px",
2159
- fontWeight: "500",
2160
- color: "#ffffff",
2161
- opacity: isReporting ? 0.6 : 1
2162
- },
2163
- "data-testid": "button-report-submit",
2164
- children: isReporting ? "Reporting..." : "Report"
2165
- }
2166
- )
2167
- ] })
2168
- ] }) }),
2169
- /* @__PURE__ */ jsxs3("div", { style: defaultStyles.buttonsColumn, children: [
2170
- showFeedback && isLastQuestion && canAddMore && /* @__PURE__ */ jsx3(
2407
+ /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "10px" }, children: [
2408
+ /* @__PURE__ */ jsx3(
2171
2409
  "button",
2172
2410
  {
2411
+ onClick: () => {
2412
+ setShowReportModal(false);
2413
+ setReportComment("");
2414
+ },
2173
2415
  style: {
2174
- ...defaultStyles.buttonAddMore,
2175
- ...isGeneratingExtra ? defaultStyles.buttonAddMoreDisabled : {}
2416
+ flex: 1,
2417
+ padding: "10px 16px",
2418
+ borderRadius: "8px",
2419
+ border: "1px solid #e5e7eb",
2420
+ backgroundColor: "#ffffff",
2421
+ cursor: "pointer",
2422
+ fontSize: "14px",
2423
+ fontWeight: "500",
2424
+ color: "#6b7280"
2176
2425
  },
2177
- onClick: handleAddMoreQuestions,
2178
- disabled: isGeneratingExtra,
2179
- "data-testid": "button-add-more-questions",
2180
- children: isGeneratingExtra ? /* @__PURE__ */ jsxs3(Fragment2, { children: [
2181
- /* @__PURE__ */ jsx3(Spinner, { size: 16, color: "#9ca3af" }),
2182
- "Generating Questions..."
2183
- ] }) : /* @__PURE__ */ jsxs3(Fragment2, { children: [
2184
- "+ Add ",
2185
- questionsToAdd,
2186
- " More Question",
2187
- questionsToAdd !== 1 ? "s" : ""
2188
- ] })
2426
+ "data-testid": "button-report-cancel",
2427
+ children: "Cancel"
2189
2428
  }
2190
2429
  ),
2191
- /* @__PURE__ */ jsx3("div", { style: { ...defaultStyles.buttons, justifyContent: "flex-end" }, children: showFeedback ? (
2192
- // After viewing feedback
2193
- isLastQuestion ? /* @__PURE__ */ jsx3(
2194
- "button",
2195
- {
2196
- style: {
2197
- ...defaultStyles.button,
2198
- ...isSubmitting || isGeneratingExtra ? defaultStyles.buttonDisabled : defaultStyles.buttonPrimary
2199
- },
2200
- onClick: handleSubmit,
2201
- disabled: isSubmitting || isGeneratingExtra,
2202
- "data-testid": "button-submit-quiz",
2203
- children: isSubmitting ? /* @__PURE__ */ jsx3(Spinner, { size: 16, color: "#9ca3af" }) : "Submit Quiz"
2204
- }
2205
- ) : /* @__PURE__ */ jsx3(
2206
- "button",
2207
- {
2208
- style: {
2209
- ...defaultStyles.button,
2210
- ...defaultStyles.buttonPrimary
2211
- },
2212
- onClick: handleContinue,
2213
- "data-testid": "button-continue",
2214
- children: "Continue"
2215
- }
2216
- )
2217
- ) : (
2218
- // Before checking answer
2219
- /* @__PURE__ */ jsx3(
2220
- "button",
2221
- {
2222
- style: {
2223
- ...defaultStyles.button,
2224
- ...isNavigating || selectedAnswer === void 0 ? defaultStyles.buttonDisabled : defaultStyles.buttonPrimary
2225
- },
2226
- onClick: handleCheckAnswer,
2227
- disabled: isNavigating || selectedAnswer === void 0,
2228
- "data-testid": "button-check-answer",
2229
- children: isNavigating ? /* @__PURE__ */ jsx3(Spinner, { size: 16, color: "#9ca3af" }) : "Check Answer"
2230
- }
2231
- )
2232
- ) })
2430
+ /* @__PURE__ */ jsx3(
2431
+ "button",
2432
+ {
2433
+ onClick: () => handleReportQuestion(reportComment),
2434
+ disabled: isReporting || !reportComment.trim(),
2435
+ style: {
2436
+ flex: 1,
2437
+ padding: "10px 16px",
2438
+ borderRadius: "8px",
2439
+ border: "none",
2440
+ backgroundColor: reportComment.trim() ? "#ef4444" : "#d1d5db",
2441
+ cursor: isReporting || !reportComment.trim() ? "not-allowed" : "pointer",
2442
+ fontSize: "14px",
2443
+ fontWeight: "500",
2444
+ color: "#ffffff",
2445
+ opacity: isReporting ? 0.6 : 1
2446
+ },
2447
+ "data-testid": "button-report-submit",
2448
+ children: isReporting ? "Reporting..." : "Report"
2449
+ }
2450
+ )
2233
2451
  ] })
2234
- ] }),
2235
- /* @__PURE__ */ jsx3("div", { style: defaultStyles.chatPanel, children: apiClient.current && /* @__PURE__ */ jsx3(
2236
- QuestionChatPanel,
2237
- {
2238
- apiClient: apiClient.current,
2239
- question: {
2240
- id: currentQuestion.id,
2241
- question: currentQuestion.question,
2242
- type: currentQuestion.type,
2243
- options: currentQuestion.options,
2244
- correctAnswer: currentQuestion.correctAnswer,
2245
- explanation: currentQuestion.explanation
2246
- },
2247
- quizId: quiz.id,
2248
- childId,
2249
- parentId,
2250
- lessonId,
2251
- courseId
2252
- }
2253
- ) })
2254
- ] })
2255
- ] });
2452
+ ] }) }),
2453
+ /* @__PURE__ */ jsxs3("div", { style: defaultStyles.buttonsColumn, children: [
2454
+ showFeedback && isLastQuestion && canAddMore && /* @__PURE__ */ jsx3(
2455
+ "button",
2456
+ {
2457
+ style: {
2458
+ ...defaultStyles.buttonAddMore,
2459
+ ...isGeneratingExtra ? defaultStyles.buttonAddMoreDisabled : {}
2460
+ },
2461
+ onClick: handleAddMoreQuestions,
2462
+ disabled: isGeneratingExtra,
2463
+ "data-testid": "button-add-more-questions",
2464
+ children: isGeneratingExtra ? /* @__PURE__ */ jsxs3(Fragment2, { children: [
2465
+ /* @__PURE__ */ jsx3(Spinner, { size: 16, color: "#9ca3af" }),
2466
+ "Generating Questions..."
2467
+ ] }) : /* @__PURE__ */ jsxs3(Fragment2, { children: [
2468
+ "+ Add ",
2469
+ questionsToAdd,
2470
+ " More Question",
2471
+ questionsToAdd !== 1 ? "s" : ""
2472
+ ] })
2473
+ }
2474
+ ),
2475
+ /* @__PURE__ */ jsx3("div", { style: { ...defaultStyles.buttons, justifyContent: "flex-end" }, children: showFeedback ? (
2476
+ // After viewing feedback
2477
+ isLastQuestion ? /* @__PURE__ */ jsx3(
2478
+ "button",
2479
+ {
2480
+ style: {
2481
+ ...defaultStyles.button,
2482
+ ...isSubmitting || isGeneratingExtra ? defaultStyles.buttonDisabled : defaultStyles.buttonPrimary
2483
+ },
2484
+ onClick: handleSubmit,
2485
+ disabled: isSubmitting || isGeneratingExtra,
2486
+ "data-testid": "button-submit-quiz",
2487
+ children: isSubmitting ? /* @__PURE__ */ jsx3(Spinner, { size: 16, color: "#9ca3af" }) : "Submit Quiz"
2488
+ }
2489
+ ) : /* @__PURE__ */ jsx3(
2490
+ "button",
2491
+ {
2492
+ style: {
2493
+ ...defaultStyles.button,
2494
+ ...defaultStyles.buttonPrimary
2495
+ },
2496
+ onClick: handleContinue,
2497
+ "data-testid": "button-continue",
2498
+ children: "Continue"
2499
+ }
2500
+ )
2501
+ ) : (
2502
+ // Before checking answer
2503
+ /* @__PURE__ */ jsx3(
2504
+ "button",
2505
+ {
2506
+ style: {
2507
+ ...defaultStyles.button,
2508
+ ...isNavigating || selectedAnswer === void 0 ? defaultStyles.buttonDisabled : defaultStyles.buttonPrimary
2509
+ },
2510
+ onClick: handleCheckAnswer,
2511
+ disabled: isNavigating || selectedAnswer === void 0,
2512
+ "data-testid": "button-check-answer",
2513
+ children: isNavigating ? /* @__PURE__ */ jsx3(Spinner, { size: 16, color: "#9ca3af" }) : "Check Answer"
2514
+ }
2515
+ )
2516
+ ) })
2517
+ ] })
2518
+ ] }),
2519
+ /* @__PURE__ */ jsx3("div", { style: defaultStyles.chatPanel, children: apiClient.current && /* @__PURE__ */ jsx3(
2520
+ QuestionChatPanel,
2521
+ {
2522
+ apiClient: apiClient.current,
2523
+ question: {
2524
+ id: currentQuestion.id,
2525
+ question: currentQuestion.question,
2526
+ type: currentQuestion.type,
2527
+ options: currentQuestion.options,
2528
+ correctAnswer: currentQuestion.correctAnswer,
2529
+ explanation: currentQuestion.explanation
2530
+ },
2531
+ quizId: quiz.id,
2532
+ childId,
2533
+ parentId,
2534
+ lessonId,
2535
+ courseId,
2536
+ answerResult: showFeedback && currentAnswerDetail ? {
2537
+ wasIncorrect: !currentAnswerDetail.isCorrect,
2538
+ selectedAnswer: typeof selectedAnswer === "string" ? selectedAnswer : Array.isArray(selectedAnswer) ? selectedAnswer.join(", ") : void 0,
2539
+ correctAnswer: typeof currentQuestion.correctAnswer === "string" ? currentQuestion.correctAnswer : Array.isArray(currentQuestion.correctAnswer) ? currentQuestion.correctAnswer.join(", ") : void 0,
2540
+ explanation: currentQuestion.explanation
2541
+ } : void 0
2542
+ }
2543
+ ) })
2544
+ ] }) });
2256
2545
  }
2257
2546
 
2258
2547
  // src/AttemptViewer.tsx
@@ -2378,6 +2667,46 @@ var defaultStyles2 = {
2378
2667
  fontSize: "14px",
2379
2668
  color: "#581c87"
2380
2669
  },
2670
+ chatHistorySection: {
2671
+ marginTop: "12px",
2672
+ borderTop: "1px solid #e5e7eb",
2673
+ paddingTop: "12px"
2674
+ },
2675
+ chatToggleButton: {
2676
+ display: "flex",
2677
+ alignItems: "center",
2678
+ gap: "6px",
2679
+ padding: "6px 12px",
2680
+ backgroundColor: "#f3f4f6",
2681
+ border: "none",
2682
+ borderRadius: "6px",
2683
+ fontSize: "13px",
2684
+ color: "#6b7280",
2685
+ cursor: "pointer",
2686
+ fontWeight: "500"
2687
+ },
2688
+ chatMessages: {
2689
+ marginTop: "12px",
2690
+ display: "flex",
2691
+ flexDirection: "column",
2692
+ gap: "8px"
2693
+ },
2694
+ chatMessage: {
2695
+ padding: "8px 12px",
2696
+ borderRadius: "8px",
2697
+ fontSize: "13px",
2698
+ maxWidth: "85%"
2699
+ },
2700
+ chatMessageUser: {
2701
+ backgroundColor: "#6721b0",
2702
+ color: "#ffffff",
2703
+ alignSelf: "flex-end"
2704
+ },
2705
+ chatMessageAssistant: {
2706
+ backgroundColor: "#f3f4f6",
2707
+ color: "#111827",
2708
+ alignSelf: "flex-start"
2709
+ },
2381
2710
  loading: {
2382
2711
  textAlign: "center",
2383
2712
  padding: "40px 20px"
@@ -2435,11 +2764,14 @@ function AttemptViewer({
2435
2764
  onError,
2436
2765
  className,
2437
2766
  showExplanations = true,
2767
+ showConversation = false,
2438
2768
  title
2439
2769
  }) {
2440
2770
  const [attempt, setAttempt] = useState4(null);
2441
2771
  const [loading, setLoading] = useState4(true);
2442
2772
  const [error, setError] = useState4(null);
2773
+ const [chatHistories, setChatHistories] = useState4({});
2774
+ const [expandedChats, setExpandedChats] = useState4(/* @__PURE__ */ new Set());
2443
2775
  useEffect4(() => {
2444
2776
  const apiClient = new QuizApiClient({
2445
2777
  baseUrl: apiBaseUrl,
@@ -2457,6 +2789,14 @@ function AttemptViewer({
2457
2789
  }
2458
2790
  const data = await response.json();
2459
2791
  setAttempt(data);
2792
+ if (showConversation) {
2793
+ try {
2794
+ const chats = await apiClient.getChatsByAttempt(attemptId);
2795
+ setChatHistories(chats);
2796
+ } catch (chatErr) {
2797
+ console.error("Failed to load chat histories:", chatErr);
2798
+ }
2799
+ }
2460
2800
  } catch (err) {
2461
2801
  const errorMessage = err instanceof Error ? err.message : "Failed to load attempt";
2462
2802
  setError(errorMessage);
@@ -2466,7 +2806,18 @@ function AttemptViewer({
2466
2806
  }
2467
2807
  }
2468
2808
  fetchAttempt();
2469
- }, [attemptId, apiBaseUrl, authToken, onError]);
2809
+ }, [attemptId, apiBaseUrl, authToken, onError, showConversation]);
2810
+ const toggleChatExpanded = (questionId) => {
2811
+ setExpandedChats((prev) => {
2812
+ const newSet = new Set(prev);
2813
+ if (newSet.has(questionId)) {
2814
+ newSet.delete(questionId);
2815
+ } else {
2816
+ newSet.add(questionId);
2817
+ }
2818
+ return newSet;
2819
+ });
2820
+ };
2470
2821
  const handleRetry = () => {
2471
2822
  setLoading(true);
2472
2823
  setError(null);
@@ -2558,6 +2909,34 @@ function AttemptViewer({
2558
2909
  /* @__PURE__ */ jsx4("strong", { children: "Explanation:" }),
2559
2910
  " ",
2560
2911
  answer.explanation
2912
+ ] }),
2913
+ showConversation && chatHistories[answer.questionId] && chatHistories[answer.questionId].messages.length > 0 && /* @__PURE__ */ jsxs4("div", { style: defaultStyles2.chatHistorySection, children: [
2914
+ /* @__PURE__ */ jsxs4(
2915
+ "button",
2916
+ {
2917
+ style: defaultStyles2.chatToggleButton,
2918
+ onClick: () => toggleChatExpanded(answer.questionId),
2919
+ "data-testid": `button-toggle-chat-${answer.questionId}`,
2920
+ children: [
2921
+ /* @__PURE__ */ jsx4("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx4("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" }) }),
2922
+ expandedChats.has(answer.questionId) ? "Hide" : "View",
2923
+ " Chat History (",
2924
+ chatHistories[answer.questionId].messages.length,
2925
+ " messages)"
2926
+ ]
2927
+ }
2928
+ ),
2929
+ expandedChats.has(answer.questionId) && /* @__PURE__ */ jsx4("div", { style: defaultStyles2.chatMessages, children: chatHistories[answer.questionId].messages.map((msg, msgIndex) => /* @__PURE__ */ jsx4(
2930
+ "div",
2931
+ {
2932
+ style: {
2933
+ ...defaultStyles2.chatMessage,
2934
+ ...msg.role === "user" ? defaultStyles2.chatMessageUser : defaultStyles2.chatMessageAssistant
2935
+ },
2936
+ children: msg.content
2937
+ },
2938
+ msgIndex
2939
+ )) })
2561
2940
  ] })
2562
2941
  ]
2563
2942
  },