@nualang/nualang-ui-components 0.1.1419 → 0.1.1421

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.
@@ -2,7 +2,7 @@ import { useState, useEffect, useRef } from "react";
2
2
  import { Joyride, STATUS } from "react-joyride";
3
3
  import { makeStyles } from "tss-react/mui";
4
4
  import { useTheme } from "@mui/material/styles";
5
- import { Grid, Slide, IconButton, Button, Toolbar, AppBar, Dialog, Typography, Box, Tooltip } from "@mui/material";
5
+ import { Grid, Slide, IconButton, Button, Toolbar, AppBar, Dialog, Typography, Box, Tooltip, CircularProgress } from "@mui/material";
6
6
  import { styled } from "@mui/system";
7
7
  import LightbulbIcon from "@mui/icons-material/Lightbulb";
8
8
  import Replay from "@mui/icons-material/Replay";
@@ -228,9 +228,12 @@ function Pronouncer({
228
228
  });
229
229
  }
230
230
  if (recognizing && interimTranscript === "" && finalTranscript === "") {
231
- // Set a timer to stop listening if no audio is detected after 10 seconds
231
+ // Set a timer to stop listening if no audio is detected after 10 seconds.
232
+ // Without the snackbar here the mic just switches off with no
233
+ // explanation, indistinguishable from the student stopping it themselves.
232
234
  timer = setTimeout(() => {
233
235
  stopListening();
236
+ openSnackbar?.("We didn't hear anything, please try again.", "warning");
234
237
  }, 10000);
235
238
  }
236
239
  return () => clearTimeout(timer);
@@ -467,6 +470,12 @@ function Pronouncer({
467
470
  }
468
471
  };
469
472
  const isListeningIndicatorTextVisible = recognizing && interimTranscript === "" && finalTranscript === "" ? true : false;
473
+
474
+ // isRecognizingDisabled spans the async mic start/stop window (permission
475
+ // prompt, audio worklet setup, socket teardown) - without a distinct label
476
+ // and icon here the button just goes inert with no explanation.
477
+ const isMicBusy = isRecognizingDisabled;
478
+ const micButtonLabel = isMicBusy ? t("loading") : recognizing ? t("click_to_stop") : t("click_to_speak");
470
479
  const navigate = useNavigate();
471
480
  const goBackToAssignment = () => {
472
481
  navigate(`/classrooms/${classroomId}#Assignments`);
@@ -751,32 +760,33 @@ function Pronouncer({
751
760
  [theme.breakpoints.down("md")]: {
752
761
  height: "50px",
753
762
  width: "200px",
754
- color: "white",
755
763
  padding: 2,
756
764
  fontSize: "16px",
757
765
  borderRadius: 3
758
766
  },
759
767
  height: "70px",
760
768
  width: "250px",
761
- color: "white",
762
- backgroundColor: recognizing ? "#d32f2f" : "#2d7b30",
763
769
  padding: 1,
764
770
  fontSize: "20px",
765
771
  borderRadius: 5,
766
772
  border: "1.5px solid transparent",
767
773
  "&:hover": {
768
- backgroundColor: recognizing ? "#d32f2f" : "#2d7b30",
769
774
  border: "1.5px solid black"
770
775
  }
771
776
  }),
772
777
  onClick: () => handleMic(currentPhrase ? currentPhrase.phrase : ""),
773
- startIcon: recognizing ? /*#__PURE__*/_jsx(MicOffIcon, {}) : /*#__PURE__*/_jsx(Mic, {}),
778
+ startIcon: isMicBusy ? /*#__PURE__*/_jsx(CircularProgress, {
779
+ size: 20,
780
+ color: "inherit"
781
+ }) : recognizing ? /*#__PURE__*/_jsx(MicOffIcon, {}) : /*#__PURE__*/_jsx(Mic, {}),
774
782
  color: recognizing ? "error" : "success",
775
783
  disabled: isSpeaking || !isSocketConnected || isRecognizingDisabled,
784
+ "aria-busy": isMicBusy,
785
+ "aria-label": micButtonLabel,
776
786
  id: "voiceInput",
777
787
  variant: "contained",
778
788
  ref: btnRef,
779
- children: recognizing ? t("click_to_stop") : t("click_to_speak")
789
+ children: micButtonLabel
780
790
  })
