@nualang/nualang-ui-components 0.1.1435 → 0.1.1437

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 (25) hide show
  1. package/dist/Assignments/CreateAssignmentDialog/CreateAssignmentDialog.js +3 -1
  2. package/dist/Cards/Course/Course.js +2 -2
  3. package/dist/Cards/FeedbackCard/FeedbackCard.js +6 -1
  4. package/dist/Chat/Messages/Messages.js +1 -1
  5. package/dist/Chat/Messages/Question/Question.js +2 -2
  6. package/dist/Dialogs/DynamicAIWarning/DynamicAIWarning.js +2 -2
  7. package/dist/Dialogs/GroupedFeedbackDialog/GroupedFeedbackDialog.js +7 -3
  8. package/dist/Dialogs/GroupedFeedbackDialog/GroupedFeedbackDialog.test.js +39 -0
  9. package/dist/Dialogs/QuickCreateClassroomDialog/QuickCreateClassroomDialog.js +0 -1
  10. package/dist/Exercises/Bot/Bot.js +12 -6
  11. package/dist/Forms/CreateClassroom/Steps/AISettings/AISettings.js +235 -0
  12. package/dist/Forms/CreateClassroom/Steps/AISettings/AISettings.test.js +237 -0
  13. package/dist/Forms/CreateClassroom/Steps/ClassroomSettings/ClassroomSettings.js +22 -79
  14. package/dist/Forms/CreateClassroom/Steps/CourseSettings/CourseSettings.js +16 -13
  15. package/dist/Forms/CreateClassroom/Steps/index.js +2 -1
  16. package/dist/Forms/UpdateClassroom/UpdateClassroom.js +75 -10
  17. package/dist/Lists/CourseOutline/CourseOutline.js +363 -69
  18. package/dist/Lists/CourseOutline/CourseOutline.test.js +209 -2
  19. package/dist/Lists/Timeline/EventItem.js +15 -0
  20. package/dist/Tables/Attempt/Attempt.js +2 -2
  21. package/dist/Tables/MeetingPrompstList/MeetingPromptsList.js +4 -0
  22. package/dist/hooks/useExerciseState.js +10 -8
  23. package/dist/hooks/useRecognition.js +71 -22
  24. package/dist/utils/aiControls.js +63 -0
  25. package/package.json +1 -1
@@ -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 ? sectionsQuery.data.Items : [];
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: sectionsQuery.isSuccess ? sectionsQuery.data.Items : [],
1266
+ sections: courseSections,
1267
1267
  isLoading: sectionsQuery.isLoading,
1268
1268
  isMember: isMember,
1269
1269
  completions: completions,
