@nualang/nualang-ui-components 0.1.1413 → 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 (270) hide show
  1. package/dist/Assignments/AssignmentCardsList/AssignmentCardsList.js +4 -2
  2. package/dist/Cards/Course/Course.js +145 -13
  3. package/dist/Chat/Messages/Message/Message.js +4 -1
  4. package/dist/Chat/Messages/Message/Message.test.js +55 -0
  5. package/dist/Containers/App/App.js +10 -6
  6. package/dist/Dialogs/CreatePhrase/CreatePhrase.js +3 -1
  7. package/dist/Dialogs/CreatePhraseGroupDialog/CreatePhraseGroupDialog.js +4 -2
  8. package/dist/Dialogs/GenerateRoleplay/GenerateRoleplay.js +5 -2
  9. package/dist/Dialogs/ImageBuilder/ImageBuilderDialog.js +3 -14
  10. package/dist/Dialogs/ImageBuilder/imageBuilderUtils.js +47 -5
  11. package/dist/Dialogs/QuickCreateChatbotDialog/QuickCreateChatbotDialog.js +2 -1
  12. package/dist/Dialogs/QuickCreateClassroomDialog/QuickCreateClassroomDialog.js +4 -2
  13. package/dist/Dialogs/QuickCreateCourseDialog/QuickCreateCourseDialog.js +4 -2
  14. package/dist/Dialogs/QuickCreateTopicDialog/QuickCreateTopicDialog.js +4 -2
  15. package/dist/Editors/Bot/Bot.js +10 -6
  16. package/dist/Editors/Bot/Editor/BotDesigner/AddImageModal/AddImageModal.js +4 -2
  17. package/dist/Editors/Bot/Editor/BotDesigner/BotDesigner.js +4 -2
  18. package/dist/Editors/Bot/Editor/BotDesigner/VarList.js +7 -3
  19. package/dist/Editors/Bot/Editor/Editor.js +6 -3
  20. package/dist/Editors/PhraseList/PhraseList.js +6 -3
  21. package/dist/Editors/Phrases/Phrases.js +4 -2
  22. package/dist/Editors/Roleplay/Roleplay.js +9 -4
  23. package/dist/Exercises/ListenAndSpeak/ListenAndSpeak.js +1 -1
  24. package/dist/Exercises/Listener/Listener.js +1 -1
  25. package/dist/Exercises/Meaning/Meaning.js +1 -1
  26. package/dist/Exercises/MeaningReadWrite/MeaningReadWriteGame.js +4 -1
  27. package/dist/Exercises/Pronouncer/Pronouncer.js +1 -1
  28. package/dist/Exercises/Roleplay/Roleplay.js +5 -2
  29. package/dist/Exercises/Translator/Translator.js +1 -1
  30. package/dist/Forms/CreateBot/Steps/BotInformation/BotInformation.js +17 -4
  31. package/dist/Forms/CreateClassroom/Steps/Customization/Customization.js +6 -3
  32. package/dist/Forms/CreateCourse/Steps/Customization/Customization.js +6 -3
  33. package/dist/Forms/CreateMeetingMultiStepForm/CreateMeetingMultiStepForm.js +6 -3
  34. package/dist/Forms/CreateMeetingMultiStepForm/MeetingForm.js +6 -3
  35. package/dist/Forms/CreateRoleplay/Steps/RoleplayInformation/RoleplayInformation.js +8 -4
  36. package/dist/Forms/CreateTopic/Steps/Customization/Customization.js +6 -3
  37. package/dist/Forms/ImageSelector/ImageSelector.js +39 -9
  38. package/dist/Forms/UpdateBot/UpdateBot.js +3 -1
  39. package/dist/Forms/UpdateClassroom/UpdateClassroom.js +3 -1
  40. package/dist/Forms/UpdateCourse/UpdateCourse.js +3 -1
  41. package/dist/Forms/UpdatePhraseList/UpdatePhraseList.js +9 -5
  42. package/dist/Forms/UpdateRoleplay/UpdateRoleplay.js +4 -2
  43. package/dist/Forms/UpdateTopic/UpdateTopic.js +3 -1
  44. package/dist/Forms/VoiceSelector/VoiceSelector.js +29 -14
  45. package/dist/Forms/VoiceSelector/VoiceSelector.test.js +125 -3
  46. package/dist/Lists/CourseOutline/CourseOutline.js +3 -1
  47. package/dist/Lists/Courses/Courses.js +5 -2
  48. package/dist/Lists/Courses/Courses.test.js +10 -0
  49. package/dist/Lists/Exercises/Exercises.js +2 -1
  50. package/dist/Lists/Phrases/Phrases.js +4 -2
  51. package/dist/Lists/Roleplays/Roleplays.js +5 -2
  52. package/dist/Live/LiveListener/LiveListener.js +1 -1
  53. package/dist/Live/LiveMeaning/LiveMeaning.js +1 -1
  54. package/dist/Live/LiveMeaningReadWrite/LiveMeaningReadWrite.js +1 -1
  55. package/dist/Live/LiveTranslator/LiveTranslator.js +1 -1
  56. package/dist/Misc/ChatBubble/ChatBubble.js +2 -0
  57. package/dist/Misc/HoverText/HoverText.js +7 -2
  58. package/dist/Navigation/ResponsiveTabs/ResponsiveTabs.js +1 -1
  59. package/dist/Tables/MeetingPrompstList/MeetingPromptsList.js +4 -2
  60. package/dist/hooks/useExerciseState.js +2 -0
  61. package/dist/index.js +1 -1
  62. package/dist/utils/canvasUtils.js +1 -1
  63. package/dist/utils/voiceImages.js +19 -4
  64. package/dist/utils/voices.js +12 -2
  65. package/package.json +1 -1
  66. package/dist/Cards/SubmissionsCard/SubmissionsCard.js +0 -138
  67. package/dist/Cards/index.js +0 -2
  68. package/dist/Dialogs/Code/Code.js +0 -147
  69. package/dist/Dialogs/Code/Code.test.js +0 -20
  70. package/dist/Dialogs/Code/package.json +0 -6
  71. package/dist/Dialogs/CreateTopic/CreateTopic.js +0 -135
  72. package/dist/Dialogs/CreateTopic/CreateTopic.test.js +0 -15
  73. package/dist/Dialogs/CreateTopic/package.json +0 -6
  74. package/dist/Dialogs/FeedbackDialog/FeedbackDialog.js +0 -123
  75. package/dist/Dialogs/Icons/Icons.js +0 -18
  76. package/dist/Dialogs/Icons/package.json +0 -6
  77. package/dist/Dialogs/InactivePopover/InactivePopover.js +0 -71
  78. package/dist/Dialogs/InviteDialog/upload-emails-csv.png +0 -0
  79. package/dist/Dialogs/PDFViewer/pdf24.pdf +0 -0
  80. package/dist/Dialogs/SearchTopic/SearchTopic.js +0 -87
  81. package/dist/Dialogs/UploadPhrases/upload-phrases-csv.png +0 -0
  82. package/dist/Exercises/Roleplay/OriginalRoleplay.js +0 -944
  83. package/dist/Forms/CreateBot/CreateBot.js +0 -77
  84. package/dist/Forms/CreateBot/CreateBot.test.js +0 -20
  85. package/dist/Forms/CreateBot/package.json +0 -6
  86. package/dist/Forms/CreateClassroom/CreateClassroom.js +0 -56
  87. package/dist/Forms/CreateClassroom/CreateClassroom.test.js +0 -20
  88. package/dist/Forms/CreateClassroom/package.json +0 -6
  89. package/dist/Forms/CreateCourse/CreateCourse.js +0 -66
  90. package/dist/Forms/CreateCourse/CreateCourse.test.js +0 -20
  91. package/dist/Forms/CreateCourse/package.json +0 -6
  92. package/dist/Forms/CreateMeetingMultiStepForm/images/input1.js +0 -2
  93. package/dist/Forms/CreateMeetingMultiStepForm/images/input5.js +0 -2
  94. package/dist/Forms/CreateMeetingMultiStepForm/images/mode1.js +0 -2
  95. package/dist/Forms/CreateMeetingMultiStepForm/images/mode2.js +0 -2
  96. package/dist/Forms/CreateMeetingMultiStepForm/sampleTemplates.js +0 -44
  97. package/dist/Forms/CreateRoleplay/CreateRoleplay.js +0 -87
  98. package/dist/Forms/CreateRoleplay/CreateRoleplay.test.js +0 -20
  99. package/dist/Forms/CreateRoleplay/package.json +0 -6
  100. package/dist/Forms/CreateRoleplayHook/CreateRoleplayHook.js +0 -83
  101. package/dist/Forms/CreateRoleplayHook/CreateRoleplayHook.test.js +0 -20
  102. package/dist/Forms/CreateRoleplayHook/Steps/Confirmation/Confirmation.js +0 -174
  103. package/dist/Forms/CreateRoleplayHook/Steps/Confirmation/package.json +0 -6
  104. package/dist/Forms/CreateRoleplayHook/Steps/RoleplayInformation/RoleplayInformation.js +0 -487
  105. package/dist/Forms/CreateRoleplayHook/Steps/RoleplayInformation/package.json +0 -6
  106. package/dist/Forms/CreateRoleplayHook/Steps/RoleplaySettings/RoleplaySettings.js +0 -330
  107. package/dist/Forms/CreateRoleplayHook/Steps/RoleplaySettings/package.json +0 -6
  108. package/dist/Forms/CreateRoleplayHook/Steps/index.js +0 -4
  109. package/dist/Forms/CreateRoleplayHook/package.json +0 -6
  110. package/dist/Forms/CreateUser/CreateUser.js +0 -77
  111. package/dist/Forms/FeedbackForm/FeedbackForm.js +0 -1036
  112. package/dist/Forms/MultiStepFormDialog/MultiStepFormDialog.js +0 -354
  113. package/dist/Forms/MultiStepFormDialog/MultiStepFormDialog.test.js +0 -20
  114. package/dist/Forms/MultiStepFormDialog/package.json +0 -6
  115. package/dist/Forms/MultiStepFormHook/MultiStepFormHook.js +0 -279
  116. package/dist/Forms/RecordingInfo/RecordingInfo.js +0 -140
  117. package/dist/Lists/Categories/sample.png +0 -0
  118. package/dist/Lists/Sections/Sections.js +0 -238
  119. package/dist/Lists/Sections/Sections.test.js +0 -20
  120. package/dist/Lists/Sections/package.json +0 -6
  121. package/dist/Lists/Voices/horse.ogm +0 -0
  122. package/dist/Misc/AxiosRestExample/AxiosRestExample.js +0 -46
  123. package/dist/Misc/AxiosRestExample/AxiosRestPostExample.js +0 -65
  124. package/dist/Misc/ColorSelector/ColorSelector.js +0 -143
  125. package/dist/Misc/ColorSelector/package.json +0 -6
  126. package/dist/Misc/EmojiInput/css/emoji-mart.css +0 -497
  127. package/dist/Misc/MedalSelector/PlaceholderMedal.svg +0 -27
  128. package/dist/Misc/NotFound/Bot.js +0 -62
  129. package/dist/Misc/NotFound/Bots.js +0 -75
  130. package/dist/Misc/NotFound/Discussions.js +0 -52
  131. package/dist/Misc/NotFound/Roleplay.js +0 -62
  132. package/dist/Misc/NotFound/Roleplays.js +0 -75
  133. package/dist/Misc/NotFound/Script.js +0 -47
  134. package/dist/Misc/Notes/Notes.js +0 -77
  135. package/dist/Misc/Notes/Notes.test.js +0 -20
  136. package/dist/Misc/Notes/package.json +0 -6
  137. package/dist/Misc/NualaCreating/Nuala1.js +0 -352
  138. package/dist/Misc/OnboardingEnrolment/CheckIfCourse.js +0 -33
  139. package/dist/Misc/OnboardingEnrolment/ClassCodeInvalid.js +0 -14
  140. package/dist/Misc/OnboardingEnrolment/ClassroomCodeInvalid.js +0 -14
  141. package/dist/Misc/OnboardingEnrolment/EnrolKey.js +0 -14
  142. package/dist/Misc/RoleplayChatWaveForm/RoleplayChatWaveForm.js +0 -199
  143. package/dist/Misc/RoleplayChatWaveForm/package.json +0 -6
  144. package/dist/Misc/RoleplayWaveform/RoleplayWaveform.js +0 -95
  145. package/dist/Misc/RoleplayWaveform/package.json +0 -6
  146. package/dist/Misc/SpeedDial/SpeedDial.js +0 -49
  147. package/dist/Misc/SpeedDial/package.json +0 -6
  148. package/dist/Misc/SubmitTextField/SubmitTextField.js +0 -48
  149. package/dist/Misc/newGenerateCardBackground/Generate Card Background (standalone) (1).html +0 -186
  150. package/dist/Navigation/ResponsiveStepper/ResponsiveStepper.js +0 -253
  151. package/dist/Navigation/ResponsiveStepper/ResponsiveStepper.test.js +0 -20
  152. package/dist/Navigation/ResponsiveStepper/package.json +0 -6
  153. package/dist/Navigation/ResponsiveTabs/TabContainer.js +0 -28
  154. package/dist/Tables/CreatedDiscussionsTable/CreatedDiscussionsTable.js +0 -188
  155. package/dist/Tables/GameRows/GameRows.js +0 -87
  156. package/dist/Tables/GameRows/package.json +0 -6
  157. package/dist/Tables/Games/Games.js +0 -258
  158. package/dist/Tables/Games/package.json +0 -6
  159. package/dist/Tables/Phrases/Phrases.js +0 -200
  160. package/dist/Tables/Phrases/Phrases.test.js +0 -20
  161. package/dist/Tables/Phrases/package.json +0 -6
  162. package/dist/Tables/Plans/Plans.js +0 -74
  163. package/dist/Tables/Plans/plans.json +0 -162
  164. package/dist/Tables/Plans/plans2.json +0 -151
  165. package/dist/Tables/Progress/Header.js +0 -559
  166. package/dist/Tables/Progress/README.md +0 -53
  167. package/dist/Tables/SubmissionsTableCards/SubmissionsTableCards.js +0 -296
  168. package/dist/Tables/Users/UsersTable.js +0 -159
  169. package/dist/img/Bronze Medal.svg +0 -545
  170. package/dist/img/Bronze.svg +0 -349
  171. package/dist/img/BronzeBadge.svg +0 -493
  172. package/dist/img/BronzeBadgeWithoutRibbon.svg +0 -317
  173. package/dist/img/Frame 51.svg +0 -98
  174. package/dist/img/Frame 52.svg +0 -131
  175. package/dist/img/Frame 53.svg +0 -139
  176. package/dist/img/Gold Badge.svg +0 -362
  177. package/dist/img/Gold Medal.svg +0 -481
  178. package/dist/img/GoldBadge.svg +0 -470
  179. package/dist/img/GoldBadgeWithoutRibbon.svg +0 -324
  180. package/dist/img/NualaCreateImage.svg +0 -129
  181. package/dist/img/NualangLiveAnwerQuicklyListening.svg +0 -3608
  182. package/dist/img/NualangLiveListen.png +0 -0
  183. package/dist/img/NualangLiveListenQuickly.png +0 -0
  184. package/dist/img/NualangLiveListenToPhrase.svg +0 -4286
  185. package/dist/img/NualangLivePickAnswer.svg +0 -134
  186. package/dist/img/NualangLiveResult.png +0 -0
  187. package/dist/img/NualangLiveRoleplayAnswer.png +0 -0
  188. package/dist/img/NualangLiveRoleplayConvo.png +0 -0
  189. package/dist/img/NualangLiveSeePlace.svg +0 -134
  190. package/dist/img/NualangLiveTranslate.png +0 -0
  191. package/dist/img/NualangLiveTranslateQuickly.png +0 -0
  192. package/dist/img/NualangLiveViewConversations.svg +0 -135
  193. package/dist/img/Polly.svg +0 -18
  194. package/dist/img/Silver Badge.svg +0 -523
  195. package/dist/img/Silver Medal.svg +0 -549
  196. package/dist/img/SilverBadge.svg +0 -655
  197. package/dist/img/SilverBadgeWithoutRibbon.svg +0 -491
  198. package/dist/img/avatars/0-nuala-001.svg +0 -34
  199. package/dist/img/avatars/0-nuala-002.svg +0 -25
  200. package/dist/img/avatars/0-nuala-003.svg +0 -27
  201. package/dist/img/avatars/0-nuala-004.svg +0 -41
  202. package/dist/img/avatars/0-nuala-005.svg +0 -33
  203. package/dist/img/avatars/0-nuala-006.svg +0 -32
  204. package/dist/img/avatars/0-nuala-007.svg +0 -37
  205. package/dist/img/avatars/0-nuala-008.svg +0 -21
  206. package/dist/img/avatars/0-nuala-009.svg +0 -21
  207. package/dist/img/avatars/0-nuala-010.svg +0 -39
  208. package/dist/img/avatars/0-nuala-011.svg +0 -33
  209. package/dist/img/avatars/0-nuala-012.svg +0 -43
  210. package/dist/img/avatars/0-nuala-013.svg +0 -36
  211. package/dist/img/avatars/0-nuala-014.svg +0 -30
  212. package/dist/img/avatars/0-nuala-015.svg +0 -28
  213. package/dist/img/avatars/0-nuala-016.svg +0 -24
  214. package/dist/img/avatars/0-nuala-017.svg +0 -31
  215. package/dist/img/avatars/0-nuala-018.svg +0 -29
  216. package/dist/img/avatars/0-nuala-019.svg +0 -57
  217. package/dist/img/avatars/0-nuala-020.svg +0 -30
  218. package/dist/img/avatars/0-nuala-021.svg +0 -28
  219. package/dist/img/avatars/001-man.svg +0 -104
  220. package/dist/img/avatars/002-girl.svg +0 -92
  221. package/dist/img/avatars/006-woman-1.svg +0 -99
  222. package/dist/img/avatars/028-woman-7.svg +0 -95
  223. package/dist/img/avatars/029-man-8.svg +0 -95
  224. package/dist/img/avatars/035-woman-9.svg +0 -86
  225. package/dist/img/avatars/066-boy-6.svg +0 -91
  226. package/dist/img/avatars/1-teacher-001.svg +0 -38
  227. package/dist/img/avatars/1-teacher-002.svg +0 -52
  228. package/dist/img/avatars/1-teacher-003.svg +0 -86
  229. package/dist/img/avatars/avatar1.png +0 -0
  230. package/dist/img/avatars/avatar2.png +0 -0
  231. package/dist/img/avatars/nuala_10_speaking.svg +0 -45
  232. package/dist/img/avatars/nuala_11_speaking.svg +0 -41
  233. package/dist/img/avatars/nuala_12_speaking.svg +0 -49
  234. package/dist/img/avatars/nuala_13_speaking.svg +0 -44
  235. package/dist/img/avatars/nuala_14_speaking.svg +0 -36
  236. package/dist/img/avatars/nuala_15_speaking.svg +0 -36
  237. package/dist/img/avatars/nuala_16_speaking.svg +0 -32
  238. package/dist/img/avatars/nuala_17_speaking.svg +0 -39
  239. package/dist/img/avatars/nuala_18_speaking.svg +0 -37
  240. package/dist/img/avatars/nuala_19_speaking.svg +0 -65
  241. package/dist/img/avatars/nuala_1_celebrating.svg +0 -30
  242. package/dist/img/avatars/nuala_1_speaking.svg +0 -37
  243. package/dist/img/avatars/nuala_20_speaking.svg +0 -38
  244. package/dist/img/avatars/nuala_21_speaking.svg +0 -36
  245. package/dist/img/avatars/nuala_2_speaking.svg +0 -29
  246. package/dist/img/avatars/nuala_3_speaking.svg +0 -28
  247. package/dist/img/avatars/nuala_4_speaking.svg +0 -41
  248. package/dist/img/avatars/nuala_5_speaking.svg +0 -36
  249. package/dist/img/avatars/nuala_6_speaking.svg +0 -36
  250. package/dist/img/avatars/nuala_7_speaking.svg +0 -43
  251. package/dist/img/avatars/nuala_8_speaking.svg +0 -29
  252. package/dist/img/avatars/nuala_9_speaking.svg +0 -27
  253. package/dist/img/dashboard.svg +0 -104
  254. package/dist/img/defaultImg.png +0 -0
  255. package/dist/img/nua-glyph.svg +0 -6
  256. package/dist/img/nuala-silhouette.svg +0 -43
  257. package/dist/img/nuala_1_celebrating.svg +0 -33
  258. package/dist/img/nuala_2_celebrating.svg +0 -20
  259. package/dist/img/nuala_3_celebrating.svg +0 -21
  260. package/dist/img/nuala_4_celebrating.svg +0 -30
  261. package/dist/img/nuala_5_celebrating.svg +0 -30
  262. package/dist/img/nuala_celebrating.svg +0 -39
  263. package/dist/img/nuala_speaking_4.svg +0 -41
  264. package/dist/img/nualas.svg +0 -185
  265. package/dist/img/soundWave.png +0 -0
  266. package/dist/img/stars1.svg +0 -44
  267. package/dist/img/stars2.svg +0 -30
  268. package/dist/img/teacher.svg +0 -89
  269. package/dist/img/work-chat.svg +0 -1
  270. package/dist/img/world.svg +0 -41
