@nualang/nualang-ui-components 0.1.1414 → 0.1.1416

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (243) hide show
  1. package/dist/Cards/Course/Course.js +145 -13
  2. package/dist/Chat/Messages/Message/Message.js +4 -1
  3. package/dist/Chat/Messages/Message/Message.test.js +55 -0
  4. package/dist/Containers/App/App.js +25 -8
  5. package/dist/Dialogs/FindChatbotDialog/FindChatbotDialog.js +23 -9
  6. package/dist/Dialogs/FindScenarioDialog/FindScenarioDialog.js +23 -9
  7. package/dist/Dialogs/FindTopicsDialog/FindTopicsDialog.js +22 -8
  8. package/dist/Dialogs/GenerateBot/GenerateBot.js +29 -8
  9. package/dist/Dialogs/GenerateRoleplay/GenerateRoleplay.js +5 -2
  10. package/dist/Dialogs/QuickCreateChatbotDialog/QuickCreateChatbotDialog.js +22 -4
  11. package/dist/Editors/Bot/Bot.js +54 -11
  12. package/dist/Editors/Bot/Editor/Editor.js +78 -49
  13. package/dist/Editors/Bot/Editor/QuestionsEditor/AnswerOptionsList.js +94 -0
  14. package/dist/Editors/Bot/Editor/QuestionsEditor/QuestionRow.js +211 -0
  15. package/dist/Editors/Bot/Editor/QuestionsEditor/QuestionsEditor.js +115 -0
  16. package/dist/Editors/Roleplay/Roleplay.js +5 -2
  17. package/dist/Exercises/ListenAndSpeak/ListenAndSpeak.js +1 -1
  18. package/dist/Exercises/Listener/Listener.js +1 -1
  19. package/dist/Exercises/Meaning/Meaning.js +1 -1
  20. package/dist/Exercises/MeaningReadWrite/MeaningReadWriteGame.js +4 -1
  21. package/dist/Exercises/Pronouncer/Pronouncer.js +1 -1
  22. package/dist/Exercises/Roleplay/Roleplay.js +5 -2
  23. package/dist/Exercises/Translator/Translator.js +1 -1
  24. package/dist/Forms/CreateBot/Steps/BotInformation/BotInformation.js +13 -2
  25. package/dist/Forms/CreateRoleplay/Steps/RoleplayInformation/RoleplayInformation.js +4 -2
  26. package/dist/Forms/UpdatePhraseList/UpdatePhraseList.js +5 -3
  27. package/dist/Forms/VoiceSelector/VoiceSelector.js +29 -14
  28. package/dist/Forms/VoiceSelector/VoiceSelector.test.js +125 -3
  29. package/dist/Lists/CourseOutline/CourseOutline.js +4 -0
  30. package/dist/Lists/Courses/Courses.js +5 -2
  31. package/dist/Lists/Courses/Courses.test.js +10 -0
  32. package/dist/Lists/Exercises/Exercises.js +6 -3
  33. package/dist/Lists/Roleplays/Roleplays.js +5 -2
  34. package/dist/Live/LiveListener/LiveListener.js +1 -1
  35. package/dist/Live/LiveMeaning/LiveMeaning.js +1 -1
  36. package/dist/Live/LiveMeaningReadWrite/LiveMeaningReadWrite.js +1 -1
  37. package/dist/Live/LiveTranslator/LiveTranslator.js +1 -1
  38. package/dist/Misc/ChatBubble/ChatBubble.js +2 -0
  39. package/dist/Misc/HoverText/HoverText.js +7 -2
  40. package/dist/Misc/NotFound/Classroom.js +1 -1
  41. package/dist/Misc/NotFound/Course.js +1 -1
  42. package/dist/hooks/useExerciseState.js +2 -0
  43. package/dist/index.js +1 -1
  44. package/dist/theme/getComponents.js +5 -1
  45. package/dist/utils/index.js +138 -0
  46. package/dist/utils/voiceImages.js +19 -4
  47. package/dist/utils/voices.js +12 -2
  48. package/package.json +1 -1
  49. package/dist/Cards/SubmissionsCard/SubmissionsCard.js +0 -138
  50. package/dist/Cards/index.js +0 -2
  51. package/dist/Dialogs/Code/Code.js +0 -147
  52. package/dist/Dialogs/Code/Code.test.js +0 -20
  53. package/dist/Dialogs/Code/package.json +0 -6
  54. package/dist/Dialogs/CreateTopic/CreateTopic.js +0 -135
  55. package/dist/Dialogs/CreateTopic/CreateTopic.test.js +0 -15
  56. package/dist/Dialogs/CreateTopic/package.json +0 -6
  57. package/dist/Dialogs/FeedbackDialog/FeedbackDialog.js +0 -123
  58. package/dist/Dialogs/Icons/Icons.js +0 -18
  59. package/dist/Dialogs/Icons/package.json +0 -6
  60. package/dist/Dialogs/InactivePopover/InactivePopover.js +0 -71
  61. package/dist/Dialogs/InviteDialog/upload-emails-csv.png +0 -0
  62. package/dist/Dialogs/PDFViewer/pdf24.pdf +0 -0
  63. package/dist/Dialogs/SearchTopic/SearchTopic.js +0 -87
  64. package/dist/Dialogs/UploadPhrases/upload-phrases-csv.png +0 -0
  65. package/dist/Exercises/Roleplay/OriginalRoleplay.js +0 -944
  66. package/dist/Forms/CreateBot/CreateBot.js +0 -77
  67. package/dist/Forms/CreateBot/CreateBot.test.js +0 -20
  68. package/dist/Forms/CreateBot/package.json +0 -6
  69. package/dist/Forms/CreateClassroom/CreateClassroom.js +0 -56
  70. package/dist/Forms/CreateClassroom/CreateClassroom.test.js +0 -20
  71. package/dist/Forms/CreateClassroom/package.json +0 -6
  72. package/dist/Forms/CreateCourse/CreateCourse.js +0 -66
  73. package/dist/Forms/CreateCourse/CreateCourse.test.js +0 -20
  74. package/dist/Forms/CreateCourse/package.json +0 -6
  75. package/dist/Forms/CreateMeetingMultiStepForm/images/input1.js +0 -2
  76. package/dist/Forms/CreateMeetingMultiStepForm/images/input5.js +0 -2
  77. package/dist/Forms/CreateMeetingMultiStepForm/images/mode1.js +0 -2
  78. package/dist/Forms/CreateMeetingMultiStepForm/images/mode2.js +0 -2
  79. package/dist/Forms/CreateMeetingMultiStepForm/sampleTemplates.js +0 -44
  80. package/dist/Forms/CreateRoleplay/CreateRoleplay.js +0 -87
  81. package/dist/Forms/CreateRoleplay/CreateRoleplay.test.js +0 -20
  82. package/dist/Forms/CreateRoleplay/package.json +0 -6
  83. package/dist/Forms/CreateUser/CreateUser.js +0 -77
  84. package/dist/Forms/FeedbackForm/FeedbackForm.js +0 -1036
  85. package/dist/Forms/MultiStepFormDialog/MultiStepFormDialog.js +0 -354
  86. package/dist/Forms/MultiStepFormDialog/MultiStepFormDialog.test.js +0 -20
  87. package/dist/Forms/MultiStepFormDialog/package.json +0 -6
  88. package/dist/Forms/MultiStepFormHook/MultiStepFormHook.js +0 -279
  89. package/dist/Forms/RecordingInfo/RecordingInfo.js +0 -140
  90. package/dist/Lists/Categories/sample.png +0 -0
  91. package/dist/Lists/Sections/Sections.js +0 -238
  92. package/dist/Lists/Sections/Sections.test.js +0 -20
  93. package/dist/Lists/Sections/package.json +0 -6
  94. package/dist/Lists/Voices/horse.ogm +0 -0
  95. package/dist/Misc/AxiosRestExample/AxiosRestExample.js +0 -46
  96. package/dist/Misc/AxiosRestExample/AxiosRestPostExample.js +0 -65
  97. package/dist/Misc/ColorSelector/ColorSelector.js +0 -143
  98. package/dist/Misc/ColorSelector/package.json +0 -6
  99. package/dist/Misc/EmojiInput/css/emoji-mart.css +0 -497
  100. package/dist/Misc/MedalSelector/PlaceholderMedal.svg +0 -27
  101. package/dist/Misc/NotFound/Bot.js +0 -62
  102. package/dist/Misc/NotFound/Bots.js +0 -75
  103. package/dist/Misc/NotFound/Discussions.js +0 -52
  104. package/dist/Misc/NotFound/Roleplay.js +0 -62
  105. package/dist/Misc/NotFound/Roleplays.js +0 -75
  106. package/dist/Misc/NotFound/Script.js +0 -47
  107. package/dist/Misc/Notes/Notes.js +0 -77
  108. package/dist/Misc/Notes/Notes.test.js +0 -20
  109. package/dist/Misc/Notes/package.json +0 -6
  110. package/dist/Misc/NualaCreating/Nuala1.js +0 -352
  111. package/dist/Misc/OnboardingEnrolment/CheckIfCourse.js +0 -33
  112. package/dist/Misc/OnboardingEnrolment/ClassCodeInvalid.js +0 -14
  113. package/dist/Misc/OnboardingEnrolment/ClassroomCodeInvalid.js +0 -14
  114. package/dist/Misc/OnboardingEnrolment/EnrolKey.js +0 -14
  115. package/dist/Misc/RoleplayChatWaveForm/RoleplayChatWaveForm.js +0 -199
  116. package/dist/Misc/RoleplayChatWaveForm/package.json +0 -6
  117. package/dist/Misc/RoleplayWaveform/RoleplayWaveform.js +0 -95
  118. package/dist/Misc/RoleplayWaveform/package.json +0 -6
  119. package/dist/Misc/SpeedDial/SpeedDial.js +0 -49
  120. package/dist/Misc/SpeedDial/package.json +0 -6
  121. package/dist/Misc/SubmitTextField/SubmitTextField.js +0 -48
  122. package/dist/Misc/newGenerateCardBackground/Generate Card Background (standalone) (1).html +0 -186
  123. package/dist/Navigation/ResponsiveStepper/ResponsiveStepper.js +0 -253
  124. package/dist/Navigation/ResponsiveStepper/ResponsiveStepper.test.js +0 -20
  125. package/dist/Navigation/ResponsiveStepper/package.json +0 -6
  126. package/dist/Navigation/ResponsiveTabs/TabContainer.js +0 -28
  127. package/dist/Tables/CreatedDiscussionsTable/CreatedDiscussionsTable.js +0 -188
  128. package/dist/Tables/GameRows/GameRows.js +0 -87
  129. package/dist/Tables/GameRows/package.json +0 -6
  130. package/dist/Tables/Games/Games.js +0 -258
  131. package/dist/Tables/Games/package.json +0 -6
  132. package/dist/Tables/Phrases/Phrases.js +0 -200
  133. package/dist/Tables/Phrases/Phrases.test.js +0 -20
  134. package/dist/Tables/Phrases/package.json +0 -6
  135. package/dist/Tables/Plans/Plans.js +0 -74
  136. package/dist/Tables/Plans/plans.json +0 -162
  137. package/dist/Tables/Plans/plans2.json +0 -151
  138. package/dist/Tables/Progress/Header.js +0 -559
  139. package/dist/Tables/Progress/README.md +0 -53
  140. package/dist/Tables/SubmissionsTableCards/SubmissionsTableCards.js +0 -296
  141. package/dist/Tables/Users/UsersTable.js +0 -159
  142. package/dist/img/Bronze Medal.svg +0 -545
  143. package/dist/img/Bronze.svg +0 -349
  144. package/dist/img/BronzeBadge.svg +0 -493
  145. package/dist/img/BronzeBadgeWithoutRibbon.svg +0 -317
  146. package/dist/img/Frame 51.svg +0 -98
  147. package/dist/img/Frame 52.svg +0 -131
  148. package/dist/img/Frame 53.svg +0 -139
  149. package/dist/img/Gold Badge.svg +0 -362
  150. package/dist/img/Gold Medal.svg +0 -481
  151. package/dist/img/GoldBadge.svg +0 -470
  152. package/dist/img/GoldBadgeWithoutRibbon.svg +0 -324
  153. package/dist/img/NualaCreateImage.svg +0 -129
  154. package/dist/img/NualangLiveAnwerQuicklyListening.svg +0 -3608
  155. package/dist/img/NualangLiveListen.png +0 -0
  156. package/dist/img/NualangLiveListenQuickly.png +0 -0
  157. package/dist/img/NualangLiveListenToPhrase.svg +0 -4286
  158. package/dist/img/NualangLivePickAnswer.svg +0 -134
  159. package/dist/img/NualangLiveResult.png +0 -0
  160. package/dist/img/NualangLiveRoleplayAnswer.png +0 -0
  161. package/dist/img/NualangLiveRoleplayConvo.png +0 -0
  162. package/dist/img/NualangLiveSeePlace.svg +0 -134
  163. package/dist/img/NualangLiveTranslate.png +0 -0
  164. package/dist/img/NualangLiveTranslateQuickly.png +0 -0
  165. package/dist/img/NualangLiveViewConversations.svg +0 -135
  166. package/dist/img/Polly.svg +0 -18
  167. package/dist/img/Silver Badge.svg +0 -523
  168. package/dist/img/Silver Medal.svg +0 -549
  169. package/dist/img/SilverBadge.svg +0 -655
  170. package/dist/img/SilverBadgeWithoutRibbon.svg +0 -491
  171. package/dist/img/avatars/0-nuala-001.svg +0 -34
  172. package/dist/img/avatars/0-nuala-002.svg +0 -25
  173. package/dist/img/avatars/0-nuala-003.svg +0 -27
  174. package/dist/img/avatars/0-nuala-004.svg +0 -41
  175. package/dist/img/avatars/0-nuala-005.svg +0 -33
  176. package/dist/img/avatars/0-nuala-006.svg +0 -32
  177. package/dist/img/avatars/0-nuala-007.svg +0 -37
  178. package/dist/img/avatars/0-nuala-008.svg +0 -21
  179. package/dist/img/avatars/0-nuala-009.svg +0 -21
  180. package/dist/img/avatars/0-nuala-010.svg +0 -39
  181. package/dist/img/avatars/0-nuala-011.svg +0 -33
  182. package/dist/img/avatars/0-nuala-012.svg +0 -43
  183. package/dist/img/avatars/0-nuala-013.svg +0 -36
  184. package/dist/img/avatars/0-nuala-014.svg +0 -30
  185. package/dist/img/avatars/0-nuala-015.svg +0 -28
  186. package/dist/img/avatars/0-nuala-016.svg +0 -24
  187. package/dist/img/avatars/0-nuala-017.svg +0 -31
  188. package/dist/img/avatars/0-nuala-018.svg +0 -29
  189. package/dist/img/avatars/0-nuala-019.svg +0 -57
  190. package/dist/img/avatars/0-nuala-020.svg +0 -30
  191. package/dist/img/avatars/0-nuala-021.svg +0 -28
  192. package/dist/img/avatars/001-man.svg +0 -104
  193. package/dist/img/avatars/002-girl.svg +0 -92
  194. package/dist/img/avatars/006-woman-1.svg +0 -99
  195. package/dist/img/avatars/028-woman-7.svg +0 -95
  196. package/dist/img/avatars/029-man-8.svg +0 -95
  197. package/dist/img/avatars/035-woman-9.svg +0 -86
  198. package/dist/img/avatars/066-boy-6.svg +0 -91
  199. package/dist/img/avatars/1-teacher-001.svg +0 -38
  200. package/dist/img/avatars/1-teacher-002.svg +0 -52
  201. package/dist/img/avatars/1-teacher-003.svg +0 -86
  202. package/dist/img/avatars/avatar1.png +0 -0
  203. package/dist/img/avatars/avatar2.png +0 -0
  204. package/dist/img/avatars/nuala_10_speaking.svg +0 -45
  205. package/dist/img/avatars/nuala_11_speaking.svg +0 -41
  206. package/dist/img/avatars/nuala_12_speaking.svg +0 -49
  207. package/dist/img/avatars/nuala_13_speaking.svg +0 -44
  208. package/dist/img/avatars/nuala_14_speaking.svg +0 -36
  209. package/dist/img/avatars/nuala_15_speaking.svg +0 -36
  210. package/dist/img/avatars/nuala_16_speaking.svg +0 -32
  211. package/dist/img/avatars/nuala_17_speaking.svg +0 -39
  212. package/dist/img/avatars/nuala_18_speaking.svg +0 -37
  213. package/dist/img/avatars/nuala_19_speaking.svg +0 -65
  214. package/dist/img/avatars/nuala_1_celebrating.svg +0 -30
  215. package/dist/img/avatars/nuala_1_speaking.svg +0 -37
  216. package/dist/img/avatars/nuala_20_speaking.svg +0 -38
  217. package/dist/img/avatars/nuala_21_speaking.svg +0 -36
  218. package/dist/img/avatars/nuala_2_speaking.svg +0 -29
  219. package/dist/img/avatars/nuala_3_speaking.svg +0 -28
  220. package/dist/img/avatars/nuala_4_speaking.svg +0 -41
  221. package/dist/img/avatars/nuala_5_speaking.svg +0 -36
  222. package/dist/img/avatars/nuala_6_speaking.svg +0 -36
  223. package/dist/img/avatars/nuala_7_speaking.svg +0 -43
  224. package/dist/img/avatars/nuala_8_speaking.svg +0 -29
  225. package/dist/img/avatars/nuala_9_speaking.svg +0 -27
  226. package/dist/img/dashboard.svg +0 -104
  227. package/dist/img/defaultImg.png +0 -0
  228. package/dist/img/nua-glyph.svg +0 -6
  229. package/dist/img/nuala-silhouette.svg +0 -43
  230. package/dist/img/nuala_1_celebrating.svg +0 -33
  231. package/dist/img/nuala_2_celebrating.svg +0 -20
  232. package/dist/img/nuala_3_celebrating.svg +0 -21
  233. package/dist/img/nuala_4_celebrating.svg +0 -30
  234. package/dist/img/nuala_5_celebrating.svg +0 -30
  235. package/dist/img/nuala_celebrating.svg +0 -39
  236. package/dist/img/nuala_speaking_4.svg +0 -41
  237. package/dist/img/nualas.svg +0 -185
  238. package/dist/img/soundWave.png +0 -0
  239. package/dist/img/stars1.svg +0 -44
  240. package/dist/img/stars2.svg +0 -30
  241. package/dist/img/teacher.svg +0 -89
  242. package/dist/img/work-chat.svg +0 -1
  243. package/dist/img/world.svg +0 -41