781
791
  }), /*#__PURE__*/_jsx(Grid, {
782
792
  children: /*#__PURE__*/_jsx(Typography, {
@@ -1,5 +1,5 @@
1
1
  import { useState, useEffect, useRef } from "react";
2
- import { Box, Button, Typography, Grid, Avatar, Dialog } from "@mui/material";
2
+ import { Box, Button, Typography, Grid, Avatar, Dialog, CircularProgress } from "@mui/material";
3
3
  import { makeStyles } from "tss-react/mui";
4
4
  import Person from "@mui/icons-material/Person";
5
5
  import Mic from "@mui/icons-material/Mic";
@@ -101,6 +101,12 @@ function AnswerOptions({
101
101
  buttonRef.current.focus();
102
102
  }
103
103
  }, [isSpeaking, isSocketConnected]);
104
+
105
+ // isRecognizingDisabled spans the async mic start/stop window (permission
106
+ // prompt, audio worklet setup, socket teardown) - without a distinct label
107
+ // and icon here the button just goes inert with no explanation.
108
+ const isMicBusy = isRecognizingDisabled;
109
+ const micButtonLabel = isMicBusy ? t("loading") : recognizing ? t("click_to_stop") : t("click_to_speak");
104
110
  return /*#__PURE__*/_jsxs(Box, {
105
111
  sx: theme => ({
106
112
  display: "flex",
@@ -176,27 +182,28 @@ function AnswerOptions({
176
182
  children: [/*#__PURE__*/_jsx(Button, {
177
183
  sx: theme => ({
178
184
  [theme.breakpoints.down("md")]: {
179
- color: "white",
180
185
  borderRadius: 3
181
186
  },
182
- color: "white",
183
- backgroundColor: recognizing ? "#d32f2f" : "#2d7b30",
184
187
  mb: theme.spacing(2),
185
188
  border: "1.5px solid transparent",
186
189
  "&:hover": {
187
- backgroundColor: recognizing ? "#d32f2f" : "#2d7b30",
188
190
  border: "1.5px solid black"
189
191
  }
190
192
  }),
191
193
  onClick: () => handleMic(text),
192
- startIcon: recognizing ? /*#__PURE__*/_jsx(MicOffIcon, {}) : /*#__PURE__*/_jsx(Mic, {}),
194
+ startIcon: isMicBusy ? /*#__PURE__*/_jsx(CircularProgress, {
195
+ size: 20,
196
+ color: "inherit"
197
+ }) : recognizing ? /*#__PURE__*/_jsx(MicOffIcon, {}) : /*#__PURE__*/_jsx(Mic, {}),
193
198
  color: recognizing ? "error" : "success",
194
199
  disabled: isSpeaking || !isSocketConnected || isRecognizingDisabled,
195
200
  id: "voiceInput",
196
201
  variant: "contained",
197
202
  size: "large",
198
203
  ref: buttonRef,
199
- children: recognizing ? t("click_to_stop") : t("click_to_speak")
204
+ "aria-busy": isMicBusy,
205
+ "aria-label": micButtonLabel,
206
+ children: micButtonLabel
200
207
  }), /*#__PURE__*/_jsx(Typography, {
201
208
  role: "status",
202
209
  variant: "h6",
@@ -434,9 +441,12 @@ function RoleplayGame({
434
441
  useEffect(() => {
435
442
  let timer;
436
443
  if (recognizing && interimTranscript === "" && finalTranscript === "") {
437
- // Set a timer to stop listening if no audio is detected after 10 seconds
444
+ // Set a timer to stop listening if no audio is detected after 10 seconds.
445
+ // Without the snackbar here the mic just switches off with no
446
+ // explanation, indistinguishable from the student stopping it themselves.
438
447
  timer = setTimeout(() => {
439
448
  stopListening();
449
+ openSnackbar?.("We didn't hear anything, please try again.", "warning");
440
450
  }, 10000);
441
451
  }
442
452
  return () => clearTimeout(timer);
@@ -1,5 +1,5 @@
1
1
  import { useState, useEffect, useRef } from "react";
2
- import { Box, Button, Typography, Grid, Avatar, Dialog } from "@mui/material";
2
+ import { Box, Button, Typography, Grid, Avatar, Dialog, CircularProgress } from "@mui/material";
3
3
  import { makeStyles } from "tss-react/mui";
4
4
  import Person from "@mui/icons-material/Person";
5
5
  import RecordVoiceOverIcon from "@mui/icons-material/RecordVoiceOver";
@@ -92,6 +92,12 @@ function AnswerOptions({
92
92
  buttonRef.current.focus();
93
93
  }
94
94
  }, [isSpeaking, isSocketConnected]);
95
+
96
+ // isRecognizingDisabled spans the async mic start/stop window (permission
97
+ // prompt, audio worklet setup, socket teardown) - without a distinct label
98
+ // and icon here the button just goes inert with no explanation.
99
+ const isMicBusy = isRecognizingDisabled;
100
+ const micButtonLabel = isMicBusy ? t("loading") : recognizing ? t("click_to_stop") : t("click_to_speak");
95
101
  return /*#__PURE__*/_jsxs(Box, {
96
102
  sx: theme => ({
97
103
  display: "flex",
@@ -167,28 +173,28 @@ function AnswerOptions({
167
173
  children: [/*#__PURE__*/_jsx(Button, {
168
174
  sx: theme => ({
169
175
  [theme.breakpoints.down("md")]: {
170
- color: "white",
171
176
  borderRadius: 3
172
177
  },
173
- color: "white",
174
- backgroundColor: recognizing ? "#d32f2f" : "#2d7b30",
175
178
  mb: theme.spacing(2),
176
179
  border: "1.5px solid transparent",
177
180
  "&:hover": {
178
- backgroundColor: recognizing ? "#d32f2f" : "#2d7b30",
179
181
  border: "1.5px solid black"
180
182
  }
181
183
  }),
182
184
  onClick: () => handleMic(text),
183
- startIcon: recognizing ? /*#__PURE__*/_jsx(MicOffIcon, {}) : /*#__PURE__*/_jsx(Mic, {}),
185
+ startIcon: isMicBusy ? /*#__PURE__*/_jsx(CircularProgress, {
186
+ size: 20,
187
+ color: "inherit"
188
+ }) : recognizing ? /*#__PURE__*/_jsx(MicOffIcon, {}) : /*#__PURE__*/_jsx(Mic, {}),
184
189
  color: recognizing ? "error" : "success",
185
190
  disabled: isSpeaking || !isSocketConnected || isRecognizingDisabled,
186
191
  id: "voiceInput",
187
192
  variant: "contained",
188
193
  size: "large",
189
194
  ref: buttonRef,
190
- "aria-label": ariaLiveText,
191
- children: recognizing ? t("click_to_stop") : t("click_to_speak")
195
+ "aria-busy": isMicBusy,
196
+ "aria-label": ariaLiveText || micButtonLabel,
197
+ children: micButtonLabel
192
198
  }), /*#__PURE__*/_jsx(Typography, {
193
199
  role: "status",
194
200
  variant: "h6",
@@ -427,9 +433,12 @@ function RoleplayGame({
427
433
  useEffect(() => {
428
434
  let timer;
429
435
  if (recognizing && interimTranscript === "" && finalTranscript === "") {
430
- // Set a timer to stop listening if no audio is detected after 10 seconds
436
+ // Set a timer to stop listening if no audio is detected after 10 seconds.
437
+ // Without the snackbar here the mic just switches off with no
438
+ // explanation, indistinguishable from the student stopping it themselves.
431
439
  timer = setTimeout(() => {
432
440
  stopListening();
441
+ openSnackbar?.("We didn't hear anything, please try again.", "warning");
433
442
  }, 10000);
434
443
  }
435
444
  return () => clearTimeout(timer);
@@ -3,27 +3,23 @@ import { List, ListItem, ListItemButton, ListItemText, Collapse, Avatar, Box, To
3
3
  import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
4
4
  import ExpandLessIcon from "@mui/icons-material/ExpandLess";
5
5
  import ExerciseSelection from "../ExerciseSelection/ExerciseSelection";
6
+ import { topicHasScenario } from "../RoleplaySelection/RoleplaySelection";
6
7
  import { courses as courseQuerys } from "@nualang/nualang-api-and-queries/Queries";
7
8
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
9
  function Course({
9
10
  course,
10
11
  index,
11
12
  getCourseSections,
12
- getRoleplays,
13
13
  getPhraseLists,
14
14
  username,
15
- handleCreateGame,
16
15
  t,
17
- isExerciseSelected,
18
- setIsExerciseSelected,
19
16
  isDialogOpen,
20
- handleSelectExercise,
21
- selectedExercises,
22
- useCase,
23
- assignment,
24
- handleStartExercise,
25
- isAssignment,
26
- isCreator,
17
+ sourceType,
18
+ selectedListIds,
19
+ onTogglePhraseList,
20
+ onToggleTopicAll,
21
+ selectedScenarioId,
22
+ onSelectScenario,
27
23
  courseSectionTopicId
28
24
  }) {
29
25
  const [open, setOpen] = useState(false);
@@ -32,7 +28,7 @@ function Course({
32
28
  return response;
33
29
  }, {
34
30
  courseId: course.courseId,
35
- username: username,
31
+ username,
36
32
  filters: {
37
33
  limit: 1000
38
34
  }
@@ -40,12 +36,19 @@ function Course({
40
36
  enabled: !!course.courseId && isDialogOpen
41
37
  });
42
38
  useEffect(() => {
43
- if (courseSectionTopicId.split("|")[0] === course.courseId) {
39
+ if (courseSectionTopicId?.split("|")[0] === course.courseId) {
44
40
  setOpen(true);
45
41
  }
46
42
  }, [courseSectionTopicId]);
47
43
  const courseSections = useMemo(() => sectionsQuery.isSuccess && Array.isArray(sectionsQuery.data.Items) && sectionsQuery.data.Items.length ? sectionsQuery.data.Items : [], [sectionsQuery.data, sectionsQuery.isSuccess]);
48
- const isCourseEmpty = courseSections.length === 0 && !sectionsQuery.isLoading ? true : false;
44
+ const isCourseEmpty = courseSections.length === 0 && !sectionsQuery.isLoading;
45
+
46
+ // In scenarios mode, once this course's sections have loaded, hide the whole
47
+ // course if none of its topics have scenarios.
48
+ const hasScenarios = courseSections.some(sec => (sec.topics || []).some(tp => topicHasScenario(tp.roleplays)));
49
+ if (sourceType === "scenarios" && sectionsQuery.isSuccess && !hasScenarios) {
50
+ return null;
51
+ }
49
52
  return /*#__PURE__*/_jsxs(List, {
50
53
  disablePadding: true,
51
54
  children: [/*#__PURE__*/_jsx(Tooltip, {
@@ -54,7 +57,7 @@ function Course({
54
57
  disablePadding: true,
55
58
  children: /*#__PURE__*/_jsx(ListItemButton, {
56
59
  onClick: () => setOpen(!open),
57
- disabled: isCourseEmpty || isExerciseSelected,
60
+ disabled: isCourseEmpty,
58
61
  children: /*#__PURE__*/_jsxs(Box, {
59
62
  sx: {
60
63
  display: "flex",
@@ -79,11 +82,11 @@ function Course({
79
82
  })]
80
83
  }), open ? /*#__PURE__*/_jsx(ExpandLessIcon, {
81
84
  sx: {
82
- color: "#757575"
85
+ color: "text.secondary"
83
86
  }
84
87
  }) : /*#__PURE__*/_jsx(ExpandMoreIcon, {
85
88
  sx: {
86
- color: "#757575"
89
+ color: "text.secondary"
87
90
  }
88
91
  })]
89
92
  })
@@ -95,25 +98,20 @@ function Course({
95
98
  unmountOnExit: true,
96
99
  children: /*#__PURE__*/_jsx(Box, {
97
100
  sx: {
98
- ml: 3.5
101
+ ml: 7
99
102
  },
100
103
  children: /*#__PURE__*/_jsx(ExerciseSelection, {
101
104
  t: t,
102
105
  sections: courseSections,
103
106
  isLoading: sectionsQuery.isLoading,
104
- handleCreateGame: handleCreateGame,
105
107
  courseId: course.courseId,
106
- isExerciseSelected: isExerciseSelected,
107
- setIsExerciseSelected: setIsExerciseSelected,
108
- getRoleplays: getRoleplays,
109
108
  getPhraseLists: getPhraseLists,
110
- handleSelectExercise: handleSelectExercise,
111
- selectedExercises: selectedExercises,
112
- useCase: useCase,
113
- assignment: assignment,
114
- handleStartExercise: handleStartExercise,
115
- isAssignment: isAssignment,
116
- isCreator: isCreator,
109
+ sourceType: sourceType,
110
+ selectedListIds: selectedListIds,
111
+ onTogglePhraseList: onTogglePhraseList,
112
+ onToggleTopicAll: onToggleTopicAll,
113
+ selectedScenarioId: selectedScenarioId,
114
+ onSelectScenario: onSelectScenario,
117
115
  courseSectionTopicId: courseSectionTopicId
118
116
  })
119
117
  })
@@ -121,44 +119,35 @@ function Course({
121
119
  }, index);
122
120
  }
123
121
  function CourseSelection({
124
- courses,
122
+ courses = [],
125
123
  getCourseSections,
126
- getRoleplays,
127
124
  getPhraseLists,
128
125
  username,
129
- handleCreateGame,
130
126
  t,
131
- isExerciseSelected,
132
- setIsExerciseSelected,
133
- handleSelectExercise,
134
- selectedExercises,
135
- useCase,
136
- assignment,
137
- handleStartExercise,
138
- isAssignment,
139
- isCreator,
140
- courseSectionTopicId,
141
- ...otherProps
127
+ isDialogOpen,
128
+ sourceType,
129
+ selectedListIds,
130
+ onTogglePhraseList,
131
+ onToggleTopicAll,
132
+ selectedScenarioId,
133
+ onSelectScenario,
134
+ courseSectionTopicId
142
135
  }) {
143
136
  return /*#__PURE__*/_jsx("div", {
144
137
  children: courses.map((course, index) => /*#__PURE__*/_jsx(Course, {
145
138
  course: course,
146
139
  index: index,
147
140
  getCourseSections: getCourseSections,
148
- getRoleplays: getRoleplays,
149
141
  getPhraseLists: getPhraseLists,
150
142
  username: username,
151
- handleCreateGame: handleCreateGame,
152
143
  t: t,
153
- isExerciseSelected: isExerciseSelected,
154
- setIsExerciseSelected: setIsExerciseSelected,
155
- handleSelectExercise: handleSelectExercise,
156
- selectedExercises: selectedExercises,
157
- useCase: useCase,
158
- assignment: assignment,
159
- handleStartExercise: handleStartExercise,
160
- isAssignment: isAssignment,
161
- isCreator: isCreator,
144
+ isDialogOpen: isDialogOpen,
145
+ sourceType: sourceType,
146
+ selectedListIds: selectedListIds,
147
+ onTogglePhraseList: onTogglePhraseList,
148
+ onToggleTopicAll: onToggleTopicAll,
149
+ selectedScenarioId: selectedScenarioId,
150
+ onSelectScenario: onSelectScenario,
162
151
  courseSectionTopicId: courseSectionTopicId
163
152
  }, course.courseId))
164
153
  });