@nualang/nualang-ui-components 0.1.1436 → 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/Assignments/CreateAssignmentDialog/CreateAssignmentDialog.js +3 -1
- package/dist/Cards/Course/Course.js +2 -2
- 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/Chat/Messages/Messages.js +1 -1
- package/dist/Chat/Messages/Question/Question.js +2 -2
- 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/CreateClassroom/Steps/ClassroomSettings/ClassroomSettings.js +20 -15
- package/dist/Forms/DiscussImageSelector/ImageSelector.js +1 -0
- package/dist/Forms/ImageSelector/ImageSelector.js +1 -0
- package/dist/Lists/CourseOutline/CourseOutline.js +363 -69
- package/dist/Lists/CourseOutline/CourseOutline.test.js +209 -2
- package/dist/Lists/Timeline/EventItem.js +15 -0
- package/dist/Tables/Attempt/Attempt.js +2 -2
- 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 +49 -9
- package/dist/hooks/useRecognition.js +90 -23
- 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
|
@@ -83,7 +83,9 @@ export default function CreateAssignmentDialog({
|
|
|
83
83
|
return response;
|
|
84
84
|
}, {
|
|
85
85
|
filters: {
|
|
86
|
-
courseIds: selectedClassroomCourses?.toString()
|
|
86
|
+
courseIds: selectedClassroomCourses?.toString(),
|
|
87
|
+
// Sections/topics are only returned when verbose; needed for the title autofill
|
|
88
|
+
verbose: true
|
|
87
89
|
}
|
|
88
90
|
}, {
|
|
89
91
|
enabled: !!assignment.classroomId
|
|
@@ -862,7 +862,7 @@ function CourseCard({
|
|
|
862
862
|
handleViewCourse(parentId);
|
|
863
863
|
}
|
|
864
864
|
};
|
|
865
|
-
const courseSections = sectionsQuery.isSuccess
|
|
865
|
+
const courseSections = sectionsQuery.isSuccess && sectionsQuery.data?.Items || [];
|
|
866
866
|
const topicDifficulties = sectionsQuery.isSuccess ? [...new Set(courseSections.flatMap(section => section.topics || []).map(topic => topic.difficulty).filter(Boolean))] : [];
|
|
867
867
|
const deleteCourse = async () => {
|
|
868
868
|
const confirmed = await confirm(t("delete_course"), t("delete_course_confirmation"), "", true, t("course_deletion_note", {
|
|
@@ -1263,7 +1263,7 @@ function CourseCard({
|
|
|
1263
1263
|
dialogTitle: courseName,
|
|
1264
1264
|
open: isOutlineOpen,
|
|
1265
1265
|
handleClose: handleCloseOutline,
|
|
1266
|
-
sections:
|
|
1266
|
+
sections: courseSections,
|
|
1267
1267
|
isLoading: sectionsQuery.isLoading,
|
|
1268
1268
|
isMember: isMember,
|
|
1269
1269
|
completions: completions,
|
|
@@ -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",
|
|
@@ -100,11 +100,11 @@ function Answers({
|
|
|
100
100
|
if (!isLiveGame) {
|
|
101
101
|
if (value.correct) {
|
|
102
102
|
setAnsweredCorrectly(true);
|
|
103
|
-
handleCorrectQuestionAnswer(question, value);
|
|
103
|
+
handleCorrectQuestionAnswer(question, value, answers);
|
|
104
104
|
}
|
|
105
105
|
if (!value.correct) {
|
|
106
106
|
setAnsweredCorrectly(false);
|
|
107
|
-
handleIncorrectQuestionAnswer(question, value);
|
|
107
|
+
handleIncorrectQuestionAnswer(question, value, answers);
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
};
|
|
@@ -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
|
+
}
|