@@ -2,8 +2,10 @@ import { useState, memo } from "react";
2
2
  import PropTypes from "prop-types";
3
3
  import { useInView } from "react-intersection-observer";
4
4
  // Mui
5
- import { Button, Box, Typography, IconButton, Menu, MenuItem, Checkbox, Tooltip, CardActions, CardActionArea, Divider } from "@mui/material";
5
+ import { Button, Box, Typography, IconButton, Menu, MenuItem, Checkbox, Tooltip, CardActions, CardActionArea, Divider, Chip } from "@mui/material";
6
6
  import Skeleton from "@mui/material/Skeleton";
7
+ import { styled } from "@mui/material/styles";
8
+ import { tooltipClasses } from "@mui/material/Tooltip";
7
9
  import Group from "@mui/icons-material/Group";
8
10
  import ExitToApp from "@mui/icons-material/ExitToApp";
9
11
  import Close from "@mui/icons-material/Close";
@@ -159,6 +161,141 @@ function OverflowMenu({
159
161
  })]
160
162
  });
161
163
  }
164
+ const DIFFICULTY_ORDER = ["A1", "A2", "B1", "B2", "C1", "C2"];
165
+ const MAX_VISIBLE_DIFFICULTIES = 2;
166
+ const DifficultyOverflowTooltip = styled(({
167
+ className,
168
+ ...props
169
+ }) => /*#__PURE__*/_jsx(Tooltip, {
170
+ ...props,
171
+ classes: {
172
+ popper: className
173
+ }
174
+ }))(({
175
+ theme
176
+ }) => ({
177
+ [`& .${tooltipClasses.tooltip}`]: {
178
+ backgroundColor: theme.palette.background.paper,
179
+ color: theme.palette.text.primary,
180
+ border: `1px solid ${theme.palette.divider}`,
181
+ borderRadius: theme.shape.borderRadius,
182
+ boxShadow: theme.shadows[4],
183
+ padding: theme.spacing(1.5, 2),
184
+ minWidth: 160,
185
+ maxWidth: 260
186
+ }
187
+ }));
188
+ function normalizeDifficulties(difficulty) {
189
+ const values = Array.isArray(difficulty) ? difficulty : difficulty ? [difficulty] : [];
190
+ return DIFFICULTY_ORDER.filter(level => values.includes(level));
191
+ }
192
+ function difficultyChipSx(theme, compact) {
193
+ return {
194
+ height: compact ? 20 : 24,
195
+ backgroundColor: theme.palette.action.selected,
196
+ color: theme.palette.text.secondary,
197
+ "& .MuiChip-label": {
198
+ fontSize: compact ? "0.7rem" : "0.75rem",
199
+ px: 0.75
200
+ }
201
+ };
202
+ }
203
+ function CourseDifficulty({
204
+ t,
205
+ difficulty,
206
+ compact
207
+ }) {
208
+ const levels = normalizeDifficulties(difficulty);
209
+ if (levels.length === 0) {
210
+ return null;
211
+ }
212
+ const levelLabels = levels.map(level => t(`card_${level}`));
213
+ const visibleLabels = levelLabels.slice(0, MAX_VISIBLE_DIFFICULTIES);
214
+ const overflowLabels = levelLabels.slice(MAX_VISIBLE_DIFFICULTIES);
215
+ return /*#__PURE__*/_jsxs(Box, {
216
+ component: "span",
217
+ role: "group",
218
+ "aria-label": `${t("difficulty")}: ${levelLabels.join(", ")}`,
219
+ sx: {
220
+ display: "inline-flex",
221
+ alignItems: "center",
222
+ gap: 0.5
223
+ },
224
+ "data-cy": "course-difficulty",
225
+ children: [/*#__PURE__*/_jsx(DifficultyIcon, {
226
+ sx: {
227
+ ...classes.infoIcon,
228
+ fontSize: compact ? 18 : undefined
229
+ },
230
+ fontSize: "small"
231
+ }), visibleLabels.map((label, index) => /*#__PURE__*/_jsx(Chip, {
232
+ label: label,
233
+ size: "small",
234
+ "data-cy": `course-difficulty-chip-${index}`,
235
+ sx: theme => difficultyChipSx(theme, compact)
236
+ }, label)), overflowLabels.length > 0 && /*#__PURE__*/_jsx(DifficultyOverflowTooltip, {
237
+ placement: "bottom-start",
238
+ title: /*#__PURE__*/_jsxs(Box, {
239
+ sx: {
240
+ minWidth: 140
241
+ },
242
+ children: [/*#__PURE__*/_jsx(Typography, {
243
+ variant: "caption",
244
+ sx: {
245
+ display: "block",
246
+ color: "text.secondary",
247
+ fontWeight: 700,
248
+ letterSpacing: 0.5,
249
+ textTransform: "uppercase",
250
+ pb: 1,
251
+ borderBottom: "1px solid",
252
+ borderColor: "divider"
253
+ },
254
+ children: t("difficulties")
255
+ }), levelLabels.map((label, index) => /*#__PURE__*/_jsxs(Box, {
256
+ sx: {
257
+ display: "flex",
258
+ alignItems: "center",
259
+ gap: 1,
260
+ py: 0.75,
261
+ borderBottom: index < levelLabels.length - 1 ? "1px solid" : "none",
262
+ borderColor: "divider"
263
+ },
264
+ children: [/*#__PURE__*/_jsx(Typography, {
265
+ component: "span",
266
+ sx: {
267
+ color: "text.disabled",
268
+ fontSize: "1.3rem",
269
+ lineHeight: 1
270
+ },
271
+ children: "\u2022"
272
+ }), /*#__PURE__*/_jsx(Typography, {
273
+ variant: "body2",
274
+ sx: {
275
+ color: "text.primary"
276
+ },
277
+ children: label
278
+ })]
279
+ }, label))]
280
+ }),
281
+ children: /*#__PURE__*/_jsx(Chip, {
282
+ label: `+${overflowLabels.length}`,
283
+ size: "small",
284
+ "aria-label": `${t("difficulty")}: ${levelLabels.join(", ")}`,
285
+ "data-cy": "course-difficulty-overflow",
286
+ sx: theme => ({
287
+ ...difficultyChipSx(theme, compact),
288
+ fontWeight: 600
289
+ })
290
+ })
291
+ })]
292
+ });
293
+ }
294
+ CourseDifficulty.propTypes = {
295
+ t: PropTypes.func.isRequired,
296
+ difficulty: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]),
297
+ compact: PropTypes.bool
298
+ };
162
299
  function CourseLoading({
163
300
  compact = false
164
301
  }) {
@@ -278,7 +415,7 @@ function CourseCard({
278
415
  limit: 1000
279
416
  }
280
417
  }, {
281
- enabled: !!course?.courseId && isOutlineOpen
418
+ enabled: !!course?.courseId && (isOutlineOpen || visible)
282
419
  });
