@nualang/nualang-ui-components 0.1.1414 → 0.1.1415

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 (229) 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 +10 -6
  5. package/dist/Dialogs/GenerateRoleplay/GenerateRoleplay.js +5 -2
  6. package/dist/Dialogs/QuickCreateChatbotDialog/QuickCreateChatbotDialog.js +2 -1
  7. package/dist/Editors/Bot/Bot.js +6 -4
  8. package/dist/Editors/Roleplay/Roleplay.js +5 -2
  9. package/dist/Exercises/ListenAndSpeak/ListenAndSpeak.js +1 -1
  10. package/dist/Exercises/Listener/Listener.js +1 -1
  11. package/dist/Exercises/Meaning/Meaning.js +1 -1
  12. package/dist/Exercises/MeaningReadWrite/MeaningReadWriteGame.js +4 -1
  13. package/dist/Exercises/Pronouncer/Pronouncer.js +1 -1
  14. package/dist/Exercises/Roleplay/Roleplay.js +5 -2
  15. package/dist/Exercises/Translator/Translator.js +1 -1
  16. package/dist/Forms/CreateBot/Steps/BotInformation/BotInformation.js +13 -2
  17. package/dist/Forms/CreateRoleplay/Steps/RoleplayInformation/RoleplayInformation.js +4 -2
  18. package/dist/Forms/UpdatePhraseList/UpdatePhraseList.js +5 -3
  19. package/dist/Forms/VoiceSelector/VoiceSelector.js +29 -14
  20. package/dist/Forms/VoiceSelector/VoiceSelector.test.js +125 -3
  21. package/dist/Lists/Courses/Courses.js +5 -2
  22. package/dist/Lists/Courses/Courses.test.js +10 -0
  23. package/dist/Lists/Roleplays/Roleplays.js +5 -2
  24. package/dist/Live/LiveListener/LiveListener.js +1 -1
  25. package/dist/Live/LiveMeaning/LiveMeaning.js +1 -1
  26. package/dist/Live/LiveMeaningReadWrite/LiveMeaningReadWrite.js +1 -1
  27. package/dist/Live/LiveTranslator/LiveTranslator.js +1 -1
  28. package/dist/Misc/ChatBubble/ChatBubble.js +2 -0
  29. package/dist/Misc/HoverText/HoverText.js +7 -2
  30. package/dist/hooks/useExerciseState.js +2 -0
  31. package/dist/index.js +1 -1
  32. package/dist/utils/voiceImages.js +19 -4
  33. package/dist/utils/voices.js +12 -2
  34. package/package.json +1 -1
  35. package/dist/Cards/SubmissionsCard/SubmissionsCard.js +0 -138
  36. package/dist/Cards/index.js +0 -2
  37. package/dist/Dialogs/Code/Code.js +0 -147
  38. package/dist/Dialogs/Code/Code.test.js +0 -20
  39. package/dist/Dialogs/Code/package.json +0 -6
  40. package/dist/Dialogs/CreateTopic/CreateTopic.js +0 -135
  41. package/dist/Dialogs/CreateTopic/CreateTopic.test.js +0 -15
  42. package/dist/Dialogs/CreateTopic/package.json +0 -6
  43. package/dist/Dialogs/FeedbackDialog/FeedbackDialog.js +0 -123
  44. package/dist/Dialogs/Icons/Icons.js +0 -18
  45. package/dist/Dialogs/Icons/package.json +0 -6
  46. package/dist/Dialogs/InactivePopover/InactivePopover.js +0 -71
  47. package/dist/Dialogs/InviteDialog/upload-emails-csv.png +0 -0
  48. package/dist/Dialogs/PDFViewer/pdf24.pdf +0 -0
  49. package/dist/Dialogs/SearchTopic/SearchTopic.js +0 -87
  50. package/dist/Dialogs/UploadPhrases/upload-phrases-csv.png +0 -0
  51. package/dist/Exercises/Roleplay/OriginalRoleplay.js +0 -944
  52. package/dist/Forms/CreateBot/CreateBot.js +0 -77
  53. package/dist/Forms/CreateBot/CreateBot.test.js +0 -20
  54. package/dist/Forms/CreateBot/package.json +0 -6
  55. package/dist/Forms/CreateClassroom/CreateClassroom.js +0 -56
  56. package/dist/Forms/CreateClassroom/CreateClassroom.test.js +0 -20
  57. package/dist/Forms/CreateClassroom/package.json +0 -6
  58. package/dist/Forms/CreateCourse/CreateCourse.js +0 -66
  59. package/dist/Forms/CreateCourse/CreateCourse.test.js +0 -20
  60. package/dist/Forms/CreateCourse/package.json +0 -6
  61. package/dist/Forms/CreateMeetingMultiStepForm/images/input1.js +0 -2
  62. package/dist/Forms/CreateMeetingMultiStepForm/images/input5.js +0 -2
  63. package/dist/Forms/CreateMeetingMultiStepForm/images/mode1.js +0 -2
  64. package/dist/Forms/CreateMeetingMultiStepForm/images/mode2.js +0 -2
  65. package/dist/Forms/CreateMeetingMultiStepForm/sampleTemplates.js +0 -44
  66. package/dist/Forms/CreateRoleplay/CreateRoleplay.js +0 -87
  67. package/dist/Forms/CreateRoleplay/CreateRoleplay.test.js +0 -20
  68. package/dist/Forms/CreateRoleplay/package.json +0 -6
  69. package/dist/Forms/CreateUser/CreateUser.js +0 -77
  70. package/dist/Forms/FeedbackForm/FeedbackForm.js +0 -1036
  71. package/dist/Forms/MultiStepFormDialog/MultiStepFormDialog.js +0 -354
  72. package/dist/Forms/MultiStepFormDialog/MultiStepFormDialog.test.js +0 -20
  73. package/dist/Forms/MultiStepFormDialog/package.json +0 -6
  74. package/dist/Forms/MultiStepFormHook/MultiStepFormHook.js +0 -279
  75. package/dist/Forms/RecordingInfo/RecordingInfo.js +0 -140
  76. package/dist/Lists/Categories/sample.png +0 -0
  77. package/dist/Lists/Sections/Sections.js +0 -238
  78. package/dist/Lists/Sections/Sections.test.js +0 -20
  79. package/dist/Lists/Sections/package.json +0 -6
  80. package/dist/Lists/Voices/horse.ogm +0 -0
  81. package/dist/Misc/AxiosRestExample/AxiosRestExample.js +0 -46
  82. package/dist/Misc/AxiosRestExample/AxiosRestPostExample.js +0 -65
  83. package/dist/Misc/ColorSelector/ColorSelector.js +0 -143
  84. package/dist/Misc/ColorSelector/package.json +0 -6
  85. package/dist/Misc/EmojiInput/css/emoji-mart.css +0 -497
  86. package/dist/Misc/MedalSelector/PlaceholderMedal.svg +0 -27
  87. package/dist/Misc/NotFound/Bot.js +0 -62
  88. package/dist/Misc/NotFound/Bots.js +0 -75
  89. package/dist/Misc/NotFound/Discussions.js +0 -52
  90. package/dist/Misc/NotFound/Roleplay.js +0 -62
  91. package/dist/Misc/NotFound/Roleplays.js +0 -75
  92. package/dist/Misc/NotFound/Script.js +0 -47
  93. package/dist/Misc/Notes/Notes.js +0 -77
  94. package/dist/Misc/Notes/Notes.test.js +0 -20
  95. package/dist/Misc/Notes/package.json +0 -6
  96. package/dist/Misc/NualaCreating/Nuala1.js +0 -352
  97. package/dist/Misc/OnboardingEnrolment/CheckIfCourse.js +0 -33
  98. package/dist/Misc/OnboardingEnrolment/ClassCodeInvalid.js +0 -14
  99. package/dist/Misc/OnboardingEnrolment/ClassroomCodeInvalid.js +0 -14
  100. package/dist/Misc/OnboardingEnrolment/EnrolKey.js +0 -14
  101. package/dist/Misc/RoleplayChatWaveForm/RoleplayChatWaveForm.js +0 -199
  102. package/dist/Misc/RoleplayChatWaveForm/package.json +0 -6
  103. package/dist/Misc/RoleplayWaveform/RoleplayWaveform.js +0 -95
  104. package/dist/Misc/RoleplayWaveform/package.json +0 -6
  105. package/dist/Misc/SpeedDial/SpeedDial.js +0 -49
  106. package/dist/Misc/SpeedDial/package.json +0 -6
  107. package/dist/Misc/SubmitTextField/SubmitTextField.js +0 -48
  108. package/dist/Misc/newGenerateCardBackground/Generate Card Background (standalone) (1).html +0 -186
  109. package/dist/Navigation/ResponsiveStepper/ResponsiveStepper.js +0 -253
  110. package/dist/Navigation/ResponsiveStepper/ResponsiveStepper.test.js +0 -20
  111. package/dist/Navigation/ResponsiveStepper/package.json +0 -6
  112. package/dist/Navigation/ResponsiveTabs/TabContainer.js +0 -28
  113. package/dist/Tables/CreatedDiscussionsTable/CreatedDiscussionsTable.js +0 -188
  114. package/dist/Tables/GameRows/GameRows.js +0 -87
  115. package/dist/Tables/GameRows/package.json +0 -6
  116. package/dist/Tables/Games/Games.js +0 -258
  117. package/dist/Tables/Games/package.json +0 -6
  118. package/dist/Tables/Phrases/Phrases.js +0 -200
  119. package/dist/Tables/Phrases/Phrases.test.js +0 -20
  120. package/dist/Tables/Phrases/package.json +0 -6
  121. package/dist/Tables/Plans/Plans.js +0 -74
  122. package/dist/Tables/Plans/plans.json +0 -162
  123. package/dist/Tables/Plans/plans2.json +0 -151
  124. package/dist/Tables/Progress/Header.js +0 -559
  125. package/dist/Tables/Progress/README.md +0 -53
  126. package/dist/Tables/SubmissionsTableCards/SubmissionsTableCards.js +0 -296
  127. package/dist/Tables/Users/UsersTable.js +0 -159
  128. package/dist/img/Bronze Medal.svg +0 -545
  129. package/dist/img/Bronze.svg +0 -349
  130. package/dist/img/BronzeBadge.svg +0 -493
  131. package/dist/img/BronzeBadgeWithoutRibbon.svg +0 -317
  132. package/dist/img/Frame 51.svg +0 -98
  133. package/dist/img/Frame 52.svg +0 -131
  134. package/dist/img/Frame 53.svg +0 -139
  135. package/dist/img/Gold Badge.svg +0 -362
  136. package/dist/img/Gold Medal.svg +0 -481
  137. package/dist/img/GoldBadge.svg +0 -470
  138. package/dist/img/GoldBadgeWithoutRibbon.svg +0 -324
  139. package/dist/img/NualaCreateImage.svg +0 -129
  140. package/dist/img/NualangLiveAnwerQuicklyListening.svg +0 -3608
  141. package/dist/img/NualangLiveListen.png +0 -0
  142. package/dist/img/NualangLiveListenQuickly.png +0 -0
  143. package/dist/img/NualangLiveListenToPhrase.svg +0 -4286
  144. package/dist/img/NualangLivePickAnswer.svg +0 -134
  145. package/dist/img/NualangLiveResult.png +0 -0
  146. package/dist/img/NualangLiveRoleplayAnswer.png +0 -0
  147. package/dist/img/NualangLiveRoleplayConvo.png +0 -0
  148. package/dist/img/NualangLiveSeePlace.svg +0 -134
  149. package/dist/img/NualangLiveTranslate.png +0 -0
  150. package/dist/img/NualangLiveTranslateQuickly.png +0 -0
  151. package/dist/img/NualangLiveViewConversations.svg +0 -135
  152. package/dist/img/Polly.svg +0 -18
  153. package/dist/img/Silver Badge.svg +0 -523
  154. package/dist/img/Silver Medal.svg +0 -549
  155. package/dist/img/SilverBadge.svg +0 -655
  156. package/dist/img/SilverBadgeWithoutRibbon.svg +0 -491
  157. package/dist/img/avatars/0-nuala-001.svg +0 -34
  158. package/dist/img/avatars/0-nuala-002.svg +0 -25
  159. package/dist/img/avatars/0-nuala-003.svg +0 -27
  160. package/dist/img/avatars/0-nuala-004.svg +0 -41
  161. package/dist/img/avatars/0-nuala-005.svg +0 -33
  162. package/dist/img/avatars/0-nuala-006.svg +0 -32
  163. package/dist/img/avatars/0-nuala-007.svg +0 -37
  164. package/dist/img/avatars/0-nuala-008.svg +0 -21
  165. package/dist/img/avatars/0-nuala-009.svg +0 -21
  166. package/dist/img/avatars/0-nuala-010.svg +0 -39
  167. package/dist/img/avatars/0-nuala-011.svg +0 -33
  168. package/dist/img/avatars/0-nuala-012.svg +0 -43
  169. package/dist/img/avatars/0-nuala-013.svg +0 -36
  170. package/dist/img/avatars/0-nuala-014.svg +0 -30
  171. package/dist/img/avatars/0-nuala-015.svg +0 -28
  172. package/dist/img/avatars/0-nuala-016.svg +0 -24
  173. package/dist/img/avatars/0-nuala-017.svg +0 -31
  174. package/dist/img/avatars/0-nuala-018.svg +0 -29
  175. package/dist/img/avatars/0-nuala-019.svg +0 -57
  176. package/dist/img/avatars/0-nuala-020.svg +0 -30
  177. package/dist/img/avatars/0-nuala-021.svg +0 -28
  178. package/dist/img/avatars/001-man.svg +0 -104
  179. package/dist/img/avatars/002-girl.svg +0 -92
  180. package/dist/img/avatars/006-woman-1.svg +0 -99
  181. package/dist/img/avatars/028-woman-7.svg +0 -95
  182. package/dist/img/avatars/029-man-8.svg +0 -95
  183. package/dist/img/avatars/035-woman-9.svg +0 -86
  184. package/dist/img/avatars/066-boy-6.svg +0 -91
  185. package/dist/img/avatars/1-teacher-001.svg +0 -38
  186. package/dist/img/avatars/1-teacher-002.svg +0 -52
  187. package/dist/img/avatars/1-teacher-003.svg +0 -86
  188. package/dist/img/avatars/avatar1.png +0 -0
  189. package/dist/img/avatars/avatar2.png +0 -0
  190. package/dist/img/avatars/nuala_10_speaking.svg +0 -45
  191. package/dist/img/avatars/nuala_11_speaking.svg +0 -41
  192. package/dist/img/avatars/nuala_12_speaking.svg +0 -49
  193. package/dist/img/avatars/nuala_13_speaking.svg +0 -44
  194. package/dist/img/avatars/nuala_14_speaking.svg +0 -36
  195. package/dist/img/avatars/nuala_15_speaking.svg +0 -36
  196. package/dist/img/avatars/nuala_16_speaking.svg +0 -32
  197. package/dist/img/avatars/nuala_17_speaking.svg +0 -39
  198. package/dist/img/avatars/nuala_18_speaking.svg +0 -37
  199. package/dist/img/avatars/nuala_19_speaking.svg +0 -65
  200. package/dist/img/avatars/nuala_1_celebrating.svg +0 -30
  201. package/dist/img/avatars/nuala_1_speaking.svg +0 -37
  202. package/dist/img/avatars/nuala_20_speaking.svg +0 -38
  203. package/dist/img/avatars/nuala_21_speaking.svg +0 -36
  204. package/dist/img/avatars/nuala_2_speaking.svg +0 -29
  205. package/dist/img/avatars/nuala_3_speaking.svg +0 -28
  206. package/dist/img/avatars/nuala_4_speaking.svg +0 -41
  207. package/dist/img/avatars/nuala_5_speaking.svg +0 -36
  208. package/dist/img/avatars/nuala_6_speaking.svg +0 -36
  209. package/dist/img/avatars/nuala_7_speaking.svg +0 -43
  210. package/dist/img/avatars/nuala_8_speaking.svg +0 -29
  211. package/dist/img/avatars/nuala_9_speaking.svg +0 -27
  212. package/dist/img/dashboard.svg +0 -104
  213. package/dist/img/defaultImg.png +0 -0
  214. package/dist/img/nua-glyph.svg +0 -6
  215. package/dist/img/nuala-silhouette.svg +0 -43
  216. package/dist/img/nuala_1_celebrating.svg +0 -33
  217. package/dist/img/nuala_2_celebrating.svg +0 -20
  218. package/dist/img/nuala_3_celebrating.svg +0 -21
  219. package/dist/img/nuala_4_celebrating.svg +0 -30
  220. package/dist/img/nuala_5_celebrating.svg +0 -30
  221. package/dist/img/nuala_celebrating.svg +0 -39
  222. package/dist/img/nuala_speaking_4.svg +0 -41
  223. package/dist/img/nualas.svg +0 -185
  224. package/dist/img/soundWave.png +0 -0
  225. package/dist/img/stars1.svg +0 -44
  226. package/dist/img/stars2.svg +0 -30
  227. package/dist/img/teacher.svg +0 -89
  228. package/dist/img/work-chat.svg +0 -1
  229. 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