@@ -30,7 +30,8 @@ const AssignmentCardsList = ({
30
30
  isLoadingAssignments,
31
31
  isChallengeModeStudent,
32
32
  isReadWriteModeStudent,
33
- scrollableTarget = null
33
+ scrollableTarget = null,
34
+ openSnackbar
34
35
  }) => {
35
36
  const [lastClickedExerciseId, setLastClickedExerciseId] = useState(null);
36
37
  const [filter, setFilter] = useState("all");
@@ -355,7 +356,8 @@ const AssignmentCardsList = ({
355
356
  handleViewProgress: handleViewProgress,
356
357
  assignedStudents: assignmentMembersById[assignment.assignmentId] || [],
357
358
  isChallengeModeStudent: isChallengeModeStudent,
358
- isReadWriteModeStudent: isReadWriteModeStudent
359
+ isReadWriteModeStudent: isReadWriteModeStudent,
360
+ openSnackbar: openSnackbar
359
361
  }, assignment.assignmentId))
360
362
  })]
361
363
  });
@@ -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,
@@ -57,6 +57,7 @@ function CreatePhrase({
57
57
  verificationStatus,
58
58
  makeChatGptApiRequest,
59
59
  difficulty,
60
+ openSnackbar,
60
61
  ...otherProps
61
62
  }) {
62
63
  const translationInputRef = useRef();
@@ -723,7 +724,8 @@ function CreatePhrase({
723
724
  autoSuggest: true,
724
725
  autoSuggestedImages: autoSuggestedImages,
725
726
  selectedImage: selectedImage,
726
- verificationStatus: verificationStatus
727
+ verificationStatus: verificationStatus,
728
+ openSnackbar: openSnackbar
727
729
  })
728
730
  })]