283
420
  const {
284
421
  membersQuery,
@@ -356,6 +493,7 @@ function CourseCard({
356
493
  handleLeaveCourse(courseId);
357
494
  };
358
495
  const courseSections = sectionsQuery.isSuccess ? sectionsQuery.data.Items : [];
496
+ const topicDifficulties = sectionsQuery.isSuccess ? [...new Set(courseSections.flatMap(section => section.topics || []).map(topic => topic.difficulty).filter(Boolean))] : [];
359
497
  const deleteCourse = async () => {
360
498
  const confirmed = await confirm(t("delete_course"), t("delete_course_confirmation"), "", true, t("course_deletion_note", {
361
499
  sectionCount: courseSections ? courseSections.length : 0
@@ -497,16 +635,10 @@ function CourseCard({
497
635
  columnGap: 1.5,
498
636
  rowGap: 0.5
499
637
  },
500
- children: [course?.difficulty && /*#__PURE__*/_jsxs(CardInfoSectionItem, {
501
- variant: "caption",
502
- component: "span",
503
- children: [/*#__PURE__*/_jsx(DifficultyIcon, {
504
- sx: {
505
- ...classes.infoIcon,
506
- fontSize: compact ? 18 : undefined
507
- },
508
- fontSize: "small"
509
- }), t(`card_${course.difficulty}`)]
638
+ children: [/*#__PURE__*/_jsx(CourseDifficulty, {
639
+ t: t,
640
+ difficulty: topicDifficulties.length > 0 ? topicDifficulties : course?.difficulty,
641
+ compact: compact
510
642
  }), (course?.pdf || course.topicsWithPdf && Object.keys(course?.topicsWithPdf)?.length > 0) && /*#__PURE__*/_jsxs(CardInfoSectionItem, {
511
643
  variant: "caption",
512
644
  component: "span",
@@ -673,7 +805,7 @@ MemoizedCourse.propTypes = {
673
805
  courseId: PropTypes.string.isRequired,
674
806
  courseName: PropTypes.string.isRequired,
675
807
  description: PropTypes.string.isRequired,
676
- difficulty: PropTypes.string,
808
+ difficulty: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]),
677
809
  picture: PropTypes.string.isRequired,
678
810
  learnLang: PropTypes.string.isRequired,
679
811
  forLang: PropTypes.string.isRequired,
@@ -272,6 +272,7 @@ export default function Message({
272
272
  speak,
273
273
  stopSpeaking,
274
274
  voice,
275
+ voiceLanguageCode,
275
276
  voicePitch,
276
277
  voiceSpeed,
277
278
  handleEditMessage,
@@ -471,6 +472,7 @@ export default function Message({
471
472
  forLang: forLang,
472
473
  handleSpeak: handleSpeak,
473
474
  voice: voice,
475
+ voiceLanguageCode: voiceLanguageCode,
474
476
  voicePitch: voicePitch,
475
477
  voiceSpeed: voiceSpeed,
476
478
  disableTranslation: disableTranslation,
@@ -538,6 +540,7 @@ export default function Message({
538
540
  forLang: forLang,
539
541
  handleSpeak: handleSpeak,
540
542
  voice: voice,
543
+ voiceLanguageCode: voiceLanguageCode,
541
544
  voicePitch: voicePitch,
542
545
  voiceSpeed: voiceSpeed,
543
546
  disableTranslation: disableTranslation,
@@ -623,7 +626,7 @@ export default function Message({
623
626
  children: /*#__PURE__*/_jsx(IconButton, {
624
627
  color: "inherit",
625
628
  disabled: isSpeaking || recognizing || muteSound || isMessagePlaying || showTranslation,
626
- onClick: () => speak(text, voice, false, messageContainerIdSelector, null, false, voicePitch),
629
+ onClick: () => speak(text, voice, false, messageContainerIdSelector, voiceLanguageCode, false, voicePitch),
627
630
  size: "small",
628
631
  "aria-label": messageIndex !== -1 ? accessibilityText ? accessibilityText : `Speak message ${messageIndex + 1}` : accessibilityText ? accessibilityText : `Speak message`,
629
632
  sx: {
@@ -0,0 +1,55 @@
1
+ import { render, screen, fireEvent, cleanup } from "@testing-library/react";
2
+ import { describe, it, expect, vi, afterEach } from "vitest";
3
+ import { createTheme, ThemeProvider } from "@mui/material/styles";
4
+ import Message from "./Message";
5
+
6
+ // Messages spreads each message over these props, so a roleplay message carries
7
+ // its own actor's voice and accent; a bot chat supplies one voice for the
8
+ // whole chat. Replaying must use whichever applies, not the language default.
9
+ import { jsx as _jsx } from "react/jsx-runtime";
10
+ const renderMessage = (overrides = {}) => {
11
+ const theme = createTheme();
12
+ const speak = vi.fn();
13
+ render(/*#__PURE__*/_jsx(ThemeProvider, {
14
+ theme: theme,
15
+ children: /*#__PURE__*/_jsx(Message, {
16
+ t: k => k,
17
+ text: "Kia ora",
18
+ speak: speak,
19
+ messageIndex: 0,
20
+ index: 0,
21
+ messageCount: 1,
22
+ messages: [{
23
+ text: "Kia ora"
24
+ }],
25
+ ...overrides
26
+ })
27
+ }));
28
+ return speak;
29
+ };
30
+ describe("Message replay", () => {
31
+ afterEach(cleanup);
32
+ it("speaks with the accent the message carries", () => {
33
+ const speak = renderMessage({
34
+ voice: "Aria",
35
+ voiceLanguageCode: "en-NZ"
36
+ });
37
+ fireEvent.click(screen.getByRole("button", {
38
+ name: /Speak message/i
39
+ }));
40
+
41
+ // 5th positional argument of speak() is the accent.
42
+ expect(speak).toHaveBeenCalled();
43
+ expect(speak.mock.calls[0][1]).toBe("Aria");
44
+ expect(speak.mock.calls[0][4]).toBe("en-NZ");
45
+ });
46
+ it("sends no accent when nothing supplies one", () => {
47
+ const speak = renderMessage({
48
+ voice: "Joanna"
49
+ });
50
+ fireEvent.click(screen.getByRole("button", {
51
+ name: /Speak message/i
52
+ }));
53
+ expect(speak.mock.calls[0][4]).toBeUndefined();
54
+ });
55
+ });
@@ -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: isTeacher && /*#__PURE__*/_jsxs(_Fragment, {
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: isTeacher && /*#__PURE__*/_jsx(_Fragment, {
364
+ children: isSearchAllowed && /*#__PURE__*/_jsx(_Fragment, {
352
365
  children: /*#__PURE__*/_jsx(Tooltip, {
353
366
  title: t("search"),
354
367
  enterDelay: 300,
@@ -386,16 +399,20 @@ function App({
386
399
  startIcon: /*#__PURE__*/_jsx(Add, {}),
387
400
  color: "secondary",
388
401
  id: "appbar-create-button",
402
+ disabled: !subscription?.isSubscriptionLoaded,
389
403
  children: t("create")
390
404
  }) : /*#__PURE__*/_jsx(Tooltip, {
391
405
  title: t("create"),
392
406
  enterDelay: 300,
393
- children: /*#__PURE__*/_jsx(IconButton, {
394
- color: "inherit",
395
- "aria-label": t("create"),
396
- "aria-haspopup": "true",
397
- onClick: handleClickCreate,
398
- children: /*#__PURE__*/_jsx(Add, {})
407
+ children: /*#__PURE__*/_jsx("span", {
408
+ children: /*#__PURE__*/_jsx(IconButton, {
409
+ color: "inherit",
410
+ "aria-label": t("create"),
411
+ "aria-haspopup": "true",
412
+ onClick: handleClickCreate,
413
+ disabled: !subscription?.isSubscriptionLoaded,
414
+ children: /*#__PURE__*/_jsx(Add, {})
415
+ })
399
416
  })
400
417
  })), isLgScreen && /*#__PURE__*/_jsx(WhatsNew, {
401
418
  t: t,
@@ -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 (sentinelInView && tab === 1 && hasMoreCommunity && !isLoadingMoreCommunity) {
92
+ if (isCommunityAccessRestricted) {
93
+ reset();
94
+ }
95
+ }, [isCommunityAccessRestricted]);
96
+ useEffect(() => {
97
+ if (sentinelInView && effectiveTab === 1 && hasMoreCommunity && !isLoadingMoreCommunity) {
86
98
  onLoadMoreCommunity?.();
87
99
  }
88
- }, [sentinelInView, tab, hasMoreCommunity, isLoadingMoreCommunity, onLoadMoreCommunity]);
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 = tab === 0 ? filterChatbots(libraryChatbots) : applyCommunityFilters(communityChatbots);
118
- const isLoading = tab === 0 ? isLoadingLibrary : isLoadingCommunity;
119
- const sectionLabel = tab === 0 ? t("add_own_exercise_message", {
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: tab,
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
- }), tab === 1 && /*#__PURE__*/_jsx(Box, {
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
- }), tab === 1 && /*#__PURE__*/_jsx(Box, {
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 (sentinelInView && tab === 1 && hasMoreCommunity && !isLoadingMoreCommunity) {
80
+ if (isCommunityAccessRestricted) {
81
+ reset();
82
+ }
83
+ }, [isCommunityAccessRestricted]);
84
+ useEffect(() => {
85
+ if (sentinelInView && effectiveTab === 1 && hasMoreCommunity && !isLoadingMoreCommunity) {
74
86
  onLoadMoreCommunity?.();
75
87
  }
76
- }, [sentinelInView, tab, hasMoreCommunity, isLoadingMoreCommunity, onLoadMoreCommunity]);
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 = tab === 0 ? filterScenarios(libraryScenarios) : applyCommunityFilters(communityScenarios);
106
- const isLoading = tab === 0 ? isLoadingLibrary : isLoadingCommunity;
107
- const sectionLabel = tab === 0 ? t("add_own_exercise_message", {
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: tab,
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
- }), tab === 1 && /*#__PURE__*/_jsx(Box, {
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
- }), tab === 1 && /*#__PURE__*/_jsx(Box, {
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 (sentinelInView && tab === 1 && hasMoreCommunity && !isLoadingMoreCommunity) {
213
+ if (isCommunityAccessRestricted) {
214
+ reset();
215
+ }
216
+ }, [isCommunityAccessRestricted]);
217
+ useEffect(() => {
218
+ if (sentinelInView && effectiveTab === 1 && hasMoreCommunity && !isLoadingMoreCommunity) {
207
219
  onLoadMoreCommunity?.();
208
220
  }
209
- }, [sentinelInView, tab, hasMoreCommunity, isLoadingMoreCommunity, onLoadMoreCommunity]);
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 = tab === 0 ? filterTopics(libraryTopics) : applyCommunityFilters(communityTopics);
239
- const isLoading = tab === 0 ? isLoadingLibrary : isLoadingCommunity;
240
- const sectionLabel = tab === 0 ? t("add_own_exercise_message", {
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: tab,
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
- }), tab === 1 && /*#__PURE__*/_jsx(Box, {
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
- }), tab === 1 && /*#__PURE__*/_jsx(Box, {
367
+ }), effectiveTab === 1 && /*#__PURE__*/_jsx(Box, {
354
368
  ref: sentinelRef,
355
369
  sx: {
356
370
  height: 40,