@nualang/nualang-ui-components 0.1.1435 → 0.1.1436
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 +6 -1
- package/dist/Dialogs/DynamicAIWarning/DynamicAIWarning.js +2 -2
- package/dist/Dialogs/GroupedFeedbackDialog/GroupedFeedbackDialog.js +7 -3
- package/dist/Dialogs/GroupedFeedbackDialog/GroupedFeedbackDialog.test.js +39 -0
- package/dist/Dialogs/QuickCreateClassroomDialog/QuickCreateClassroomDialog.js +0 -1
- package/dist/Exercises/Bot/Bot.js +12 -6
- package/dist/Forms/CreateClassroom/Steps/AISettings/AISettings.js +235 -0
- package/dist/Forms/CreateClassroom/Steps/AISettings/AISettings.test.js +237 -0
- package/dist/Forms/CreateClassroom/Steps/ClassroomSettings/ClassroomSettings.js +2 -64
- package/dist/Forms/CreateClassroom/Steps/CourseSettings/CourseSettings.js +16 -13
- package/dist/Forms/CreateClassroom/Steps/index.js +2 -1
- package/dist/Forms/UpdateClassroom/UpdateClassroom.js +75 -10
- package/dist/Tables/MeetingPrompstList/MeetingPromptsList.js +4 -0
- package/dist/hooks/useExerciseState.js +2 -2
- package/dist/utils/aiControls.js +63 -0
- package/package.json +1 -1
|
@@ -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,
|
|
@@ -23,7 +23,7 @@ function DynamicAIWarning({
|
|
|
23
23
|
return /*#__PURE__*/_jsxs(ResponsiveDialog, {
|
|
24
24
|
open: open,
|
|
25
25
|
handleClose: handleClose,
|
|
26
|
-
submitText: t("
|
|
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("
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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;
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { render, fireEvent, within } from "@testing-library/react";
|
|
2
|
+
import { describe, it, expect, vi } from "vitest";
|
|
3
|
+
import { createTheme, ThemeProvider } from "@mui/material/styles";
|
|
4
|
+
import AISettings, { AI_INFO_URL } from "./AISettings";
|
|
5
|
+
|
|
6
|
+
// This repo has no jest-dom matchers and no auto-cleanup, so assertions are
|
|
7
|
+
// plain and each test unmounts - matching UpdateClassroom.test.jsx.
|
|
8
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
9
|
+
const courses = [{
|
|
10
|
+
courseId: "c1",
|
|
11
|
+
courseName: "Spanish 1"
|
|
12
|
+
}, {
|
|
13
|
+
courseId: "c2",
|
|
14
|
+
courseName: "Spanish 2"
|
|
15
|
+
}];
|
|
16
|
+
const setup = (props = {}) => {
|
|
17
|
+
const utils = render(/*#__PURE__*/_jsx(ThemeProvider, {
|
|
18
|
+
theme: createTheme(),
|
|
19
|
+
children: /*#__PURE__*/_jsx(AISettings, {
|
|
20
|
+
values: {},
|
|
21
|
+
courses: courses,
|
|
22
|
+
courseSettings: {},
|
|
23
|
+
handleChange: () => {},
|
|
24
|
+
...props
|
|
25
|
+
})
|
|
26
|
+
}));
|
|
27
|
+
// One radio group per switch, named after its field.
|
|
28
|
+
const group = name => within(utils.getByRole("radiogroup", {
|
|
29
|
+
name
|
|
30
|
+
}));
|
|
31
|
+
const radio = (name, label) => group(name).getByLabelText(label);
|
|
32
|
+
return {
|
|
33
|
+
...utils,
|
|
34
|
+
radio
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
const SWITCHES = ["isAIEnabled", "isDynamicResponsesEnabled", "isDiscussAIFeedbackEnabled"];
|
|
38
|
+
describe("AISettings", () => {
|
|
39
|
+
it("renders all three switches, allowed by default", () => {
|
|
40
|
+
const {
|
|
41
|
+
radio,
|
|
42
|
+
unmount
|
|
43
|
+
} = setup();
|
|
44
|
+
SWITCHES.forEach(name => {
|
|
45
|
+
expect(radio(name, "ai_allowed").checked).toBe(true);
|
|
46
|
+
expect(radio(name, "ai_allowed").disabled).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
unmount();
|
|
49
|
+
});
|
|
50
|
+
it("shows both features as limited and fixed while all AI is limited", () => {
|
|
51
|
+
const {
|
|
52
|
+
radio,
|
|
53
|
+
queryByText,
|
|
54
|
+
unmount
|
|
55
|
+
} = setup({
|
|
56
|
+
values: {
|
|
57
|
+
isAIEnabled: "disabled",
|
|
58
|
+
isDynamicResponsesEnabled: "enabled",
|
|
59
|
+
isDiscussAIFeedbackEnabled: "enabled"
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
expect(radio("isAIEnabled", "ai_limited").checked).toBe(true);
|
|
63
|
+
["isDynamicResponsesEnabled", "isDiscussAIFeedbackEnabled"].forEach(name => {
|
|
64
|
+
expect(radio(name, "ai_limited").checked).toBe(true);
|
|
65
|
+
expect(radio(name, "ai_allowed").disabled).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
expect(queryByText("ai_limited_classroom_notice")).not.toBe(null);
|
|
68
|
+
unmount();
|
|
69
|
+
});
|
|
70
|
+
it("limits one feature on its own", () => {
|
|
71
|
+
const {
|
|
72
|
+
radio,
|
|
73
|
+
queryByText,
|
|
74
|
+
unmount
|
|
75
|
+
} = setup({
|
|
76
|
+
values: {
|
|
77
|
+
isDynamicResponsesEnabled: "disabled"
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
expect(radio("isAIEnabled", "ai_allowed").checked).toBe(true);
|
|
81
|
+
expect(radio("isDynamicResponsesEnabled", "ai_limited").checked).toBe(true);
|
|
82
|
+
expect(radio("isDiscussAIFeedbackEnabled", "ai_allowed").checked).toBe(true);
|
|
83
|
+
expect(queryByText("ai_limited_classroom_notice")).toBe(null);
|
|
84
|
+
unmount();
|
|
85
|
+
});
|
|
86
|
+
it("reports changes through handleChange so Formik owns the value", () => {
|
|
87
|
+
const handleChange = vi.fn();
|
|
88
|
+
const {
|
|
89
|
+
radio,
|
|
90
|
+
unmount
|
|
91
|
+
} = setup({
|
|
92
|
+
handleChange
|
|
93
|
+
});
|
|
94
|
+
fireEvent.click(radio("isDiscussAIFeedbackEnabled", "ai_limited"));
|
|
95
|
+
expect(handleChange).toHaveBeenCalled();
|
|
96
|
+
unmount();
|
|
97
|
+
});
|
|
98
|
+
it("names the courses that have limited their chatbot", () => {
|
|
99
|
+
const {
|
|
100
|
+
getByText,
|
|
101
|
+
unmount
|
|
102
|
+
} = setup({
|
|
103
|
+
courseSettings: {
|
|
104
|
+
c1: {
|
|
105
|
+
isDynamicResponsesEnabled: "disabled"
|
|
106
|
+
},
|
|
107
|
+
c2: {
|
|
108
|
+
isDynamicResponsesEnabled: "default"
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
const notice = getByText(/ai_course_overrides_notice/);
|
|
113
|
+
expect(notice.textContent).toContain("Spanish 1");
|
|
114
|
+
expect(notice.textContent).not.toContain("Spanish 2");
|
|
115
|
+
unmount();
|
|
116
|
+
});
|
|
117
|
+
it("hides the course overrides once the classroom's chatbot is limited", () => {
|
|
118
|
+
// The classroom already covers every course, so listing overrides would
|
|
119
|
+
// imply they still make a difference.
|
|
120
|
+
[{
|
|
121
|
+
isAIEnabled: "disabled"
|
|
122
|
+
}, {
|
|
123
|
+
isDynamicResponsesEnabled: "disabled"
|
|
124
|
+
}].forEach(values => {
|
|
125
|
+
const {
|
|
126
|
+
queryByText,
|
|
127
|
+
unmount
|
|
128
|
+
} = setup({
|
|
129
|
+
values,
|
|
130
|
+
courseSettings: {
|
|
131
|
+
c1: {
|
|
132
|
+
isDynamicResponsesEnabled: "disabled"
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
expect(queryByText(/ai_course_overrides_notice/)).toBe(null);
|
|
137
|
+
unmount();
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
it("links out to the same AI explainer the notice points at", () => {
|
|
141
|
+
const {
|
|
142
|
+
getByText,
|
|
143
|
+
unmount
|
|
144
|
+
} = setup();
|
|
145
|
+
expect(getByText("ai_learn_more").closest("a").getAttribute("href")).toBe(AI_INFO_URL);
|
|
146
|
+
unmount();
|
|
147
|
+
});
|
|
148
|
+
it("locks every switch when the customer has switched all AI off", () => {
|
|
149
|
+
const {
|
|
150
|
+
radio,
|
|
151
|
+
queryByText,
|
|
152
|
+
unmount
|
|
153
|
+
} = setup({
|
|
154
|
+
values: {
|
|
155
|
+
isAIEnabled: "disabled"
|
|
156
|
+
},
|
|
157
|
+
classroom: {
|
|
158
|
+
aiLockedBy: {
|
|
159
|
+
isAIEnabled: "customer"
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
SWITCHES.forEach(name => {
|
|
164
|
+
expect(radio(name, "ai_allowed").disabled).toBe(true);
|
|
165
|
+
expect(radio(name, "ai_limited").disabled).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
expect(queryByText("ai_locked_notice")).not.toBe(null);
|
|
168
|
+
// One notice that says who did it, not a second implying the teacher did.
|
|
169
|
+
expect(queryByText("ai_limited_classroom_notice")).toBe(null);
|
|
170
|
+
unmount();
|
|
171
|
+
});
|
|
172
|
+
it("locks only the feature the customer has limited", () => {
|
|
173
|
+
const {
|
|
174
|
+
radio,
|
|
175
|
+
queryByText,
|
|
176
|
+
unmount
|
|
177
|
+
} = setup({
|
|
178
|
+
values: {
|
|
179
|
+
isDiscussAIFeedbackEnabled: "disabled"
|
|
180
|
+
},
|
|
181
|
+
classroom: {
|
|
182
|
+
aiLockedBy: {
|
|
183
|
+
isDiscussAIFeedbackEnabled: "customer"
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
expect(radio("isDiscussAIFeedbackEnabled", "ai_allowed").disabled).toBe(true);
|
|
188
|
+
expect(radio("isDynamicResponsesEnabled", "ai_allowed").disabled).toBe(false);
|
|
189
|
+
expect(radio("isAIEnabled", "ai_allowed").disabled).toBe(false);
|
|
190
|
+
expect(queryByText("ai_partly_locked_notice")).not.toBe(null);
|
|
191
|
+
expect(queryByText("ai_locked_notice")).toBe(null);
|
|
192
|
+
unmount();
|
|
193
|
+
});
|
|
194
|
+
it("reads the older single-level lock as a full lock", () => {
|
|
195
|
+
const {
|
|
196
|
+
radio,
|
|
197
|
+
queryByText,
|
|
198
|
+
unmount
|
|
199
|
+
} = setup({
|
|
200
|
+
classroom: {
|
|
201
|
+
aiLockedBy: "customer"
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
expect(radio("isDynamicResponsesEnabled", "ai_allowed").disabled).toBe(true);
|
|
205
|
+
expect(queryByText("ai_locked_notice")).not.toBe(null);
|
|
206
|
+
unmount();
|
|
207
|
+
});
|
|
208
|
+
it("leaves every switch editable when nothing above the classroom has locked it", () => {
|
|
209
|
+
const {
|
|
210
|
+
radio,
|
|
211
|
+
queryByText,
|
|
212
|
+
unmount
|
|
213
|
+
} = setup({
|
|
214
|
+
classroom: {}
|
|
215
|
+
});
|
|
216
|
+
SWITCHES.forEach(name => {
|
|
217
|
+
expect(radio(name, "ai_allowed").disabled).toBe(false);
|
|
218
|
+
});
|
|
219
|
+
expect(queryByText("ai_locked_notice")).toBe(null);
|
|
220
|
+
expect(queryByText("ai_partly_locked_notice")).toBe(null);
|
|
221
|
+
unmount();
|
|
222
|
+
});
|
|
223
|
+
it("scrolls itself into view when the AI notice sends a teacher here", () => {
|
|
224
|
+
const scrollIntoView = vi.fn();
|
|
225
|
+
window.HTMLElement.prototype.scrollIntoView = scrollIntoView;
|
|
226
|
+
const setScrollToDynamicResponses = vi.fn();
|
|
227
|
+
const {
|
|
228
|
+
unmount
|
|
229
|
+
} = setup({
|
|
230
|
+
scrollToDynamicResponses: true,
|
|
231
|
+
setScrollToDynamicResponses
|
|
232
|
+
});
|
|
233
|
+
expect(scrollIntoView).toHaveBeenCalled();
|
|
234
|
+
expect(setScrollToDynamicResponses).toHaveBeenCalledWith(false);
|
|
235
|
+
unmount();
|
|
236
|
+
});
|
|
237
|
+
});
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { useRef, useEffect } from "react";
|
|
2
|
-
import { Grid, FormControl, FormControlLabel, FormLabel, Radio, RadioGroup, Typography, TextField
|
|
2
|
+
import { Grid, FormControl, FormControlLabel, FormLabel, Radio, RadioGroup, Typography, TextField } from "@mui/material";
|
|
3
3
|
import * as Yup from "yup";
|
|
4
4
|
import Tooltip from "@mui/material/Tooltip";
|
|
5
5
|
import ChipInput from "../../../../Forms/ChipInput/ChipInput";
|
|
6
6
|
import { domainPattern } from "../../../../utils/index";
|
|
7
7
|
import { containsInvalidSymbols, containsInvalidCollaboratorSymbols } from "../../../../utils/index";
|
|
8
|
-
import HelpIcon from "@mui/icons-material/Help";
|
|
9
8
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
10
9
|
function ClassroomSettings({
|
|
11
10
|
t,
|
|
@@ -19,8 +18,6 @@ function ClassroomSettings({
|
|
|
19
18
|
isUpdateClassroom = false,
|
|
20
19
|
scrollToDiscuss = false,
|
|
21
20
|
setScrollToDiscuss,
|
|
22
|
-
scrollToDynamicResponses = false,
|
|
23
|
-
setScrollToDynamicResponses,
|
|
24
21
|
isVideoChatEnabled,
|
|
25
22
|
collaboratorLabels
|
|
26
23
|
}) {
|
|
@@ -29,11 +26,9 @@ function ClassroomSettings({
|
|
|
29
26
|
enrolmentKey,
|
|
30
27
|
allowedDomains,
|
|
31
28
|
collaborators,
|
|
32
|
-
vchatEnabledInSettings
|
|
33
|
-
isDynamicResponsesEnabled
|
|
29
|
+
vchatEnabledInSettings
|
|
34
30
|
} = values;
|
|
35
31
|
const vchatEnabledInSettingsRef = useRef(null);
|
|
36
|
-
const dynamicResponsesRef = useRef(null);
|
|
37
32
|
const generateRandomEnrolmentKey = () => {
|
|
38
33
|
return String(Math.floor(Math.random() * 90000) + 10000);
|
|
39
34
|
};
|
|
@@ -54,15 +49,6 @@ function ClassroomSettings({
|
|
|
54
49
|
setScrollToDiscuss(false);
|
|
55
50
|
}
|
|
56
51
|
}, [scrollToDiscuss]);
|
|
57
|
-
useEffect(() => {
|
|
58
|
-
if (scrollToDynamicResponses) {
|
|
59
|
-
dynamicResponsesRef.current.scrollIntoView({
|
|
60
|
-
behavior: "smooth",
|
|
61
|
-
block: "center"
|
|
62
|
-
});
|
|
63
|
-
setScrollToDynamicResponses(false);
|
|
64
|
-
}
|
|
65
|
-
}, [scrollToDynamicResponses]);
|
|
66
52
|
const isInviteFeatureDisabled = classroomId && classroomId.toLowerCase().includes("wayside");
|
|
67
53
|
return /*#__PURE__*/_jsxs(_Fragment, {
|
|
68
54
|
children: [/*#__PURE__*/_jsx(Typography, {
|
|
@@ -279,54 +265,6 @@ function ClassroomSettings({
|
|
|
279
265
|
})]
|
|
280
266
|
})]
|
|
281
267
|
})
|
|
282
|
-
}), /*#__PURE__*/_jsx(Grid, {
|
|
283
|
-
ref: dynamicResponsesRef,
|
|
284
|
-
size: {
|
|
285
|
-
xs: 12,
|
|
286
|
-
md: 12
|
|
287
|
-
},
|
|
288
|
-
children: /*#__PURE__*/_jsxs(FormControl, {
|
|
289
|
-
component: "fieldset",
|
|
290
|
-
children: [/*#__PURE__*/_jsxs(FormLabel, {
|
|
291
|
-
component: "legend",
|
|
292
|
-
children: [t("dynamic_response"), /*#__PURE__*/_jsx(Tooltip, {
|
|
293
|
-
title: t("dynamic_responses_info"),
|
|
294
|
-
placement: "right",
|
|
295
|
-
children: /*#__PURE__*/_jsx(IconButton, {
|
|
296
|
-
sx: {
|
|
297
|
-
padding: 0,
|
|
298
|
-
marginLeft: 1
|
|
299
|
-
},
|
|
300
|
-
children: /*#__PURE__*/_jsx(HelpIcon, {
|
|
301
|
-
fontSize: "small"
|
|
302
|
-
})
|
|
303
|
-
})
|
|
304
|
-
})]
|
|
305
|
-
}), /*#__PURE__*/_jsx(Typography, {
|
|
306
|
-
color: "textSecondary",
|
|
307
|
-
children: t("dynamic_response_desc")
|
|
308
|
-
}), /*#__PURE__*/_jsxs(RadioGroup, {
|
|
309
|
-
row: true,
|
|
310
|
-
"aria-label": "isDynamicResponsesEnabled",
|
|
311
|
-
name: "isDynamicResponsesEnabled",
|
|
312
|
-
value: isDynamicResponsesEnabled || "enabled",
|
|
313
|
-
onChange: handleChange,
|
|
314
|
-
onBlur: handleBlur,
|
|
315
|
-
helperText: touched.isDynamicResponsesEnabled ? errors.isDynamicResponsesEnabled : "",
|
|
316
|
-
error: touched.isDynamicResponsesEnabled && Boolean(errors.isDynamicResponsesEnabled),
|
|
317
|
-
children: [/*#__PURE__*/_jsx(FormControlLabel, {
|
|
318
|
-
value: "enabled",
|
|
319
|
-
control: /*#__PURE__*/_jsx(Radio, {}),
|
|
320
|
-
label: t("enabled"),
|
|
321
|
-
"data-cy": "enable-dynamic-responses-in-settings"
|
|
322
|
-
}), /*#__PURE__*/_jsx(FormControlLabel, {
|
|
323
|
-
value: "disabled",
|
|
324
|
-
control: /*#__PURE__*/_jsx(Radio, {}),
|
|
325
|
-
label: t("disabled"),
|
|
326
|
-
"data-cy": "disable-dynamic-responses-in-settings"
|
|
327
|
-
})]
|
|
328
|
-
})]
|
|
329
|
-
})
|
|
330
268
|
})]
|
|
331
269
|
}), /*#__PURE__*/_jsx("br", {}), /*#__PURE__*/_jsx(Typography, {
|
|
332
270
|
children: t("add_collaborators")
|
|
@@ -3,6 +3,7 @@ import { Grid, FormControl, List, ListItem, ListItemButton, ListItemText, Box, A
|
|
|
3
3
|
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
|
4
4
|
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
|
|
5
5
|
import HelpIcon from "@mui/icons-material/Help";
|
|
6
|
+
import { AI_DEFAULT, AI_DISABLED, courseAIValue, isClassroomChatbotAIEnabled } from "../../../../utils/aiControls";
|
|
6
7
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
7
8
|
function Settings({
|
|
8
9
|
t,
|
|
@@ -12,11 +13,13 @@ function Settings({
|
|
|
12
13
|
handleBlur,
|
|
13
14
|
courseId,
|
|
14
15
|
setFieldValue,
|
|
15
|
-
|
|
16
|
+
classroomAISettings
|
|
16
17
|
}) {
|
|
17
18
|
const {
|
|
18
19
|
courseSettings
|
|
19
20
|
} = values;
|
|
21
|
+
// Courses only hold chatbots, so this per-course switch is the chatbot one.
|
|
22
|
+
const classroomAIAllowed = isClassroomChatbotAIEnabled(classroomAISettings);
|
|
20
23
|
const thisCoursesSettings = courseSettings && courseId in courseSettings ? courseSettings[courseId] : {};
|
|
21
24
|
return /*#__PURE__*/_jsxs(Grid, {
|
|
22
25
|
container: true,
|
|
@@ -180,7 +183,7 @@ function Settings({
|
|
|
180
183
|
component: "fieldset",
|
|
181
184
|
children: [/*#__PURE__*/_jsxs(FormLabel, {
|
|
182
185
|
component: "legend",
|
|
183
|
-
children: [t("
|
|
186
|
+
children: [t("chatbot_ai"), /*#__PURE__*/_jsx(Tooltip, {
|
|
184
187
|
title: t("dynamic_responses_info"),
|
|
185
188
|
placement: "right",
|
|
186
189
|
children: /*#__PURE__*/_jsx(IconButton, {
|
|
@@ -195,12 +198,12 @@ function Settings({
|
|
|
195
198
|
})]
|
|
196
199
|
}), /*#__PURE__*/_jsx(Typography, {
|
|
197
200
|
color: "textSecondary",
|
|
198
|
-
children: t("
|
|
201
|
+
children: t("limit_ai_course_desc")
|
|
199
202
|
}), /*#__PURE__*/_jsxs(RadioGroup, {
|
|
200
203
|
row: true,
|
|
201
|
-
"aria-label": "
|
|
204
|
+
"aria-label": "Limit AI",
|
|
202
205
|
name: `courseSettings.${courseId}.isDynamicResponsesEnabled`,
|
|
203
|
-
value:
|
|
206
|
+
value: classroomAIAllowed ? courseAIValue(thisCoursesSettings) : AI_DISABLED,
|
|
204
207
|
onChange: event => {
|
|
205
208
|
const newCourseSettings = {
|
|
206
209
|
...courseSettings,
|
|
@@ -215,14 +218,14 @@ function Settings({
|
|
|
215
218
|
helperText: touched.visibility ? errors.visibility : "",
|
|
216
219
|
error: touched.visibility && Boolean(errors.visibility),
|
|
217
220
|
children: [/*#__PURE__*/_jsx(FormControlLabel, {
|
|
218
|
-
value:
|
|
221
|
+
value: AI_DEFAULT,
|
|
219
222
|
control: /*#__PURE__*/_jsx(Radio, {}),
|
|
220
223
|
label: t("course_default"),
|
|
221
|
-
disabled:
|
|
224
|
+
disabled: !classroomAIAllowed
|
|
222
225
|
}), /*#__PURE__*/_jsx(FormControlLabel, {
|
|
223
|
-
value:
|
|
226
|
+
value: AI_DISABLED,
|
|
224
227
|
control: /*#__PURE__*/_jsx(Radio, {}),
|
|
225
|
-
label: t("
|
|
228
|
+
label: t("ai_limited")
|
|
226
229
|
})]
|
|
227
230
|
})]
|
|
228
231
|
})
|
|
@@ -239,7 +242,7 @@ function Course({
|
|
|
239
242
|
handleChange,
|
|
240
243
|
handleBlur,
|
|
241
244
|
setFieldValue,
|
|
242
|
-
|
|
245
|
+
classroomAISettings
|
|
243
246
|
}) {
|
|
244
247
|
const [open, setOpen] = useState(false);
|
|
245
248
|
return /*#__PURE__*/_jsxs(List, {
|
|
@@ -298,7 +301,7 @@ function Course({
|
|
|
298
301
|
handleBlur: handleBlur,
|
|
299
302
|
courseId: course.courseId,
|
|
300
303
|
setFieldValue: setFieldValue,
|
|
301
|
-
|
|
304
|
+
classroomAISettings: classroomAISettings
|
|
302
305
|
})
|
|
303
306
|
})
|
|
304
307
|
})]
|
|
@@ -313,7 +316,7 @@ function CourseSettings({
|
|
|
313
316
|
handleChange,
|
|
314
317
|
handleBlur,
|
|
315
318
|
setFieldValue,
|
|
316
|
-
|
|
319
|
+
classroomAISettings
|
|
317
320
|
}) {
|
|
318
321
|
return /*#__PURE__*/_jsx(_Fragment, {
|
|
319
322
|
children: courses.length > 0 ? /*#__PURE__*/_jsxs(_Fragment, {
|
|
@@ -329,7 +332,7 @@ function CourseSettings({
|
|
|
329
332
|
handleChange: handleChange,
|
|
330
333
|
handleBlur: handleBlur,
|
|
331
334
|
setFieldValue: setFieldValue,
|
|
332
|
-
|
|
335
|
+
classroomAISettings: classroomAISettings
|
|
333
336
|
}))]
|
|
334
337
|
}) : /*#__PURE__*/_jsx(Typography, {
|
|
335
338
|
children: t("classroom_courses_not_found_desc")
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import AISettings from "./AISettings/AISettings";
|
|
1
2
|
import ClassroomInformation from "./ClassroomInformation/ClassroomInformation";
|
|
2
3
|
import ClassroomSettings from "./ClassroomSettings/ClassroomSettings";
|
|
3
4
|
import Customization from "./Customization/Customization";
|
|
4
5
|
import Confirmation from "./Confirmation/Confirmation";
|
|
5
6
|
import CourseSettings from "./CourseSettings/CourseSettings";
|
|
6
|
-
export { ClassroomInformation, ClassroomSettings, Customization, Confirmation, CourseSettings };
|
|
7
|
+
export { AISettings, ClassroomInformation, ClassroomSettings, Customization, Confirmation, CourseSettings };
|
|
@@ -2,7 +2,8 @@ import { useEffect, useRef } from "react";
|
|
|
2
2
|
import { Divider, Button, Grid, Card, CardActions, CardContent, CardHeader, Typography } from "@mui/material";
|
|
3
3
|
import { makeStyles } from "tss-react/mui";
|
|
4
4
|
import { Formik } from "formik";
|
|
5
|
-
import { ClassroomInformation, ClassroomSettings, Customization, CourseSettings } from "../CreateClassroom/Steps";
|
|
5
|
+
import { AISettings, ClassroomInformation, ClassroomSettings, Customization, CourseSettings } from "../CreateClassroom/Steps";
|
|
6
|
+
import { AI_DEFAULT, AI_FIELDS, aiLockedBy, classroomAIFields, classroomAIValue } from "../../utils/aiControls";
|
|
6
7
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
7
8
|
const useStyles = makeStyles()(theme => ({
|
|
8
9
|
padding: {
|
|
@@ -70,9 +71,15 @@ export default function UpdateClassroom({
|
|
|
70
71
|
bannerBuilderColor,
|
|
71
72
|
bannerBuilderIcon,
|
|
72
73
|
tags,
|
|
73
|
-
courseSettings
|
|
74
|
-
isDynamicResponsesEnabled
|
|
74
|
+
courseSettings
|
|
75
75
|
} = initialValues;
|
|
76
|
+
|
|
77
|
+
// The classroom's AI switches as saved: all AI, chatbot and Discuss.
|
|
78
|
+
const aiValues = Object.fromEntries(Object.values(AI_FIELDS).map(field => [field, classroomAIValue(initialValues, field)]));
|
|
79
|
+
|
|
80
|
+
// Set by the API when the classroom's customer has switched all AI off.
|
|
81
|
+
// Nothing saved here could change that, so the section is read-only.
|
|
82
|
+
const isAILocked = Boolean(aiLockedBy(initialValues));
|
|
76
83
|
const courseIds = courses.map(course => course.courseId);
|
|
77
84
|
let validatedCourseSettings;
|
|
78
85
|
if (courseSettings === undefined) {
|
|
@@ -82,7 +89,7 @@ export default function UpdateClassroom({
|
|
|
82
89
|
translationEnabled: "default",
|
|
83
90
|
wordBankEnabled: "default",
|
|
84
91
|
shuffleEnabled: "default",
|
|
85
|
-
isDynamicResponsesEnabled:
|
|
92
|
+
isDynamicResponsesEnabled: AI_DEFAULT
|
|
86
93
|
};
|
|
87
94
|
return obj;
|
|
88
95
|
}, {});
|
|
@@ -97,7 +104,7 @@ export default function UpdateClassroom({
|
|
|
97
104
|
translationEnabled: "default",
|
|
98
105
|
wordBankEnabled: "default",
|
|
99
106
|
shuffleEnabled: "default",
|
|
100
|
-
isDynamicResponsesEnabled:
|
|
107
|
+
isDynamicResponsesEnabled: AI_DEFAULT
|
|
101
108
|
};
|
|
102
109
|
}
|
|
103
110
|
});
|
|
@@ -185,8 +192,7 @@ export default function UpdateClassroom({
|
|
|
185
192
|
allowedDomains,
|
|
186
193
|
collaborators,
|
|
187
194
|
vchatEnabled,
|
|
188
|
-
vchatEnabledInSettings
|
|
189
|
-
isDynamicResponsesEnabled
|
|
195
|
+
vchatEnabledInSettings
|
|
190
196
|
},
|
|
191
197
|
enableReinitialize: enableReinitialize,
|
|
192
198
|
validationSchema: ClassroomSettings.validationSchema,
|
|
@@ -231,8 +237,6 @@ export default function UpdateClassroom({
|
|
|
231
237
|
values: values,
|
|
232
238
|
scrollToDiscuss: scrollToDiscuss,
|
|
233
239
|
setScrollToDiscuss: setScrollToDiscuss,
|
|
234
|
-
scrollToDynamicResponses: scrollToDynamicResponses,
|
|
235
|
-
setScrollToDynamicResponses: setScrollToDynamicResponses,
|
|
236
240
|
isVideoChatEnabled: isVideoChatEnabled,
|
|
237
241
|
collaboratorLabels: collaboratorLabels,
|
|
238
242
|
...otherProps
|
|
@@ -252,6 +256,67 @@ export default function UpdateClassroom({
|
|
|
252
256
|
})
|
|
253
257
|
})
|
|
254
258
|
})
|
|
259
|
+
}), /*#__PURE__*/_jsx("br", {}), /*#__PURE__*/_jsx(Formik, {
|
|
260
|
+
initialValues: aiValues,
|
|
261
|
+
enableReinitialize: enableReinitialize,
|
|
262
|
+
validationSchema: AISettings.validationSchema
|
|
263
|
+
// Switches the customer has locked are left out, so the API's
|
|
264
|
+
// override is never saved over the teacher's own choice.
|
|
265
|
+
,
|
|
266
|
+
onSubmit: (values, actions) => onSubmit(classroomAIFields(values, initialValues), actions),
|
|
267
|
+
render: ({
|
|
268
|
+
handleChange,
|
|
269
|
+
handleSubmit,
|
|
270
|
+
isSubmitting,
|
|
271
|
+
dirty,
|
|
272
|
+
values,
|
|
273
|
+
...otherProps
|
|
274
|
+
}) => /*#__PURE__*/_jsx("form", {
|
|
275
|
+
onSubmit: handleSubmit,
|
|
276
|
+
children: /*#__PURE__*/_jsx(Grid, {
|
|
277
|
+
container: true,
|
|
278
|
+
sx: {
|
|
279
|
+
width: "100%"
|
|
280
|
+
},
|
|
281
|
+
spacing: 1,
|
|
282
|
+
children: /*#__PURE__*/_jsx(Grid, {
|
|
283
|
+
size: 12,
|
|
284
|
+
children: /*#__PURE__*/_jsxs(Card, {
|
|
285
|
+
children: [/*#__PURE__*/_jsx(CardHeader, {
|
|
286
|
+
title: /*#__PURE__*/_jsx(Typography, {
|
|
287
|
+
variant: "h5",
|
|
288
|
+
component: "h2",
|
|
289
|
+
children: t(AISettings.label)
|
|
290
|
+
}),
|
|
291
|
+
className: classes.padding
|
|
292
|
+
}), /*#__PURE__*/_jsx(Divider, {}), /*#__PURE__*/_jsx(CardContent, {
|
|
293
|
+
className: classes.formContent,
|
|
294
|
+
children: /*#__PURE__*/_jsx(AISettings, {
|
|
295
|
+
t: t,
|
|
296
|
+
values: values,
|
|
297
|
+
handleChange: handleChange,
|
|
298
|
+
courses: courses,
|
|
299
|
+
courseSettings: validatedCourseSettings,
|
|
300
|
+
classroom: initialValues,
|
|
301
|
+
scrollToDynamicResponses: scrollToDynamicResponses,
|
|
302
|
+
setScrollToDynamicResponses: setScrollToDynamicResponses,
|
|
303
|
+
...otherProps
|
|
304
|
+
})
|
|
305
|
+
}), /*#__PURE__*/_jsx(Divider, {}), /*#__PURE__*/_jsx(CardActions, {
|
|
306
|
+
className: classes.padding,
|
|
307
|
+
children: /*#__PURE__*/_jsx(Button, {
|
|
308
|
+
type: "submit",
|
|
309
|
+
color: "primary",
|
|
310
|
+
variant: "contained",
|
|
311
|
+
disabled: isSubmitting || !dirty || isClassroomArchived || isAILocked,
|
|
312
|
+
"data-cy": "save-ai-settings",
|
|
313
|
+
children: t("save_changes")
|
|
314
|
+
})
|
|
315
|
+
})]
|
|
316
|
+
})
|
|
317
|
+
})
|
|
318
|
+
})
|
|
319
|
+
})
|
|
255
320
|
}), /*#__PURE__*/_jsx("br", {}), /*#__PURE__*/_jsx(Formik, {
|
|
256
321
|
initialValues: {
|
|
257
322
|
picture,
|
|
@@ -362,7 +427,7 @@ export default function UpdateClassroom({
|
|
|
362
427
|
values: values,
|
|
363
428
|
handleChange: handleChange,
|
|
364
429
|
setFieldValue: setFieldValue,
|
|
365
|
-
|
|
430
|
+
classroomAISettings: initialValues,
|
|
366
431
|
...otherProps
|
|
367
432
|
})
|
|
368
433
|
}), /*#__PURE__*/_jsx(Divider, {}), /*#__PURE__*/_jsx(CardActions, {
|
|
@@ -35,6 +35,9 @@ const MeetingPromptsList = withStyles(({
|
|
|
35
35
|
handleCreateFeedback,
|
|
36
36
|
handleBatchGradeConversations,
|
|
37
37
|
aiGrade,
|
|
38
|
+
// Discuss AI feedback, already combined with the all-AI switch, so the
|
|
39
|
+
// generate controls below come down when either is off.
|
|
40
|
+
isDiscussAIFeedbackEnabled = true,
|
|
38
41
|
meetingRecsData,
|
|
39
42
|
resetVideoState,
|
|
40
43
|
setSelectedRecording,
|
|
@@ -490,6 +493,7 @@ const MeetingPromptsList = withStyles(({
|
|
|
490
493
|
t: t,
|
|
491
494
|
handleBatchGradeConversations: handleBatchGradeConversations,
|
|
492
495
|
aiGrade: aiGrade,
|
|
496
|
+
isDiscussAIFeedbackEnabled: isDiscussAIFeedbackEnabled,
|
|
493
497
|
selectedGroup: selectedGroup,
|
|
494
498
|
setSelectedRecording: setSelectedRecording,
|
|
495
499
|
selectedRecording: selectedRecording,
|
|
@@ -1485,7 +1485,7 @@ export default function useExerciseState({
|
|
|
1485
1485
|
setIsWordBankAttemptChanged(false);
|
|
1486
1486
|
};
|
|
1487
1487
|
const formatBotMessages = async botMessages => {
|
|
1488
|
-
const updatedConversationHistory = conversationHistory.slice(1);
|
|
1488
|
+
const updatedConversationHistory = (conversationHistory || []).slice(1);
|
|
1489
1489
|
const responses = await Promise.all(botMessages.map(async m => {
|
|
1490
1490
|
let conversationHistoryMessage = updatedConversationHistory.find(historyMsg => historyMsg.content?.trim() === m.text?.trim());
|
|
1491
1491
|
const updatedMessage = {
|
|
@@ -1719,7 +1719,7 @@ export default function useExerciseState({
|
|
|
1719
1719
|
if (response && response.reply) {
|
|
1720
1720
|
setBotText("");
|
|
1721
1721
|
setBotVars(response.vars);
|
|
1722
|
-
setConversationHistory(response.conversationHistory);
|
|
1722
|
+
setConversationHistory(response.conversationHistory || []);
|
|
1723
1723
|
handleReset();
|
|
1724
1724
|
if (clearAttemptAudio) {
|
|
1725
1725
|
clearAttemptAudio();
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// The classroom's AI switches: one for all AI, and one each for chatbot
|
|
2
|
+
// dynamic responses and Discuss AI feedback. A feature is on only when the
|
|
3
|
+
// all-AI switch and its own switch are both on. Everything downstream asks
|
|
4
|
+
// these helpers rather than reading a field directly.
|
|
5
|
+
//
|
|
6
|
+
// Each field is "enabled" or "disabled", and an absent value means on -
|
|
7
|
+
// classrooms predate the controls. `isDynamicResponsesEnabled` is the original
|
|
8
|
+
// chatbot field, so classrooms saved before the other two keep their setting.
|
|
9
|
+
|
|
10
|
+
export const AI_ENABLED = "enabled";
|
|
11
|
+
export const AI_DISABLED = "disabled";
|
|
12
|
+
export const AI_DEFAULT = "default";
|
|
13
|
+
export const AI_FIELDS = {
|
|
14
|
+
all: "isAIEnabled",
|
|
15
|
+
chatbot: "isDynamicResponsesEnabled",
|
|
16
|
+
discuss: "isDiscussAIFeedbackEnabled"
|
|
17
|
+
};
|
|
18
|
+
const read = (record, field) => record && typeof record === "object" ? record[field] : undefined;
|
|
19
|
+
|
|
20
|
+
// Anything other than an explicit "disabled" counts as on, so an unset or
|
|
21
|
+
// unrecognised value never silently turns AI off.
|
|
22
|
+
const isOn = value => value !== AI_DISABLED;
|
|
23
|
+
|
|
24
|
+
// Courses only hold the chatbot switch, so this is the one field to read.
|
|
25
|
+
const readCourse = courseSettings => read(courseSettings, AI_FIELDS.chatbot);
|
|
26
|
+
|
|
27
|
+
// The all-AI switch on its own.
|
|
28
|
+
export const isClassroomAIEnabled = classroom => isOn(read(classroom, AI_FIELDS.all));
|
|
29
|
+
export const isClassroomChatbotAIEnabled = classroom => isClassroomAIEnabled(classroom) && isOn(read(classroom, AI_FIELDS.chatbot));
|
|
30
|
+
export const isClassroomDiscussAIFeedbackEnabled = classroom => isClassroomAIEnabled(classroom) && isOn(read(classroom, AI_FIELDS.discuss));
|
|
31
|
+
|
|
32
|
+
// Chatbot dynamic responses for one course in the classroom. A course can only
|
|
33
|
+
// narrow the classroom's answer, never re-enable it.
|
|
34
|
+
export const isCourseAIEnabled = (classroom, courseSettings) => isClassroomChatbotAIEnabled(classroom) && readCourse(courseSettings) !== AI_DISABLED;
|
|
35
|
+
|
|
36
|
+
// The value a classroom radio should show for one of AI_FIELDS.
|
|
37
|
+
export const classroomAIValue = (classroom, field = AI_FIELDS.all) => read(classroom, field) || AI_ENABLED;
|
|
38
|
+
|
|
39
|
+
// The value a per-course radio should show.
|
|
40
|
+
export const courseAIValue = courseSettings => readCourse(courseSettings) || AI_DEFAULT;
|
|
41
|
+
|
|
42
|
+
// Which level above the classroom has locked a switch, if any. The API sets
|
|
43
|
+
// `aiLockedBy` per field; a lock on all AI locks every switch. An older API
|
|
44
|
+
// sent one level for everything, which is still read as a full lock.
|
|
45
|
+
export const aiLockedBy = (classroom, field = AI_FIELDS.all) => {
|
|
46
|
+
const locks = read(classroom, "aiLockedBy");
|
|
47
|
+
if (!locks) return null;
|
|
48
|
+
if (typeof locks === "string") return locks;
|
|
49
|
+
return locks[field] || locks[AI_FIELDS.all] || null;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// The switches to save: only the ones the teacher changed, so a switch still
|
|
53
|
+
// on the customer's default keeps following it. Locked ones are left out too:
|
|
54
|
+
// their value is the API's override, and saving it would overwrite the
|
|
55
|
+
// teacher's own choice.
|
|
56
|
+
export const classroomAIFields = (values, classroom) => Object.values(AI_FIELDS).reduce((fields, field) => {
|
|
57
|
+
const value = classroomAIValue(values, field);
|
|
58
|
+
const isUnchanged = value === classroomAIValue(classroom, field);
|
|
59
|
+
return aiLockedBy(classroom, field) || isUnchanged ? fields : {
|
|
60
|
+
...fields,
|
|
61
|
+
[field]: value
|
|
62
|
+
};
|
|
63
|
+
}, {});
|