@nualang/nualang-ui-components 0.1.1254 → 0.1.1256

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.
@@ -79,33 +79,51 @@ function CreateAssignmentDialog({
79
79
  exercises: submittedExercises,
80
80
  ...initialData
81
81
  });
82
- const classroomQuery = _Queries.classrooms.useClassroom(async classroomId => {
83
- const allClassroomsCourses = [];
84
- await Promise.all(classroomId.map(async cId => {
85
- if (!cId) return;
86
- const response = await getClassroom(cId);
87
- allClassroomsCourses.push(response?.Item?.courses || []);
88
- }));
89
- const flatClassrooms = allClassroomsCourses.flat();
90
- const seen = new Set();
91
- const uniqueClassroomsCourses = flatClassrooms.filter(classR => {
92
- if (seen.has(classR)) return false;
93
- seen.add(classR);
94
- return true;
95
- });
96
- return uniqueClassroomsCourses;
97
- }, {
98
- classroomId: assignment?.classroomId
99
- }, {
100
- enabled: !!(assignment.classroomId.length > 0)
82
+ const classroomQuery = useQueries({
83
+ queries: (Array.isArray(assignment.classroomId) ? assignment.classroomId : [assignment.classroomId]).map(cId => ({
84
+ queryKey: _Queries.classrooms.classroomKeys.item(cId, username),
85
+ queryFn: () => getClassroom(cId, filters),
86
+ queryOptions: {
87
+ enabled: !!(Array.isArray(assignment.classroomId) ? assignment.classroomId.length > 0 : assignment.classroomId)
88
+ }
89
+ })),
90
+ combine: results => {
91
+ const normalizedData = results.map(result => {
92
+ const data = result.data;
93
+ if (data?.classroomId) {
94
+ return data;
95
+ }
96
+ if (data?.Item?.classroomId) {
97
+ return data.Item;
98
+ }
99
+ return null;
100
+ }).filter(Boolean);
101
+ return {
102
+ data: normalizedData,
103
+ pending: results.some(result => result.isPending)
104
+ };
105
+ }
101
106
  });
102
- const selectedClassroom = (0, _react.useMemo)(() => classroomQuery.isSuccess && classroomQuery.data ? classroomQuery.data : {}, [classroomQuery.data, classroomQuery.isSuccess]);
107
+ const selectedClassroomCourses = (0, _react.useMemo)(() => {
108
+ if (!classroomQuery.pending && classroomQuery.data.length > 0) {
109
+ const allCourses = classroomQuery.data.flatMap(classroom => classroom.courses || []);
110
+ const seen = new Set();
111
+ const uniqueCourses = allCourses.filter(courseId => {
112
+ if (seen.has(courseId)) return false;
113
+ seen.add(courseId);
114
+ return true;
115
+ });
116
+ return uniqueCourses;
117
+ } else {
118
+ return [];
119
+ }
120
+ }, [classroomQuery.data, classroomQuery.pending]);
103
121
  const coursesQuery = _Queries.courses.useCourses(async filters => {
104
122
  const response = await getCourses(filters);
105
123
  return response;
106
124
  }, {
107
125
  filters: {
108
- courseIds: selectedClassroom?.toString()
126
+ courseIds: selectedClassroomCourses?.toString()
109
127
  }
110
128
  }, {
111
129
  enabled: !!assignment.classroomId
@@ -122,17 +140,22 @@ function CreateAssignmentDialog({
122
140
  }
123
141
  })),
124
142
  combine: results => {
143
+ const allMembers = results.flatMap(result => {
144
+ if (Array.isArray(result?.data?.Items)) {
145
+ return result.data.Items;
146
+ }
147
+ return [];
148
+ });
125
149
  return {
126
- data: results.map(result => result.data),
150
+ data: allMembers,
127
151
  pending: results.some(result => result.isPending)
128
152
  };
129
153
  }
130
154
  });
