@nualang/nualang-ui-components 0.1.1415 → 0.1.1417
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/Containers/App/App.js +15 -2
- package/dist/Dialogs/FindChatbotDialog/FindChatbotDialog.js +23 -9
- package/dist/Dialogs/FindScenarioDialog/FindScenarioDialog.js +23 -9
- package/dist/Dialogs/FindTopicsDialog/FindTopicsDialog.js +22 -8
- package/dist/Dialogs/GenerateBot/GenerateBot.js +29 -8
- package/dist/Dialogs/QuickCreateChatbotDialog/QuickCreateChatbotDialog.js +20 -3
- package/dist/Editors/Bot/Bot.js +48 -7
- package/dist/Editors/Bot/Editor/Editor.js +78 -49
- package/dist/Editors/Bot/Editor/QuestionsEditor/AnswerOptionsList.js +94 -0
- package/dist/Editors/Bot/Editor/QuestionsEditor/QuestionRow.js +211 -0
- package/dist/Editors/Bot/Editor/QuestionsEditor/QuestionsEditor.js +115 -0
- package/dist/Lists/CourseOutline/CourseOutline.js +4 -0
- package/dist/Lists/Exercises/Exercises.js +6 -3
- package/dist/Misc/NotFound/Classroom.js +1 -1
- package/dist/Misc/NotFound/Course.js +1 -1
- package/dist/theme/getComponents.js +5 -1
- package/dist/utils/index.js +138 -0
- package/package.json +1 -1
|
@@ -217,6 +217,7 @@ function App({
|
|
|
217
217
|
isCollapsed,
|
|
218
218
|
setIsCollapsed,
|
|
219
219
|
isUserInternal,
|
|
220
|
+
isCommunityAccessRestricted,
|
|
220
221
|
handleQuickCreateClassroom,
|
|
221
222
|
handleQuickCreateCourse,
|
|
222
223
|
learnLang,
|
|
@@ -303,6 +304,18 @@ function App({
|
|
|
303
304
|
};
|
|
304
305
|
const isSearchActive = searchText || isMobileSearchExpanded;
|
|
305
306
|
const isTeacher = user && user["custom:role"] === "teach";
|
|
307
|
+
const isSearchAllowed = isTeacher && !isCommunityAccessRestricted;
|
|
308
|
+
|
|
309
|
+
// If the restriction flag loads/flips async while search is already active
|
|
310
|
+
// (e.g. mobile search expanded, or text typed in), force-close it rather
|
|
311
|
+
// than only gating the initial render - otherwise a user already mid-search
|
|
312
|
+
// keeps access until they manually close it.
|
|
313
|
+
useEffect(() => {
|
|
314
|
+
if (!isSearchAllowed && (searchText || isMobileSearchExpanded)) {
|
|
315
|
+
setSearchText("");
|
|
316
|
+
setIsMobileSearchExpanded(false);
|
|
317
|
+
}
|
|
318
|
+
}, [isSearchAllowed, searchText, isMobileSearchExpanded]);
|
|
306
319
|
const isUpgradePossible = subscription && subscription.isUpgradePossible;
|
|
307
320
|
const hidden = useMediaQuery(theme => theme.breakpoints.down("sm"));
|
|
308
321
|
return /*#__PURE__*/_jsxs("div", {
|
|
@@ -334,7 +347,7 @@ function App({
|
|
|
334
347
|
})]
|
|
335
348
|
}) : /*#__PURE__*/_jsxs(_Fragment, {
|
|
336
349
|
children: [isLgScreen ? /*#__PURE__*/_jsx(_Fragment, {
|
|
337
|
-
children:
|
|
350
|
+
children: isSearchAllowed && /*#__PURE__*/_jsxs(_Fragment, {
|
|
338
351
|
children: [/*#__PURE__*/_jsx(AppSearch, {
|
|
339
352
|
t: t,
|
|
340
353
|
navigate: navigate,
|
|
@@ -348,7 +361,7 @@ function App({
|
|
|
348
361
|
})]
|
|
349
362
|
})
|
|
350
363
|
}) : /*#__PURE__*/_jsx(_Fragment, {
|
|
351
|
-
children:
|
|
364
|
+
children: isSearchAllowed && /*#__PURE__*/_jsx(_Fragment, {
|
|
352
365
|
children: /*#__PURE__*/_jsx(Tooltip, {
|
|
353
366
|
title: t("search"),
|
|
354
367
|
enterDelay: 300,
|
|
@@ -29,6 +29,7 @@ export default function FindChatbotDialog({
|
|
|
29
29
|
onLoadMoreCommunity,
|
|
30
30
|
hasMoreCommunity = false,
|
|
31
31
|
isLoadingMoreCommunity = false,
|
|
32
|
+
isCommunityAccessRestricted = false,
|
|
32
33
|
t = text => text
|
|
33
34
|
}) {
|
|
34
35
|
const scrollRef = useRef(null);
|
|
@@ -39,6 +40,8 @@ export default function FindChatbotDialog({
|
|
|
39
40
|
threshold: 0
|
|
40
41
|
});
|
|
41
42
|
const [tab, setTab] = useState(0);
|
|
43
|
+
// Restricted users are always kept on the library tab, regardless of state.
|
|
44
|
+
const effectiveTab = isCommunityAccessRestricted ? 0 : tab;
|
|
42
45
|
const [selectedBotIds, setSelectedBotIds] = useState([]);
|
|
43
46
|
const [search, setSearch] = useState("");
|
|
44
47
|
const [filters, setFilters] = useState({
|
|
@@ -81,12 +84,22 @@ export default function FindChatbotDialog({
|
|
|
81
84
|
reset();
|
|
82
85
|
handleClose();
|
|
83
86
|
};
|
|
87
|
+
|
|
88
|
+
// If restriction flips on while the dialog is already open (e.g. the flag
|
|
89
|
+
// loads async), a previously-selected community chatbot could otherwise
|
|
90
|
+
// stay selected/added even though the Community tab is now locked out.
|
|
84
91
|
useEffect(() => {
|
|
85
|
-
if (
|
|
92
|
+
if (isCommunityAccessRestricted) {
|
|
93
|
+
reset();
|
|
94
|
+
}
|
|
95
|
+
}, [isCommunityAccessRestricted]);
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (sentinelInView && effectiveTab === 1 && hasMoreCommunity && !isLoadingMoreCommunity) {
|
|
86
98
|
onLoadMoreCommunity?.();
|
|
87
99
|
}
|
|
88
|
-
}, [sentinelInView,
|
|
100
|
+
}, [sentinelInView, effectiveTab, hasMoreCommunity, isLoadingMoreCommunity, onLoadMoreCommunity]);
|
|
89
101
|
const handleTabChange = (_, v) => {
|
|
102
|
+
if (isCommunityAccessRestricted) return;
|
|
90
103
|
setTab(v);
|
|
91
104
|
setSearch("");
|
|
92
105
|
setFilters({
|
|
@@ -114,9 +127,9 @@ export default function FindChatbotDialog({
|
|
|
114
127
|
}
|
|
115
128
|
return result;
|
|
116
129
|
};
|
|
117
|
-
const currentChatbots =
|
|
118
|
-
const isLoading =
|
|
119
|
-
const sectionLabel =
|
|
130
|
+
const currentChatbots = effectiveTab === 0 ? filterChatbots(libraryChatbots) : applyCommunityFilters(communityChatbots);
|
|
131
|
+
const isLoading = effectiveTab === 0 ? isLoadingLibrary : isLoadingCommunity;
|
|
132
|
+
const sectionLabel = effectiveTab === 0 ? t("add_own_exercise_message", {
|
|
120
133
|
exerciseName: t("chatbots").toLowerCase()
|
|
121
134
|
}) : t("or_select_a_community_template_desc");
|
|
122
135
|
const addButtonLabel = selectedBotIds.length > 0 ? `${t("add_chatbots")} (${selectedBotIds.length})` : t("add_chatbots");
|
|
@@ -166,17 +179,18 @@ export default function FindChatbotDialog({
|
|
|
166
179
|
borderColor: "divider"
|
|
167
180
|
},
|
|
168
181
|
children: /*#__PURE__*/_jsxs(Tabs, {
|
|
169
|
-
value:
|
|
182
|
+
value: effectiveTab,
|
|
170
183
|
onChange: handleTabChange,
|
|
171
184
|
indicatorColor: "primary",
|
|
172
185
|
textColor: "primary",
|
|
173
186
|
children: [/*#__PURE__*/_jsx(Tab, {
|
|
174
187
|
label: t("my_library") || "My Library"
|
|
175
188
|
}), /*#__PURE__*/_jsx(Tab, {
|
|
176
|
-
label: t("community") || "Community"
|
|
189
|
+
label: t("community") || "Community",
|
|
190
|
+
disabled: isCommunityAccessRestricted
|
|
177
191
|
})]
|
|
178
192
|
})
|
|
179
|
-
}),
|
|
193
|
+
}), effectiveTab === 1 && /*#__PURE__*/_jsx(Box, {
|
|
180
194
|
sx: {
|
|
181
195
|
px: 3,
|
|
182
196
|
py: {
|
|
@@ -232,7 +246,7 @@ export default function FindChatbotDialog({
|
|
|
232
246
|
isSelected: bot => selectedBotIds.includes(bot.botId),
|
|
233
247
|
compact: true,
|
|
234
248
|
handleViewUserProfile: () => {}
|
|
235
|
-
}),
|
|
249
|
+
}), effectiveTab === 1 && /*#__PURE__*/_jsx(Box, {
|
|
236
250
|
ref: sentinelRef,
|
|
237
251
|
sx: {
|
|
238
252
|
height: 40,
|
|
@@ -17,6 +17,7 @@ export default function FindScenarioDialog({
|
|
|
17
17
|
onLoadMoreCommunity,
|
|
18
18
|
hasMoreCommunity = false,
|
|
19
19
|
isLoadingMoreCommunity = false,
|
|
20
|
+
isCommunityAccessRestricted = false,
|
|
20
21
|
t = text => text
|
|
21
22
|
}) {
|
|
22
23
|
const scrollRef = useRef(null);
|
|
@@ -27,6 +28,8 @@ export default function FindScenarioDialog({
|
|
|
27
28
|
threshold: 0
|
|
28
29
|
});
|
|
29
30
|
const [tab, setTab] = useState(0);
|
|
31
|
+
// Restricted users are always kept on the library tab, regardless of state.
|
|
32
|
+
const effectiveTab = isCommunityAccessRestricted ? 0 : tab;
|
|
30
33
|
const [selectedRoleplayIds, setSelectedRoleplayIds] = useState([]);
|
|
31
34
|
const [search, setSearch] = useState("");
|
|
32
35
|
const [filters, setFilters] = useState({
|
|
@@ -69,12 +72,22 @@ export default function FindScenarioDialog({
|
|
|
69
72
|
reset();
|
|
70
73
|
handleClose();
|
|
71
74
|
};
|
|
75
|
+
|
|
76
|
+
// If restriction flips on while the dialog is already open (e.g. the flag
|
|
77
|
+
// loads async), a previously-selected community scenario could otherwise
|
|
78
|
+
// stay selected/added even though the Community tab is now locked out.
|
|
72
79
|
useEffect(() => {
|
|
73
|
-
if (
|
|
80
|
+
if (isCommunityAccessRestricted) {
|
|
81
|
+
reset();
|
|
82
|
+
}
|
|
83
|
+
}, [isCommunityAccessRestricted]);
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (sentinelInView && effectiveTab === 1 && hasMoreCommunity && !isLoadingMoreCommunity) {
|
|
74
86
|
onLoadMoreCommunity?.();
|
|
75
87
|
}
|
|
76
|
-
}, [sentinelInView,
|
|
88
|
+
}, [sentinelInView, effectiveTab, hasMoreCommunity, isLoadingMoreCommunity, onLoadMoreCommunity]);
|
|
77
89
|
const handleTabChange = (_, v) => {
|
|
90
|
+
if (isCommunityAccessRestricted) return;
|
|
78
91
|
setTab(v);
|
|
79
92
|
setSearch("");
|
|
80
93
|
setFilters({
|
|
@@ -102,9 +115,9 @@ export default function FindScenarioDialog({
|
|
|
102
115
|
}
|
|
103
116
|
return result;
|
|
104
117
|
};
|
|
105
|
-
const currentScenarios =
|
|
106
|
-
const isLoading =
|
|
107
|
-
const sectionLabel =
|
|
118
|
+
const currentScenarios = effectiveTab === 0 ? filterScenarios(libraryScenarios) : applyCommunityFilters(communityScenarios);
|
|
119
|
+
const isLoading = effectiveTab === 0 ? isLoadingLibrary : isLoadingCommunity;
|
|
120
|
+
const sectionLabel = effectiveTab === 0 ? t("add_own_exercise_message", {
|
|
108
121
|
exerciseName: t("scenarios").toLowerCase()
|
|
109
122
|
}) : t("or_select_a_community_template_desc");
|
|
110
123
|
const addButtonLabel = selectedRoleplayIds.length > 0 ? `${t("add_scenarios")} (${selectedRoleplayIds.length})` : t("add_scenarios");
|
|
@@ -154,17 +167,18 @@ export default function FindScenarioDialog({
|
|
|
154
167
|
borderColor: "divider"
|
|
155
168
|
},
|
|
156
169
|
children: /*#__PURE__*/_jsxs(Tabs, {
|
|
157
|
-
value:
|
|
170
|
+
value: effectiveTab,
|
|
158
171
|
onChange: handleTabChange,
|
|
159
172
|
indicatorColor: "primary",
|
|
160
173
|
textColor: "primary",
|
|
161
174
|
children: [/*#__PURE__*/_jsx(Tab, {
|
|
162
175
|
label: t("my_library") || "My Library"
|
|
163
176
|
}), /*#__PURE__*/_jsx(Tab, {
|
|
164
|
-
label: t("community") || "Community"
|
|
177
|
+
label: t("community") || "Community",
|
|
178
|
+
disabled: isCommunityAccessRestricted
|
|
165
179
|
})]
|
|
166
180
|
})
|
|
167
|
-
}),
|
|
181
|
+
}), effectiveTab === 1 && /*#__PURE__*/_jsx(Box, {
|
|
168
182
|
sx: {
|
|
169
183
|
px: 3,
|
|
170
184
|
py: {
|
|
@@ -220,7 +234,7 @@ export default function FindScenarioDialog({
|
|
|
220
234
|
isSelected: roleplay => selectedRoleplayIds.includes(roleplay.roleplayId),
|
|
221
235
|
compact: true,
|
|
222
236
|
handleViewUserProfile: () => {}
|
|
223
|
-
}),
|
|
237
|
+
}), effectiveTab === 1 && /*#__PURE__*/_jsx(Box, {
|
|
224
238
|
ref: sentinelRef,
|
|
225
239
|
sx: {
|
|
226
240
|
height: 40,
|
|
@@ -150,6 +150,7 @@ export default function FindTopicsDialog({
|
|
|
150
150
|
onLoadMoreCommunity,
|
|
151
151
|
hasMoreCommunity = false,
|
|
152
152
|
isLoadingMoreCommunity = false,
|
|
153
|
+
isCommunityAccessRestricted = false,
|
|
153
154
|
t = text => text
|
|
154
155
|
}) {
|
|
155
156
|
const scrollRef = useRef(null);
|
|
@@ -160,6 +161,8 @@ export default function FindTopicsDialog({
|
|
|
160
161
|
threshold: 0
|
|
161
162
|
});
|
|
162
163
|
const [tab, setTab] = useState(0);
|
|
164
|
+
// Restricted users are always kept on the library tab, regardless of state.
|
|
165
|
+
const effectiveTab = isCommunityAccessRestricted ? 0 : tab;
|
|
163
166
|
const [selectedTopicIds, setSelectedTopicIds] = useState([]);
|
|
164
167
|
const [search, setSearch] = useState("");
|
|
165
168
|
const [filters, setFilters] = useState({
|
|
@@ -202,12 +205,22 @@ export default function FindTopicsDialog({
|
|
|
202
205
|
reset();
|
|
203
206
|
handleClose();
|
|
204
207
|
};
|
|
208
|
+
|
|
209
|
+
// If restriction flips on while the dialog is already open (e.g. the flag
|
|
210
|
+
// loads async), a previously-selected community topic could otherwise stay
|
|
211
|
+
// selected/added even though the Community tab is now locked out - clear it.
|
|
205
212
|
useEffect(() => {
|
|
206
|
-
if (
|
|
213
|
+
if (isCommunityAccessRestricted) {
|
|
214
|
+
reset();
|
|
215
|
+
}
|
|
216
|
+
}, [isCommunityAccessRestricted]);
|
|
217
|
+
useEffect(() => {
|
|
218
|
+
if (sentinelInView && effectiveTab === 1 && hasMoreCommunity && !isLoadingMoreCommunity) {
|
|
207
219
|
onLoadMoreCommunity?.();
|
|
208
220
|
}
|
|
209
|
-
}, [sentinelInView,
|
|
221
|
+
}, [sentinelInView, effectiveTab, hasMoreCommunity, isLoadingMoreCommunity, onLoadMoreCommunity]);
|
|
210
222
|
const handleTabChange = (_, v) => {
|
|
223
|
+
if (isCommunityAccessRestricted) return;
|
|
211
224
|
setTab(v);
|
|
212
225
|
setSearch("");
|
|
213
226
|
setFilters({
|
|
@@ -235,9 +248,9 @@ export default function FindTopicsDialog({
|
|
|
235
248
|
}
|
|
236
249
|
return result;
|
|
237
250
|
};
|
|
238
|
-
const currentTopics =
|
|
239
|
-
const isLoading =
|
|
240
|
-
const sectionLabel =
|
|
251
|
+
const currentTopics = effectiveTab === 0 ? filterTopics(libraryTopics) : applyCommunityFilters(communityTopics);
|
|
252
|
+
const isLoading = effectiveTab === 0 ? isLoadingLibrary : isLoadingCommunity;
|
|
253
|
+
const sectionLabel = effectiveTab === 0 ? t("add_own_exercise_message", {
|
|
241
254
|
exerciseName: t("topic").toLowerCase()
|
|
242
255
|
}) : t("or_select_a_community_template_desc") || "Community Topics";
|
|
243
256
|
const addButtonLabel = selectedTopicIds.length > 0 ? `${t("add_topics") || "Add Topics"} (${selectedTopicIds.length})` : t("add_topics") || "Add Topics";
|
|
@@ -281,7 +294,7 @@ export default function FindTopicsDialog({
|
|
|
281
294
|
borderColor: "divider"
|
|
282
295
|
},
|
|
283
296
|
children: /*#__PURE__*/_jsxs(Tabs, {
|
|
284
|
-
value:
|
|
297
|
+
value: effectiveTab,
|
|
285
298
|
onChange: handleTabChange,
|
|
286
299
|
indicatorColor: "primary",
|
|
287
300
|
textColor: "primary",
|
|
@@ -291,10 +304,11 @@ export default function FindTopicsDialog({
|
|
|
291
304
|
"data-cy": "find-topics-tab-library"
|
|
292
305
|
}), /*#__PURE__*/_jsx(Tab, {
|
|
293
306
|
label: t("community") || "Community",
|
|
307
|
+
disabled: isCommunityAccessRestricted,
|
|
294
308
|
"data-cy": "find-topics-tab-community"
|
|
295
309
|
})]
|
|
296
310
|
})
|
|
297
|
-
}),
|
|
311
|
+
}), effectiveTab === 1 && /*#__PURE__*/_jsx(Box, {
|
|
298
312
|
sx: {
|
|
299
313
|
px: 3,
|
|
300
314
|
py: {
|
|
@@ -350,7 +364,7 @@ export default function FindTopicsDialog({
|
|
|
350
364
|
selectionEnabled: true,
|
|
351
365
|
isSelected: topic => selectedTopicIds.includes(topic.topicId),
|
|
352
366
|
compact: true
|
|
353
|
-
}),
|
|
367
|
+
}), effectiveTab === 1 && /*#__PURE__*/_jsx(Box, {
|
|
354
368
|
ref: sentinelRef,
|
|
355
369
|
sx: {
|
|
356
370
|
height: 40,
|
|
@@ -9,7 +9,9 @@ import NualaCreating from "../../Misc/NualaCreating/NualaCreating";
|
|
|
9
9
|
import ResponsiveDialog from "../ResponsiveDialog/ResponsiveDialog";
|
|
10
10
|
import InputHelper from "../../Forms/InputHelper/InputHelper";
|
|
11
11
|
import AutoFixHighIcon from "@mui/icons-material/AutoFixHigh";
|
|
12
|
+
import { getBotContentTemplateType, multipleOptionsRegex, sanitizeBotContent } from "../../utils/index";
|
|
12
13
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
14
|
+
const QUESTION_ANSWER_TEMPLATES = ["nualang.quiz.0.0.1", "nualang.emoji-quiz.0.0.1", "nualang.picture-quiz.0.0.1", "nualang.picture-quiz.0.0.2"];
|
|
13
15
|
function validateResponse(response, template) {
|
|
14
16
|
if (!Array.isArray(response)) {
|
|
15
17
|
return false;
|
|
@@ -18,7 +20,7 @@ function validateResponse(response, template) {
|
|
|
18
20
|
if (typeof item !== "object" || item === null || Array.isArray(item)) {
|
|
19
21
|
return false;
|
|
20
22
|
}
|
|
21
|
-
if (template
|
|
23
|
+
if (QUESTION_ANSWER_TEMPLATES.includes(template)) {
|
|
22
24
|
if (!Object.hasOwn(item, "question") || !Object.hasOwn(item, "answer")) {
|
|
23
25
|
return false;
|
|
24
26
|
}
|
|
@@ -29,18 +31,27 @@ function validateResponse(response, template) {
|
|
|
29
31
|
return false;
|
|
30
32
|
}
|
|
31
33
|
}
|
|
32
|
-
if (template === "nualang.open-ended.0.0.1"
|
|
33
|
-
|
|
34
|
+
if (template === "nualang.open-ended.0.0.1") {
|
|
35
|
+
if (typeof item.text !== "string" || item.text.trim() === "") {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
34
38
|
}
|
|
35
39
|
}
|
|
36
40
|
return true;
|
|
37
41
|
}
|
|
42
|
+
|
|
43
|
+
// Splits a RiveScript-style answer string, e.g. "(Tuesday)" or "(7|seven)", into a plain array.
|
|
44
|
+
function parseAnswerString(answer) {
|
|
45
|
+
if (typeof answer !== "string") return [];
|
|
46
|
+
const match = answer.match(multipleOptionsRegex);
|
|
47
|
+
const inner = match ? match[1] : answer;
|
|
48
|
+
return inner.split("|").map(entry => entry.trim()).filter(Boolean);
|
|
49
|
+
}
|
|
38
50
|
function GenerateBotDialog({
|
|
39
51
|
t = text => text,
|
|
40
52
|
isGenerateBotDialogOpen,
|
|
41
53
|
setIsGenerateBotDialogOpen,
|
|
42
54
|
makeChatGptApiRequest,
|
|
43
|
-
generateRiveFile,
|
|
44
55
|
learnLang,
|
|
45
56
|
initialValues = {
|
|
46
57
|
botTopic: "",
|
|
@@ -52,7 +63,7 @@ function GenerateBotDialog({
|
|
|
52
63
|
siteLanguage,
|
|
53
64
|
topicPhrases,
|
|
54
65
|
phraseLists,
|
|
55
|
-
|
|
66
|
+
updateBotContent,
|
|
56
67
|
initialTopicGoal
|
|
57
68
|
}) {
|
|
58
69
|
const [usePhrasesContext, setUsePhrasesContext] = useState(true);
|
|
@@ -205,11 +216,21 @@ function GenerateBotDialog({
|
|
|
205
216
|
};
|
|
206
217
|
const handleSubmit = async () => {
|
|
207
218
|
const generatedBot = await handleGenerateBotScript(autoSelect);
|
|
208
|
-
const
|
|
219
|
+
const templateType = getBotContentTemplateType(bot.selectedTemplate);
|
|
220
|
+
const botContent = sanitizeBotContent({
|
|
221
|
+
schemaVersion: 1,
|
|
222
|
+
templateType,
|
|
223
|
+
items: generatedBot.map((item, index) => ({
|
|
224
|
+
id: `q${index + 1}`,
|
|
225
|
+
question: templateType === "open-ended" ? item.text : item.question,
|
|
226
|
+
answers: templateType === "open-ended" ? [] : parseAnswerString(item.answer),
|
|
227
|
+
image: null
|
|
228
|
+
}))
|
|
229
|
+
});
|
|
209
230
|
await handleUpdateBot({
|
|
210
|
-
|
|
231
|
+
botContent
|
|
211
232
|
});
|
|
212
|
-
|
|
233
|
+
updateBotContent(botContent);
|
|
213
234
|
handleCloseDialog();
|
|
214
235
|
};
|
|
215
236
|
return /*#__PURE__*/_jsx(ResponsiveDialog, {
|
|
@@ -404,7 +404,7 @@ export default function QuickCreateChatbotDialog({
|
|
|
404
404
|
const [difficulty, setDifficulty] = useState(topicDifficulty || "");
|
|
405
405
|
const [templateId, setTemplateId] = useState(getDefaultTemplateId(templates));
|
|
406
406
|
const [template, setTemplate] = useState(templates.find(tpl => tpl.botId === getDefaultTemplateId(templates)) || {});
|
|
407
|
-
const [
|
|
407
|
+
const [botContent, setBotContent] = useState(null);
|
|
408
408
|
const [numberOfQuestions, setNumberOfQuestions] = useState(defaultNumberOfQuestions);
|
|
409
409
|
const [isLoading, setIsLoading] = useState(false);
|
|
410
410
|
const [selectedVoice, setSelectedVoice] = useState(null);
|
|
@@ -428,7 +428,24 @@ export default function QuickCreateChatbotDialog({
|
|
|
428
428
|
useEffect(() => {
|
|
429
429
|
const matched = templates.find(tpl => tpl.botId === templateId);
|
|
430
430
|
setTemplate(matched || null);
|
|
431
|
-
|
|
431
|
+
if (matched && matched.sampleBotContent) {
|
|
432
|
+
const sampleItems = matched.sampleBotContent.items;
|
|
433
|
+
// Cycle the sample items to match the requested question count, reassigning
|
|
434
|
+
// sequential ids - this is placeholder content the teacher edits immediately
|
|
435
|
+
// after creation, not a precise question bank.
|
|
436
|
+
const items = Array.from({
|
|
437
|
+
length: numberOfQuestions
|
|
438
|
+
}, (_, index) => ({
|
|
439
|
+
...sampleItems[index % sampleItems.length],
|
|
440
|
+
id: `q${index + 1}`
|
|
441
|
+
}));
|
|
442
|
+
setBotContent({
|
|
443
|
+
...matched.sampleBotContent,
|
|
444
|
+
items
|
|
445
|
+
});
|
|
446
|
+
} else {
|
|
447
|
+
setBotContent(null);
|
|
448
|
+
}
|
|
432
449
|
}, [templateId, templates, numberOfQuestions]);
|
|
433
450
|
const resetAndClose = () => {
|
|
434
451
|
setBotTitle("");
|
|
@@ -472,7 +489,7 @@ export default function QuickCreateChatbotDialog({
|
|
|
472
489
|
description: t(template.description) || "",
|
|
473
490
|
difficulty,
|
|
474
491
|
tags: [],
|
|
475
|
-
|
|
492
|
+
botContent,
|
|
476
493
|
learnLang: template.learnLang,
|
|
477
494
|
selectedTemplate: templateId,
|
|
478
495
|
numberOfQuestions: Number(numberOfQuestions),
|
package/dist/Editors/Bot/Bot.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useEffect } from "react";
|
|
1
|
+
import { useState, useEffect, useRef } from "react";
|
|
2
2
|
import { Joyride, STATUS } from "react-joyride";
|
|
3
3
|
import useMediaQuery from "@mui/material/useMediaQuery";
|
|
4
4
|
import { useTheme } from "@mui/material/styles";
|
|
@@ -25,7 +25,7 @@ import Tutorial from "./Tutorial";
|
|
|
25
25
|
import Editor from "./Editor/Editor";
|
|
26
26
|
|
|
27
27
|
// utils
|
|
28
|
-
import { jsonToRivescript, rivescriptToJson } from "../../utils/index";
|
|
28
|
+
import { jsonToRivescript, rivescriptToJson, getBotContentTemplateType, buildInitialBotContent, decompileBotContent, sanitizeBotContent, AUTHOR_FROM_SCRATCH_TEMPLATE } from "../../utils/index";
|
|
29
29
|
import { startJoyrideWhenReady } from "../../utils/joyride";
|
|
30
30
|
import useDebounce from "../../hooks/useDebounce";
|
|
31
31
|
import useRecognition from "../../hooks/useRecognition";
|
|
@@ -104,7 +104,6 @@ function Bot({
|
|
|
104
104
|
handleBotInstructionsImageUpload,
|
|
105
105
|
makeChatGptApiRequest,
|
|
106
106
|
handleUpdateBot,
|
|
107
|
-
generateRiveFile,
|
|
108
107
|
siteLanguage,
|
|
109
108
|
topicPhrases,
|
|
110
109
|
phraseLists,
|
|
@@ -131,6 +130,18 @@ function Bot({
|
|
|
131
130
|
recognizing
|
|
132
131
|
} = recognitionState;
|
|
133
132
|
const [bot, setBot] = useState(initialValues);
|
|
133
|
+
const isAuthorFromScratch = bot.selectedTemplate === AUTHOR_FROM_SCRATCH_TEMPLATE;
|
|
134
|
+
const botContentTemplateType = getBotContentTemplateType(bot.selectedTemplate);
|
|
135
|
+
// Only set by genuine user edits (via updateBotContent) - not by the mount-time
|
|
136
|
+
// backfill below - so opening an old bot never autosaves before anyone touches it.
|
|
137
|
+
const hasEditedBotContentRef = useRef(false);
|
|
138
|
+
const updateBotContent = newBotContent => {
|
|
139
|
+
hasEditedBotContentRef.current = true;
|
|
140
|
+
setBot(prevState => ({
|
|
141
|
+
...prevState,
|
|
142
|
+
botContent: newBotContent
|
|
143
|
+
}));
|
|
144
|
+
};
|
|
134
145
|
const [isDynamicResponsesEnabled, setIsDynamicResponsesEnabled] = useState("disable");
|
|
135
146
|
const languageInformation = config.languages[learnLang];
|
|
136
147
|
const languageTag = languageInformation.Code;
|
|
@@ -320,12 +331,21 @@ function Bot({
|
|
|
320
331
|
|
|
321
332
|
// Effect for API call
|
|
322
333
|
useEffect(() => {
|
|
323
|
-
if (debouncedRiveFile !== initialValues.riveFile) {
|
|
334
|
+
if (isAuthorFromScratch && debouncedRiveFile !== initialValues.riveFile) {
|
|
324
335
|
handleSaveBot();
|
|
325
336
|
}
|
|
326
337
|
}, [debouncedRiveFile] // Only call effect if debounced rive file changes
|
|
327
338
|
);
|
|
328
339
|
|
|
340
|
+
// Templated bots: botContent is the source of truth, riveFile is compiled from it
|
|
341
|
+
// server-side on save - see handleSaveBotContent.
|
|
342
|
+
const debouncedBotContent = useDebounce(bot.botContent, 3000);
|
|
343
|
+
useEffect(() => {
|
|
344
|
+
if (!isAuthorFromScratch && hasEditedBotContentRef.current && debouncedBotContent) {
|
|
345
|
+
handleSaveBotContent();
|
|
346
|
+
}
|
|
347
|
+
}, [debouncedBotContent]);
|
|
348
|
+
|
|
329
349
|
// Debounce botJSON so that it only gives us latest value ...
|
|
330
350
|
// ... if botJSON has not been updated within last 3000ms.
|
|
331
351
|
// The goal is to only have the bot flow chart fire when user stops typing ...
|
|
@@ -340,7 +360,17 @@ function Bot({
|
|
|
340
360
|
if (bot.riveFile === true) {
|
|
341
361
|
bot.riveFile = "";
|
|
342
362
|
}
|
|
343
|
-
|
|
363
|
+
if (isAuthorFromScratch) {
|
|
364
|
+
updateRivescriptJSON(bot.riveFile);
|
|
365
|
+
} else if (!bot.botContent) {
|
|
366
|
+
// Legacy templated bot with no structured record yet: recover its real questions from
|
|
367
|
+
// riveFile rather than seeding a blank one, so the next autosave can't wipe them out.
|
|
368
|
+
const decompiled = decompileBotContent(bot.riveFile, bot.selectedTemplate);
|
|
369
|
+
setBot(prevState => ({
|
|
370
|
+
...prevState,
|
|
371
|
+
botContent: decompiled || buildInitialBotContent(prevState.selectedTemplate)
|
|
372
|
+
}));
|
|
373
|
+
}
|
|
344
374
|
}, []); // Only run on mount
|
|
345
375
|
|
|
346
376
|
useEffect(() => {
|
|
@@ -560,6 +590,15 @@ function Bot({
|
|
|
560
590
|
console.error(error);
|
|
561
591
|
}
|
|
562
592
|
};
|
|
593
|
+
const handleSaveBotContent = async () => {
|
|
594
|
+
try {
|
|
595
|
+
await onSubmit({
|
|
596
|
+
botContent: sanitizeBotContent(bot.botContent)
|
|
597
|
+
});
|
|
598
|
+
} catch (error) {
|
|
599
|
+
console.error(error);
|
|
600
|
+
}
|
|
601
|
+
};
|
|
563
602
|
const handleJoyrideCallback = data => {
|
|
564
603
|
const {
|
|
565
604
|
status,
|
|
@@ -763,16 +802,18 @@ function Bot({
|
|
|
763
802
|
handleUploadBotExerciseImage: handleUploadBotExerciseImage,
|
|
764
803
|
makeChatGptApiRequest: makeChatGptApiRequest,
|
|
765
804
|
handleUpdateBot: handleUpdateBot,
|
|
766
|
-
generateRiveFile: generateRiveFile,
|
|
767
805
|
learnLang: learnLang,
|
|
768
806
|
siteLanguage: siteLanguage,
|
|
769
807
|
topicPhrases: topicPhrases,
|
|
770
808
|
phraseLists: phraseLists,
|
|
771
|
-
updateRivescriptJSON: updateRivescriptJSON,
|
|
772
809
|
courseId: courseId,
|
|
773
810
|
sectionId: sectionId,
|
|
774
811
|
topicId: topicId,
|
|
775
812
|
initialTopicGoal: initialTopicGoal,
|
|
813
|
+
verificationStatus: verificationStatus,
|
|
814
|
+
isAuthorFromScratch: isAuthorFromScratch,
|
|
815
|
+
botContentTemplateType: botContentTemplateType,
|
|
816
|
+
updateBotContent: updateBotContent,
|
|
776
817
|
openSnackbar: openSnackbar
|
|
777
818
|
}) : null, tabValue === 1 ? /*#__PURE__*/_jsx(Box, {
|
|
778
819
|
className: hidden ? classes.paneFull : classes.pane,
|