@@ -55,6 +55,10 @@ export default function FeedbackCard({
55
55
  userData = [],
56
56
  hasSubmission = false,
57
57
  aiGrade = {},
58
+ // Discuss AI feedback, already combined with the all-AI switch. It stops new
59
+ // AI feedback being generated; assessments already saved stay visible, since
60
+ // a teacher may have set them.
61
+ isDiscussAIFeedbackEnabled = true,
58
62
  onGenerateFeedback = () => {},
59
63
  onSubmitFeedback = () => {},
60
64
  setSelectedRecording = () => {},
@@ -201,6 +205,7 @@ export default function FeedbackCard({
201
205
  }
202
206
  }, [submitTrigger]);
203
207
  const handleGenerateAI = async () => {
208
+ if (!isDiscussAIFeedbackEnabled) return;
204
209
  setAiJustGenerated(true);
205
210
  setIsGeneratingFeedback(true);
206
211
  setIsGeneratingIndividual(true);
@@ -566,7 +571,7 @@ export default function FeedbackCard({
566
571
  disabled: !hasSubmission || isGeneratingFeedback,
567
572
  onClick: handleOpenRecordingDialog,
568
573
  children: t("view_recording")
569
- }), /*#__PURE__*/_jsx(Button, {
574
+ }), isDiscussAIFeedbackEnabled && /*#__PURE__*/_jsx(Button, {
570
575
  variant: "contained",
571
576
  color: "secondary",
572
577
  disabled: !hasSubmission || isGeneratingFeedback,
@@ -14,7 +14,7 @@ const useStyles = makeStyles()(theme => ({
14
14
  overflow: "auto",
15
15
  padding: theme.spacing(2),
16
16
  "@media print": {
17
- position: "right"
17
+ overflow: "visible"
18
18
  }
19
19
  }
20
20
  }));
@@ -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
  };
@@ -23,7 +23,7 @@ function DynamicAIWarning({
23
23
  return /*#__PURE__*/_jsxs(ResponsiveDialog, {
24
24
  open: open,
25
25
  handleClose: handleClose,
26
- submitText: t("classroom_settings"),
26
+ submitText: t("ai_settings"),
27
27
  dialogTitle: dialogTitle,
28
28
  children: [/*#__PURE__*/_jsxs(Box, {
29
29
  sx: {
@@ -168,7 +168,7 @@ function DynamicAIWarning({
168
168
  fontWeight: "bold"
169
169
  },
170
170
  onClick: submit,
171
- children: t("classroom_settings")
171
+ children: t("ai_settings")
172
172
  }), " ", "for this class."]
173
173
  })]
174
174
  }), /*#__PURE__*/_jsx(Divider, {
@@ -28,6 +28,9 @@ export default function GroupFeedbackDialog({
28
28
  t = x => x,
29
29
  handleBatchGradeConversations = () => {},
30
30
  aiGrade = {},
31
+ // Discuss AI feedback, already combined with the all-AI switch, so the
32
+ // generate controls disappear when either is off.
33
+ isDiscussAIFeedbackEnabled = true,
31
34
  selectedGroup = {},
32
35
  setSelectedRecording = () => {},
33
36
  selectedRecording = {},
@@ -200,7 +203,7 @@ export default function GroupFeedbackDialog({
200
203
  }).filter(member => member.name !== "Unknown");
201
204
  };
202
205
  const handleGenerateFeedback = async discussionData => {
203
- if (!handleBatchGradeConversations) return;
206
+ if (!isDiscussAIFeedbackEnabled || !handleBatchGradeConversations) return;
204
207
  setIsGeneratingIndividual(true);
205
208
  try {
206
209
  const joinedMembersWithNames = getJoinedMembersNames(discussionData.joinedMembers || [], userData);
@@ -243,7 +246,7 @@ export default function GroupFeedbackDialog({
243
246
  customFeedback: customFeedbackText
244
247
  };
245
248
  });
246
- if (conversationsToGrade.length > 0 && handleBatchGradeConversations) {
249
+ if (isDiscussAIFeedbackEnabled && conversationsToGrade.length > 0 && handleBatchGradeConversations) {
247
250
  setIsGeneratingAll(true);
248
251
  setGenerateAllUsed(true);
249
252
  try {
@@ -546,7 +549,7 @@ export default function GroupFeedbackDialog({
546
549
  sx: {
547
550
  flex: 1
548
551
  }
549
- }), /*#__PURE__*/_jsx(Button, {
552
+ }), isDiscussAIFeedbackEnabled && /*#__PURE__*/_jsx(Button, {
550
553
  variant: "contained",
551
554
  color: "secondary",
552
555
  endIcon: /*#__PURE__*/_jsx(AutoFixHighIcon, {}),
@@ -603,6 +606,7 @@ export default function GroupFeedbackDialog({
603
606
  existingFeedback: feedbackByMeetingId[feedbackMeetingId] || {},
604
607
  onGenerateFeedback: () => openCustomPromptForSingle(discussionData),
605
608
  onSubmitFeedback: handleSubmitFeedback,
609
+ isDiscussAIFeedbackEnabled: isDiscussAIFeedbackEnabled,
606
610
  setSelectedRecording: setSelectedRecording,
607
611
  selectedRecording: selectedRecording,
608
612
  s3Url: s3Url,
@@ -0,0 +1,39 @@
1
+ import { render } from "@testing-library/react";
2
+ import { describe, it, expect } from "vitest";
3
+ import { createTheme, ThemeProvider } from "@mui/material/styles";
4
+ import GroupedFeedbackDialog from "./GroupedFeedbackDialog";
5
+ import { WithData } from "./GroupedFeedbackDialog.stories";
6
+
7
+ // This repo has no jest-dom matchers and no auto-cleanup, so assertions are
8
+ // plain and each test unmounts - matching AISettings.test.jsx.
9
+ import { jsx as _jsx } from "react/jsx-runtime";
10
+ const setup = (props = {}) => render(/*#__PURE__*/_jsx(ThemeProvider, {
11
+ theme: createTheme(),
12
+ children: /*#__PURE__*/_jsx(GroupedFeedbackDialog, {
13
+ ...WithData.args,
14
+ ...props
15
+ })
16
+ }));
17
+ describe("GroupedFeedbackDialog AI controls", () => {
18
+ it("offers feedback generation when the classroom allows AI", () => {
19
+ const {
20
+ queryAllByText,
21
+ unmount
22
+ } = setup();
23
+ expect(queryAllByText("generate_feedback").length).toBeGreaterThan(0);
24
+ unmount();
25
+ });
26
+ it("removes every generate control when the classroom limits AI", () => {
27
+ // Teachers can still write and submit their own feedback - only the AI
28
+ // route away from the page is closed.
29
+ const {
30
+ queryAllByText,
31
+ unmount
32
+ } = setup({
33
+ isDiscussAIFeedbackEnabled: false
34
+ });
35
+ expect(queryAllByText("generate_feedback").length).toBe(0);
36
+ expect(queryAllByText("submit_feedback").length).toBeGreaterThan(0);
37
+ unmount();
38
+ });
39
+ });
@@ -145,7 +145,6 @@ export default function QuickCreateClassroomDialog({
145
145
  visibility: "private",
146
146
  vchatEnabled: true,
147
147
  vchatEnabledInSettings: true,
148
- isDynamicResponsesEnabled: "enabled",
149
148
  enrolmentKey: generateRandomEnrolmentKey(),
150
149
  allowedDomains: [],
151
150
  collaborators: [],
@@ -29,8 +29,8 @@ import ReportProblem from "../../Misc/ReportProblem/ReportProblem";
29
29
  import NualaSignInImage from "../../img/nuala_sign_in.svg";
30
30
  import { startJoyrideWhenReady } from "../../utils/joyride";
31
31
  import { collectCorrections } from "../../utils/correction";
32
- import { getDynamicResponsesSetting } from "../../utils/dynamicResponses";
33
32
  import { getBotContentTemplateType } from "../../utils";
33
+ import { AI_ENABLED, courseAIValue, isCourseAIEnabled } from "../../utils/aiControls";
34
34
  import { useTheme } from "@mui/material/styles";
35
35
  import config from "../../config";
36
36
  import { useNavigate } from "react-router";
@@ -288,11 +288,17 @@ function Bot({
288
288
  } = getRecognitionParams();
289
289
  toggleListen(learnLang, speechContexts, model, undefined, autoStopOptions);
290
290
  };
291
- const isDynamicResponsesClassroomEnabled = getDynamicResponsesSetting({
292
- botSetting: isDynamicResponsesEnabled,
293
- parentClassroom,
294
- courseSettings
295
- });
291
+
292
+ // The classroom's all-AI and chatbot switches, and the course's, can each
293
+ // silence dynamic responses whatever the bot's own setting. Only when they
294
+ // allow AI does the bot's setting decide - except for courses saved as
295
+ // "enabled", which older classroom records can still carry.
296
+ let isDynamicResponsesClassroomEnabled = isDynamicResponsesEnabled;
297
+ if (!isCourseAIEnabled(parentClassroom, courseSettings)) {
298
+ isDynamicResponsesClassroomEnabled = "disable";
299
+ } else if (courseAIValue(courseSettings) === AI_ENABLED) {
300
+ isDynamicResponsesClassroomEnabled = "enable";
301
+ }
296
302
  const exerciseState = useExerciseState({
297
303
  t,
298
304
  open,
@@ -0,0 +1,235 @@
1
+ import { useEffect, useRef } from "react";
2
+ import * as Yup from "yup";
3
+ import { Alert, Box, FormControl, FormControlLabel, FormLabel, Grid, IconButton, Radio, RadioGroup, Tooltip, Typography } from "@mui/material";
4
+ import HelpIcon from "@mui/icons-material/Help";
5
+ import SearchIcon from "@mui/icons-material/Search";
6
+ import EastIcon from "@mui/icons-material/East";
7
+ import { AI_DISABLED, AI_ENABLED, AI_FIELDS, aiLockedBy, classroomAIValue, courseAIValue } from "../../../../utils/aiControls";
8
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
9
+ export const AI_INFO_URL = "https://www.waysidepublishing.com/digital-solutions/ai-safety-and-trust";
10
+
11
+ // One Allowed / Limited choice for one of AI_FIELDS.
12
+ function AISwitch({
13
+ t,
14
+ name,
15
+ label,
16
+ description,
17
+ info,
18
+ value,
19
+ disabled,
20
+ dataCy,
21
+ handleChange,
22
+ handleBlur
23
+ }) {
24
+ return /*#__PURE__*/_jsxs(FormControl, {
25
+ component: "fieldset",
26
+ children: [/*#__PURE__*/_jsxs(FormLabel, {
27
+ component: "legend",
28
+ children: [t(label), info && /*#__PURE__*/_jsx(Tooltip, {
29
+ title: t(info),
30
+ placement: "right",
31
+ children: /*#__PURE__*/_jsx(IconButton, {
32
+ sx: {
33
+ padding: 0,
34
+ marginLeft: 1
35
+ },
36
+ children: /*#__PURE__*/_jsx(HelpIcon, {
37
+ fontSize: "small"
38
+ })
39
+ })
40
+ })]
41
+ }), /*#__PURE__*/_jsx(Typography, {
42
+ color: "textSecondary",
43
+ children: t(description)
44
+ }), /*#__PURE__*/_jsxs(RadioGroup, {
45
+ row: true,
46
+ "aria-label": name,
47
+ name: name,
48
+ value: value,
49
+ onChange: handleChange,
50
+ onBlur: handleBlur,
51
+ children: [/*#__PURE__*/_jsx(FormControlLabel, {
52
+ value: AI_ENABLED,
53
+ control: /*#__PURE__*/_jsx(Radio, {}),
54
+ label: t("ai_allowed"),
55
+ disabled: disabled,
56
+ "data-cy": `allow-${dataCy}-in-settings`
57
+ }), /*#__PURE__*/_jsx(FormControlLabel, {
58
+ value: AI_DISABLED,
59
+ control: /*#__PURE__*/_jsx(Radio, {}),
60
+ label: t("ai_limited"),
61
+ disabled: disabled,
62
+ "data-cy": `limit-${dataCy}-in-settings`
63
+ })]
64
+ })]
65
+ });
66
+ }
67
+
68
+ // The classroom's AI section: a switch for all AI, and one each for chatbot
69
+ // dynamic responses and Discuss AI feedback. Per-course chatbot overrides stay
70
+ // in Course Settings, so this section only reports them.
71
+ function AISettings({
72
+ t = text => text,
73
+ values,
74
+ handleChange,
75
+ handleBlur,
76
+ courses = [],
77
+ courseSettings = {},
78
+ scrollToDynamicResponses = false,
79
+ setScrollToDynamicResponses,
80
+ // The saved classroom record, which the API stamps with `aiLockedBy` when
81
+ // the classroom's customer has limited AI. `values` is only the form state.
82
+ classroom
83
+ }) {
84
+ const aiControlRef = useRef(null);
85
+ useEffect(() => {
86
+ // Guarded: the notice can fire before this section has mounted.
87
+ if (scrollToDynamicResponses && aiControlRef.current) {
88
+ aiControlRef.current.scrollIntoView({
89
+ behavior: "smooth",
90
+ block: "center"
91
+ });
92
+ setScrollToDynamicResponses && setScrollToDynamicResponses(false);
93
+ }
94
+ }, [scrollToDynamicResponses]);
95
+ const isAllLimited = classroomAIValue(values) === AI_DISABLED;
96
+ const isAllLocked = Boolean(aiLockedBy(classroom));
97
+
98
+ // While all AI is off, a feature shows as limited - which is what students
99
+ // get - but keeps its own value for when all AI is turned back on.
100
+ const featureValue = field => isAllLimited ? AI_DISABLED : classroomAIValue(values, field);
101
+ const isChatbotLimited = featureValue(AI_FIELDS.chatbot) === AI_DISABLED;
102
+
103
+ // Courses that have turned the chatbot off for themselves. Read from the
104
+ // saved classroom, so this is what students currently get.
105
+ const overriddenCourses = courses.filter(course => courseAIValue(courseSettings?.[course.courseId]) === AI_DISABLED);
106
+ const switchProps = {
107
+ t,
108
+ handleChange,
109
+ handleBlur
110
+ };
111
+ return /*#__PURE__*/_jsxs(Grid, {
112
+ container: true,
113
+ spacing: 2,
114
+ ref: aiControlRef,
115
+ children: [/*#__PURE__*/_jsxs(Grid, {
116
+ size: 12,
117
+ children: [/*#__PURE__*/_jsx(Typography, {
118
+ color: "textSecondary",
119
+ gutterBottom: true,
120
+ children: t("ai_settings_desc")
121
+ }), /*#__PURE__*/_jsxs(Box, {
122
+ component: "a",
123
+ href: AI_INFO_URL,
124
+ target: "_blank",
125
+ rel: "noopener noreferrer",
126
+ sx: {
127
+ display: "inline-flex",
128
+ alignItems: "center",
129
+ textDecoration: "none",
130
+ color: "primary.main"
131
+ },
132
+ children: [/*#__PURE__*/_jsx(SearchIcon, {
133
+ sx: {
134
+ mr: 0.5
135
+ },
136
+ fontSize: "small"
137
+ }), /*#__PURE__*/_jsx(Typography, {
138
+ variant: "body2",
139
+ children: t("ai_learn_more")
140
+ }), /*#__PURE__*/_jsx(EastIcon, {
141
+ sx: {
142
+ ml: 0.5
143
+ },
144
+ fontSize: "small"
145
+ })]
146
+ })]
147
+ }), /*#__PURE__*/_jsxs(Grid, {
148
+ size: 12,
149
+ children: [/*#__PURE__*/_jsx(AISwitch, {
150
+ ...switchProps,
151
+ name: AI_FIELDS.all,
152
+ label: "limit_ai",
153
+ description: "limit_ai_desc",
154
+ info: "limit_ai_info",
155
+ value: classroomAIValue(values),
156
+ disabled: isAllLocked,
157
+ dataCy: "ai"
158
+ }), isAllLocked && /*#__PURE__*/_jsx(Alert, {
159
+ severity: "info",
160
+ "data-cy": "ai-locked-notice",
161
+ sx: {
162
+ mt: 1
163
+ },
164
+ children: t("ai_locked_notice")
165
+ }), !isAllLocked && isAllLimited && /*#__PURE__*/_jsx(Alert, {
166
+ severity: "info",
167
+ "data-cy": "ai-limited-notice",
168
+ sx: {
169
+ mt: 1
170
+ },
171
+ children: t("ai_limited_classroom_notice")
172
+ })]
173
+ }), /*#__PURE__*/_jsx(Grid, {
174
+ size: 12,
175
+ children: /*#__PURE__*/_jsxs(Box, {
176
+ sx: {
177
+ pl: 4,
178
+ display: "grid",
179
+ gap: 2
180
+ },
181
+ children: [/*#__PURE__*/_jsxs(Box, {
182
+ children: [/*#__PURE__*/_jsx(AISwitch, {
183
+ ...switchProps,
184
+ name: AI_FIELDS.chatbot,
185
+ label: "chatbot_ai",
186
+ description: "chatbot_ai_desc",
187
+ info: "dynamic_responses_info",
188
+ value: featureValue(AI_FIELDS.chatbot),
189
+ disabled: isAllLimited || Boolean(aiLockedBy(classroom, AI_FIELDS.chatbot)),
190
+ dataCy: "chatbot-ai"
191
+ }), !isAllLocked && Boolean(aiLockedBy(classroom, AI_FIELDS.chatbot)) && /*#__PURE__*/_jsx(Alert, {
192
+ severity: "info",
193
+ "data-cy": "ai-partly-locked-notice",
194
+ sx: {
195
+ mt: 1
196
+ },
197
+ children: t("ai_partly_locked_notice")
198
+ }), !isChatbotLimited && overriddenCourses.length > 0 && /*#__PURE__*/_jsxs(Alert, {
199
+ severity: "info",
200
+ "data-cy": "ai-course-overrides-notice",
201
+ sx: {
202
+ mt: 1
203
+ },
204
+ children: [t("ai_course_overrides_notice"), ": ", overriddenCourses.map(course => course.courseName).join(", ")]
205
+ })]
206
+ }), /*#__PURE__*/_jsxs(Box, {
207
+ children: [/*#__PURE__*/_jsx(AISwitch, {
208
+ ...switchProps,
209
+ name: AI_FIELDS.discuss,
210
+ label: "discuss_ai",
211
+ description: "discuss_ai_desc",
212
+ value: featureValue(AI_FIELDS.discuss),
213
+ disabled: isAllLimited || Boolean(aiLockedBy(classroom, AI_FIELDS.discuss)),
214
+ dataCy: "discuss-ai"
215
+ }), !isAllLocked && Boolean(aiLockedBy(classroom, AI_FIELDS.discuss)) && /*#__PURE__*/_jsx(Alert, {
216
+ severity: "info",
217
+ "data-cy": "ai-partly-locked-notice",
218
+ sx: {
219
+ mt: 1
220
+ },
221
+ children: t("ai_partly_locked_notice")
222
+ })]
223
+ })]
224
+ })
225
+ })]
226
+ });
227
+ }
228
+ AISettings.propTypes = {};
229
+ AISettings.label = "ai_settings";
230
+ AISettings.validationSchema = Yup.object().shape({
231
+ isAIEnabled: Yup.string(),
232
+ isDynamicResponsesEnabled: Yup.string(),
233
+ isDiscussAIFeedbackEnabled: Yup.string()
234
+ });
235
+ export default AISettings;