729
731
  })]
@@ -71,7 +71,8 @@ export default function CreatePhraseGroupDialog({
71
71
  onCreated,
72
72
  t = text => text,
73
73
  fileSizeLimit,
74
- verificationStatus
74
+ verificationStatus,
75
+ openSnackbar
75
76
  }) {
76
77
  const [name, setName] = useState("");
77
78
  const [description, setDescription] = useState("");
@@ -180,7 +181,8 @@ export default function CreatePhraseGroupDialog({
180
181
  autoCrop: true,
181
182
  autoSuggest: false,
182
183
  selectedImage: selectedImage,
183
- verificationStatus: verificationStatus
184
+ verificationStatus: verificationStatus,
185
+ openSnackbar: openSnackbar
184
186
  })
185
187
  })]
186
188
  })
@@ -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);
@@ -7,13 +7,8 @@ import ImageBuilderControls from "./ImageBuilderControls";
7
7
  import ImageBuilderPreview from "./ImageBuilderPreview";
8
8
  import ResponsiveDialog from "../ResponsiveDialog/ResponsiveDialog";
9
9
  import { PRESET_COLORS, ICON_KEYS } from "./iconPaths";
10
- import { generatePatternCanvas, getCenterCropDataUrl, getOptimalIconOpacity, shouldUseDarkIcons } from "./imageBuilderUtils";
11
-
12
- // Matches placeholder SVG ratio (21144 x 4890), scaled down 10x
10
+ import { generateBuilderImages, getOptimalIconOpacity, shouldUseDarkIcons } from "./imageBuilderUtils";
13
11
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
14
- const BANNER_W = 2114;
15
- const BANNER_H = 489;
16
- const CARD_SIZE = 489;
17
12
  const useStyles = makeStyles()(theme => ({
18
13
  body: {
19
14
  display: "flex",
@@ -51,19 +46,13 @@ export default function ImageBuilderDialog({
51
46
  cardDataUrl: null
52
47
  };
53
48
  }
54
- const canvas = generatePatternCanvas({
49
+ return generateBuilderImages({
55
50
  color,
56
51
  iconKey,
57
52
  density: "dense",
58
53
  iconOpacity,
59
- darkIcons,
60
- width: BANNER_W,
61
- height: BANNER_H
54
+ darkIcons
62
55
  });
63
- return {
64
- bannerDataUrl: canvas.toDataURL("image/png"),
65
- cardDataUrl: getCenterCropDataUrl(canvas, CARD_SIZE)
66
- };
67
56
  }, [color, iconKey, iconOpacity, darkIcons]);
68
57
 
69
58
  // Reset state when dialog opens
@@ -4,6 +4,19 @@ const DENSITY_COLS = {
4
4
  medium: 10,
5
5
  dense: 14
6
6
  };
7
+
8
+ // Base (logical) dimensions of a builder image. Matches placeholder SVG ratio
9
+ // (21144 x 4890), scaled down 10x. The card is a centre-cropped square.
10
+ export const BANNER_W = 2114;
11
+ export const BANNER_H = 489;
12
+ export const CARD_SIZE = 489;
13
+
14
+ // Supersampling factor applied to the generated pixel dimensions so the stored
15
+ // image stays crisp when scaled up on large / HiDPI screens. Device-independent
16
+ // on purpose — the image is persisted and shared across devices, so we must not
17
+ // bake window.devicePixelRatio into it. Tune this single knob to trade payload
18
+ // size against sharpness.
19
+ export const OUTPUT_SCALE = 2;
7
20
  function hexToRgb(hex) {
8
21
  const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
9
22
  return result ? {
@@ -87,6 +100,7 @@ export function generatePatternCanvas({
87
100
  canvas.width = width;
88
101
  canvas.height = height;
89
102
  const ctx = canvas.getContext("2d");
103
+ ctx.imageSmoothingQuality = "high";
90
104
 
91
105
  // 1. Solid background
92
106
  ctx.fillStyle = color;
@@ -107,10 +121,18 @@ export function generatePatternCanvas({
107
121
  };
108
122
  const maxDim = Math.max(vb.width, vb.height);
109
123
  const scale = iconSize / maxDim;
124
+
125
+ // Rasterise the stamp at its final on-canvas size by filling the vector paths
126
+ // through a scaled context. Previously the stamp was drawn at the raw viewBox
127
+ // size (e.g. 24px) and then upscaled ~4x when blitted onto the pattern canvas,
128
+ // which baked a blurry raster upscale into the source image. Filling from
129
+ // vectors at the target size keeps icons crisp while preserving the single
130
+ // stamp (so overlapping sub-paths don't double-composite at globalAlpha < 1).
110
131
  const stampCanvas = document.createElement("canvas");
111
- stampCanvas.width = vb.width;
112
- stampCanvas.height = vb.height;
132
+ stampCanvas.width = Math.ceil(vb.width * scale);
133
+ stampCanvas.height = Math.ceil(vb.height * scale);
113
134
  const stampCtx = stampCanvas.getContext("2d");
135
+ stampCtx.scale(scale, scale);
114
136
  stampCtx.fillStyle = darkIcons ? "#000" : "#fff";
115
137
  const path2Ds = iconData.paths.map(d => new Path2D(d));
116
138
  for (const p of path2Ds) {
@@ -131,9 +153,8 @@ export function generatePatternCanvas({
131
153
  ctx.save();
132
154
  ctx.translate(x, y);
133
155
  ctx.rotate(rotation);
134
- ctx.scale(scale, scale);
135
- ctx.translate(-vb.width / 2, -vb.height / 2);
136
- ctx.drawImage(stampCanvas, 0, 0);
156
+ // Draw the pre-scaled stamp 1:1 (centred) — no upscaling here.
157
+ ctx.drawImage(stampCanvas, -stampCanvas.width / 2, -stampCanvas.height / 2);
137
158
  ctx.restore();
138
159
  }
139
160
  }
@@ -156,4 +177,25 @@ export function getCenterCropDataUrl(source, size) {
156
177
  const sx = (source.width - source.height) / 2;
157
178
  ctx.drawImage(source, sx, 0, source.height, source.height, 0, 0, size, size);
158
179
  return cropCanvas.toDataURL("image/png");
180
+ }
181
+
182
+ /**
183
+ * Generate the builder images (banner + centre-cropped card) at the shared
184
+ * supersampled output resolution. Single source of truth for the builder
185
+ * dimensions so callers don't duplicate the magic numbers.
186
+ *
187
+ * @param {Object} opts – forwarded to generatePatternCanvas (color, iconKey,
188
+ * density, iconOpacity, darkIcons, seed)
189
+ * @returns {{ bannerDataUrl: string, cardDataUrl: string }}
190
+ */
191
+ export function generateBuilderImages(opts) {
192
+ const canvas = generatePatternCanvas({
193
+ ...opts,
194
+ width: BANNER_W * OUTPUT_SCALE,
195
+ height: BANNER_H * OUTPUT_SCALE
196
+ });
197
+ return {
198
+ bannerDataUrl: canvas.toDataURL("image/png"),
199
+ cardDataUrl: getCenterCropDataUrl(canvas, CARD_SIZE * OUTPUT_SCALE)
200
+ };
159
201
  }
@@ -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,
@@ -80,7 +80,8 @@ export default function QuickCreateClassroomDialog({
80
80
  userImage,
81
81
  forLang,
82
82
  languages,
83
- forLanguages
83
+ forLanguages,
84
+ openSnackbar
84
85
  }) {
85
86
  const [classroomName, setClassroomName] = useState("");
86
87
  const [classroomDescription, setClassroomDescription] = useState("");
@@ -275,7 +276,8 @@ export default function QuickCreateClassroomDialog({
275
276
  autoSuggest: false,
276
277
  selectedImage: selectedImage,
277
278
  verificationStatus: verificationStatus,
278
- onBuilderSettingsChange: setBuilderSettings
279
+ onBuilderSettingsChange: setBuilderSettings,
280
+ openSnackbar: openSnackbar
279
281
  })
280
282
  })]
281
283
  })
@@ -83,7 +83,8 @@ export default function QuickCreateCourseDialog({
83
83
  difficulties = ["A1", "A2", "B1", "B2", "C1", "C2"],
84
84
  fileSizeLimit,
85
85
  verificationStatus,
86
- userImage
86
+ userImage,
87
+ openSnackbar
87
88
  }) {
88
89
  const defaultSelectedImage = {
89
90
  image: "",
@@ -289,7 +290,8 @@ export default function QuickCreateCourseDialog({
289
290
  autoSuggest: false,
290
291
  selectedImage: selectedImage,
291
292
  verificationStatus: verificationStatus,
292
- onBuilderSettingsChange: setBuilderSettings
293
+ onBuilderSettingsChange: setBuilderSettings,
294
+ openSnackbar: openSnackbar
293
295
  })
294
296
  })]
295
297
  })
@@ -83,7 +83,8 @@ export default function QuickCreateTopicDialog({
83
83
  userImage,
84
84
  coursePicture,
85
85
  coursePictureBuilderColor,
86
- coursePictureBuilderIcon
86
+ coursePictureBuilderIcon,
87
+ openSnackbar
87
88
  }) {
88
89
  // Only inherit the parent course image when it's a real user-set image.
89
90
  // If the course has no image, or its image was itself generated by the image
@@ -274,7 +275,8 @@ export default function QuickCreateTopicDialog({
274
275
  autoSuggest: false,
275
276
  selectedImage: selectedImage,
276
277
  verificationStatus: verificationStatus,
277
- onBuilderSettingsChange: setBuilderSettings
278
+ onBuilderSettingsChange: setBuilderSettings,
279
+ openSnackbar: openSnackbar
278
280
  })
279
281
  })]
280
282
  })