+ });
@@ -386,16 +386,20 @@ function App({
386
386
  startIcon: /*#__PURE__*/_jsx(Add, {}),
387
387
  color: "secondary",
388
388
  id: "appbar-create-button",
389
+ disabled: !subscription?.isSubscriptionLoaded,
389
390
  children: t("create")
390
391
  }) : /*#__PURE__*/_jsx(Tooltip, {
391
392
  title: t("create"),
392
393
  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, {})
394
+ children: /*#__PURE__*/_jsx("span", {
395
+ children: /*#__PURE__*/_jsx(IconButton, {
396
+ color: "inherit",
397
+ "aria-label": t("create"),
398
+ "aria-haspopup": "true",
399
+ onClick: handleClickCreate,
400
+ disabled: !subscription?.isSubscriptionLoaded,
401
+ children: /*#__PURE__*/_jsx(Add, {})
402
+ })
399
403
  })
400
404
  })), isLgScreen && /*#__PURE__*/_jsx(WhatsNew, {
401
405
  t: t,
@@ -110,13 +110,16 @@ function GenerateRoleplayDialog({
110
110
  if (generatedRoleplay && generatedRoleplay.script) {
111
111
  const tempMessages = generatedRoleplay.script.filter(l => !l?.question).map(line => {
112
112
  const isActor1 = line.actor === 1;
113
+ const actor = (isActor1 ? generatedRoleplay.actor1 : generatedRoleplay.actor2) || {};
113
114
  return {
114
115
  ...line,
115
116
  text: line.text,
116
117
  isMyMessage: !isActor1,
117
118
  name: getActorName(isActor1),
118
- userImage: isActor1 ? generatedRoleplay.actor1 && generatedRoleplay.actor1.picture ? generatedRoleplay.actor1.picture : null : generatedRoleplay.actor2 && generatedRoleplay.actor2.picture ? generatedRoleplay.actor2.picture : null,
119
- voice: isActor1 ? generatedRoleplay.actor1 && generatedRoleplay.actor1.voice ? generatedRoleplay.actor1.voice : null : generatedRoleplay.actor2 && generatedRoleplay.actor2.voice ? generatedRoleplay.actor2.voice : null
119
+ userImage: actor.picture || null,
120
+ voice: actor.voice || null,
121
+ voiceLanguageCode: actor.voiceLanguageCode || null,
122
+ voicePitch: actor.voicePitch || null
120
123
  };
121
124
  });
122
125
  setPreviewMessages(tempMessages);
@@ -459,7 +459,7 @@ export default function QuickCreateChatbotDialog({
459
459
  setSelectedVoicePitch(null);
460
460
  setBotPicture(picked.voice.userImage || "");
461
461
  }, [open, selectedVoice, voices]);
462
- const selectedVoiceOption = findVoice(voices, selectedVoice)?.voice;
462
+ const selectedVoiceOption = findVoice(voices, selectedVoice, selectedVoiceLanguageCode)?.voice;
463
463
  const hasAvailableVoice = Boolean(selectedVoice && selectedVoiceLanguageCode);
464
464
  const isValid = Boolean(botTitle.trim()) && Boolean(difficulty) && Boolean(templateId) && Boolean(template && template.botId === templateId) && Number(numberOfQuestions) > 0 && hasAvailableVoice;
465
465
  const handleSubmit = async () => {
@@ -600,6 +600,7 @@ export default function QuickCreateChatbotDialog({
600
600
  name: "voiceSelector",
601
601
  label: t("bot_voice"),
602
602
  value: selectedVoice || "",
603
+ valueLanguageCode: selectedVoiceLanguageCode,
603
604
  margin: "normal",
604
605
  variant: "outlined",
605
606
  fullWidth: true,
@@ -486,18 +486,20 @@ function Bot({
486
486
  return;
487
487
  }
488
488
  const voice = initialValues.voice;
489
+ const voiceLanguageCode = initialValues.voiceLanguageCode;
489
490
  const message = {
490
491
  isMyMessage: isMyMessage,
491
492
  userImage: isErrorMsg ? BotNualaImage : isMyMessage ? userAttributes && userAttributes.picture ? userAttributes.picture : null : bot && bot.botAvatar ? bot.botAvatar : null,
492
493
  name: isErrorMsg ? "Bot" : isMyMessage ? userAttributes && userAttributes.preferred_username ? userAttributes.preferred_username : "Me" : bot && bot.name ? bot.name : "Bot",
493
494
  text: text,
494
- datetime: new Date()
495
+ datetime: new Date(),
496
+ // Recorded so the replay button reproduces what was spoken.
497
+ voice: voice,
498
+ voiceLanguageCode: voiceLanguageCode
495
499
  };
496
500
  setMessages(prevState => [...prevState, message]);
497
501
  if (playAudio !== false && learnLang !== "irish") {
498
- speak(message.text, voice, false, `#message${messages.length + 1}`
499
- //voiceLanguageCode
500
- );
502
+ speak(message.text, voice, false, `#message${messages.length + 1}`, voiceLanguageCode);
501
503
  }
502
504
  };
503
505
  const handleSubmitText = async () => {
@@ -627,6 +627,7 @@ function Roleplay({
627
627
  };
628
628
  const currentMessages = (roleplay && roleplay.script ? roleplay.script : []).map(line => {
629
629
  const isActor1 = line.actor === 1;
630
+ const actor = (isActor1 ? roleplay.actor1 : roleplay.actor2) || {};
630
631
  return {
631
632
  ...line,
632
633
  text: line.text && formatMarkdownNewlines(line.text),
@@ -636,8 +637,10 @@ function Roleplay({
636
637
  }) : roleplay.actor2 && roleplay.actor2.name ? roleplay.actor2.name : t("actor", {
637
638
  number: "2"
638
639
  }),
639
- userImage: isActor1 ? roleplay.actor1 && roleplay.actor1.picture ? roleplay.actor1.picture : null : roleplay.actor2 && roleplay.actor2.picture ? roleplay.actor2.picture : null,
640
- voice: isActor1 ? roleplay.actor1 && roleplay.actor1.voice ? roleplay.actor1.voice : null : roleplay.actor2 && roleplay.actor2.voice ? roleplay.actor2.voice : null
640
+ userImage: actor.picture || null,
641
+ voice: actor.voice || null,
642
+ voiceLanguageCode: actor.voiceLanguageCode || null,
643
+ voicePitch: actor.voicePitch || null
641
644
  };
642
645
  });
643
646
  const afterElements = /*#__PURE__*/_jsx("div", {
@@ -444,7 +444,7 @@ function ListenAndSpeak({
444
444
  voiceLanguageCode,
445
445
  voicePitch
446
446
  } = topicInfo;
447
- const voiceMap = getVoiceImages(voices || []);
447
+ const voiceMap = getVoiceImages(voices || [], voiceLanguageCode);
448
448
  const getNualaCelebratingImage = () => {
449
449
  try {
450
450
  const exerciseVoice = voiceMap.get(voice) || {};
@@ -354,7 +354,7 @@ function Listener({
354
354
  voiceLanguageCode,
355
355
  voicePitch
356
356
  } = topicInfo;
357
- const voiceMap = getVoiceImages(voices || []);
357
+ const voiceMap = getVoiceImages(voices || [], voiceLanguageCode);
358
358
  const getNualaCelebratingImage = () => {
359
359
  try {
360
360
  const exerciseVoice = voiceMap.get(voice) || {};
@@ -358,7 +358,7 @@ function Meaning({
358
358
  voiceLanguageCode,
359
359
  voicePitch
360
360
  } = topicInfo;
361
- const voiceMap = getVoiceImages(voices || []);
361
+ const voiceMap = getVoiceImages(voices || [], voiceLanguageCode);
362
362
  const getNualaCelebratingImage = () => {
363
363
  try {
364
364
  const exerciseVoice = voiceMap.get(voice) || {};
@@ -338,9 +338,10 @@ export default function MeaningReadWrite({
338
338
  })();
339
339
  const {
340
340
  voice,
341
+ voiceLanguageCode,
341
342
  voicePitch
342
343
  } = topicInfo;
343
- const voiceMap = getVoiceImages(voices || []);
344
+ const voiceMap = getVoiceImages(voices || [], voiceLanguageCode);
344
345
  const getNualaCelebratingImage = () => {
345
346
  try {
346
347
  const exerciseVoice = voiceMap.get(voice) || {};
@@ -374,6 +375,7 @@ export default function MeaningReadWrite({
374
375
  text: currentPhrase.phrase,
375
376
  learnLanguage: learnLang,
376
377
  voiceName: voice || null,
378
+ languageCode: voiceLanguageCode || null,
377
379
  voiceSpeed,
378
380
  pitch: voicePitch || null
379
381
  });
@@ -559,6 +561,7 @@ export default function MeaningReadWrite({
559
561
  stopAudio: stopAudio,
560
562
  learnLang: learnLang,
561
563
  voice: voice || null,
564
+ voiceLanguageCode: voiceLanguageCode || null,
562
565
  voicePitch: voicePitch || null,
563
566
  voiceSpeed: voiceSpeed,
564
567
  forLang: forLang,
@@ -456,7 +456,7 @@ function Pronouncer({
456
456
  voiceLanguageCode,
457
457
  voicePitch
458
458
  } = topicInfo;
459
- const voiceMap = getVoiceImages(voices || []);
459
+ const voiceMap = getVoiceImages(voices || [], voiceLanguageCode);
460
460
  const getNualaCelebratingImage = () => {
461
461
  try {
462
462
  const exerciseVoice = voiceMap.get(voice) || {};
@@ -177,13 +177,16 @@ function Roleplay({
177
177
  if (roleplay && roleplay.script) {
178
178
  const tempMessages = roleplay.script.filter(l => !l?.question).map(line => {
179
179
  const isActor1 = line.actor === 1;
180
+ const actor = (isActor1 ? roleplay.actor1 : roleplay.actor2) || {};
180
181
  return {
181
182
  ...line,
182
183
  text: formatMarkdownNewlines(line.text),
183
184
  isMyMessage: !isActor1,
184
185
  name: getActorName(isActor1),
185
- userImage: isActor1 ? roleplay.actor1 && roleplay.actor1.picture ? roleplay.actor1.picture : null : roleplay.actor2 && roleplay.actor2.picture ? roleplay.actor2.picture : null,
186
- voice: isActor1 ? roleplay.actor1 && roleplay.actor1.voice ? roleplay.actor1.voice : null : roleplay.actor2 && roleplay.actor2.voice ? roleplay.actor2.voice : null
186
+ userImage: actor.picture || null,
187
+ voice: actor.voice || null,
188
+ voiceLanguageCode: actor.voiceLanguageCode || null,
189
+ voicePitch: actor.voicePitch || null
187
190
  };
188
191
  });
189
192
  setPreviewMessges(tempMessages);
@@ -523,7 +523,7 @@ function Translator({
523
523
  voiceLanguageCode,
524
524
  voicePitch
525
525
  } = topicInfo;
526
- const voiceMap = getVoiceImages(voices || []);
526
+ const voiceMap = getVoiceImages(voices || [], voiceLanguageCode);
527
527
  const getNualaCelebratingImage = () => {
528
528
  try {
529
529
  const exerciseVoice = voiceMap.get(voice) || {};
@@ -451,6 +451,8 @@ function BotInformation({
451
451
  }), /*#__PURE__*/_jsxs(Grid, {
452
452
  size: 12,
453
453
  children: [learnLang !== "irish" && /*#__PURE__*/_jsx(VoiceSelector, {
454
+ handleSpeak: handleSpeak,
455
+ learnLang: learnLang,
454
456
  id: "voice",
455
457
  name: "voice",
456
458
  "data-cy": "voice",
@@ -462,12 +464,12 @@ function BotInformation({
462
464
  voices: voiceOptions,
463
465
  label: t("bot_voice"),
464
466
  value: voice,
467
+ valueLanguageCode: voiceLanguageCode,
465
468
  onChange: e => {
466
469
  if (e && e.target && e.target.value) {
467
470
  handleChange(e);
468
471
  const voiceSelected = e.target.value;
469
- const voiceOptionElem = document.getElementById(`voice-${voiceSelected}`);
470
- const languageCode = voiceOptionElem.getAttribute("data-language-code");
472
+ const languageCode = e.target.languageCode;
471
473
  handleChange({
472
474
  target: {
473
475
  name: "voice",
@@ -486,6 +488,15 @@ function BotInformation({
486
488
  value: languageCode
487
489
  }
488
490
  });
491
+ // The avatar belongs to the voice, and two accents can share a
492
+ // voice name with different avatars, so it follows the accent
493
+ // the selector resolved rather than the name alone.
494
+ handleChange({
495
+ target: {
496
+ name: "botAvatar",
497
+ value: e.target.picture
498
+ }
499
+ });
489
500
  handleChange({
490
501
  target: {
491
502
  name: "voicePitch",
@@ -165,6 +165,8 @@ export function ActorForm({
165
165
  children: t("choose_avatar")
166
166
  })]
167
167
  }), learnLang !== "irish" && /*#__PURE__*/_jsx(VoiceSelector, {
168
+ handleSpeak: handleSpeak,
169
+ learnLang: learnLang,
168
170
  id: `actor${number}.voice`,
169
171
  "data-cy": `actor${number}.voice`,
170
172
  name: `actor${number}.voice`,
@@ -176,11 +178,11 @@ export function ActorForm({
176
178
  t: t,
177
179
  voices: voiceOptions,
178
180
  value: voice,
181
+ valueLanguageCode: voiceLanguageCode,
179
182
  onChange: e => {
180
183
  if (e && e.target && e.target.value) {
181
184
  const voiceSelected = e.target.value;
182
- const voiceOptionElem = document.getElementById(`voice-${voiceSelected}`);
183
- const languageCode = voiceOptionElem.getAttribute("data-language-code");
185
+ const languageCode = e.target.languageCode;
184
186
  handleChange({
185
187
  target: {
186
188
  name: `actor${number}.voice`,
@@ -83,7 +83,7 @@ export default function UpdatePhraseList({
83
83
  isKeyboardHiddenPhraseGroupGames: rawInitialValues.isKeyboardHiddenPhraseGroupGames ?? {},
84
84
  hiddenWordBankPhraseGroupGames: rawInitialValues.hiddenWordBankPhraseGroupGames ?? {},
85
85
  hiddenImagePhraseGroupGames: rawInitialValues.hiddenImagePhraseGroupGames ?? {
86
- "meaning": true,
86
+ meaning: true,
87
87
  "meaning-listen-and-speak": true,
88
88
  "meaning-read-and-write": true
89
89
  }
@@ -288,6 +288,8 @@ export default function UpdatePhraseList({
288
288
  gutterBottom: true,
289
289
  children: t("excercise_voice_desc")
290
290
  }), /*#__PURE__*/_jsx(VoiceSelector, {
291
+ handleSpeak: handleSpeak,
292
+ learnLang: learnLang,
291
293
  id: "voice",
292
294
  name: "voice",
293
295
  "data-cy": "voice",
@@ -299,11 +301,11 @@ export default function UpdatePhraseList({
299
301
  voices: voiceOptions,
300
302
  label: t("excercise_voice"),
301
303
  value: values.voice,
304
+ valueLanguageCode: values.voiceLanguageCode,
302
305
  onChange: e => {
303
306
  if (e && e.target && e.target.value) {
304
307
  const voiceSelected = e.target.value;
305
- const voiceOptionElem = document.getElementById(`voice-${voiceSelected}`);
306
- const languageCode = voiceOptionElem.getAttribute("data-language-code");
308
+ const languageCode = e.target.languageCode;
307
309
  handleChange({
308
310
  target: {
309
311
  name: "voice",