@nualang/nualang-ui-components 0.1.1437 → 0.1.1440

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.
Files changed (56) hide show
  1. package/dist/Cards/FeedbackCard/FeedbackCard.js +114 -8
  2. package/dist/Cards/FeedbackCard/FeedbackCard.test.js +187 -0
  3. package/dist/Cards/RecordingCard/RecordingCard.js +10 -0
  4. package/dist/Dialogs/ExerciseMenu/CompletionDialog/AttemptsCompletionDialog.js +77 -0
  5. package/dist/Dialogs/ExerciseMenu/CompletionDialog/AttemptsCompletionDialog.test.js +37 -0
  6. package/dist/Dialogs/ExerciseMenu/CompletionDialog/CompletionActions.js +82 -0
  7. package/dist/Dialogs/ExerciseMenu/CompletionDialog/CompletionActions.test.js +30 -0
  8. package/dist/Dialogs/ExerciseMenu/CompletionDialog/CompletionDialog.js +216 -0
  9. package/dist/Dialogs/ExerciseMenu/CompletionDialog/CompletionDialog.test.js +34 -0
  10. package/dist/Dialogs/ExerciseMenu/CompletionDialog/Sections/AnswersSummary.js +247 -0
  11. package/dist/Dialogs/ExerciseMenu/CompletionDialog/Sections/AttemptSections.test.js +235 -0
  12. package/dist/Dialogs/ExerciseMenu/CompletionDialog/Sections/AttemptsToReview.js +220 -0
  13. package/dist/Dialogs/ExerciseMenu/CompletionDialog/Sections/LanguageCorrections.js +82 -0
  14. package/dist/Dialogs/ExerciseMenu/CompletionDialog/Sections/SessionFeedback.js +76 -0
  15. package/dist/Dialogs/ExerciseMenu/CompletionDialog/Sections/attemptResults.js +37 -0
  16. package/dist/Dialogs/ExerciseMenu/CompletionDialog/package.json +6 -0
  17. package/dist/Dialogs/GroupedFeedbackDialog/GroupedFeedbackDialog.js +147 -35
  18. package/dist/Dialogs/GroupedFeedbackDialog/GroupedFeedbackDialog.test.js +202 -3
  19. package/dist/Dialogs/GroupedFeedbackDialog/aiReview.js +72 -0
  20. package/dist/Dialogs/QuickCreateScenarioDialog/QuickCreateScenarioDialog.js +3 -1
  21. package/dist/Exercises/Bot/Bot.js +57 -20
  22. package/dist/Exercises/ListenAndSpeak/ListenAndSpeak.js +20 -9
  23. package/dist/Exercises/Listener/Listener.js +18 -8
  24. package/dist/Exercises/Meaning/Meaning.js +20 -9
  25. package/dist/Exercises/MeaningReadWrite/MeaningReadWriteGame.js +21 -9
  26. package/dist/Exercises/PhraseGroup/Games/ListenAndSpeakGame/ListenAndSpeakGame.js +5 -0
  27. package/dist/Exercises/PhraseGroup/Games/ListeningGame/ListeningGame.js +4 -0
  28. package/dist/Exercises/PhraseGroup/Games/MeaningGame/MeaningGame.js +5 -0
  29. package/dist/Exercises/PhraseGroup/Games/MeaningReadWriteGame/MeaningReadWriteGame.js +5 -0
  30. package/dist/Exercises/PhraseGroup/Games/PronunciationGame/PronunciationGame.js +4 -0
  31. package/dist/Exercises/PhraseGroup/Games/TranslationGame/TranslationGame.js +4 -0
  32. package/dist/Exercises/PhraseGroup/PhraseGroup.js +2 -0
  33. package/dist/Exercises/Pronouncer/Pronouncer.js +18 -8
  34. package/dist/Exercises/Roleplay/Games/ActItOutGame/ActItOutGame.js +24 -11
  35. package/dist/Exercises/Roleplay/Games/ActItOutGameListening/ActItOutGameListening.js +24 -11
  36. package/dist/Exercises/Roleplay/Games/ActItOutGameListeningAndSpeaking/ActItOutGame.js +24 -11
  37. package/dist/Exercises/Roleplay/Games/ActItOutGameSpeaking/ActItOutGame.js +24 -11
  38. package/dist/Exercises/Roleplay/Games/FillInTheBlanksGame/FillInTheBlanksGame.js +20 -11
  39. package/dist/Exercises/Roleplay/Games/StoryGame/StoryGame.js +20 -11
  40. package/dist/Exercises/Roleplay/Roleplay.js +9 -2
  41. package/dist/Exercises/Translator/Translator.js +18 -8
  42. package/dist/Forms/DiscussImageSelector/ImageSelector.js +1 -0
  43. package/dist/Forms/ImageSelector/ImageSelector.js +1 -0
  44. package/dist/Misc/TopicDifficultyBadge/TopicDifficultyBadge.js +74 -0
  45. package/dist/Misc/TopicDifficultyBadge/package.json +6 -0
  46. package/dist/Tables/MeetingPrompstList/MeetingPromptsList.js +4 -2
  47. package/dist/Tables/RecordingListCards/RecordingListCards.js +14 -11
  48. package/dist/Tables/RecordingListCards/RecordingListCards.test.js +43 -0
  49. package/dist/hooks/useExerciseState.js +41 -3
  50. package/dist/hooks/useRecognition.js +19 -1
  51. package/dist/index.js +2 -1
  52. package/dist/theme/getComponents.js +14 -0
  53. package/dist/utils/voiceImages.js +6 -0
  54. package/package.json +1 -1
  55. package/dist/Dialogs/ExerciseMenu/Menus/ChatbotCompleted/ChatbotCompleted.js +0 -395
  56. package/dist/Dialogs/ExerciseMenu/Menus/ChatbotCompleted/package.json +0 -6