@@ -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 () => {
@@ -770,7 +772,8 @@ function Bot({
770
772
  courseId: courseId,
771
773
  sectionId: sectionId,
772
774
  topicId: topicId,
773
- initialTopicGoal: initialTopicGoal
775
+ initialTopicGoal: initialTopicGoal,
776
+ openSnackbar: openSnackbar
774
777
  }) : null, tabValue === 1 ? /*#__PURE__*/_jsx(Box, {
775
778
  className: hidden ? classes.paneFull : classes.pane,
776
779
  children: BotPreview
@@ -791,7 +794,8 @@ function Bot({
791
794
  subscription: subscription,
792
795
  handleBotInstructionsImageUpload: handleBotInstructionsImageUpload,
793
796
  isUserInternal: isUserInternal,
794
- verificationStatus: verificationStatus
797
+ verificationStatus: verificationStatus,
798
+ openSnackbar: openSnackbar
795
799
  })
796
800
  }) : null]
797
801
  }), /*#__PURE__*/_jsx(ResponsiveDialog, {
@@ -14,7 +14,8 @@ function AddImageModal(props) {
14
14
  open,
15
15
  handleUploadBotExerciseImage,
16
16
  botId,
17
- verificationStatus
17
+ verificationStatus,
18
+ openSnackbar
18
19
  } = props;
19
20
  const theme = useTheme();
20
21
  const fullScreen = useMediaQuery(theme.breakpoints.down("sm"));
@@ -76,7 +77,8 @@ function AddImageModal(props) {
76
77
  name: "image",
77
78
  value: selectedImage.image,
78
79
  handleChange: handleImageChange,
79
- verificationStatus: verificationStatus
80
+ verificationStatus: verificationStatus,
81
+ openSnackbar: openSnackbar
80
82
  })]
81
83
  }), /*#__PURE__*/_jsxs(DialogActions, {
82
84
  children: [/*#__PURE__*/_jsx(Button, {