131
- const selectedMembers = (0, _react.useMemo)(() => {
132
- if (!classroomMembersQueries.pending && Array.isArray(classroomMembersQueries.data[0].Items) && classroomMembersQueries.data[0].Items.length) {
133
- const flatMembers = classroomMembersQueries.data[0].Items.flat();
155
+ const uniqueClassroomMembers = (0, _react.useMemo)(() => {
156
+ if (!classroomMembersQueries.pending && classroomMembersQueries.data.length > 0) {
134
157
  const seen = new Set();
135
- const uniqueMembers = flatMembers.filter(member => {
158
+ const uniqueMembers = classroomMembersQueries.data.filter(member => {
136
159
  if (seen.has(member.memberId)) return false;
137
160
  seen.add(member.memberId);
138
161
  return true;
@@ -160,13 +183,13 @@ function CreateAssignmentDialog({
160
183
  courses: filteredCourses
161
184
  });
162
185
  }, [selectedCourses, submittedExercises]);
163
- const classroomMembers = async selectedMembers => {
164
- const formattedMembers = await getMemberDetails(selectedMembers);
186
+ const classroomMembers = async uniqueClassroomMembers => {
187
+ const formattedMembers = await getMemberDetails(uniqueClassroomMembers);
165
188
  setMembers(formattedMembers);
166
189
  };
167
190
  (0, _react.useEffect)(() => {
168
- classroomMembers(selectedMembers);
169
- }, [selectedMembers]);
191
+ classroomMembers(uniqueClassroomMembers);
192
+ }, [uniqueClassroomMembers]);
170
193
  (0, _react.useEffect)(() => {
171
194
  setAssignment(prev => ({
172
195
  ...prev,
@@ -227,7 +250,7 @@ function CreateAssignmentDialog({
227
250
  const handleCreateAssignment = async () => {
228
251
  try {
229
252
  await Promise.all(assignment.classroomId.map(async cId => {
230
- const selectedClassroomMembers = selectedMembers.filter(member => member.classroomId === cId).map(student => student.memberId).filter(studentId => assignment?.assignedStudents.includes(studentId));
253
+ const selectedClassroomMembers = uniqueClassroomMembers.filter(member => member.classroomId === cId).map(student => student.memberId).filter(studentId => assignment?.assignedStudents.includes(studentId));
231
254
  const formattedAssignment = {
232
255
  ...assignment,
233
256
  classroomId: cId,
@@ -108,18 +108,6 @@ const PlanButton = (0, _system.styled)(_material.Button)(({
108
108
  border: bgPrimary ? `1px solid ${theme.palette.primary.dark}` : null
109
109
  }
110
110
  }));
111
- const Badge = (0, _system.styled)(_material.Typography)(({
112
- theme
113
- }) => ({
114
- backgroundColor: theme.palette.common.white,
115
- color: theme.palette.common.black,
116
- fontSize: theme.typography.pxToRem(10),
117
- borderRadius: "9px",
118
- padding: theme.spacing(0.25, 1),
119
- display: "flex",
120
- alignItems: "center",
121
- marginBottom: "2px"
122
- }));
123
111
  const Plus = (0, _system.styled)("img")(({
124
112
  theme
125
113
  }) => ({
@@ -239,8 +227,7 @@ function SubscriptionPlan({
239
227
  variant: "h3",
240
228
  priceSmall: displayPlan.priceSmall,
241
229
  children: displayPlan.price ? displayPlan.price : displayPrice
242
- }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_material.Grid, {
243
- direction: "column",
230
+ }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_material.Stack, {
244
231
  sx: {
245
232
  display: "flex",
246
233
  justifyContent: "center"
@@ -253,8 +240,18 @@ function SubscriptionPlan({
253
240
  children: t(displayPlan.footnote)
254
241
  })
255
242
  }),
256
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(Badge, {
257
- component: "div",
243
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_material.Box, {
244
+ sx: {
245
+ fontSize: theme => theme.typography.pxToRem(10),
246
+ backgroundColor: "common.white",
247
+ color: "common.black",
248
+ borderRadius: "9px",
249
+ py: 0.25,
250
+ px: 1,
251
+ display: "flex",
252
+ alignItems: "center",
253
+ mb: "2px"
254
+ },
258
255
  children: t(displayPlan.badge)
259
256
  })
260
257
  }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_jsxRuntime.Fragment, {}), priceInfo && priceInfo.interval && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_material.Typography, {
@@ -208,7 +208,7 @@ function CreatePhrase({
208
208
  };
209
209
 
210
210
  // // translation
211
- // const generateTranslation = async () => {
211
+ // const generateTranslations = async () => {
212
212
  // const translatedText = await handleTranslate(phrase, learnLang, forLang);
213
213
  // if (translatedText && translatedText !== "") {
214
214
  // setAutoTranslatedText(translatedText);
@@ -221,19 +221,35 @@ function CreatePhrase({
221
221
  // }
222
222
  // };
223
223
 
224
- const generateTranslation = async () => {
224
+ function validateResponse(response) {
225
+ if (!Array.isArray(response)) {
226
+ return false;
227
+ }
228
+ for (const item of response) {
229
+ if (item === null || Array.isArray(item)) {
230
+ return false;
231
+ }
232
+ if (typeof item !== "string" || item.trim() === "") {
233
+ return false;
234
+ }
235
+ }
236
+ return true;
237
+ }
238
+ const generateTranslations = async () => {
225
239
  try {
226
240
  let chatGptResponse = await makeChatGptApiRequest({
227
241
  model: "gpt-4o-mini",
228
- promptKey: "generateTranslation",
242
+ promptKey: "generateTranslations",
229
243
  promptVariables: {
230
244
  learnLang,
231
245
  forLang,
232
246
  phrase
233
247
  }
234
248
  });
235
- chatGptResponse = chatGptResponse.replaceAll("```", '').replaceAll("json", '');
236
- const translations = JSON.parse(chatGptResponse);
249
+ const translations = chatGptResponse.translations;
250
+ if (!validateResponse(translations)) {
251
+ throw new Error("Invalid AI response");
252
+ }
237
253
  setTranslationList(prevTranslationList => {
238
254
  const newTranslations = translations.filter(text => !prevTranslationList.includes(text));
239
255
  return [...prevTranslationList, ...newTranslations];
@@ -409,7 +425,7 @@ function CreatePhrase({
409
425
  children: t("add_alternative_version")
410
426
  })
411
427
  }), learnLang !== forLang && /*#__PURE__*/(0, _jsxRuntime.jsx)(_DefaultColourButton.default, {
412
- onClick: generateTranslation,
428
+ onClick: generateTranslations,
413
429
  disabled: !phrase || Array.isArray(translationList) && translationList.includes(autoTranslatedText),
414
430
  size: "sm",
415
431
  className: classes.alternativeVersionButton,
@@ -17,6 +17,26 @@ var _jsxRuntime = require("react/jsx-runtime");
17
17
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
18
18
  //components
19
19
 
20
+ function validateResponse(response) {
21
+ if (!Array.isArray(response)) {
22
+ return false;
23
+ }
24
+ for (const item of response) {
25
+ if (typeof item !== "object" || item === null || Array.isArray(item)) {
26
+ return false;
27
+ }
28
+ if (!Object.hasOwn(item, "question") || !Object.hasOwn(item, "answer")) {
29
+ return false;
30
+ }
31
+ if (typeof item.question !== "string" || item.question.trim() === "") {
32
+ return false;
33
+ }
34
+ if (typeof item.answer !== "string" || item.answer.trim() === "") {
35
+ return false;
36
+ }
37
+ }
38
+ return true;
39
+ }
20
40
  function GenerateBotDialog({
21
41
  t = text => text,
22
42
  isGenerateBotDialogOpen,
@@ -69,39 +89,42 @@ function GenerateBotDialog({
69
89
  const handleCloseDialog = () => {
70
90
  setIsGenerateBotDialogOpen(false);
71
91
  };
72
- const removeExtraTextFromChatGptResponse = response => {
73
- let stack = [];
74
- let startIndex = -1;
75
- let endIndex = -1;
76
- for (let i = 0; i < response.length; i++) {
77
- if (response[i] === "[") {
78
- if (stack.length === 0) {
79
- startIndex = i;
80
- }
81
- stack.push("[");
82
- } else if (response[i] === "]") {
83
- stack.pop();
84
- if (stack.length === 0) {
85
- endIndex = i;
86
- break;
87
- }
88
- }
89
- }
90
- if (startIndex !== -1 && endIndex !== -1) {
91
- return response.substring(startIndex, endIndex + 1);
92
- }
93
- return "";
94
- };
95
- function fixMissingComma(jsonString) {
96
- const fixedJsonString = jsonString.replace(/}\s*{/g, "},{");
97
- try {
98
- const jsonObject = JSON.parse(fixedJsonString);
99
- return jsonObject;
100
- } catch (error) {
101
- console.error("Invalid JSON after attempting to fix:", error);
102
- return null;
103
- }
104
- }
92
+
93
+ // const removeExtraTextFromChatGptResponse = (response) => {
94
+ // let stack = [];
95
+ // let startIndex = -1;
96
+ // let endIndex = -1;
97
+ // for (let i = 0; i < response.length; i++) {
98
+ // if (response[i] === "[") {
99
+ // if (stack.length === 0) {
100
+ // startIndex = i;
101
+ // }
102
+ // stack.push("[");
103
+ // } else if (response[i] === "]") {
104
+ // stack.pop();
105
+ // if (stack.length === 0) {
106
+ // endIndex = i;
107
+ // break;
108
+ // }
109
+ // }
110
+ // }
111
+ // if (startIndex !== -1 && endIndex !== -1) {
112
+ // return response.substring(startIndex, endIndex + 1);
113
+ // }
114
+ // return "";
115
+ // };
116
+
117
+ // function fixMissingComma(jsonString) {
118
+ // const fixedJsonString = jsonString.replace(/}\s*{/g, "},{");
119
+ // try {
120
+ // const jsonObject = JSON.parse(fixedJsonString);
121
+ // return jsonObject;
122
+ // } catch (error) {
123
+ // console.error("Invalid JSON after attempting to fix:", error);
124
+ // return null;
125
+ // }
126
+ // }
127
+
105
128
  const sampleQuestionArray = [{
106
129
  question: "What day comes after Monday? ",
107
130
  answer: "(Tuesday)"
@@ -137,7 +160,6 @@ function GenerateBotDialog({
137
160
  try {
138
161
  setIsBotGenerating(true);
139
162
  setErrorMessage("");
140
- let response;
141
163
  let chatGptResponse = await makeChatGptApiRequest({
142
164
  model: "gpt-4o-mini",
143
165
  promptKey: "generateBot",
@@ -154,9 +176,10 @@ function GenerateBotDialog({
154
176
  usePhrasesContext: usePhrasesContext
155
177
  }
156
178
  });
157
- response = chatGptResponse;
158
- const jsonResponse = removeExtraTextFromChatGptResponse(response);
159
- const botScript = fixMissingComma(jsonResponse);
179
+ const botScript = chatGptResponse.result;
180
+ if (!validateResponse(botScript)) {
181
+ throw new Error("Invalid AI response");
182
+ }
160
183
  setIsBotGenerating(false);
161
184
  return botScript;
162
185
  } catch (error) {
@@ -8,12 +8,68 @@ var _react = require("react");
8
8
  var _ResponsiveDialog = _interopRequireDefault(require("../ResponsiveDialog/ResponsiveDialog"));
9
9
  var _iconsMaterial = require("@mui/icons-material");
10
10
  var _material = require("@mui/material");
11
+ var _styles = require("@mui/material/styles");
11
12
  var _reactRouterDom = require("react-router-dom");
12
13
  var _Info = _interopRequireDefault(require("@mui/icons-material/Info"));
13
14
  var _NualaCreating = _interopRequireDefault(require("../../Misc/NualaCreating/NualaCreating"));
14
15
  var _InputHelper = _interopRequireDefault(require("../../Forms/InputHelper/InputHelper"));
16
+ var _mui = require("tss-react/mui");
15
17
  var _jsxRuntime = require("react/jsx-runtime");
16
18
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
19
+ const useStyles = (0, _mui.makeStyles)()(theme => ({
20
+ phraseText: {
21
+ width: "80%",
22
+ wordBreak: "break-all"
23
+ },
24
+ phraseTextSmall: {
25
+ width: "60%",
26
+ wordBreak: "break-all"
27
+ },
28
+ avatar: {
29
+ cursor: "pointer",
30
+ margin: theme.spacing()
31
+ },
32
+ avatarNoClick: {
33
+ margin: theme.spacing()
34
+ }
35
+ }));
36
+ function Phrase({
37
+ alternativeVersions,
38
+ translations,
39
+ phrase
40
+ }) {
41
+ const theme = (0, _styles.useTheme)();
42
+ const {
43
+ classes
44
+ } = useStyles();
45
+ const isLargeScreen = (0, _material.useMediaQuery)(theme.breakpoints.up("sm"));
46
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_material.ListItem, {
47
+ "data-cy": "phrase-list-item",
48
+ children: isLargeScreen ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_material.Grid, {
49
+ container: true,
50
+ sx: {
51
+ width: "100%"
52
+ },
53
+ className: classes.phraseText,
54
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_material.ListItemText, {
55
+ className: classes.phrases,
56
+ primary: `${phrase}${Array.isArray(alternativeVersions) && alternativeVersions.length ? " - " : ""}${(alternativeVersions || []).join(", ")}`,
57
+ secondary: (translations || []).join(", ")
58
+ })
59
+ }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_material.Grid, {
60
+ container: true,
61
+ sx: {
62
+ width: "100%"
63
+ },
64
+ className: classes.phraseTextSmall,
65
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_material.ListItemText, {
66
+ className: classes.phrases,
67
+ primary: `${phrase}${Array.isArray(alternativeVersions) && alternativeVersions.length ? " - " : ""}${(alternativeVersions || []).join(", ")}`,
68
+ secondary: (translations || []).join(", ")
69
+ })
70
+ })
71
+ });
72
+ }
17
73
  function GeneratePhrases({
18
74
  t,
19
75
  open,
@@ -31,6 +87,7 @@ function GeneratePhrases({
31
87
  const [values, setValues] = (0, _react.useState)(initialValues);
32
88
  const [errorMessage, setErrorMessage] = (0, _react.useState)(null);
33
89
  const [phraseType, setPhraseType] = (0, _react.useState)("words");
90
+ const [generatedPhrases, setGeneratedPhrases] = (0, _react.useState)([]);
34
91
  const isPhrases = currentPhrases && currentPhrases.length > 0;
35
92
  const phrases = isPhrases ? currentPhrases.map(({
36
93
  phrase,
@@ -56,29 +113,6 @@ function GeneratePhrases({
56
113
  [name]: value
57
114
  }));
58
115
  };
59
- const extractPhrasesArray = response => {
60
- let stack = [];
61
- let startIndex = -1;
62
- let endIndex = -1;
63
- for (let i = 0; i < response.length; i++) {
64
- if (response[i] === "[") {
65
- if (stack.length === 0) {
66
- startIndex = i;
67
- }
68
- stack.push("[");
69
- } else if (response[i] === "]") {
70
- stack.pop();
71
- if (stack.length === 0) {
72
- endIndex = i;
73
- break;
74
- }
75
- }
76
- }
77
- if (startIndex !== -1 && endIndex !== -1) {
78
- return response.substring(startIndex, endIndex + 1);
79
- }
80
- return "";
81
- };
82
116
  function validateResponse(response) {
83
117
  if (!Array.isArray(response)) {
84
118
  return false;
@@ -127,25 +161,32 @@ function GeneratePhrases({
127
161
  phrases
128
162
  }
129
163
  });
130
- const phrasesArray = extractPhrasesArray(chatGptResponse);
131
- const parsedPhrases = JSON.parse(phrasesArray);
132
- if (!validateResponse(parsedPhrases)) {
133
- throw new Error("Invalid response from GPT-3.5");
164
+ const phrasesArray = chatGptResponse.result;
165
+ if (!validateResponse(phrasesArray)) {
166
+ throw new Error("Invalid AI response");
134
167
  }
135
- handleAppendPhrases(parsedPhrases);
168
+ setGeneratedPhrases(phrasesArray);
136
169
  setIsGenerating(false);
137
- handleClose();
138
170
  } catch (error) {
139
171
  console.error(error);
140
172
  setIsGenerating(false);
141
173
  setErrorMessage("There was a problem generating phrases. Please try again.");
142
174
  }
143
175
  };
176
+ const handleConfirm = () => {
177
+ handleAppendPhrases(generatedPhrases);
178
+ setGeneratedPhrases([]);
179
+ handleClose();
180
+ };
181
+ const handleCancel = () => {
182
+ setGeneratedPhrases([]);
183
+ handleClose();
184
+ };
144
185
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_ResponsiveDialog.default, {
145
186
  open: open,
146
- handleClose: handleClose,
147
- handleSubmit: handleGeneratePhrases,
148
- submitText: t("generate_phrases"),
187
+ handleClose: handleCancel,
188
+ handleSubmit: generatedPhrases.length > 0 ? handleConfirm : handleGeneratePhrases,
189
+ submitText: generatedPhrases.length > 0 ? t("confirm") : t("generate_phrases"),
149
190
  dialogTitle: isGenerating ? t("generating_phrases") : t("generate_phrases"),
150
191
  isSubmitDisabled: isGenerating,
151
192
  isExperimentalFeature: true,
@@ -170,111 +211,119 @@ function GeneratePhrases({
170
211
  size: 12,
171
212
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_material.LinearProgress, {})
172
213
  }), !isGenerating && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
173
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_material.Typography, {
174
- fontSize: 14,
175
- sx: {
176
- opacity: 0.8,
177
- marginBottom: 1
178
- },
179
- children: t("required_field")
180
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_material.Grid, {
181
- size: 12,
182
- children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_material.Alert, {
183
- severity: "info",
184
- children: [t("experimental_feature_desc_phrases"), " -", " ", /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactRouterDom.Link, {
185
- to: "/contact",
186
- children: [t("contact_form"), "."]
187
- })]
188
- })
189
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_material.Grid, {
190
- size: 12,
191
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_InputHelper.default, {
192
- t: t,
193
- id: "phrasesTopic",
194
- name: "phrasesTopic",
195
- "data-cy": "phrasesTopic",
196
- label: t("choose_topic_for_phrases"),
197
- value: values.phrasesTopic,
198
- type: "text",
199
- margin: "normal",
200
- variant: "outlined",
201
- fullWidth: true,
202
- required: true,
203
- onChange: handleChange,
204
- multiline: true,
205
- rows: 3
206
- })
207
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_material.Grid, {
208
- mb: 2,
209
- size: 12,
210
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_material.TextField, {
211
- id: "topicGoal",
212
- name: "topicGoal",
213
- "data-cy": "topicGoal",
214
- label: t("topic_goal"),
215
- placeholder: t("topic_goal_placeholder"),
216
- value: values.topicGoal,
217
- type: "text",
218
- margin: "normal",
219
- variant: "outlined",
220
- fullWidth: true,
221
- onChange: handleChange,
222
- InputProps: {
223
- endAdornment: /*#__PURE__*/(0, _jsxRuntime.jsx)(_material.InputAdornment, {
224
- position: "end",
225
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_material.Tooltip, {
226
- title: t("topic_goal_info_text"),
227
- placement: "top",
228
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_Info.default, {
229
- fontSize: "small",
230
- style: {
231
- color: "lightgrey"
232
- }
214
+ children: [generatedPhrases.length === 0 && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
215
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_material.Typography, {
216
+ fontSize: 14,
217
+ sx: {
218
+ opacity: 0.8,
219
+ marginBottom: 1
220
+ },
221
+ children: t("required_field")
222
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_material.Grid, {
223
+ size: 12,
224
+ children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_material.Alert, {
225
+ severity: "info",
226
+ children: [t("experimental_feature_desc_phrases"), " -", " ", /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactRouterDom.Link, {
227
+ to: "/contact",
228
+ children: [t("contact_form"), "."]
229
+ })]
230
+ })
231
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_material.Grid, {
232
+ size: 12,
233
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_InputHelper.default, {
234
+ t: t,
235
+ id: "phrasesTopic",
236
+ name: "phrasesTopic",
237
+ "data-cy": "phrasesTopic",
238
+ label: t("choose_topic_for_phrases"),
239
+ value: values.phrasesTopic,
240
+ type: "text",
241
+ margin: "normal",
242
+ variant: "outlined",
243
+ fullWidth: true,
244
+ required: true,
245
+ onChange: handleChange,
246
+ multiline: true,
247
+ rows: 3
248
+ })
249
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_material.Grid, {
250
+ mb: 2,
251
+ size: 12,
252
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_material.TextField, {
253
+ id: "topicGoal",
254
+ name: "topicGoal",
255
+ "data-cy": "topicGoal",
256
+ label: t("topic_goal"),
257
+ placeholder: t("topic_goal_placeholder"),
258
+ value: values.topicGoal,
259
+ type: "text",
260
+ margin: "normal",
261
+ variant: "outlined",
262
+ fullWidth: true,
263
+ onChange: handleChange,
264
+ InputProps: {
265
+ endAdornment: /*#__PURE__*/(0, _jsxRuntime.jsx)(_material.InputAdornment, {
266
+ position: "end",
267
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_material.Tooltip, {
268
+ title: t("topic_goal_info_text"),
269
+ placement: "top",
270
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_Info.default, {
271
+ fontSize: "small",
272
+ style: {
273
+ color: "lightgrey"
274
+ }
275
+ })
233
276
  })
234
277
  })
278
+ }
279
+ })
280
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_material.Grid, {
281
+ mb: 2,
282
+ size: 12,
283
+ children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_material.TextField, {
284
+ select: true,
285
+ label: t("phrase_type"),
286
+ value: phraseType,
287
+ onChange: e => setPhraseType(e.target.value),
288
+ fullWidth: true,
289
+ variant: "outlined",
290
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_material.MenuItem, {
291
+ value: "words",
292
+ children: t("words")
293
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_material.MenuItem, {
294
+ value: "sentences",
295
+ children: t("sentences")
296
+ })]
297
+ })
298
+ }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_material.Grid, {
299
+ size: 12,
300
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_material.Typography, {
301
+ id: "input-slider",
302
+ gutterBottom: true,
303
+ children: t("how_many_phrases", {
304
+ phraseAmount: values.phraseAmount
235
305
  })
236
- }
237
- })
238
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_material.Grid, {
239
- mb: 2,
240
- size: 12,
241
- children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_material.TextField, {
242
- select: true,
243
- label: t("phrase_type"),
244
- value: phraseType,
245
- onChange: e => setPhraseType(e.target.value),
246
- fullWidth: true,
247
- variant: "outlined",
248
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_material.MenuItem, {
249
- value: "words",
250
- children: t("words")
251
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_material.MenuItem, {
252
- value: "sentences",
253
- children: t("sentences")
306
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_material.Slider, {
307
+ "aria-label": "Number of phrases",
308
+ defaultValue: values.phraseAmount,
309
+ valueLabelDisplay: "auto",
310
+ value: values.phraseAmount,
311
+ onChange: (e, value) => setValues({
312
+ ...values,
313
+ phraseAmount: value
314
+ }),
315
+ step: 2,
316
+ marks: true,
317
+ min: 4,
318
+ max: 16
254
319
  })]
255
- })
256
- }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_material.Grid, {
257
- size: 12,
258
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_material.Typography, {
259
- id: "input-slider",
260
- gutterBottom: true,
261
- children: t("how_many_phrases", {
262
- phraseAmount: values.phraseAmount
263
- })
264
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_material.Slider, {
265
- "aria-label": "Number of phrases",
266
- defaultValue: values.phraseAmount,
267
- valueLabelDisplay: "auto",
268
- value: values.phraseAmount,
269
- onChange: (e, value) => setValues({
270
- ...values,
271
- phraseAmount: value
272
- }),
273
- step: 2,
274
- marks: true,
275
- min: 4,
276
- max: 16
277
320
  })]
321
+ }), generatedPhrases.length > 0 && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
322
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_material.Typography, {
323
+ children: t("generated_phrases")
324
+ }), generatedPhrases.map((phrase, index) => /*#__PURE__*/(0, _jsxRuntime.jsx)(Phrase, {
325
+ ...phrase
326
+ }, index))]
278
327
  }), errorMessage && /*#__PURE__*/(0, _jsxRuntime.jsx)(_material.Grid, {
279
328
  mt: 2,
280
329
  size: 12,
@@ -87,7 +87,7 @@ function GenerateQuestion({
87
87
  try {
88
88
  setIsMessageGenerating(true);
89
89
  setErrorMessage(null);
90
- const chatGptResponse = await makeChatGptApiRequest({
90
+ const newQuestion = await makeChatGptApiRequest({
91
91
  model: "gpt-4o-mini",
92
92
  promptKey: "generateAnswer",
93
93
  promptVariables: {
@@ -100,8 +100,6 @@ function GenerateQuestion({
100
100
  questionLanguage
101
101
  }
102
102
  });
103
- const jsonResponse = extractJsonObject(chatGptResponse);
104
- const newQuestion = JSON.parse(jsonResponse);
105
103
  if (!validateResponse(newQuestion)) {
106
104
  throw new Error("Invalid response from GPT-3.5");
107
105
  }
@@ -231,25 +231,37 @@ function GenerateRoleplayDialog({
231
231
  }
232
232
  });
233
233
  let isComplete = false;
234
+ let fullResult = [];
234
235
  while (!isComplete) {
235
- if (chatGptResponse.endsWith("...") || chatGptResponse.trim().endsWith(",")) {
236
+ let parsed;
237
+ try {
238
+ parsed = typeof chatGptResponse === "string" ? JSON.parse(chatGptResponse) : chatGptResponse;
239
+ } catch (e) {
240
+ console.warn("Failed to parse response – assuming truncation");
241
+ parsed = {
242
+ result: []
243
+ }; // fallback
244
+ }
245
+ if (Array.isArray(parsed.result)) {
246
+ fullResult = [...fullResult, ...parsed.result];
247
+ }
248
+ // Check last item for signs of truncation
249
+ const lastItem = parsed.result?.[parsed.result.length - 1];
250
+ const endsSuspiciously = lastItem?.text?.trim?.().endsWith("...") || lastItem?.text?.trim?.().endsWith(",");
251
+ if (endsSuspiciously) {
252
+ const continuationPrompt = `Continue the following in the same JSON format:\n\n${JSON.stringify(parsed.result, null, 2)}`;
236
253
  const continuationResponse = await makeChatGptApiRequest({
237
254
  model: "gpt-4o-mini",
238
- prompt: "Continue: " + chatGptResponse
255
+ prompt: continuationPrompt
239
256
  });
240
- chatGptResponse += continuationResponse;
257
+ chatGptResponse = continuationResponse;
241
258
  } else {
242
259
  isComplete = true;
243
260
  }
244
261
  }
245
- response = chatGptResponse;
262
+ response = fullResult;
246
263
  }
247
-
248
- // Log the full response for debugging
249
-
250
- const jsonResponse = removeExtraTextFromChatGptResponse(response);
251
- const roleplayScript = fixMissingComma(jsonResponse);
252
- if (validateScript(roleplayScript)) {
264
+ if (validateScript(response)) {
253
265
  const generatedRoleplay = {
254
266
  roleplayId: (0, _utils.randomId)(),
255
267
  roleplayName: values.roleplayTopic,
@@ -260,7 +272,7 @@ function GenerateRoleplayDialog({
260
272
  learnLang: learnLang,
261
273
  translationEnabled: "enable",
262
274
  repliesEnabled: "enable",
263
- script: roleplayScript,
275
+ script: response,
264
276
  actor1: {
265
277
  name: getAllVoices()[0].name,
266
278
  voice: getAllVoices()[0].name,
@@ -88,8 +88,7 @@ const useStyles = (0, _mui.makeStyles)()((theme, {
88
88
  color: theme.palette.text.secondary
89
89
  },
90
90
  characterDropdown: {
91
- margin: theme.spacing(1),
92
- marginTop: "26px"
91
+ margin: theme.spacing(1)
93
92
  },
94
93
  formattingOptionsButton: {
95
94
  backgroundColor: isFormattingOptionsOpen ? theme.palette.action.selected : null
@@ -1423,7 +1423,7 @@ function Classroom({
1423
1423
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_ResponsiveTabs.default, {
1424
1424
  t: t,
1425
1425
  tabs: tabs,
1426
- fabs: isCreator && isVideoChatEnabled && isVideoChatEnabledInSettings && !isArchived ? courses?.length ? fabs : [[vchatFab]] : isCreator && courses?.length && !isArchived ? [[fabs[0][0]]] : [],
1426
+ fabs: isCreator && isVideoChatEnabled && isVideoChatEnabledInSettings && !isArchived ? courses?.length ? fabs : [[vchatFab]] : isCreator && courses?.length && !isArchived ? [[fabs[0][0], assignmentsFab]] : [],
1427
1427
  centered: true,
1428
1428
  scrollable: true,
1429
1429
  hasNewAssignments: hasNewAssignments,
@@ -1480,7 +1480,7 @@ function Classroom({
1480
1480
  open: isInviteDialogOpen,
1481
1481
  handleClose: handleCloseInviteDialog,
1482
1482
  onSubmit: handleInvites,
1483
- inviteLink: `${window.location.href}?plan=learn&learnlang=${classroom.learnLang}`,
1483
+ inviteLink: `${window.location.origin}/classrooms/${classroomId}?plan=learn&learnlang=${classroom.learnLang}`,
1484
1484
  enrolmentKey: classroom.visibility === "private" ? classroom.enrolmentKey : "",
1485
1485
  inviteType: "classroom",
1486
1486
  attributes: attributes,
@@ -960,7 +960,7 @@ function Course({
960
960
  open: isInviteDialogOpen,
961
961
  handleClose: handleCloseInviteDialog,
962
962
  onSubmit: handleInvites,
963
- inviteLink: `${window.location.href}?plan=learn&learnlang=${course.learnLang}`,
963
+ inviteLink: `${window.location.origin}/courses/${courseId}?plan=learn&learnlang=${course.learnLang}`,
964
964
  enrolmentKey: course.visibility === "private" ? course.enrolmentKey : "",
965
965
  inviteType: "course",
966
966
  attributes: attributes,
@@ -346,10 +346,10 @@ function AssignmentCellData({
346
346
  if (filter === "percentage_complete" && data.percentComplete) {
347
347
  cellData = data.percentComplete;
348
348
  cellData.type = "percent";
349
- } else if (filter === "correct_answer_percentage" && data.percentCorrect) {
349
+ } else if (filter === "correct_answer_percentage" && data.percentCorrect !== null) {
350
350
  cellData = data.percentCorrect;
351
351
  cellData.type = "correct";
352
- } else if (filter === "avg_pronunciation_score" && data.avgPronunciationScore) {
352
+ } else if (filter === "avg_pronunciation_score" && data.avgPronunciationScore !== null) {
353
353
  cellData = data.avgPronunciationScore;
354
354
  cellData.type = "avgPronuc";
355
355
  }
@@ -89,25 +89,16 @@ const formatMemberCourseCompletions = (courses = [], completions = [], assignmen
89
89
  };
90
90
  exports.formatMemberCourseCompletions = formatMemberCourseCompletions;
91
91
  const formatMemberAssignmentCompletions = (assignments = [], courses = [], completions = []) => {
92
- const stats = assignments.reduce((obj, v) => {
93
- const assignmentCourseCompletions = formatMemberCourseCompletions(courses, completions, v?.exercises);
94
- const result = Object.values(assignmentCourseCompletions).reduce((a, course) => {
95
- if (a && course && course.percentComplete) {
96
- a.percentComplete.completed += course.percentComplete.completed;
97
- a.percentComplete.total += course.percentComplete.total;
98
- a.percentComplete.percentage = Math.floor(a.percentComplete.completed / a.percentComplete.total * 100);
99
- }
100
- if (a && course && course.percentCorrect) {
101
- a.percentCorrect.completed += course.percentCorrect.completed;
102
- a.percentCorrect.total += course.percentCorrect.total;
103
- a.percentCorrect.percentage = Math.floor(a.percentCorrect.completed / a.percentCorrect.total * 100);
104
- }
105
- if (a && course && course.avgPronunciationScore && course.avgPronunciationScore.percentage > 0) {
106
- a.avgPronunciationScore.total += 1;
107
- a.avgPronunciationScore.percentage = Math.floor(course.avgPronunciationScore.percentage / a.avgPronunciationScore.total);
108
- }
109
- return a;
110
- }, {
92
+ const stats = assignments.reduce((obj, assignment) => {
93
+ const assignmentCompletions = completions.filter(completion => assignment.exercises?.some(e => {
94
+ if (e.name && completion.exercise !== e.name) return false;
95
+ if (e.roleplayId && completion.roleplayId !== e.roleplayId) return false;
96
+ if (e.botId && completion.botId !== e.botId) return false;
97
+ if (e.courseSectionTopicId && `${completion.courseSectionId}|${completion.topicId}` !== e.courseSectionTopicId) return false;
98
+ return true;
99
+ }));
100
+ const assignmentCourseCompletions = formatMemberCourseCompletions(courses, assignmentCompletions, assignment?.exercises);
101
+ const accumulator = {
111
102
  percentComplete: {
112
103
  completed: 0,
113
104
  total: 0,
@@ -120,10 +111,35 @@ const formatMemberAssignmentCompletions = (assignments = [], courses = [], compl
120
111
  },
121
112
  avgPronunciationScore: {
122
113
  total: 0,
114
+ sum: 0,
123
115
  percentage: 0
124
116
  }
117
+ };
118
+ Object.values(assignmentCourseCompletions).forEach(course => {
119
+ if (course?.percentComplete) {
120
+ accumulator.percentComplete.completed += course.percentComplete.completed || 0;
121
+ accumulator.percentComplete.total += course.percentComplete.total || 0;
122
+ }
123
+ if (course?.percentCorrect) {
124
+ accumulator.percentCorrect.completed += course.percentCorrect.completed || 0;
125
+ accumulator.percentCorrect.total += course.percentCorrect.total || 0;
126
+ }
127
+ if (course?.avgPronunciationScore && course.avgPronunciationScore.percentage >= 0) {
128
+ accumulator.avgPronunciationScore.total += 1;
129
+ accumulator.avgPronunciationScore.sum += course.avgPronunciationScore.percentage;
130
+ }
125
131
  });
126
- obj[v.assignmentId] = result;
132
+ if (accumulator.percentComplete.total > 0) {
133
+ accumulator.percentComplete.percentage = Math.floor(accumulator.percentComplete.completed / accumulator.percentComplete.total * 100);
134
+ }
135
+ if (accumulator.percentCorrect.total > 0) {
136
+ accumulator.percentCorrect.percentage = Math.floor(accumulator.percentCorrect.completed / accumulator.percentCorrect.total * 100);
137
+ }
138
+ if (accumulator.avgPronunciationScore.total > 0) {
139
+ accumulator.avgPronunciationScore.percentage = Math.floor(accumulator.avgPronunciationScore.sum / accumulator.avgPronunciationScore.total);
140
+ }
141
+ delete accumulator.avgPronunciationScore.sum;
142
+ obj[assignment.assignmentId] = accumulator;
127
143
  return obj;
128
144
  }, {});
129
145
  return stats;
@@ -75,7 +75,7 @@ const RecordingListCards = (0, _mui.withStyles)(({
75
75
  variant: "headline",
76
76
  color: "inherit",
77
77
  fontSize: 24,
78
- children: t("recordings")
78
+ children: t("submissions")
79
79
  })
80
80
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
81
81
  className: classes.grow
@@ -144,7 +144,15 @@ const getScoreValues = (type, id, completions) => {
144
144
  percentCorrect.total = totalAnswers;
145
145
  percentCorrect.percentage = totalAnswers > 0 ? Math.floor(totalAnswersCorrect / totalAnswers * 100) : 0;
146
146
  const avgPronunciationScore = pronunciationScoreCount > 0 ? Math.floor(totalPronunciationScore / pronunciationScoreCount) : -1;
147
- return [percentCorrect, avgPronunciationScore];
147
+ if (totalAnswers > 0) {
148
+ return [percentCorrect, avgPronunciationScore];
149
+ } else {
150
+ return [{
151
+ completed: 0,
152
+ total: 0,
153
+ percentage: 0
154
+ }, avgPronunciationScore];
155
+ }
148
156
  };
149
157
  exports.getScoreValues = getScoreValues;
150
158
  const calcTopicCompletions = (topic, completions = [], isSectionHidden, assignmentExercises) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nualang/nualang-ui-components",
3
- "version": "0.1.1254",
3
+ "version": "0.1.1256",
4
4
  "main": "dist/index.js",
5
5
  "files": [
6
6
  "dist",