@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.js CHANGED
@@ -113,6 +113,29 @@ var QuizApiClient = class {
113
113
  `/api/external/question-chat/${questionId}/${childId}`
114
114
  );
115
115
  }
116
+ async getChatsByAttempt(attemptId) {
117
+ return this.request(
118
+ "GET",
119
+ `/api/external/quiz-attempts/${attemptId}/chats`
120
+ );
121
+ }
122
+ async getTextToSpeech(text, voice = "nova") {
123
+ const headers = {
124
+ "Content-Type": "application/json"
125
+ };
126
+ if (this.authToken) {
127
+ headers["Authorization"] = `Bearer ${this.authToken}`;
128
+ }
129
+ const response = await fetch(`${this.baseUrl}/api/external/tts`, {
130
+ method: "POST",
131
+ headers,
132
+ body: JSON.stringify({ text, voice })
133
+ });
134
+ if (!response.ok) {
135
+ throw new Error(`TTS request failed: HTTP ${response.status}`);
136
+ }
137
+ return response.blob();
138
+ }
116
139
  };
117
140
 
118
141
  // src/utils.ts
@@ -515,9 +538,13 @@ var panelStyles = {
515
538
  messageRow: {
516
539
  display: "flex"
517
540
  },
541
+ messageContent: {
542
+ display: "flex",
543
+ alignItems: "flex-end",
544
+ gap: "4px",
545
+ maxWidth: "85%"
546
+ },
518
547
  userMessage: {
519
- maxWidth: "85%",
520
- marginLeft: "auto",
521
548
  padding: "10px 14px",
522
549
  borderRadius: "16px 16px 4px 16px",
523
550
  backgroundColor: "#6721b0",
@@ -526,8 +553,6 @@ var panelStyles = {
526
553
  lineHeight: 1.4
527
554
  },
528
555
  assistantMessage: {
529
- maxWidth: "85%",
530
- marginRight: "auto",
531
556
  padding: "10px 14px",
532
557
  borderRadius: "16px 16px 16px 4px",
533
558
  backgroundColor: "#ffffff",
@@ -541,7 +566,8 @@ var panelStyles = {
541
566
  borderTop: "1px solid #e2e8f0",
542
567
  backgroundColor: "#ffffff",
543
568
  display: "flex",
544
- gap: "8px"
569
+ gap: "8px",
570
+ alignItems: "center"
545
571
  },
546
572
  input: {
547
573
  flex: 1,
@@ -551,13 +577,11 @@ var panelStyles = {
551
577
  fontSize: "14px",
552
578
  outline: "none"
553
579
  },
554
- sendButton: {
580
+ buttonBase: {
555
581
  width: "40px",
556
582
  height: "40px",
557
583
  borderRadius: "50%",
558
584
  border: "none",
559
- backgroundColor: "#6721b0",
560
- color: "#ffffff",
561
585
  cursor: "pointer",
562
586
  display: "flex",
563
587
  alignItems: "center",
@@ -565,10 +589,47 @@ var panelStyles = {
565
589
  fontSize: "16px",
566
590
  transition: "all 0.2s ease"
567
591
  },
592
+ sendButton: {
593
+ backgroundColor: "#6721b0",
594
+ color: "#ffffff"
595
+ },
568
596
  sendButtonDisabled: {
569
597
  backgroundColor: "#d1d5db",
570
598
  cursor: "not-allowed"
571
599
  },
600
+ micButton: {
601
+ backgroundColor: "#ffffff",
602
+ border: "1px solid #e2e8f0",
603
+ color: "#374151"
604
+ },
605
+ micButtonActive: {
606
+ backgroundColor: "#ef4444",
607
+ color: "#ffffff",
608
+ border: "none"
609
+ },
610
+ speakButton: {
611
+ width: "28px",
612
+ height: "28px",
613
+ borderRadius: "50%",
614
+ border: "none",
615
+ backgroundColor: "transparent",
616
+ color: "#9ca3af",
617
+ cursor: "pointer",
618
+ display: "flex",
619
+ alignItems: "center",
620
+ justifyContent: "center",
621
+ transition: "all 0.2s ease",
622
+ flexShrink: 0
623
+ },
624
+ speakButtonReady: {
625
+ color: "#22c55e"
626
+ },
627
+ speakButtonPlaying: {
628
+ color: "#6721b0"
629
+ },
630
+ speakButtonLoading: {
631
+ color: "#f97316"
632
+ },
572
633
  loadingDots: {
573
634
  display: "flex",
574
635
  alignItems: "center",
@@ -606,6 +667,33 @@ var STARTER_PROMPTS = [
606
667
  { id: "word_help", label: "I don't understand a word", message: "I don't understand a word in this question. Can you help?" },
607
668
  { id: "explain_again", label: "Explain this again", message: "Can you explain this question to me again in a different way?" }
608
669
  ];
670
+ var MicIcon = () => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
671
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" }),
672
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }),
673
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "12", x2: "12", y1: "19", y2: "22" })
674
+ ] });
675
+ var MicOffIcon = () => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
676
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "2", x2: "22", y1: "2", y2: "22" }),
677
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M18.89 13.23A7.12 7.12 0 0 0 19 12v-2" }),
678
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M5 10v2a7 7 0 0 0 12 5" }),
679
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M15 9.34V5a3 3 0 0 0-5.68-1.33" }),
680
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M9 9v3a3 3 0 0 0 5.12 2.12" }),
681
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "12", x2: "12", y1: "19", y2: "22" })
682
+ ] });
683
+ var VolumeIcon2 = () => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
684
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("polygon", { points: "11 5 6 9 2 9 2 15 6 15 11 19 11 5" }),
685
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M15.54 8.46a5 5 0 0 1 0 7.07" }),
686
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M19.07 4.93a10 10 0 0 1 0 14.14" })
687
+ ] });
688
+ var SendIcon = () => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
689
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "22", y1: "2", x2: "11", y2: "13" }),
690
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("polygon", { points: "22 2 15 22 11 13 2 9 22 2" })
691
+ ] });
692
+ var HelpIcon = () => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("svg", { width: "48", height: "48", viewBox: "0 0 24 24", fill: "none", stroke: "#6721b0", strokeWidth: "2", children: [
693
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "12", cy: "12", r: "10" }),
694
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" }),
695
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "12", y1: "17", x2: "12.01", y2: "17" })
696
+ ] });
609
697
  function QuestionChatPanel({
610
698
  apiClient,
611
699
  question,
@@ -613,20 +701,130 @@ function QuestionChatPanel({
613
701
  childId,
614
702
  parentId,
615
703
  lessonId,
616
- courseId
704
+ courseId,
705
+ answerResult
617
706
  }) {
618
707
  const [messages, setMessages] = (0, import_react2.useState)([]);
619
708
  const [inputValue, setInputValue] = (0, import_react2.useState)("");
620
709
  const [isLoading, setIsLoading] = (0, import_react2.useState)(false);
621
710
  const [chatId, setChatId] = (0, import_react2.useState)(null);
622
711
  const [hoveredButton, setHoveredButton] = (0, import_react2.useState)(null);
712
+ const [isListening, setIsListening] = (0, import_react2.useState)(false);
713
+ const [speakingIndex, setSpeakingIndex] = (0, import_react2.useState)(null);
714
+ const [audioReadyMap, setAudioReadyMap] = (0, import_react2.useState)(/* @__PURE__ */ new Map());
715
+ const [hasOfferedHelp, setHasOfferedHelp] = (0, import_react2.useState)(false);
623
716
  const messagesContainerRef = (0, import_react2.useRef)(null);
624
717
  const messagesEndRef = (0, import_react2.useRef)(null);
718
+ const recognitionRef = (0, import_react2.useRef)(null);
719
+ const audioRef = (0, import_react2.useRef)(null);
720
+ const audioCacheRef = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
721
+ const isSpeechSupported = typeof window !== "undefined" && (window.SpeechRecognition || window.webkitSpeechRecognition);
625
722
  const scrollToBottom = (0, import_react2.useCallback)(() => {
626
723
  if (messagesContainerRef.current) {
627
724
  messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight;
628
725
  }
629
726
  }, []);
727
+ const preCacheAudio = (0, import_react2.useCallback)(async (text) => {
728
+ if (audioCacheRef.current.has(text)) {
729
+ setAudioReadyMap((prev) => new Map(prev).set(text, true));
730
+ return;
731
+ }
732
+ setAudioReadyMap((prev) => new Map(prev).set(text, false));
733
+ try {
734
+ const audioBlob = await apiClient.getTextToSpeech(text, "nova");
735
+ audioCacheRef.current.set(text, audioBlob);
736
+ setAudioReadyMap((prev) => new Map(prev).set(text, true));
737
+ } catch (error) {
738
+ console.error("Pre-cache TTS error:", error);
739
+ }
740
+ }, [apiClient]);
741
+ const startListening = (0, import_react2.useCallback)(() => {
742
+ const SpeechRecognitionAPI = window.SpeechRecognition || window.webkitSpeechRecognition;
743
+ if (!SpeechRecognitionAPI) {
744
+ console.log("Speech recognition not supported");
745
+ return;
746
+ }
747
+ const recognition = new SpeechRecognitionAPI();
748
+ recognition.continuous = true;
749
+ recognition.interimResults = true;
750
+ recognition.lang = "en-US";
751
+ recognition.onstart = () => {
752
+ console.log("Speech recognition started");
753
+ setIsListening(true);
754
+ };
755
+ recognition.onresult = (event) => {
756
+ let finalTranscript = "";
757
+ for (let i = 0; i < Object.keys(event.results).length; i++) {
758
+ const result = event.results[i];
759
+ if (result && result[0]) {
760
+ finalTranscript += result[0].transcript;
761
+ }
762
+ }
763
+ if (finalTranscript) {
764
+ setInputValue(finalTranscript);
765
+ }
766
+ };
767
+ recognition.onerror = (event) => {
768
+ console.log("Speech recognition error:", event.error);
769
+ if (event.error === "not-allowed") {
770
+ alert("Microphone access was denied. Please allow microphone access and try again.");
771
+ }
772
+ setIsListening(false);
773
+ };
774
+ recognition.onend = () => {
775
+ console.log("Speech recognition ended");
776
+ setIsListening(false);
777
+ };
778
+ recognitionRef.current = recognition;
779
+ try {
780
+ recognition.start();
781
+ } catch (e) {
782
+ console.log("Failed to start recognition:", e);
783
+ setIsListening(false);
784
+ }
785
+ }, []);
786
+ const stopListening = (0, import_react2.useCallback)(() => {
787
+ if (recognitionRef.current) {
788
+ recognitionRef.current.stop();
789
+ setIsListening(false);
790
+ }
791
+ }, []);
792
+ const speakMessage = (0, import_react2.useCallback)(async (text, index) => {
793
+ if (audioRef.current) {
794
+ audioRef.current.pause();
795
+ audioRef.current = null;
796
+ }
797
+ if (speakingIndex === index) {
798
+ setSpeakingIndex(null);
799
+ return;
800
+ }
801
+ setSpeakingIndex(index);
802
+ try {
803
+ let audioBlob;
804
+ const cachedBlob = audioCacheRef.current.get(text);
805
+ if (cachedBlob) {
806
+ audioBlob = cachedBlob;
807
+ } else {
808
+ audioBlob = await apiClient.getTextToSpeech(text, "nova");
809
+ audioCacheRef.current.set(text, audioBlob);
810
+ }
811
+ const audioUrl = URL.createObjectURL(audioBlob);
812
+ const audio = new Audio(audioUrl);
813
+ audioRef.current = audio;
814
+ audio.onended = () => {
815
+ setSpeakingIndex(null);
816
+ URL.revokeObjectURL(audioUrl);
817
+ };
818
+ audio.onerror = () => {
819
+ setSpeakingIndex(null);
820
+ URL.revokeObjectURL(audioUrl);
821
+ };
822
+ await audio.play();
823
+ } catch (error) {
824
+ console.error("TTS error:", error);
825
+ setSpeakingIndex(null);
826
+ }
827
+ }, [speakingIndex, apiClient]);
630
828
  (0, import_react2.useEffect)(() => {
631
829
  scrollToBottom();
632
830
  }, [messages, scrollToBottom]);
@@ -634,19 +832,35 @@ function QuestionChatPanel({
634
832
  setMessages([]);
635
833
  setChatId(null);
636
834
  setInputValue("");
835
+ setHasOfferedHelp(false);
637
836
  const loadHistory = async () => {
638
837
  try {
639
838
  const history = await apiClient.getChatHistory(question.id, childId);
640
839
  if (history.chatId && history.messages.length > 0) {
641
840
  setChatId(history.chatId);
642
841
  setMessages(history.messages);
842
+ history.messages.filter((m) => m.role === "assistant").forEach((m) => preCacheAudio(m.content));
643
843
  }
644
844
  } catch (err) {
645
845
  console.error("Failed to load chat history:", err);
646
846
  }
647
847
  };
648
848
  loadHistory();
649
- }, [question.id, childId, apiClient]);
849
+ }, [question.id, childId, apiClient, preCacheAudio]);
850
+ (0, import_react2.useEffect)(() => {
851
+ if (answerResult?.wasIncorrect && !hasOfferedHelp) {
852
+ setHasOfferedHelp(true);
853
+ const selectedAnswerText = answerResult.selectedAnswer || "that answer";
854
+ const helpMessage = `Looks like you chose "${selectedAnswerText}" which was incorrect. Would you like me to help explain the correct answer?`;
855
+ const assistantMessage = {
856
+ role: "assistant",
857
+ content: helpMessage,
858
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
859
+ };
860
+ setMessages((prev) => [...prev, assistantMessage]);
861
+ preCacheAudio(helpMessage);
862
+ }
863
+ }, [answerResult, hasOfferedHelp, preCacheAudio]);
650
864
  const initializeChat = async () => {
651
865
  if (chatId) return chatId;
652
866
  try {
@@ -668,6 +882,9 @@ function QuestionChatPanel({
668
882
  };
669
883
  const sendMessage = async (messageText) => {
670
884
  if (!messageText.trim() || isLoading) return;
885
+ if (isListening) {
886
+ stopListening();
887
+ }
671
888
  setIsLoading(true);
672
889
  const userMsg = {
673
890
  role: "user",
@@ -687,15 +904,23 @@ function QuestionChatPanel({
687
904
  questionContext: question,
688
905
  childId
689
906
  });
690
- setMessages((prev) => [...prev, response.assistantMessage]);
907
+ const assistantMessage = response.assistantMessage || {
908
+ role: "assistant",
909
+ content: "I'm here to help!",
910
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
911
+ };
912
+ setMessages((prev) => [...prev, assistantMessage]);
913
+ preCacheAudio(assistantMessage.content);
691
914
  } catch (err) {
692
915
  console.error("Failed to send message:", err);
916
+ const content = "Sorry, I'm having trouble right now. Please try again!";
693
917
  const errorMsg = {
694
918
  role: "assistant",
695
- content: "Sorry, I'm having trouble right now. Please try again!",
919
+ content,
696
920
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
697
921
  };
698
922
  setMessages((prev) => [...prev, errorMsg]);
923
+ preCacheAudio(content);
699
924
  } finally {
700
925
  setIsLoading(false);
701
926
  }
@@ -712,14 +937,14 @@ function QuestionChatPanel({
712
937
  0%, 60%, 100% { transform: translateY(0); }
713
938
  30% { transform: translateY(-4px); }
714
939
  }
940
+ @keyframes pulse {
941
+ 0%, 100% { opacity: 1; }
942
+ 50% { opacity: 0.5; }
943
+ }
715
944
  ` }),
716
945
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: panelStyles.header, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: "Need Help?" }) }),
717
946
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { ref: messagesContainerRef, style: panelStyles.messagesContainer, children: messages.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: panelStyles.emptyState, children: [
718
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: panelStyles.helperIcon, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("svg", { width: "48", height: "48", viewBox: "0 0 24 24", fill: "none", stroke: "#6721b0", strokeWidth: "2", children: [
719
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "12", cy: "12", r: "10" }),
720
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" }),
721
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "12", y1: "17", x2: "12.01", y2: "17" })
722
- ] }) }),
947
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: panelStyles.helperIcon, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(HelpIcon, {}) }),
723
948
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { fontSize: "14px", fontWeight: "500", marginBottom: "8px" }, children: "Hi! I'm your question helper" }),
724
949
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { fontSize: "13px", color: "#9ca3af" }, children: "Ask me if you need help understanding this question" }),
725
950
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { ...panelStyles.starterPrompts, marginTop: "16px" }, children: STARTER_PROMPTS.map((prompt) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
@@ -746,7 +971,26 @@ function QuestionChatPanel({
746
971
  ...panelStyles.messageRow,
747
972
  justifyContent: msg.role === "user" ? "flex-end" : "flex-start"
748
973
  },
749
- children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: msg.role === "user" ? panelStyles.userMessage : panelStyles.assistantMessage, children: msg.content })
974
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: {
975
+ ...panelStyles.messageContent,
976
+ flexDirection: msg.role === "user" ? "row-reverse" : "row"
977
+ }, children: [
978
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: msg.role === "user" ? panelStyles.userMessage : panelStyles.assistantMessage, children: msg.content }),
979
+ msg.role === "assistant" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
980
+ "button",
981
+ {
982
+ style: {
983
+ ...panelStyles.speakButton,
984
+ ...speakingIndex === idx ? panelStyles.speakButtonPlaying : audioReadyMap.get(msg.content) === true ? panelStyles.speakButtonReady : audioReadyMap.get(msg.content) === false ? panelStyles.speakButtonLoading : {},
985
+ ...audioReadyMap.get(msg.content) === false ? { animation: "pulse 1.5s ease-in-out infinite" } : {}
986
+ },
987
+ onClick: () => speakMessage(msg.content, idx),
988
+ "data-testid": `button-speak-${idx}`,
989
+ title: "Listen to this message",
990
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(VolumeIcon2, {})
991
+ }
992
+ )
993
+ ] })
750
994
  },
751
995
  idx
752
996
  )),
@@ -765,26 +1009,38 @@ function QuestionChatPanel({
765
1009
  value: inputValue,
766
1010
  onChange: (e) => setInputValue(e.target.value),
767
1011
  onKeyPress: handleKeyPress,
768
- placeholder: "Ask about this question...",
1012
+ placeholder: isListening ? "Listening..." : "Ask about this question...",
769
1013
  style: panelStyles.input,
770
- disabled: isLoading,
1014
+ disabled: isLoading || isListening,
771
1015
  "data-testid": "input-chat-message"
772
1016
  }
773
1017
  ),
1018
+ isSpeechSupported && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1019
+ "button",
1020
+ {
1021
+ onClick: isListening ? stopListening : startListening,
1022
+ disabled: isLoading,
1023
+ style: {
1024
+ ...panelStyles.buttonBase,
1025
+ ...isListening ? panelStyles.micButtonActive : panelStyles.micButton
1026
+ },
1027
+ "data-testid": "button-voice-input",
1028
+ title: isListening ? "Stop listening" : "Speak your question",
1029
+ children: isListening ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(MicOffIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(MicIcon, {})
1030
+ }
1031
+ ),
774
1032
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
775
1033
  "button",
776
1034
  {
777
1035
  onClick: () => sendMessage(inputValue),
778
1036
  disabled: isLoading || !inputValue.trim(),
779
1037
  style: {
1038
+ ...panelStyles.buttonBase,
780
1039
  ...panelStyles.sendButton,
781
1040
  ...isLoading || !inputValue.trim() ? panelStyles.sendButtonDisabled : {}
782
1041
  },
783
1042
  "data-testid": "button-send-chat",
784
- children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
785
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "22", y1: "2", x2: "11", y2: "13" }),
786
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("polygon", { points: "22 2 15 22 11 13 2 9 22 2" })
787
- ] })
1043
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(SendIcon, {})
788
1044
  }
789
1045
  )
790
1046
  ] })
@@ -797,15 +1053,28 @@ var defaultStyles = {
797
1053
  container: {
798
1054
  fontFamily: "system-ui, -apple-system, sans-serif",
799
1055
  width: "100%",
1056
+ height: "100%",
800
1057
  padding: "20px",
801
1058
  backgroundColor: "#ffffff",
802
1059
  borderRadius: "12px",
803
- boxSizing: "border-box"
1060
+ boxSizing: "border-box",
1061
+ display: "flex",
1062
+ flexDirection: "column"
804
1063
  },
805
1064
  header: {
806
1065
  marginBottom: "20px",
807
1066
  borderBottom: "1px solid #e5e7eb",
808
- paddingBottom: "16px"
1067
+ paddingBottom: "16px",
1068
+ display: "flex",
1069
+ flexDirection: "column"
1070
+ },
1071
+ headerTop: {
1072
+ display: "flex",
1073
+ justifyContent: "space-between",
1074
+ alignItems: "flex-start"
1075
+ },
1076
+ headerLeft: {
1077
+ flex: 1
809
1078
  },
810
1079
  title: {
811
1080
  fontSize: "24px",
@@ -817,7 +1086,8 @@ var defaultStyles = {
817
1086
  color: "#6b7280"
818
1087
  },
819
1088
  progressBar: {
820
- width: "100%",
1089
+ width: "50%",
1090
+ maxWidth: "300px",
821
1091
  height: "8px",
822
1092
  backgroundColor: "#e5e7eb",
823
1093
  borderRadius: "4px",
@@ -932,16 +1202,21 @@ var defaultStyles = {
932
1202
  },
933
1203
  mainLayout: {
934
1204
  display: "flex",
935
- gap: "24px"
1205
+ gap: "24px",
1206
+ flex: 1,
1207
+ minHeight: 0,
1208
+ alignItems: "stretch"
936
1209
  },
937
1210
  quizContent: {
938
1211
  flex: 1,
939
- minWidth: 0
1212
+ minWidth: 0,
1213
+ overflow: "auto"
940
1214
  },
941
1215
  chatPanel: {
942
1216
  width: "320px",
943
1217
  flexShrink: 0,
944
- height: "460px"
1218
+ display: "flex",
1219
+ flexDirection: "column"
945
1220
  },
946
1221
  timer: {
947
1222
  fontSize: "14px",
@@ -1149,6 +1424,16 @@ function QuizPlayer({
1149
1424
  const apiClient = (0, import_react3.useRef)(null);
1150
1425
  const timerRef = (0, import_react3.useRef)(null);
1151
1426
  const startTimeRef = (0, import_react3.useRef)(0);
1427
+ const onCompleteRef = (0, import_react3.useRef)(onComplete);
1428
+ const onErrorRef = (0, import_react3.useRef)(onError);
1429
+ const onProgressRef = (0, import_react3.useRef)(onProgress);
1430
+ const onGenerateMoreQuestionsRef = (0, import_react3.useRef)(onGenerateMoreQuestions);
1431
+ (0, import_react3.useEffect)(() => {
1432
+ onCompleteRef.current = onComplete;
1433
+ onErrorRef.current = onError;
1434
+ onProgressRef.current = onProgress;
1435
+ onGenerateMoreQuestionsRef.current = onGenerateMoreQuestions;
1436
+ });
1152
1437
  (0, import_react3.useEffect)(() => {
1153
1438
  apiClient.current = new QuizApiClient({ baseUrl: apiBaseUrl, authToken });
1154
1439
  }, [apiBaseUrl, authToken]);
@@ -1195,11 +1480,11 @@ function QuizPlayer({
1195
1480
  const message = err instanceof Error ? err.message : "Failed to load quiz";
1196
1481
  setError(message);
1197
1482
  setIsLoading(false);
1198
- onError?.(err instanceof Error ? err : new Error(message));
1483
+ onErrorRef.current?.(err instanceof Error ? err : new Error(message));
1199
1484
  }
1200
1485
  }
1201
1486
  initialize();
1202
- }, [quizId, lessonId, assignLessonId, courseId, childId, parentId, forceNewAttempt, onError]);
1487
+ }, [quizId, lessonId, assignLessonId, courseId, childId, parentId, forceNewAttempt]);
1203
1488
  (0, import_react3.useEffect)(() => {
1204
1489
  if (timerStarted && !isCompleted && !error) {
1205
1490
  startTimeRef.current = Date.now();
@@ -1225,14 +1510,14 @@ function QuizPlayer({
1225
1510
  const totalQuestions = allQuestions.length;
1226
1511
  const maxQuestions = 50;
1227
1512
  (0, import_react3.useEffect)(() => {
1228
- if (quiz && onProgress) {
1229
- onProgress({
1513
+ if (quiz && onProgressRef.current) {
1514
+ onProgressRef.current({
1230
1515
  currentQuestion: currentQuestionIndex + 1,
1231
1516
  totalQuestions,
1232
1517
  answeredQuestions: answers.size
1233
1518
  });
1234
1519
  }
1235
- }, [currentQuestionIndex, answers.size, quiz, onProgress, totalQuestions]);
1520
+ }, [currentQuestionIndex, answers.size, quiz, totalQuestions]);
1236
1521
  const currentQuestion = allQuestions[currentQuestionIndex];
1237
1522
  const handleAnswerChange = (0, import_react3.useCallback)((value) => {
1238
1523
  if (!currentQuestion) return;
@@ -1277,7 +1562,7 @@ function QuizPlayer({
1277
1562
  if (totalQuestions >= maxQuestions) return;
1278
1563
  setIsGeneratingExtra(true);
1279
1564
  try {
1280
- const result2 = await onGenerateMoreQuestions(attempt.id, totalQuestions);
1565
+ const result2 = await onGenerateMoreQuestionsRef.current(attempt.id, totalQuestions);
1281
1566
  if (result2.extraQuestions && result2.extraQuestions.length > 0) {
1282
1567
  const slotsAvailable = maxQuestions - totalQuestions;
1283
1568
  const questionsToAppend = result2.extraQuestions.slice(0, slotsAvailable);
@@ -1290,11 +1575,11 @@ function QuizPlayer({
1290
1575
  }
1291
1576
  } catch (err) {
1292
1577
  console.error("Failed to generate extra questions:", err);
1293
- onError?.(err instanceof Error ? err : new Error("Failed to generate extra questions"));
1578
+ onErrorRef.current?.(err instanceof Error ? err : new Error("Failed to generate extra questions"));
1294
1579
  } finally {
1295
1580
  setIsGeneratingExtra(false);
1296
1581
  }
1297
- }, [attempt, onGenerateMoreQuestions, isGeneratingExtra, totalQuestions, maxQuestions, onError]);
1582
+ }, [attempt, isGeneratingExtra, totalQuestions, maxQuestions]);
1298
1583
  const handleSubmit = (0, import_react3.useCallback)(async () => {
1299
1584
  if (!quiz || !attempt || !apiClient.current) return;
1300
1585
  setIsSubmitting(true);
@@ -1333,15 +1618,15 @@ function QuizPlayer({
1333
1618
  if (timerRef.current) {
1334
1619
  clearInterval(timerRef.current);
1335
1620
  }
1336
- onComplete?.(quizResult);
1621
+ onCompleteRef.current?.(quizResult);
1337
1622
  } catch (err) {
1338
1623
  const message = err instanceof Error ? err.message : "Failed to submit quiz";
1339
1624
  setError(message);
1340
- onError?.(err instanceof Error ? err : new Error(message));
1625
+ onErrorRef.current?.(err instanceof Error ? err : new Error(message));
1341
1626
  } finally {
1342
1627
  setIsSubmitting(false);
1343
1628
  }
1344
- }, [quiz, attempt, currentQuestion, answers, answersDetail, onComplete, onError, totalQuestions, timerStarted, elapsedSeconds]);
1629
+ }, [quiz, attempt, currentQuestion, answers, answersDetail, totalQuestions, timerStarted, elapsedSeconds]);
1345
1630
  const isExtraQuestion = currentQuestion && extraQuestions.some((q) => q.id === currentQuestion.id);
1346
1631
  const handleSkipQuestion = (0, import_react3.useCallback)(async (reason, comment) => {
1347
1632
  if (!currentQuestion || !apiClient.current || !attempt) return;
@@ -1377,7 +1662,7 @@ function QuizPlayer({
1377
1662
  timeSpentSeconds: elapsedSeconds
1378
1663
  };
1379
1664
  setResult(quizResult);
1380
- onComplete?.(quizResult);
1665
+ onCompleteRef.current?.(quizResult);
1381
1666
  } else if (currentQuestionIndex >= newTotalQuestions) {
1382
1667
  setCurrentQuestionIndex(newTotalQuestions - 1);
1383
1668
  }
@@ -1389,7 +1674,7 @@ function QuizPlayer({
1389
1674
  } finally {
1390
1675
  setIsSkipping(false);
1391
1676
  }
1392
- }, [currentQuestion, apiClient, attempt, quiz, childId, parentId, lessonId, courseId, assignLessonId, skippedQuestionIds, extraQuestions, currentQuestionIndex, elapsedSeconds, onComplete]);
1677
+ }, [currentQuestion, apiClient, attempt, quiz, childId, parentId, lessonId, courseId, assignLessonId, skippedQuestionIds, extraQuestions, currentQuestionIndex, elapsedSeconds]);
1393
1678
  const handleReportQuestion = (0, import_react3.useCallback)(async (comment) => {
1394
1679
  if (!currentQuestion || !apiClient.current || !attempt || !comment.trim()) return;
1395
1680
  setIsReporting(true);
@@ -1759,533 +2044,537 @@ function QuizPlayer({
1759
2044
  const remainingSlots = maxQuestions - totalQuestions;
1760
2045
  const questionsToAdd = Math.min(5, remainingSlots);
1761
2046
  const canAddMore = onGenerateMoreQuestions && remainingSlots > 0;
1762
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className, style: defaultStyles.container, children: [
1763
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: defaultStyles.header, children: [
1764
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: defaultStyles.title, children: quiz.title }),
1765
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: defaultStyles.progress, children: [
1766
- "Question ",
1767
- currentQuestionIndex + 1,
1768
- " of ",
1769
- totalQuestions
2047
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className, style: defaultStyles.container, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: defaultStyles.mainLayout, children: [
2048
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: defaultStyles.quizContent, children: [
2049
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: defaultStyles.header, children: [
2050
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: defaultStyles.title, children: quiz.title }),
2051
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: defaultStyles.progress, children: [
2052
+ "Question ",
2053
+ currentQuestionIndex + 1,
2054
+ " of ",
2055
+ totalQuestions
2056
+ ] }),
2057
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: defaultStyles.progressBar, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { ...defaultStyles.progressFill, width: `${progressPercent}%` } }) })
1770
2058
  ] }),
1771
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: defaultStyles.progressBar, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { ...defaultStyles.progressFill, width: `${progressPercent}%` } }) })
1772
- ] }),
1773
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: defaultStyles.mainLayout, children: [
1774
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: defaultStyles.quizContent, children: [
1775
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { ...defaultStyles.question, position: "relative", paddingBottom: "40px" }, children: [
1776
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: defaultStyles.questionText, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TextToSpeech, { text: currentQuestion.question, inline: true, size: "md" }) }),
1777
- isExtraQuestion && !showFeedback && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1778
- "button",
2059
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { ...defaultStyles.question, position: "relative", paddingBottom: "40px" }, children: [
2060
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: defaultStyles.questionText, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TextToSpeech, { text: currentQuestion.question, inline: true, size: "md" }) }),
2061
+ isExtraQuestion && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2062
+ "button",
2063
+ {
2064
+ onClick: () => setShowSkipModal(true),
2065
+ title: "Skip question",
2066
+ style: {
2067
+ position: "absolute",
2068
+ bottom: "8px",
2069
+ left: "0",
2070
+ background: "transparent",
2071
+ border: "none",
2072
+ cursor: "pointer",
2073
+ padding: "6px 10px",
2074
+ borderRadius: "6px",
2075
+ color: "#9ca3af",
2076
+ display: "flex",
2077
+ alignItems: "center",
2078
+ justifyContent: "center",
2079
+ gap: "4px",
2080
+ fontSize: "12px",
2081
+ opacity: 0.6,
2082
+ transition: "opacity 0.2s, color 0.2s"
2083
+ },
2084
+ onMouseEnter: (e) => {
2085
+ e.currentTarget.style.opacity = "1";
2086
+ e.currentTarget.style.color = "#6b7280";
2087
+ },
2088
+ onMouseLeave: (e) => {
2089
+ e.currentTarget.style.opacity = "0.6";
2090
+ e.currentTarget.style.color = "#9ca3af";
2091
+ },
2092
+ "data-testid": "button-skip-question",
2093
+ children: [
2094
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
2095
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("polygon", { points: "5 4 15 12 5 20 5 4" }),
2096
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("line", { x1: "19", y1: "5", x2: "19", y2: "19" })
2097
+ ] }),
2098
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "Skip" })
2099
+ ]
2100
+ }
2101
+ ),
2102
+ !isExtraQuestion && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2103
+ "button",
2104
+ {
2105
+ onClick: () => setShowReportModal(true),
2106
+ title: "Report an issue with this question",
2107
+ style: {
2108
+ position: "absolute",
2109
+ bottom: "8px",
2110
+ left: "0",
2111
+ background: "transparent",
2112
+ border: "none",
2113
+ cursor: "pointer",
2114
+ padding: "6px 10px",
2115
+ borderRadius: "6px",
2116
+ color: "#9ca3af",
2117
+ display: "flex",
2118
+ alignItems: "center",
2119
+ justifyContent: "center",
2120
+ gap: "4px",
2121
+ fontSize: "12px",
2122
+ opacity: 0.6,
2123
+ transition: "opacity 0.2s, color 0.2s"
2124
+ },
2125
+ onMouseEnter: (e) => {
2126
+ e.currentTarget.style.opacity = "1";
2127
+ e.currentTarget.style.color = "#ef4444";
2128
+ },
2129
+ onMouseLeave: (e) => {
2130
+ e.currentTarget.style.opacity = "0.6";
2131
+ e.currentTarget.style.color = "#9ca3af";
2132
+ },
2133
+ "data-testid": "button-report-question",
2134
+ children: [
2135
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
2136
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("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" }),
2137
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("line", { x1: "4", y1: "22", x2: "4", y2: "15" })
2138
+ ] }),
2139
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "Report" })
2140
+ ]
2141
+ }
2142
+ ),
2143
+ (currentQuestion.type === "single" || currentQuestion.type === "true-false") && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: defaultStyles.options, children: currentQuestion.options?.map((option, idx) => {
2144
+ const isSelected = selectedAnswer === option;
2145
+ const isCorrectOption = currentQuestion.correctAnswer === option;
2146
+ let optionStyle = { ...defaultStyles.option };
2147
+ if (showFeedback) {
2148
+ if (isCorrectOption) {
2149
+ optionStyle = { ...optionStyle, ...defaultStyles.optionCorrect };
2150
+ } else if (isSelected && !isCorrectOption) {
2151
+ optionStyle = { ...optionStyle, ...defaultStyles.optionIncorrect };
2152
+ }
2153
+ } else if (isSelected) {
2154
+ optionStyle = { ...optionStyle, ...defaultStyles.optionSelected };
2155
+ }
2156
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2157
+ "div",
1779
2158
  {
1780
- onClick: () => setShowSkipModal(true),
1781
- title: "Skip question",
1782
2159
  style: {
1783
- position: "absolute",
1784
- bottom: "8px",
1785
- left: "0",
1786
- background: "transparent",
1787
- border: "none",
1788
- cursor: "pointer",
1789
- padding: "6px 10px",
1790
- borderRadius: "6px",
1791
- color: "#9ca3af",
2160
+ ...optionStyle,
2161
+ cursor: showFeedback ? "default" : "pointer",
1792
2162
  display: "flex",
1793
2163
  alignItems: "center",
1794
- justifyContent: "center",
1795
- gap: "4px",
1796
- fontSize: "12px",
1797
- opacity: 0.6,
1798
- transition: "opacity 0.2s, color 0.2s"
1799
- },
1800
- onMouseEnter: (e) => {
1801
- e.currentTarget.style.opacity = "1";
1802
- e.currentTarget.style.color = "#6b7280";
2164
+ gap: "8px"
1803
2165
  },
1804
- onMouseLeave: (e) => {
1805
- e.currentTarget.style.opacity = "0.6";
1806
- e.currentTarget.style.color = "#9ca3af";
1807
- },
1808
- "data-testid": "button-skip-question",
2166
+ onClick: () => !showFeedback && handleAnswerChange(option),
1809
2167
  children: [
1810
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1811
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("polygon", { points: "5 4 15 12 5 20 5 4" }),
1812
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("line", { x1: "19", y1: "5", x2: "19", y2: "19" })
1813
- ] }),
1814
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "Skip" })
2168
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TextToSpeech, { text: option, size: "sm" }) }),
2169
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { flex: 1 }, children: option })
1815
2170
  ]
2171
+ },
2172
+ idx
2173
+ );
2174
+ }) }),
2175
+ currentQuestion.type === "multiple" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: defaultStyles.options, children: currentQuestion.options?.map((option, idx) => {
2176
+ const selected = Array.isArray(selectedAnswer) && selectedAnswer.includes(option);
2177
+ const correctAnswers = Array.isArray(currentQuestion.correctAnswer) ? currentQuestion.correctAnswer : currentQuestion.correctAnswer ? [currentQuestion.correctAnswer] : [];
2178
+ const isCorrectOption = correctAnswers.includes(option);
2179
+ let optionStyle = { ...defaultStyles.option };
2180
+ if (showFeedback) {
2181
+ if (isCorrectOption) {
2182
+ optionStyle = { ...optionStyle, ...defaultStyles.optionCorrect };
2183
+ } else if (selected && !isCorrectOption) {
2184
+ optionStyle = { ...optionStyle, ...defaultStyles.optionIncorrect };
1816
2185
  }
1817
- ),
1818
- !isExtraQuestion && !showFeedback && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1819
- "button",
2186
+ } else if (selected) {
2187
+ optionStyle = { ...optionStyle, ...defaultStyles.optionSelected };
2188
+ }
2189
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2190
+ "div",
1820
2191
  {
1821
- onClick: () => setShowReportModal(true),
1822
- title: "Report an issue with this question",
1823
2192
  style: {
1824
- position: "absolute",
1825
- bottom: "8px",
1826
- left: "0",
1827
- background: "transparent",
1828
- border: "none",
1829
- cursor: "pointer",
1830
- padding: "6px 10px",
1831
- borderRadius: "6px",
1832
- color: "#9ca3af",
2193
+ ...optionStyle,
2194
+ cursor: showFeedback ? "default" : "pointer",
1833
2195
  display: "flex",
1834
2196
  alignItems: "center",
1835
- justifyContent: "center",
1836
- gap: "4px",
1837
- fontSize: "12px",
1838
- opacity: 0.6,
1839
- transition: "opacity 0.2s, color 0.2s"
1840
- },
1841
- onMouseEnter: (e) => {
1842
- e.currentTarget.style.opacity = "1";
1843
- e.currentTarget.style.color = "#ef4444";
2197
+ gap: "8px"
1844
2198
  },
1845
- onMouseLeave: (e) => {
1846
- e.currentTarget.style.opacity = "0.6";
1847
- e.currentTarget.style.color = "#9ca3af";
2199
+ onClick: () => {
2200
+ if (showFeedback) return;
2201
+ const current = Array.isArray(selectedAnswer) ? selectedAnswer : [];
2202
+ if (selected) {
2203
+ handleAnswerChange(current.filter((o) => o !== option));
2204
+ } else {
2205
+ handleAnswerChange([...current, option]);
2206
+ }
1848
2207
  },
1849
- "data-testid": "button-report-question",
1850
2208
  children: [
1851
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1852
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("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" }),
1853
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("line", { x1: "4", y1: "22", x2: "4", y2: "15" })
1854
- ] }),
1855
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "Report" })
2209
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TextToSpeech, { text: option, size: "sm" }) }),
2210
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { flex: 1 }, children: option })
1856
2211
  ]
2212
+ },
2213
+ idx
2214
+ );
2215
+ }) }),
2216
+ (currentQuestion.type === "free" || currentQuestion.type === "essay") && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2217
+ "textarea",
2218
+ {
2219
+ style: { ...defaultStyles.input, minHeight: currentQuestion.type === "essay" ? "150px" : "60px" },
2220
+ value: selectedAnswer || "",
2221
+ onChange: (e) => handleAnswerChange(e.target.value),
2222
+ placeholder: "Type your answer here...",
2223
+ disabled: showFeedback
2224
+ }
2225
+ ),
2226
+ currentQuestion.type === "fill" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: defaultStyles.options, children: currentQuestion.blanks?.map((_, idx) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2227
+ "input",
2228
+ {
2229
+ style: defaultStyles.input,
2230
+ value: (Array.isArray(selectedAnswer) ? selectedAnswer[idx] : "") || "",
2231
+ onChange: (e) => {
2232
+ const current = Array.isArray(selectedAnswer) ? [...selectedAnswer] : [];
2233
+ current[idx] = e.target.value;
2234
+ handleAnswerChange(current);
2235
+ },
2236
+ placeholder: `Blank ${idx + 1}`,
2237
+ disabled: showFeedback
2238
+ },
2239
+ idx
2240
+ )) }),
2241
+ showFeedback && currentAnswerDetail && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: {
2242
+ ...defaultStyles.feedback,
2243
+ ...currentAnswerDetail.isCorrect ? defaultStyles.feedbackCorrect : defaultStyles.feedbackIncorrect
2244
+ }, children: [
2245
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: {
2246
+ ...defaultStyles.feedbackTitle,
2247
+ ...currentAnswerDetail.isCorrect ? defaultStyles.feedbackTitleCorrect : defaultStyles.feedbackTitleIncorrect
2248
+ }, children: currentAnswerDetail.isCorrect ? "\u2713 Correct!" : "\u2717 Incorrect" }),
2249
+ currentQuestion.explanation && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: defaultStyles.feedbackExplanation, children: currentQuestion.explanation })
2250
+ ] })
2251
+ ] }),
2252
+ showSkipModal && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: {
2253
+ position: "fixed",
2254
+ top: 0,
2255
+ left: 0,
2256
+ right: 0,
2257
+ bottom: 0,
2258
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
2259
+ display: "flex",
2260
+ alignItems: "center",
2261
+ justifyContent: "center",
2262
+ zIndex: 1e3
2263
+ }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: {
2264
+ backgroundColor: "#ffffff",
2265
+ borderRadius: "12px",
2266
+ padding: "24px",
2267
+ maxWidth: "400px",
2268
+ width: "90%",
2269
+ boxShadow: "0 20px 40px rgba(0, 0, 0, 0.2)"
2270
+ }, children: [
2271
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h3", { style: { margin: "0 0 8px 0", fontSize: "18px", fontWeight: "600", color: "#1f2937" }, children: "Skip Question" }),
2272
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: { margin: "0 0 16px 0", fontSize: "14px", color: "#6b7280" }, children: "Help us improve by telling us why you're skipping this question." }),
2273
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "8px", marginBottom: "16px" }, children: [
2274
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2275
+ "button",
2276
+ {
2277
+ onClick: () => setSelectedSkipReason("question_issue"),
2278
+ disabled: isSkipping,
2279
+ style: {
2280
+ padding: "12px 16px",
2281
+ borderRadius: "8px",
2282
+ border: selectedSkipReason === "question_issue" ? "2px solid #8b5cf6" : "1px solid #e5e7eb",
2283
+ backgroundColor: selectedSkipReason === "question_issue" ? "#f5f3ff" : "#f9fafb",
2284
+ cursor: isSkipping ? "not-allowed" : "pointer",
2285
+ fontSize: "14px",
2286
+ fontWeight: "500",
2287
+ color: "#374151",
2288
+ textAlign: "left",
2289
+ opacity: isSkipping ? 0.6 : 1
2290
+ },
2291
+ "data-testid": "button-skip-reason-issue",
2292
+ children: "Question has an issue"
1857
2293
  }
1858
2294
  ),
1859
- (currentQuestion.type === "single" || currentQuestion.type === "true-false") && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: defaultStyles.options, children: currentQuestion.options?.map((option, idx) => {
1860
- const isSelected = selectedAnswer === option;
1861
- const isCorrectOption = currentQuestion.correctAnswer === option;
1862
- let optionStyle = { ...defaultStyles.option };
1863
- if (showFeedback) {
1864
- if (isCorrectOption) {
1865
- optionStyle = { ...optionStyle, ...defaultStyles.optionCorrect };
1866
- } else if (isSelected && !isCorrectOption) {
1867
- optionStyle = { ...optionStyle, ...defaultStyles.optionIncorrect };
1868
- }
1869
- } else if (isSelected) {
1870
- optionStyle = { ...optionStyle, ...defaultStyles.optionSelected };
1871
- }
1872
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1873
- "div",
1874
- {
1875
- style: {
1876
- ...optionStyle,
1877
- cursor: showFeedback ? "default" : "pointer",
1878
- display: "flex",
1879
- alignItems: "center",
1880
- gap: "8px"
1881
- },
1882
- onClick: () => !showFeedback && handleAnswerChange(option),
1883
- children: [
1884
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TextToSpeech, { text: option, size: "sm" }) }),
1885
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { flex: 1 }, children: option })
1886
- ]
2295
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2296
+ "button",
2297
+ {
2298
+ onClick: () => setSelectedSkipReason("dont_know"),
2299
+ disabled: isSkipping,
2300
+ style: {
2301
+ padding: "12px 16px",
2302
+ borderRadius: "8px",
2303
+ border: selectedSkipReason === "dont_know" ? "2px solid #8b5cf6" : "1px solid #e5e7eb",
2304
+ backgroundColor: selectedSkipReason === "dont_know" ? "#f5f3ff" : "#f9fafb",
2305
+ cursor: isSkipping ? "not-allowed" : "pointer",
2306
+ fontSize: "14px",
2307
+ fontWeight: "500",
2308
+ color: "#374151",
2309
+ textAlign: "left",
2310
+ opacity: isSkipping ? 0.6 : 1
1887
2311
  },
1888
- idx
1889
- );
1890
- }) }),
1891
- currentQuestion.type === "multiple" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: defaultStyles.options, children: currentQuestion.options?.map((option, idx) => {
1892
- const selected = Array.isArray(selectedAnswer) && selectedAnswer.includes(option);
1893
- const correctAnswers = Array.isArray(currentQuestion.correctAnswer) ? currentQuestion.correctAnswer : currentQuestion.correctAnswer ? [currentQuestion.correctAnswer] : [];
1894
- const isCorrectOption = correctAnswers.includes(option);
1895
- let optionStyle = { ...defaultStyles.option };
1896
- if (showFeedback) {
1897
- if (isCorrectOption) {
1898
- optionStyle = { ...optionStyle, ...defaultStyles.optionCorrect };
1899
- } else if (selected && !isCorrectOption) {
1900
- optionStyle = { ...optionStyle, ...defaultStyles.optionIncorrect };
1901
- }
1902
- } else if (selected) {
1903
- optionStyle = { ...optionStyle, ...defaultStyles.optionSelected };
2312
+ "data-testid": "button-skip-reason-dont-know",
2313
+ children: "I don't know the answer"
1904
2314
  }
1905
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1906
- "div",
1907
- {
1908
- style: {
1909
- ...optionStyle,
1910
- cursor: showFeedback ? "default" : "pointer",
1911
- display: "flex",
1912
- alignItems: "center",
1913
- gap: "8px"
1914
- },
1915
- onClick: () => {
1916
- if (showFeedback) return;
1917
- const current = Array.isArray(selectedAnswer) ? selectedAnswer : [];
1918
- if (selected) {
1919
- handleAnswerChange(current.filter((o) => o !== option));
1920
- } else {
1921
- handleAnswerChange([...current, option]);
1922
- }
1923
- },
1924
- children: [
1925
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TextToSpeech, { text: option, size: "sm" }) }),
1926
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { flex: 1 }, children: option })
1927
- ]
1928
- },
1929
- idx
1930
- );
1931
- }) }),
1932
- (currentQuestion.type === "free" || currentQuestion.type === "essay") && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2315
+ )
2316
+ ] }),
2317
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { marginBottom: "16px" }, children: [
2318
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { style: { display: "block", fontSize: "13px", fontWeight: "500", color: "#374151", marginBottom: "6px" }, children: "Additional details (optional)" }),
2319
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1933
2320
  "textarea",
1934
2321
  {
1935
- style: { ...defaultStyles.input, minHeight: currentQuestion.type === "essay" ? "150px" : "60px" },
1936
- value: selectedAnswer || "",
1937
- onChange: (e) => handleAnswerChange(e.target.value),
1938
- placeholder: "Type your answer here...",
1939
- disabled: showFeedback
2322
+ value: skipComment,
2323
+ onChange: (e) => setSkipComment(e.target.value.slice(0, 200)),
2324
+ placeholder: "Tell us more about the issue...",
2325
+ disabled: isSkipping,
2326
+ style: {
2327
+ width: "100%",
2328
+ minHeight: "80px",
2329
+ padding: "10px 12px",
2330
+ borderRadius: "8px",
2331
+ border: "1px solid #e5e7eb",
2332
+ fontSize: "14px",
2333
+ resize: "vertical",
2334
+ fontFamily: "inherit",
2335
+ boxSizing: "border-box"
2336
+ },
2337
+ "data-testid": "input-skip-comment"
1940
2338
  }
1941
2339
  ),
1942
- currentQuestion.type === "fill" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: defaultStyles.options, children: currentQuestion.blanks?.map((_, idx) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1943
- "input",
2340
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { fontSize: "12px", color: "#9ca3af", marginTop: "4px", textAlign: "right" }, children: [
2341
+ skipComment.length,
2342
+ "/200"
2343
+ ] })
2344
+ ] }),
2345
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "10px" }, children: [
2346
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2347
+ "button",
1944
2348
  {
1945
- style: defaultStyles.input,
1946
- value: (Array.isArray(selectedAnswer) ? selectedAnswer[idx] : "") || "",
1947
- onChange: (e) => {
1948
- const current = Array.isArray(selectedAnswer) ? [...selectedAnswer] : [];
1949
- current[idx] = e.target.value;
1950
- handleAnswerChange(current);
2349
+ onClick: () => {
2350
+ setShowSkipModal(false);
2351
+ setSkipComment("");
2352
+ setSelectedSkipReason(null);
1951
2353
  },
1952
- placeholder: `Blank ${idx + 1}`,
1953
- disabled: showFeedback
1954
- },
1955
- idx
1956
- )) }),
1957
- showFeedback && currentAnswerDetail && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: {
1958
- ...defaultStyles.feedback,
1959
- ...currentAnswerDetail.isCorrect ? defaultStyles.feedbackCorrect : defaultStyles.feedbackIncorrect
1960
- }, children: [
1961
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: {
1962
- ...defaultStyles.feedbackTitle,
1963
- ...currentAnswerDetail.isCorrect ? defaultStyles.feedbackTitleCorrect : defaultStyles.feedbackTitleIncorrect
1964
- }, children: currentAnswerDetail.isCorrect ? "\u2713 Correct!" : "\u2717 Incorrect" }),
1965
- currentQuestion.explanation && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: defaultStyles.feedbackExplanation, children: currentQuestion.explanation })
2354
+ style: {
2355
+ flex: 1,
2356
+ padding: "10px 16px",
2357
+ borderRadius: "8px",
2358
+ border: "1px solid #e5e7eb",
2359
+ backgroundColor: "#ffffff",
2360
+ cursor: "pointer",
2361
+ fontSize: "14px",
2362
+ fontWeight: "500",
2363
+ color: "#6b7280"
2364
+ },
2365
+ "data-testid": "button-skip-cancel",
2366
+ children: "Cancel"
2367
+ }
2368
+ ),
2369
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2370
+ "button",
2371
+ {
2372
+ onClick: () => selectedSkipReason && handleSkipQuestion(selectedSkipReason, skipComment),
2373
+ disabled: isSkipping || !selectedSkipReason,
2374
+ style: {
2375
+ flex: 1,
2376
+ padding: "10px 16px",
2377
+ borderRadius: "8px",
2378
+ border: "none",
2379
+ backgroundColor: selectedSkipReason ? "#8b5cf6" : "#d1d5db",
2380
+ cursor: isSkipping || !selectedSkipReason ? "not-allowed" : "pointer",
2381
+ fontSize: "14px",
2382
+ fontWeight: "500",
2383
+ color: "#ffffff",
2384
+ opacity: isSkipping ? 0.6 : 1
2385
+ },
2386
+ "data-testid": "button-skip-submit",
2387
+ children: isSkipping ? "Skipping..." : "Skip Question"
2388
+ }
2389
+ )
2390
+ ] })
2391
+ ] }) }),
2392
+ showReportModal && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: {
2393
+ position: "fixed",
2394
+ top: 0,
2395
+ left: 0,
2396
+ right: 0,
2397
+ bottom: 0,
2398
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
2399
+ display: "flex",
2400
+ alignItems: "center",
2401
+ justifyContent: "center",
2402
+ zIndex: 1e3
2403
+ }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: {
2404
+ backgroundColor: "#ffffff",
2405
+ borderRadius: "12px",
2406
+ padding: "24px",
2407
+ maxWidth: "400px",
2408
+ width: "90%",
2409
+ boxShadow: "0 20px 40px rgba(0, 0, 0, 0.2)"
2410
+ }, children: [
2411
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h3", { style: { margin: "0 0 8px 0", fontSize: "18px", fontWeight: "600", color: "#1f2937" }, children: "Report Question" }),
2412
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: { margin: "0 0 16px 0", fontSize: "14px", color: "#6b7280" }, children: "What's wrong with this question?" }),
2413
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { marginBottom: "16px" }, children: [
2414
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2415
+ "textarea",
2416
+ {
2417
+ value: reportComment,
2418
+ onChange: (e) => setReportComment(e.target.value.slice(0, 300)),
2419
+ placeholder: "Describe the issue with this question...",
2420
+ disabled: isReporting,
2421
+ style: {
2422
+ width: "100%",
2423
+ minHeight: "120px",
2424
+ padding: "10px 12px",
2425
+ borderRadius: "8px",
2426
+ border: "1px solid #e5e7eb",
2427
+ fontSize: "14px",
2428
+ resize: "vertical",
2429
+ fontFamily: "inherit",
2430
+ boxSizing: "border-box"
2431
+ },
2432
+ "data-testid": "input-report-comment"
2433
+ }
2434
+ ),
2435
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { fontSize: "12px", color: "#9ca3af", marginTop: "4px", textAlign: "right" }, children: [
2436
+ reportComment.length,
2437
+ "/300"
1966
2438
  ] })
1967
2439
  ] }),
1968
- showSkipModal && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: {
1969
- position: "fixed",
1970
- top: 0,
1971
- left: 0,
1972
- right: 0,
1973
- bottom: 0,
1974
- backgroundColor: "rgba(0, 0, 0, 0.5)",
1975
- display: "flex",
1976
- alignItems: "center",
1977
- justifyContent: "center",
1978
- zIndex: 1e3
1979
- }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: {
1980
- backgroundColor: "#ffffff",
1981
- borderRadius: "12px",
1982
- padding: "24px",
1983
- maxWidth: "400px",
1984
- width: "90%",
1985
- boxShadow: "0 20px 40px rgba(0, 0, 0, 0.2)"
1986
- }, children: [
1987
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h3", { style: { margin: "0 0 8px 0", fontSize: "18px", fontWeight: "600", color: "#1f2937" }, children: "Skip Question" }),
1988
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: { margin: "0 0 16px 0", fontSize: "14px", color: "#6b7280" }, children: "Help us improve by telling us why you're skipping this question." }),
1989
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "8px", marginBottom: "16px" }, children: [
1990
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1991
- "button",
1992
- {
1993
- onClick: () => setSelectedSkipReason("question_issue"),
1994
- disabled: isSkipping,
1995
- style: {
1996
- padding: "12px 16px",
1997
- borderRadius: "8px",
1998
- border: selectedSkipReason === "question_issue" ? "2px solid #8b5cf6" : "1px solid #e5e7eb",
1999
- backgroundColor: selectedSkipReason === "question_issue" ? "#f5f3ff" : "#f9fafb",
2000
- cursor: isSkipping ? "not-allowed" : "pointer",
2001
- fontSize: "14px",
2002
- fontWeight: "500",
2003
- color: "#374151",
2004
- textAlign: "left",
2005
- opacity: isSkipping ? 0.6 : 1
2006
- },
2007
- "data-testid": "button-skip-reason-issue",
2008
- children: "Question has an issue"
2009
- }
2010
- ),
2011
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2012
- "button",
2013
- {
2014
- onClick: () => setSelectedSkipReason("dont_know"),
2015
- disabled: isSkipping,
2016
- style: {
2017
- padding: "12px 16px",
2018
- borderRadius: "8px",
2019
- border: selectedSkipReason === "dont_know" ? "2px solid #8b5cf6" : "1px solid #e5e7eb",
2020
- backgroundColor: selectedSkipReason === "dont_know" ? "#f5f3ff" : "#f9fafb",
2021
- cursor: isSkipping ? "not-allowed" : "pointer",
2022
- fontSize: "14px",
2023
- fontWeight: "500",
2024
- color: "#374151",
2025
- textAlign: "left",
2026
- opacity: isSkipping ? 0.6 : 1
2027
- },
2028
- "data-testid": "button-skip-reason-dont-know",
2029
- children: "I don't know the answer"
2030
- }
2031
- )
2032
- ] }),
2033
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { marginBottom: "16px" }, children: [
2034
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { style: { display: "block", fontSize: "13px", fontWeight: "500", color: "#374151", marginBottom: "6px" }, children: "Additional details (optional)" }),
2035
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2036
- "textarea",
2037
- {
2038
- value: skipComment,
2039
- onChange: (e) => setSkipComment(e.target.value.slice(0, 200)),
2040
- placeholder: "Tell us more about the issue...",
2041
- disabled: isSkipping,
2042
- style: {
2043
- width: "100%",
2044
- minHeight: "80px",
2045
- padding: "10px 12px",
2046
- borderRadius: "8px",
2047
- border: "1px solid #e5e7eb",
2048
- fontSize: "14px",
2049
- resize: "vertical",
2050
- fontFamily: "inherit",
2051
- boxSizing: "border-box"
2052
- },
2053
- "data-testid": "input-skip-comment"
2054
- }
2055
- ),
2056
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { fontSize: "12px", color: "#9ca3af", marginTop: "4px", textAlign: "right" }, children: [
2057
- skipComment.length,
2058
- "/200"
2059
- ] })
2060
- ] }),
2061
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "10px" }, children: [
2062
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2063
- "button",
2064
- {
2065
- onClick: () => {
2066
- setShowSkipModal(false);
2067
- setSkipComment("");
2068
- setSelectedSkipReason(null);
2069
- },
2070
- style: {
2071
- flex: 1,
2072
- padding: "10px 16px",
2073
- borderRadius: "8px",
2074
- border: "1px solid #e5e7eb",
2075
- backgroundColor: "#ffffff",
2076
- cursor: "pointer",
2077
- fontSize: "14px",
2078
- fontWeight: "500",
2079
- color: "#6b7280"
2080
- },
2081
- "data-testid": "button-skip-cancel",
2082
- children: "Cancel"
2083
- }
2084
- ),
2085
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2086
- "button",
2087
- {
2088
- onClick: () => selectedSkipReason && handleSkipQuestion(selectedSkipReason, skipComment),
2089
- disabled: isSkipping || !selectedSkipReason,
2090
- style: {
2091
- flex: 1,
2092
- padding: "10px 16px",
2093
- borderRadius: "8px",
2094
- border: "none",
2095
- backgroundColor: selectedSkipReason ? "#8b5cf6" : "#d1d5db",
2096
- cursor: isSkipping || !selectedSkipReason ? "not-allowed" : "pointer",
2097
- fontSize: "14px",
2098
- fontWeight: "500",
2099
- color: "#ffffff",
2100
- opacity: isSkipping ? 0.6 : 1
2101
- },
2102
- "data-testid": "button-skip-submit",
2103
- children: isSkipping ? "Skipping..." : "Skip Question"
2104
- }
2105
- )
2106
- ] })
2107
- ] }) }),
2108
- showReportModal && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: {
2109
- position: "fixed",
2110
- top: 0,
2111
- left: 0,
2112
- right: 0,
2113
- bottom: 0,
2114
- backgroundColor: "rgba(0, 0, 0, 0.5)",
2115
- display: "flex",
2116
- alignItems: "center",
2117
- justifyContent: "center",
2118
- zIndex: 1e3
2119
- }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: {
2120
- backgroundColor: "#ffffff",
2121
- borderRadius: "12px",
2122
- padding: "24px",
2123
- maxWidth: "400px",
2124
- width: "90%",
2125
- boxShadow: "0 20px 40px rgba(0, 0, 0, 0.2)"
2126
- }, children: [
2127
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h3", { style: { margin: "0 0 8px 0", fontSize: "18px", fontWeight: "600", color: "#1f2937" }, children: "Report Question" }),
2128
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: { margin: "0 0 16px 0", fontSize: "14px", color: "#6b7280" }, children: "What's wrong with this question?" }),
2129
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { marginBottom: "16px" }, children: [
2130
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2131
- "textarea",
2132
- {
2133
- value: reportComment,
2134
- onChange: (e) => setReportComment(e.target.value.slice(0, 300)),
2135
- placeholder: "Describe the issue with this question...",
2136
- disabled: isReporting,
2137
- style: {
2138
- width: "100%",
2139
- minHeight: "120px",
2140
- padding: "10px 12px",
2141
- borderRadius: "8px",
2142
- border: "1px solid #e5e7eb",
2143
- fontSize: "14px",
2144
- resize: "vertical",
2145
- fontFamily: "inherit",
2146
- boxSizing: "border-box"
2147
- },
2148
- "data-testid": "input-report-comment"
2149
- }
2150
- ),
2151
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { fontSize: "12px", color: "#9ca3af", marginTop: "4px", textAlign: "right" }, children: [
2152
- reportComment.length,
2153
- "/300"
2154
- ] })
2155
- ] }),
2156
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "10px" }, children: [
2157
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2158
- "button",
2159
- {
2160
- onClick: () => {
2161
- setShowReportModal(false);
2162
- setReportComment("");
2163
- },
2164
- style: {
2165
- flex: 1,
2166
- padding: "10px 16px",
2167
- borderRadius: "8px",
2168
- border: "1px solid #e5e7eb",
2169
- backgroundColor: "#ffffff",
2170
- cursor: "pointer",
2171
- fontSize: "14px",
2172
- fontWeight: "500",
2173
- color: "#6b7280"
2174
- },
2175
- "data-testid": "button-report-cancel",
2176
- children: "Cancel"
2177
- }
2178
- ),
2179
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2180
- "button",
2181
- {
2182
- onClick: () => handleReportQuestion(reportComment),
2183
- disabled: isReporting || !reportComment.trim(),
2184
- style: {
2185
- flex: 1,
2186
- padding: "10px 16px",
2187
- borderRadius: "8px",
2188
- border: "none",
2189
- backgroundColor: reportComment.trim() ? "#ef4444" : "#d1d5db",
2190
- cursor: isReporting || !reportComment.trim() ? "not-allowed" : "pointer",
2191
- fontSize: "14px",
2192
- fontWeight: "500",
2193
- color: "#ffffff",
2194
- opacity: isReporting ? 0.6 : 1
2195
- },
2196
- "data-testid": "button-report-submit",
2197
- children: isReporting ? "Reporting..." : "Report"
2198
- }
2199
- )
2200
- ] })
2201
- ] }) }),
2202
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: defaultStyles.buttonsColumn, children: [
2203
- showFeedback && isLastQuestion && canAddMore && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2440
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "10px" }, children: [
2441
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2204
2442
  "button",
2205
2443
  {
2444
+ onClick: () => {
2445
+ setShowReportModal(false);
2446
+ setReportComment("");
2447
+ },
2206
2448
  style: {
2207
- ...defaultStyles.buttonAddMore,
2208
- ...isGeneratingExtra ? defaultStyles.buttonAddMoreDisabled : {}
2449
+ flex: 1,
2450
+ padding: "10px 16px",
2451
+ borderRadius: "8px",
2452
+ border: "1px solid #e5e7eb",
2453
+ backgroundColor: "#ffffff",
2454
+ cursor: "pointer",
2455
+ fontSize: "14px",
2456
+ fontWeight: "500",
2457
+ color: "#6b7280"
2209
2458
  },
2210
- onClick: handleAddMoreQuestions,
2211
- disabled: isGeneratingExtra,
2212
- "data-testid": "button-add-more-questions",
2213
- children: isGeneratingExtra ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
2214
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Spinner, { size: 16, color: "#9ca3af" }),
2215
- "Generating Questions..."
2216
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
2217
- "+ Add ",
2218
- questionsToAdd,
2219
- " More Question",
2220
- questionsToAdd !== 1 ? "s" : ""
2221
- ] })
2459
+ "data-testid": "button-report-cancel",
2460
+ children: "Cancel"
2222
2461
  }
2223
2462
  ),
2224
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { ...defaultStyles.buttons, justifyContent: "flex-end" }, children: showFeedback ? (
2225
- // After viewing feedback
2226
- isLastQuestion ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2227
- "button",
2228
- {
2229
- style: {
2230
- ...defaultStyles.button,
2231
- ...isSubmitting || isGeneratingExtra ? defaultStyles.buttonDisabled : defaultStyles.buttonPrimary
2232
- },
2233
- onClick: handleSubmit,
2234
- disabled: isSubmitting || isGeneratingExtra,
2235
- "data-testid": "button-submit-quiz",
2236
- children: isSubmitting ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Spinner, { size: 16, color: "#9ca3af" }) : "Submit Quiz"
2237
- }
2238
- ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2239
- "button",
2240
- {
2241
- style: {
2242
- ...defaultStyles.button,
2243
- ...defaultStyles.buttonPrimary
2244
- },
2245
- onClick: handleContinue,
2246
- "data-testid": "button-continue",
2247
- children: "Continue"
2248
- }
2249
- )
2250
- ) : (
2251
- // Before checking answer
2252
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2253
- "button",
2254
- {
2255
- style: {
2256
- ...defaultStyles.button,
2257
- ...isNavigating || selectedAnswer === void 0 ? defaultStyles.buttonDisabled : defaultStyles.buttonPrimary
2258
- },
2259
- onClick: handleCheckAnswer,
2260
- disabled: isNavigating || selectedAnswer === void 0,
2261
- "data-testid": "button-check-answer",
2262
- children: isNavigating ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Spinner, { size: 16, color: "#9ca3af" }) : "Check Answer"
2263
- }
2264
- )
2265
- ) })
2463
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2464
+ "button",
2465
+ {
2466
+ onClick: () => handleReportQuestion(reportComment),
2467
+ disabled: isReporting || !reportComment.trim(),
2468
+ style: {
2469
+ flex: 1,
2470
+ padding: "10px 16px",
2471
+ borderRadius: "8px",
2472
+ border: "none",
2473
+ backgroundColor: reportComment.trim() ? "#ef4444" : "#d1d5db",
2474
+ cursor: isReporting || !reportComment.trim() ? "not-allowed" : "pointer",
2475
+ fontSize: "14px",
2476
+ fontWeight: "500",
2477
+ color: "#ffffff",
2478
+ opacity: isReporting ? 0.6 : 1
2479
+ },
2480
+ "data-testid": "button-report-submit",
2481
+ children: isReporting ? "Reporting..." : "Report"
2482
+ }
2483
+ )
2266
2484
  ] })
2267
- ] }),
2268
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: defaultStyles.chatPanel, children: apiClient.current && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2269
- QuestionChatPanel,
2270
- {
2271
- apiClient: apiClient.current,
2272
- question: {
2273
- id: currentQuestion.id,
2274
- question: currentQuestion.question,
2275
- type: currentQuestion.type,
2276
- options: currentQuestion.options,
2277
- correctAnswer: currentQuestion.correctAnswer,
2278
- explanation: currentQuestion.explanation
2279
- },
2280
- quizId: quiz.id,
2281
- childId,
2282
- parentId,
2283
- lessonId,
2284
- courseId
2285
- }
2286
- ) })
2287
- ] })
2288
- ] });
2485
+ ] }) }),
2486
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: defaultStyles.buttonsColumn, children: [
2487
+ showFeedback && isLastQuestion && canAddMore && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2488
+ "button",
2489
+ {
2490
+ style: {
2491
+ ...defaultStyles.buttonAddMore,
2492
+ ...isGeneratingExtra ? defaultStyles.buttonAddMoreDisabled : {}
2493
+ },
2494
+ onClick: handleAddMoreQuestions,
2495
+ disabled: isGeneratingExtra,
2496
+ "data-testid": "button-add-more-questions",
2497
+ children: isGeneratingExtra ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
2498
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Spinner, { size: 16, color: "#9ca3af" }),
2499
+ "Generating Questions..."
2500
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
2501
+ "+ Add ",
2502
+ questionsToAdd,
2503
+ " More Question",
2504
+ questionsToAdd !== 1 ? "s" : ""
2505
+ ] })
2506
+ }
2507
+ ),
2508
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { ...defaultStyles.buttons, justifyContent: "flex-end" }, children: showFeedback ? (
2509
+ // After viewing feedback
2510
+ isLastQuestion ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2511
+ "button",
2512
+ {
2513
+ style: {
2514
+ ...defaultStyles.button,
2515
+ ...isSubmitting || isGeneratingExtra ? defaultStyles.buttonDisabled : defaultStyles.buttonPrimary
2516
+ },
2517
+ onClick: handleSubmit,
2518
+ disabled: isSubmitting || isGeneratingExtra,
2519
+ "data-testid": "button-submit-quiz",
2520
+ children: isSubmitting ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Spinner, { size: 16, color: "#9ca3af" }) : "Submit Quiz"
2521
+ }
2522
+ ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2523
+ "button",
2524
+ {
2525
+ style: {
2526
+ ...defaultStyles.button,
2527
+ ...defaultStyles.buttonPrimary
2528
+ },
2529
+ onClick: handleContinue,
2530
+ "data-testid": "button-continue",
2531
+ children: "Continue"
2532
+ }
2533
+ )
2534
+ ) : (
2535
+ // Before checking answer
2536
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2537
+ "button",
2538
+ {
2539
+ style: {
2540
+ ...defaultStyles.button,
2541
+ ...isNavigating || selectedAnswer === void 0 ? defaultStyles.buttonDisabled : defaultStyles.buttonPrimary
2542
+ },
2543
+ onClick: handleCheckAnswer,
2544
+ disabled: isNavigating || selectedAnswer === void 0,
2545
+ "data-testid": "button-check-answer",
2546
+ children: isNavigating ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Spinner, { size: 16, color: "#9ca3af" }) : "Check Answer"
2547
+ }
2548
+ )
2549
+ ) })
2550
+ ] })
2551
+ ] }),
2552
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: defaultStyles.chatPanel, children: apiClient.current && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2553
+ QuestionChatPanel,
2554
+ {
2555
+ apiClient: apiClient.current,
2556
+ question: {
2557
+ id: currentQuestion.id,
2558
+ question: currentQuestion.question,
2559
+ type: currentQuestion.type,
2560
+ options: currentQuestion.options,
2561
+ correctAnswer: currentQuestion.correctAnswer,
2562
+ explanation: currentQuestion.explanation
2563
+ },
2564
+ quizId: quiz.id,
2565
+ childId,
2566
+ parentId,
2567
+ lessonId,
2568
+ courseId,
2569
+ answerResult: showFeedback && currentAnswerDetail ? {
2570
+ wasIncorrect: !currentAnswerDetail.isCorrect,
2571
+ selectedAnswer: typeof selectedAnswer === "string" ? selectedAnswer : Array.isArray(selectedAnswer) ? selectedAnswer.join(", ") : void 0,
2572
+ correctAnswer: typeof currentQuestion.correctAnswer === "string" ? currentQuestion.correctAnswer : Array.isArray(currentQuestion.correctAnswer) ? currentQuestion.correctAnswer.join(", ") : void 0,
2573
+ explanation: currentQuestion.explanation
2574
+ } : void 0
2575
+ }
2576
+ ) })
2577
+ ] }) });
2289
2578
  }
2290
2579
 
2291
2580
  // src/AttemptViewer.tsx
@@ -2411,6 +2700,46 @@ var defaultStyles2 = {
2411
2700
  fontSize: "14px",
2412
2701
  color: "#581c87"
2413
2702
  },
2703
+ chatHistorySection: {
2704
+ marginTop: "12px",
2705
+ borderTop: "1px solid #e5e7eb",
2706
+ paddingTop: "12px"
2707
+ },
2708
+ chatToggleButton: {
2709
+ display: "flex",
2710
+ alignItems: "center",
2711
+ gap: "6px",
2712
+ padding: "6px 12px",
2713
+ backgroundColor: "#f3f4f6",
2714
+ border: "none",
2715
+ borderRadius: "6px",
2716
+ fontSize: "13px",
2717
+ color: "#6b7280",
2718
+ cursor: "pointer",
2719
+ fontWeight: "500"
2720
+ },
2721
+ chatMessages: {
2722
+ marginTop: "12px",
2723
+ display: "flex",
2724
+ flexDirection: "column",
2725
+ gap: "8px"
2726
+ },
2727
+ chatMessage: {
2728
+ padding: "8px 12px",
2729
+ borderRadius: "8px",
2730
+ fontSize: "13px",
2731
+ maxWidth: "85%"
2732
+ },
2733
+ chatMessageUser: {
2734
+ backgroundColor: "#6721b0",
2735
+ color: "#ffffff",
2736
+ alignSelf: "flex-end"
2737
+ },
2738
+ chatMessageAssistant: {
2739
+ backgroundColor: "#f3f4f6",
2740
+ color: "#111827",
2741
+ alignSelf: "flex-start"
2742
+ },
2414
2743
  loading: {
2415
2744
  textAlign: "center",
2416
2745
  padding: "40px 20px"
@@ -2468,11 +2797,14 @@ function AttemptViewer({
2468
2797
  onError,
2469
2798
  className,
2470
2799
  showExplanations = true,
2800
+ showConversation = false,
2471
2801
  title
2472
2802
  }) {
2473
2803
  const [attempt, setAttempt] = (0, import_react4.useState)(null);
2474
2804
  const [loading, setLoading] = (0, import_react4.useState)(true);
2475
2805
  const [error, setError] = (0, import_react4.useState)(null);
2806
+ const [chatHistories, setChatHistories] = (0, import_react4.useState)({});
2807
+ const [expandedChats, setExpandedChats] = (0, import_react4.useState)(/* @__PURE__ */ new Set());
2476
2808
  (0, import_react4.useEffect)(() => {
2477
2809
  const apiClient = new QuizApiClient({
2478
2810
  baseUrl: apiBaseUrl,
@@ -2490,6 +2822,14 @@ function AttemptViewer({
2490
2822
  }
2491
2823
  const data = await response.json();
2492
2824
  setAttempt(data);
2825
+ if (showConversation) {
2826
+ try {
2827
+ const chats = await apiClient.getChatsByAttempt(attemptId);
2828
+ setChatHistories(chats);
2829
+ } catch (chatErr) {
2830
+ console.error("Failed to load chat histories:", chatErr);
2831
+ }
2832
+ }
2493
2833
  } catch (err) {
2494
2834
  const errorMessage = err instanceof Error ? err.message : "Failed to load attempt";
2495
2835
  setError(errorMessage);
@@ -2499,7 +2839,18 @@ function AttemptViewer({
2499
2839
  }
2500
2840
  }
2501
2841
  fetchAttempt();
2502
- }, [attemptId, apiBaseUrl, authToken, onError]);
2842
+ }, [attemptId, apiBaseUrl, authToken, onError, showConversation]);
2843
+ const toggleChatExpanded = (questionId) => {
2844
+ setExpandedChats((prev) => {
2845
+ const newSet = new Set(prev);
2846
+ if (newSet.has(questionId)) {
2847
+ newSet.delete(questionId);
2848
+ } else {
2849
+ newSet.add(questionId);
2850
+ }
2851
+ return newSet;
2852
+ });
2853
+ };
2503
2854
  const handleRetry = () => {
2504
2855
  setLoading(true);
2505
2856
  setError(null);
@@ -2591,6 +2942,34 @@ function AttemptViewer({
2591
2942
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("strong", { children: "Explanation:" }),
2592
2943
  " ",
2593
2944
  answer.explanation
2945
+ ] }),
2946
+ showConversation && chatHistories[answer.questionId] && chatHistories[answer.questionId].messages.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: defaultStyles2.chatHistorySection, children: [
2947
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2948
+ "button",
2949
+ {
2950
+ style: defaultStyles2.chatToggleButton,
2951
+ onClick: () => toggleChatExpanded(answer.questionId),
2952
+ "data-testid": `button-toggle-chat-${answer.questionId}`,
2953
+ children: [
2954
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" }) }),
2955
+ expandedChats.has(answer.questionId) ? "Hide" : "View",
2956
+ " Chat History (",
2957
+ chatHistories[answer.questionId].messages.length,
2958
+ " messages)"
2959
+ ]
2960
+ }
2961
+ ),
2962
+ expandedChats.has(answer.questionId) && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: defaultStyles2.chatMessages, children: chatHistories[answer.questionId].messages.map((msg, msgIndex) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2963
+ "div",
2964
+ {
2965
+ style: {
2966
+ ...defaultStyles2.chatMessage,
2967
+ ...msg.role === "user" ? defaultStyles2.chatMessageUser : defaultStyles2.chatMessageAssistant
2968
+ },
2969
+ children: msg.content
2970
+ },
2971
+ msgIndex
2972
+ )) })
2594
2973
  ] })
2595
2974
  ]
2596
2975
  },