@nualang/nualang-ui-components 0.1.1437 → 0.1.1438
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/Cards/FeedbackCard/FeedbackCard.js +114 -8
- package/dist/Cards/FeedbackCard/FeedbackCard.test.js +187 -0
- package/dist/Cards/RecordingCard/RecordingCard.js +10 -0
- package/dist/Dialogs/ExerciseMenu/CompletionDialog/AttemptsCompletionDialog.js +77 -0
- package/dist/Dialogs/ExerciseMenu/CompletionDialog/AttemptsCompletionDialog.test.js +37 -0
- package/dist/Dialogs/ExerciseMenu/CompletionDialog/CompletionActions.js +82 -0
- package/dist/Dialogs/ExerciseMenu/CompletionDialog/CompletionActions.test.js +30 -0
- package/dist/Dialogs/ExerciseMenu/CompletionDialog/CompletionDialog.js +216 -0
- package/dist/Dialogs/ExerciseMenu/CompletionDialog/CompletionDialog.test.js +34 -0
- package/dist/Dialogs/ExerciseMenu/CompletionDialog/Sections/AnswersSummary.js +247 -0
- package/dist/Dialogs/ExerciseMenu/CompletionDialog/Sections/AttemptSections.test.js +235 -0
- package/dist/Dialogs/ExerciseMenu/CompletionDialog/Sections/AttemptsToReview.js +220 -0
- package/dist/Dialogs/ExerciseMenu/CompletionDialog/Sections/LanguageCorrections.js +82 -0
- package/dist/Dialogs/ExerciseMenu/CompletionDialog/Sections/SessionFeedback.js +76 -0
- package/dist/Dialogs/ExerciseMenu/CompletionDialog/Sections/attemptResults.js +37 -0
- package/dist/Dialogs/ExerciseMenu/CompletionDialog/package.json +6 -0
- package/dist/Dialogs/GroupedFeedbackDialog/GroupedFeedbackDialog.js +147 -35
- package/dist/Dialogs/GroupedFeedbackDialog/GroupedFeedbackDialog.test.js +202 -3
- package/dist/Dialogs/GroupedFeedbackDialog/aiReview.js +72 -0
- package/dist/Dialogs/QuickCreateScenarioDialog/QuickCreateScenarioDialog.js +3 -1
- package/dist/Exercises/Bot/Bot.js +57 -20
- package/dist/Exercises/ListenAndSpeak/ListenAndSpeak.js +20 -9
- package/dist/Exercises/Listener/Listener.js +18 -8
- package/dist/Exercises/Meaning/Meaning.js +20 -9
- package/dist/Exercises/MeaningReadWrite/MeaningReadWriteGame.js +21 -9
- package/dist/Exercises/PhraseGroup/Games/ListenAndSpeakGame/ListenAndSpeakGame.js +5 -0
- package/dist/Exercises/PhraseGroup/Games/ListeningGame/ListeningGame.js +4 -0
- package/dist/Exercises/PhraseGroup/Games/MeaningGame/MeaningGame.js +5 -0
- package/dist/Exercises/PhraseGroup/Games/MeaningReadWriteGame/MeaningReadWriteGame.js +5 -0
- package/dist/Exercises/PhraseGroup/Games/PronunciationGame/PronunciationGame.js +4 -0
- package/dist/Exercises/PhraseGroup/Games/TranslationGame/TranslationGame.js +4 -0
- package/dist/Exercises/PhraseGroup/PhraseGroup.js +2 -0
- package/dist/Exercises/Pronouncer/Pronouncer.js +18 -8
- package/dist/Exercises/Roleplay/Games/ActItOutGame/ActItOutGame.js +24 -11
- package/dist/Exercises/Roleplay/Games/ActItOutGameListening/ActItOutGameListening.js +24 -11
- package/dist/Exercises/Roleplay/Games/ActItOutGameListeningAndSpeaking/ActItOutGame.js +24 -11
- package/dist/Exercises/Roleplay/Games/ActItOutGameSpeaking/ActItOutGame.js +24 -11
- package/dist/Exercises/Roleplay/Games/FillInTheBlanksGame/FillInTheBlanksGame.js +20 -11
- package/dist/Exercises/Roleplay/Games/StoryGame/StoryGame.js +20 -11
- package/dist/Exercises/Roleplay/Roleplay.js +9 -2
- package/dist/Exercises/Translator/Translator.js +18 -8
- package/dist/Forms/DiscussImageSelector/ImageSelector.js +1 -0
- package/dist/Forms/ImageSelector/ImageSelector.js +1 -0
- package/dist/Tables/MeetingPrompstList/MeetingPromptsList.js +4 -2
- package/dist/Tables/RecordingListCards/RecordingListCards.js +14 -11
- package/dist/Tables/RecordingListCards/RecordingListCards.test.js +43 -0
- package/dist/hooks/useExerciseState.js +41 -3
- package/dist/hooks/useRecognition.js +19 -1
- package/dist/theme/getComponents.js +14 -0
- package/dist/utils/voiceImages.js +6 -0
- package/package.json +1 -1
- package/dist/Dialogs/ExerciseMenu/Menus/ChatbotCompleted/ChatbotCompleted.js +0 -395
- 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
|
-
|
|
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__*/
|
|
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
|
+
});
|