@@ -16,7 +16,35 @@ import level2Rubric from "../../Dialogs/PDFViewer/rubrics/level2Rubric.pdf";
16
16
  import level3Rubric from "../../Dialogs/PDFViewer/rubrics/level3Rubric.pdf";
17
17
  import level4Rubric from "../../Dialogs/PDFViewer/rubrics/level4Rubric.pdf";
18
18
  import HelpOutlinedIcon from "@mui/icons-material/HelpOutlined";
19
+ import CheckIcon from "@mui/icons-material/Check";
20
+ import EditIcon from "@mui/icons-material/Edit";
21
+ import UndoIcon from "@mui/icons-material/Undo";
22
+ import { EMPTY_REVIEW, isPending } from "../../Dialogs/GroupedFeedbackDialog/aiReview";
19
23
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
24
+ const aiDraftChipSx = {
25
+ bgcolor: "#FFF4E5",
26
+ color: "#663C00",
27
+ fontWeight: 600,
28
+ "& .MuiChip-icon": {
29
+ color: "#ed6c02"
30
+ }
31
+ };
32
+ const editedChipSx = {
33
+ bgcolor: "#f5f5f5",
34
+ color: "rgba(0, 0, 0, 0.87)",
35
+ fontWeight: 600,
36
+ "& .MuiChip-icon": {
37
+ color: "rgba(0, 0, 0, 0.6)"
38
+ }
39
+ };
40
+ const approvedChipSx = {
41
+ bgcolor: "#EDF7ED",
42
+ color: "#1E4620",
43
+ fontWeight: 600,
44
+ "& .MuiChip-icon": {
45
+ color: "#2e7d32"
46
+ }
47
+ };
20
48
  const getPerformanceLevel = (student, aiGrade) => {
21
49
  if (!aiGrade || Object.keys(aiGrade).length === 0) return null;
22
50
  const keyVariants = [student.username, `${student.username}Student`, student.username?.replace(/\s+/g, "")];
@@ -79,14 +107,22 @@ export default function FeedbackCard({
79
107
  canSubmitAll = {},
80
108
  setCanSubmit = () => {},
81
109
  setIsGeneratingIndividual = () => {},
82
- hasBadLanguage
110
+ hasBadLanguage,
111
+ aiReview = {},
112
+ onAiDraftsApplied = () => {},
113
+ onAiEdit = () => {},
114
+ onAiApprove = () => {},
115
+ onAiUndo = () => {}
83
116
  }) {
117
+ const meetingId = discussionData.meetingId || discussionData.meetingID;
84
118
  const [isRecordingDialogOpen, setIsRecordingDialogOpen] = useState(false);
85
119
  const [hasBeenEdited, setHasBeenEdited] = useState(false);
86
120
  const [isSubmitted, setIsSubmitted] = useState(false);
87
121
  const [aiJustGenerated, setAiJustGenerated] = useState(false);
88
122
  const formikRef = useRef(null);
89
123
  const initialValuesRef = useRef(null);
124
+ // The grade object is replaced whenever new AI results arrive for this discussion
125
+ const lastAppliedGradeRef = useRef(aiGrade);
90
126
  const [openPDF, setOpenPDF] = useState(false);
91
127
  const [pageNumber, setPageNumber] = useState(1);
92
128
  const [scale, setScale] = useState(1);
@@ -170,16 +206,25 @@ export default function FeedbackCard({
170
206
  if (!formik) return;
171
207
  if (!hasSubmission || !aiGrade || Object.keys(aiGrade).length === 0) return;
172
208
  if (!aiJustGenerated && !generateAllUsed) return;
173
- userData.forEach(student => {
209
+ // Skip grades already applied, e.g. when generating for other discussions
210
+ if (aiGrade === lastAppliedGradeRef.current) return;
211
+ lastAppliedGradeRef.current = aiGrade;
212
+ const draftedMemberIds = [];
213
+ // Only this group's students: names aren't unique across a classroom
214
+ attendedMembers.forEach(student => {
174
215
  const keyVariants = [student.username, `${student.username}Student`, student.username?.replace(/\s+/g, "")];
175
216
  const aiData = keyVariants.map(k => aiGrade?.[k]).find(v => v && typeof v === "object") || null;
176
217
  if (aiData?.Feedback) {
177
218
  formik.setFieldValue(`feedbackData.${student.memberId}.feedbackText`, aiData.Feedback);
219
+ draftedMemberIds.push(student.memberId);
178
220
  }
179
221
  if (aiData?.LevelPerformance) {
180
222
  formik.setFieldValue(`feedbackData.${student.memberId}.levelPerformance`, aiData.LevelPerformance);
181
223
  }
182
224
  });
225
+ if (draftedMemberIds.length > 0) {
226
+ onAiDraftsApplied(meetingId, draftedMemberIds);
227
+ }
183
228
  setHasBeenEdited(true);
184
229
  setIsSubmitted(false);
185
230
  setAiJustGenerated(false);
@@ -223,6 +268,7 @@ export default function FeedbackCard({
223
268
  setOpenPDF(false);
224
269
  };
225
270
  const initialValues = getInitialValues();
271
+ const pendingAiMemberIds = attendedMembers.map(m => m.memberId).filter(memberId => isPending(aiReview[memberId]));
226
272
  return /*#__PURE__*/_jsxs(_Fragment, {
227
273
  children: [/*#__PURE__*/_jsx(Formik, {
228
274
  innerRef: formikRef,
@@ -231,6 +277,8 @@ export default function FeedbackCard({
231
277
  onSubmit: async (values, {
232
278
  setSubmitting
233
279
  }) => {
280
+ // AI drafts can't be sent until the teacher approves them
281
+ if (pendingAiMemberIds.length > 0) return;
234
282
  setSubmitting(true);
235
283
  try {
236
284
  const allStudents = selectedMembers.map(m => m.memberId);
@@ -240,9 +288,17 @@ export default function FeedbackCard({
240
288
  selectedMembers.forEach(student => {
241
289
  const existingLevel = feedbackDataWithAbsent[student.memberId]?.levelPerformance;
242
290
  const aiLevel = getPerformanceLevel(student, aiGrade);
291
+ const {
292
+ aiGenerated,
293
+ teacherEdited,
294
+ teacherApproved
295
+ } = aiReview[student.memberId] || EMPTY_REVIEW;
243
296
  feedbackDataWithAbsent[student.memberId] = {
244
297
  ...feedbackDataWithAbsent[student.memberId],
245
- levelPerformance: existingLevel ?? aiLevel ?? null
298
+ levelPerformance: existingLevel ?? aiLevel ?? null,
299
+ aiGenerated,
300
+ teacherEdited,
301
+ teacherApproved
246
302
  };
247
303
  });
248
304
  notAttendedMembers.forEach(student => {
@@ -287,7 +343,7 @@ export default function FeedbackCard({
287
343
  return current !== initial;
288
344
  });
289
345
  };
290
- const canSubmit = hasSubmission && !isSubmitting && isFormValid() && (hasChanges() || hasBeenEdited) && !isSubmitted;
346
+ const canSubmit = hasSubmission && !isSubmitting && isFormValid() && (hasChanges() || hasBeenEdited) && !isSubmitted && pendingAiMemberIds.length === 0;
291
347
  // eslint-disable-next-line react-hooks/rules-of-hooks
292
348
  useEffect(() => {
293
349
  setCanSubmit({
@@ -411,6 +467,8 @@ export default function FeedbackCard({
411
467
  const studentFeedback = values.feedbackData[student.memberId] || {};
412
468
  const perfLevel = studentFeedback.levelPerformance || getPerformanceLevel(student, aiGrade);
413
469
  const chipProps = getPerformanceChipProps(perfLevel, t);
470
+ const review = aiReview[student.memberId] || EMPTY_REVIEW;
471
+ const showReview = review.aiGenerated && !disabled;
414
472
  return /*#__PURE__*/_jsx(Box, {
415
473
  sx: {
416
474
  py: 2
@@ -503,12 +561,12 @@ export default function FeedbackCard({
503
561
  })]
504
562
  })]
505
563
  })
506
- }), /*#__PURE__*/_jsx(Grid, {
564
+ }), /*#__PURE__*/_jsxs(Grid, {
507
565
  size: {
508
566
  xs: 12,
509
567
  md: 9.5
510
568
  },
511
- children: /*#__PURE__*/_jsx(TextField, {
569
+ children: [/*#__PURE__*/_jsx(TextField, {
512
570
  variant: "outlined",
513
571
  fullWidth: true,
514
572
  multiline: true,
@@ -518,9 +576,57 @@ export default function FeedbackCard({
518
576
  setFieldValue(`feedbackData.${student.memberId}.feedbackText`, e.target.value);
519
577
  setHasBeenEdited(true);
520
578
  setIsSubmitted(false);
579
+ onAiEdit(meetingId, student.memberId);
521
580
  },
522
- placeholder: t("write_your_feedback_here")
523
- })
581
+ placeholder: t("write_your_feedback_here"),
582
+ slotProps: {
583
+ htmlInput: {
584
+ id: `feedback-${meetingId}-${student.memberId}`,
585
+ "data-ai-key": `${meetingId}:${student.memberId}`,
586
+ "data-ai-pending": String(showReview && isPending(review))
587
+ }
588
+ },
589
+ sx: showReview && isPending(review) && !review.teacherEdited ? {
590
+ "& .MuiOutlinedInput-notchedOutline": {
591
+ borderColor: "#ed6c02",
592
+ borderWidth: "1px"
593
+ }
594
+ } : undefined
595
+ }), showReview && /*#__PURE__*/_jsxs(Box, {
596
+ sx: {
597
+ display: "flex",
598
+ alignItems: "center",
599
+ flexWrap: "wrap",
600
+ gap: 1,
601
+ mt: 1
602
+ },
603
+ children: [!review.teacherEdited && !review.teacherApproved && /*#__PURE__*/_jsx(Chip, {
604
+ size: "small",
605
+ icon: /*#__PURE__*/_jsx(AutoFixHighIcon, {}),
606
+ label: t("ai_draft_not_edited"),
607
+ sx: aiDraftChipSx
608
+ }), review.teacherApproved && /*#__PURE__*/_jsx(Chip, {
609
+ size: "small",
610
+ icon: /*#__PURE__*/_jsx(CheckCircleIcon, {}),
611
+ label: t("approved"),
612
+ sx: approvedChipSx
613
+ }), review.teacherEdited && /*#__PURE__*/_jsx(Chip, {
614
+ size: "small",
615
+ icon: /*#__PURE__*/_jsx(EditIcon, {}),
616
+ label: t("edited_by_you"),
617
+ sx: editedChipSx
618
+ }), review.teacherApproved ? /*#__PURE__*/_jsx(Button, {
619
+ size: "small",
620
+ startIcon: /*#__PURE__*/_jsx(UndoIcon, {}),
621
+ onClick: () => onAiUndo(meetingId, student.memberId),
622
+ children: t("undo")
623
+ }) : /*#__PURE__*/_jsx(Button, {
624
+ size: "small",
625
+ startIcon: /*#__PURE__*/_jsx(CheckIcon, {}),
626
+ onClick: () => onAiApprove(meetingId, student.memberId),
627
+ children: t("approve")
628
+ })]
629
+ })]
524
630
  })]
525
631
  })
526
632
  }, student.memberId);
@@ -0,0 +1,187 @@
1
+ import FeedbackCard from "./FeedbackCard";
2
+ import { props } from "./FeedbackCard.stories";
3
+ import { render, screen, fireEvent, waitFor, cleanup } from "@testing-library/react";
4
+ import { describe, it, expect, vi, afterEach } from "vitest";
5
+
6
+ // react-pdf needs DOMMatrix, which jsdom doesn't provide
7
+ import { jsx as _jsx } from "react/jsx-runtime";
8
+ vi.mock("../../Dialogs/PDFViewer/PDFViewer", () => ({
9
+ default: () => null
10
+ }));
11
+ const discussionData = {
12
+ ...props.discussionData,
13
+ meetingId: "m1"
14
+ };
15
+ const existingFeedback = {
16
+ 1: {
17
+ feedbackText: "AI feedback for Aaron"
18
+ },
19
+ 2: {
20
+ feedbackText: "Teacher feedback for Connor"
21
+ },
22
+ 3: {
23
+ feedbackText: "Teacher feedback for Stephen"
24
+ }
25
+ };
26
+ const pending = {
27
+ aiGenerated: true,
28
+ teacherEdited: false,
29
+ teacherApproved: false
30
+ };
31
+ const renderCard = (overrides = {}) => render(/*#__PURE__*/_jsx(FeedbackCard, {
32
+ ...props,
33
+ hasBadLanguage: false,
34
+ hasSubmission: true,
35
+ discussionData: discussionData,
36
+ existingFeedback: existingFeedback,
37
+ ...overrides
38
+ }));
39
+ const submitButton = () => screen.getByRole("button", {
40
+ name: "submit_feedback"
41
+ });
42
+ describe("FeedbackCard", () => {
43
+ afterEach(cleanup);
44
+ it("renders without crashing", () => {
45
+ const {
46
+ unmount
47
+ } = renderCard();
48
+ unmount();
49
+ });
50
+ it("shows no review controls for teacher-written feedback", () => {
51
+ renderCard();
52
+ expect(screen.queryByText("ai_draft_not_edited")).toBeNull();
53
+ expect(screen.queryByRole("button", {
54
+ name: "approve"
55
+ })).toBeNull();
56
+ });
57
+ it("flags an unedited AI draft and blocks submit until it is approved", () => {
58
+ const onAiEdit = vi.fn();
59
+ const onAiApprove = vi.fn();
60
+ renderCard({
61
+ aiReview: {
62
+ 1: pending
63
+ },
64
+ onAiEdit,
65
+ onAiApprove
66
+ });
67
+ expect(screen.getByText("ai_draft_not_edited")).toBeTruthy();
68
+ expect(screen.queryByText("edited_by_you")).toBeNull();
69
+ fireEvent.change(document.getElementById("feedback-m1-2"), {
70
+ target: {
71
+ value: "Edited teacher feedback"
72
+ }
73
+ });
74
+ expect(onAiEdit).toHaveBeenCalledWith("m1", "2");
75
+ expect(submitButton().disabled).toBe(true);
76
+ fireEvent.click(screen.getByRole("button", {
77
+ name: "approve"
78
+ }));
79
+ expect(onAiApprove).toHaveBeenCalledWith("m1", "1");
80
+ });
81
+ it("shows edited and approved states", () => {
82
+ const {
83
+ rerender
84
+ } = renderCard({
85
+ aiReview: {
86
+ 1: {
87
+ ...pending,
88
+ teacherEdited: true
89
+ }
90
+ }
91
+ });
92
+ expect(screen.queryByText("ai_draft_not_edited")).toBeNull();
93
+ expect(screen.getByText("edited_by_you")).toBeTruthy();
94
+ expect(screen.getByRole("button", {
95
+ name: "approve"
96
+ })).toBeTruthy();
97
+ rerender(/*#__PURE__*/_jsx(FeedbackCard, {
98
+ ...props,
99
+ hasBadLanguage: false,
100
+ hasSubmission: true,
101
+ discussionData: discussionData,
102
+ existingFeedback: existingFeedback,
103
+ aiReview: {
104
+ 1: {
105
+ ...pending,
106
+ teacherEdited: true,
107
+ teacherApproved: true
108
+ }
109
+ }
110
+ }));
111
+ expect(screen.getByText("approved")).toBeTruthy();
112
+ expect(screen.getByText("edited_by_you")).toBeTruthy();
113
+ expect(screen.getByRole("button", {
114
+ name: "undo"
115
+ })).toBeTruthy();
116
+ });
117
+ it("only drafts for this group's students when classmates share a name", () => {
118
+ const onAiDraftsApplied = vi.fn();
119
+ const userData = [...props.userData, {
120
+ memberId: "99",
121
+ username: "Aaron Cullen"
122
+ } // same name, not in this group
123
+ ];
124
+ const cardProps = {
125
+ ...props,
126
+ userData,
127
+ hasBadLanguage: false,
128
+ hasSubmission: true,
129
+ discussionData,
130
+ existingFeedback: {},
131
+ generateAllUsed: true,
132
+ onAiDraftsApplied
133
+ };
134
+ const {
135
+ rerender
136
+ } = render(/*#__PURE__*/_jsx(FeedbackCard, {
137
+ ...cardProps,
138
+ aiGrade: {}
139
+ }));
140
+ rerender(/*#__PURE__*/_jsx(FeedbackCard, {
141
+ ...cardProps,
142
+ aiGrade: {
143
+ "Aaron Cullen": {
144
+ Feedback: "¡Muy bien!"
145
+ },
146
+ "Connor Cawley": {
147
+ Feedback: "Buen trabajo."
148
+ }
149
+ }
150
+ }));
151
+ expect(onAiDraftsApplied).toHaveBeenCalledTimes(1);
152
+ expect(onAiDraftsApplied).toHaveBeenCalledWith("m1", ["1", "2"]);
153
+ });
154
+ it("submits approved AI drafts with their review flags", async () => {
155
+ const onSubmitFeedback = vi.fn();
156
+ renderCard({
157
+ aiReview: {
158
+ 1: {
159
+ ...pending,
160
+ teacherApproved: true
161
+ }
162
+ },
163
+ onSubmitFeedback
164
+ });
165
+ fireEvent.change(document.getElementById("feedback-m1-2"), {
166
+ target: {
167
+ value: "Edited teacher feedback"
168
+ }
169
+ });
170
+ expect(submitButton().disabled).toBe(false);
171
+ fireEvent.click(submitButton());
172
+ await waitFor(() => expect(onSubmitFeedback).toHaveBeenCalled());
173
+ const {
174
+ feedbackData
175
+ } = onSubmitFeedback.mock.calls[0][0];
176
+ expect(feedbackData[1]).toMatchObject({
177
+ aiGenerated: true,
178
+ teacherEdited: false,
179
+ teacherApproved: true
180
+ });
181
+ expect(feedbackData[2]).toMatchObject({
182
+ aiGenerated: false,
183
+ teacherEdited: false,
184
+ teacherApproved: false
185
+ });
186
+ });
187
+ });
@@ -216,6 +216,16 @@ export default function RecordingCard({
216
216
  },
217
217
  children: /*#__PURE__*/_jsx(HelpOutlinedIcon, {})
218
218
  })
219
+ }), recordingsListData.aiGenerated === true && recordingsListData.teacherEdited !== true && /*#__PURE__*/_jsx(Tooltip, {
220
+ title: t("ai_suggested_tooltip"),
221
+ children: /*#__PURE__*/_jsx(Chip, {
222
+ size: "small",
223
+ label: t("ai_suggested"),
224
+ sx: {
225
+ ml: 0.5,
226
+ fontWeight: 600
227
+ }
228
+ })
219
229
  })]
220
230
  }), /*#__PURE__*/_jsx(Typography, {
221
231
  variant: "body2",
@@ -0,0 +1,77 @@
1
+ import CompletionDialog from "./CompletionDialog";
2
+ import { CompletionExitAction, CompletionMainActions, useAssignmentId } from "./CompletionActions";
3
+ import AnswersSummary from "./Sections/AnswersSummary";
4
+ import AttemptsToReview from "./Sections/AttemptsToReview";
5
+ import { jsx as _jsx } from "react/jsx-runtime";
6
+ function AttemptsCompletionDialog({
7
+ open,
8
+ t = text => text,
9
+ icon,
10
+ celebration = null,
11
+ exerciseLabel,
12
+ contentName,
13
+ answerAttempts = [],
14
+ startTime,
15
+ showAnswerMethod = true,
16
+ rolePlayed = null,
17
+ viewAttempt = null,
18
+ viewReport,
19
+ disableViewReport,
20
+ restartExcercise,
21
+ handleNextGame = null,
22
+ goBack,
23
+ goBackToAssignment,
24
+ inTopic = false,
25
+ "data-cy": dataCy
26
+ }) {
27
+ const assignmentId = useAssignmentId();
28
+ const attempts = answerAttempts.filter(Boolean);
29
+ const hasAttempts = attempts.length > 0;
30
+ const onViewAttempt = assignmentId ? null : viewAttempt;
31
+ return /*#__PURE__*/_jsx(CompletionDialog, {
32
+ open: open,
33
+ maxWidth: hasAttempts ? "lg" : "md",
34
+ icon: icon,
35
+ celebration: celebration,
36
+ title: t("exercise_complete"),
37
+ subtitle: [exerciseLabel, contentName, attempts.length > 0 && t("attempt_count", {
38
+ count: attempts.length
39
+ })].filter(Boolean).join(" · "),
40
+ "data-cy": dataCy,
41
+ sections: [{
42
+ key: "summary",
43
+ content: /*#__PURE__*/_jsx(AnswersSummary, {
44
+ answerAttempts: attempts,
45
+ startTime: startTime,
46
+ showAnswerMethod: showAnswerMethod,
47
+ rolePlayed: rolePlayed,
48
+ onViewAttempt: hasAttempts ? null : onViewAttempt,
49
+ t: t
50
+ })
51
+ }, hasAttempts && {
52
+ key: "review",
53
+ muted: true,
54
+ scroll: "content",
55
+ content: /*#__PURE__*/_jsx(AttemptsToReview, {
56
+ answerAttempts: attempts,
57
+ onViewAll: onViewAttempt,
58
+ showMethod: showAnswerMethod,
59
+ t: t
60
+ })
61
+ }],
62
+ actionsStart: /*#__PURE__*/_jsx(CompletionExitAction, {
63
+ t: t,
64
+ goBack: goBack,
65
+ goBackToAssignment: goBackToAssignment,
66
+ inTopic: inTopic
67
+ }),
68
+ actions: /*#__PURE__*/_jsx(CompletionMainActions, {
69
+ t: t,
70
+ viewReport: viewReport,
71
+ disableViewReport: disableViewReport,
72
+ restartExcercise: restartExcercise,
73
+ handleNextGame: handleNextGame
74
+ })
75
+ });
76
+ }
77
+ export default AttemptsCompletionDialog;
@@ -0,0 +1,37 @@
1
+ import { cleanup, render, screen } from "@testing-library/react";
2
+ import { afterEach, describe, it } from "vitest";
3
+ import { MemoryRouter } from "react-router";
4
+ import AttemptsCompletionDialog from "./AttemptsCompletionDialog";
5
+ import { jsx as _jsx } from "react/jsx-runtime";
6
+ afterEach(cleanup);
7
+ const t = (key, values) => key === "attempt_count" ? `${values.count} attempts` : key;
8
+ const renderDialog = props => render(/*#__PURE__*/_jsx(MemoryRouter, {
9
+ children: /*#__PURE__*/_jsx(AttemptsCompletionDialog, {
10
+ open: true,
11
+ t: t,
12
+ ...props
13
+ })
14
+ }));
15
+ describe("AttemptsCompletionDialog header", () => {
16
+ it("titles every exercise the same and names it in the subtitle", () => {
17
+ renderDialog({
18
+ exerciseLabel: "Act it out - Listening and Speaking",
19
+ contentName: "Completion",
20
+ answerAttempts: [{
21
+ result: "correct"
22
+ }, {
23
+ result: "incorrect"
24
+ }]
25
+ });
26
+ screen.getByText("exercise_complete");
27
+ screen.getByText("Act it out - Listening and Speaking · Completion · 2 attempts");
28
+ });
29
+ it("leaves out the attempt count when nothing was answered", () => {
30
+ renderDialog({
31
+ exerciseLabel: "Story",
32
+ contentName: "Completion",
33
+ answerAttempts: []
34
+ });
35
+ screen.getByText("Story · Completion");
36
+ });
37
+ });
@@ -0,0 +1,82 @@
1
+ import { useLocation } from "react-router";
2
+ import { Box, Button, Tooltip } from "@mui/material";
3
+ import ReplayIcon from "@mui/icons-material/Replay";
4
+ import ChevronRightIcon from "@mui/icons-material/ChevronRight";
5
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
6
+ export function useAssignmentId() {
7
+ const location = useLocation();
8
+ return new URLSearchParams(location.search).get("assignmentId");
9
+ }
10
+ export function CompletionExitAction({
11
+ t = text => text,
12
+ goBack,
13
+ goBackToAssignment,
14
+ inTopic = false
15
+ }) {
16
+ const assignmentId = useAssignmentId();
17
+ return assignmentId ? /*#__PURE__*/_jsx(Button, {
18
+ color: "inherit",
19
+ onClick: goBackToAssignment,
20
+ "data-cy": "go_back_to_assignments",
21
+ sx: {
22
+ fontWeight: 700
23
+ },
24
+ children: t("go_back_to_assignments")
25
+ }) : /*#__PURE__*/_jsx(Button, {
26
+ color: "inherit",
27
+ onClick: goBack,
28
+ "data-cy": "go_back_to_topic",
29
+ sx: {
30
+ fontWeight: 700
31
+ },
32
+ children: inTopic ? t("go_back_to_topic") : t("quit")
33
+ });
34
+ }
35
+ export function CompletionMainActions({
36
+ t = text => text,
37
+ viewReport,
38
+ disableViewReport,
39
+ restartExcercise,
40
+ handleNextGame = null
41
+ }) {
42
+ const assignmentId = useAssignmentId();
43
+ const viewReportDisabled = Boolean(disableViewReport || assignmentId);
44
+ return /*#__PURE__*/_jsxs(_Fragment, {
45
+ children: [/*#__PURE__*/_jsx(Tooltip, {
46
+ title: assignmentId ? "Reports are not available during assignments. You can review your attempts in the Progress tab." : "",
47
+ children: /*#__PURE__*/_jsx(Box, {
48
+ component: "span",
49
+ sx: {
50
+ display: {
51
+ xs: "block",
52
+ sm: "inline-block"
53
+ }
54
+ },
55
+ children: /*#__PURE__*/_jsx(Button, {
56
+ variant: "outlined",
57
+ color: "primary",
58
+ onClick: viewReport,
59
+ disabled: viewReportDisabled,
60
+ "data-cy": "view_report",
61
+ children: t("view_report")
62
+ })
63
+ })
64
+ }), /*#__PURE__*/_jsx(Button, {
65
+ variant: handleNextGame ? "outlined" : "contained",
66
+ color: "primary",
67
+ startIcon: /*#__PURE__*/_jsx(ReplayIcon, {}),
68
+ onClick: restartExcercise,
69
+ "data-cy": "play_again",
70
+ autoFocus: !handleNextGame,
71
+ children: t("play_again")
72
+ }), handleNextGame && /*#__PURE__*/_jsx(Button, {
73
+ variant: "contained",
74
+ color: "primary",
75
+ endIcon: /*#__PURE__*/_jsx(ChevronRightIcon, {}),
76
+ onClick: handleNextGame,
77
+ "data-cy": "next_game",
78
+ autoFocus: true,
79
+ children: t("next_game")
80
+ })]
81
+ });
82
+ }
@@ -0,0 +1,30 @@
1
+ import { cleanup, render, screen } from "@testing-library/react";
2
+ import { afterEach, describe, it, expect, vi } from "vitest";
3
+ import { MemoryRouter } from "react-router";
4
+ import { CompletionMainActions } from "./CompletionActions";
5
+ import { jsx as _jsx } from "react/jsx-runtime";
6
+ afterEach(cleanup);
7
+ const renderActions = props => render(/*#__PURE__*/_jsx(MemoryRouter, {
8
+ children: /*#__PURE__*/_jsx(CompletionMainActions, {
9
+ t: key => key,
10
+ restartExcercise: () => {},
11
+ viewReport: () => {},
12
+ ...props
13
+ })
14
+ }));
15
+ describe("CompletionMainActions", () => {
16
+ it("makes play again the main action without a next game", () => {
17
+ renderActions();
18
+ expect(screen.queryByText("next_game")).toBeNull();
19
+ expect(screen.getByText("play_again").closest("button").className).toContain("MuiButton-contained");
20
+ });
21
+ it("adds next game as the main action when there is one", () => {
22
+ const handleNextGame = vi.fn();
23
+ renderActions({
24
+ handleNextGame
25
+ });
26
+ expect(screen.getByText("play_again").closest("button").className).toContain("MuiButton-outlined");
27
+ screen.getByText("next_game").click();
28
+ expect(handleNextGame).toHaveBeenCalled();
29
+ });
30
+ });