@nualang/nualang-ui-components 0.1.1144 → 0.1.1145

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.
@@ -61,10 +61,12 @@ function CreateQuestion({
61
61
  const [question, setQuestion] = (0, _react.useState)(otherProps.question);
62
62
  const [answer, setAnswer] = (0, _react.useState)(initialAnswerState);
63
63
  const [answerList, setAnswerList] = (0, _react.useState)(otherProps.answers);
64
+ const [error, setError] = (0, _react.useState)("");
64
65
  const handleAnswerInputChange = event => {
65
66
  const target = event.target;
66
67
  const value = target.type === "checkbox" ? target.checked : target.value;
67
68
  const name = target.name;
69
+ setError("");
68
70
  setAnswer(prevState => ({
69
71
  ...prevState,
70
72
  [name]: value
@@ -72,11 +74,18 @@ function CreateQuestion({
72
74
  };
73
75
  const handleAddAnswer = () => {
74
76
  if (answer && answer.text) {
75
- setAnswerList(prevAnswerList => [...prevAnswerList, {
76
- ...answer,
77
- text: answer.text.trim()
78
- }]);
79
- setAnswer(initialAnswerState);
77
+ const trimmedText = answer.text.trim().toLowerCase();
78
+ const isDuplicate = answerList.some(existingAnswer => existingAnswer.text.toLowerCase() === trimmedText);
79
+ if (isDuplicate) {
80
+ setError(t("duplicate_answer_desc"));
81
+ } else {
82
+ setAnswerList(prevAnswerList => [...prevAnswerList, {
83
+ ...answer,
84
+ text: answer.text.trim()
85
+ }]);
86
+ setAnswer(initialAnswerState);
87
+ setError("");
88
+ }
80
89
  }
81
90
  };
82
91
  const handleRemoveAnswer = index => {
@@ -101,7 +110,8 @@ function CreateQuestion({
101
110
  };
102
111
  const handleSubmit = () => {
103
112
  const correctAnswers = answerList.filter(a => a.correct);
104
- if (correctAnswers.length && question !== "") {
113
+ const duplicateAnswers = answerList.filter((a, index) => answerList.findIndex(b => b.text.toLowerCase() === a.text.toLowerCase()) !== index);
114
+ if (correctAnswers.length && question !== "" && !duplicateAnswers.length) {
105
115
  onSubmit({
106
116
  question: question.trim(),
107
117
  answers: answerList
@@ -110,7 +120,7 @@ function CreateQuestion({
110
120
  resetQuestionState();
111
121
  }
112
122
  } else {
113
- alert(t("must_question_least_correct_answer"));
123
+ alert(!correctAnswers.length ? t("must_question_least_correct_answer") : t("duplicate_answer_desc"));
114
124
  }
115
125
  };
116
126
  const handleOnDragEnd = result => {
@@ -170,6 +180,7 @@ function CreateQuestion({
170
180
  item: true,
171
181
  xs: 12
172
182
  }, /*#__PURE__*/_react.default.createElement(_InputHelper.default, {
183
+ error: !!error,
173
184
  t: t,
174
185
  value: answer.text,
175
186
  onChange: handleAnswerInputChange,
@@ -182,7 +193,7 @@ function CreateQuestion({
182
193
  fullWidth: true,
183
194
  variant: "outlined",
184
195
  characters: forLangCharacters ? forLangCharacters : null,
185
- helperText: answer.text && answer.text.trim() !== "" ? t("answer_helper_text") : "",
196
+ helperText: error || (answer.text && answer.text.trim() !== "" ? t("answer_helper_text") : ""),
186
197
  multiline: true
187
198
  }))), /*#__PURE__*/_react.default.createElement(_reactBeautifulDnd.DragDropContext, {
188
199
  onDragEnd: handleOnDragEnd
@@ -173,6 +173,7 @@ function Pronouncer({
173
173
  cx
174
174
  } = useStyles();
175
175
  const [viewMoreTranslations, setViewMoreTranslations] = (0, _react.useState)(false);
176
+ const [currentPhraseText, setCurrentPhraseText] = (0, _react.useState)("");
176
177
  const {
177
178
  isSocketConnected,
178
179
  recognitionState,
@@ -181,7 +182,12 @@ function Pronouncer({
181
182
  resetTranscript,
182
183
  clearAttemptAudio,
183
184
  indicateEndStream
184
- } = (0, _useRecognition.default)();
185
+ } = (0, _useRecognition.default)({
186
+ currentPhraseText,
187
+ trackRecommendedEvent,
188
+ exerciseName,
189
+ learnLang
190
+ });
185
191
  const {
186
192
  interimTranscript,
187
193
  finalTranscript,
@@ -192,7 +198,6 @@ function Pronouncer({
192
198
  attemptAudioURL,
193
199
  audioBlob
194
200
  } = recognitionState;
195
- const [currentAttemptAudioURL, setCurrentAttemptAudioURL] = (0, _react.useState)("");
196
201
  const handleMic = expectedTranscript => {
197
202
  const speechContexts = expectedTranscript ? [{
198
203
  phrases: [...(expectedTranscript.length < 100 ? [(0, _utils.removeExtraWhiteSpaces)(expectedTranscript)] : (0, _utils.removeExtraWhiteSpaces)(expectedTranscript).split(" "))],
@@ -200,42 +205,16 @@ function Pronouncer({
200
205
  }] : null;
201
206
  const model = expectedTranscript && expectedTranscript.length < 50 ? "latest_short" : "default";
202
207
 
203
- // Stop Recording required to get final confidence score
204
- toggleListen(learnLang, speechContexts, model);
208
+ // Stop Recording required to get final confidence score and trigger event if needed
209
+ toggleListen(learnLang, speechContexts, model, true);
205
210
  };
206
211
  (0, _react.useEffect)(() => {
207
- if (open && !recognizing) {
208
- // Check if recording has ended and has detected at least one word
209
- if (finalTranscript) {
210
- // Submit attempt
211
- handleContinue(false, currentPhrase);
212
- } else if (attemptAudioURL && attemptAudioURL !== currentAttemptAudioURL) {
213
- setCurrentAttemptAudioURL(attemptAudioURL);
214
- const captureSpeechRecognitionNoTranscript = async () => {
215
- const {
216
- userAgent,
217
- language,
218
- mediaDevices
219
- } = navigator;
220
- const browser = (0, _utils.getBrowserInfo)(userAgent);
221
- const machineInfo = (0, _utils.getMachineInfo)(userAgent);
222
- const [availableMicrophones, activeMicrophone] = await (0, _utils.getMicrophoneInfo)(mediaDevices);
223
- trackRecommendedEvent("speech_recognition_no_transcript", {
224
- A1_phrase: `${currentPhrase.phrase}`,
225
- A2_active_microphone: `${activeMicrophone}`,
226
- A3_available_microphones: `${availableMicrophones}`,
227
- A4_browser: `${browser}`,
228
- A5_socket_state: isSocketConnected ? "connected" : "disconnected",
229
- A6_machine_info: `${machineInfo}`,
230
- A7_user_language: `${language}`,
231
- A8_learn_language: `${learnLang}`,
232
- A9_exercise: `${exerciseName}`
233
- });
234
- };
235
- captureSpeechRecognitionNoTranscript().catch(console.error);
236
- }
212
+ // Check if recording has ended and has detected at least one word
213
+ if (open && !recognizing && finalTranscript) {
214
+ // Submit attempt
215
+ handleContinue(false, currentPhrase);
237
216
  }
238
- }, [open, recognizing, finalTranscript, attemptAudioURL]);
217
+ }, [open, recognizing, finalTranscript]);
239
218
  const initialVoiceSpeed = topicInfo && topicInfo.voiceSpeed ? topicInfo.voiceSpeed : "";
240
219
  const exerciseState = (0, _useExerciseState.default)({
241
220
  open,
@@ -322,6 +301,11 @@ function Pronouncer({
322
301
  checked,
323
302
  setChecked
324
303
  } = exerciseState;
304
+ (0, _react.useEffect)(() => {
305
+ if (currentPhrase.phrase) {
306
+ setCurrentPhraseText(currentPhrase.phrase);
307
+ }
308
+ }, [currentPhrase]);
325
309
  const unattemptedQuestionTotal = questions.filter(q => !q.questionAttempted).length;
326
310
  const joyrideSteps = [{
327
311
  content: /*#__PURE__*/_react.default.createElement("h2", null, t("pronunciation_exercise_welcome")),
@@ -284,6 +284,7 @@ function RoleplayGame({
284
284
  open
285
285
  });
286
286
  const scriptWithoutQuestions = roleplay.script.filter(scriptLine => !scriptLine?.question);
287
+ const [currentPhraseText, setCurrentPhraseText] = (0, _react.useState)("");
287
288
  const {
288
289
  isSocketConnected,
289
290
  recognitionState,
@@ -291,7 +292,13 @@ function RoleplayGame({
291
292
  resetTranscript,
292
293
  indicateEndStream,
293
294
  toggleListen
294
- } = (0, _useRecognition.default)();
295
+ } = (0, _useRecognition.default)({
296
+ currentPhraseText,
297
+ trackRecommendedEvent,
298
+ exerciseName,
299
+ learnLang,
300
+ gameId
301
+ });
295
302
  const {
296
303
  interimTranscript,
297
304
  finalTranscript,
@@ -304,7 +311,6 @@ function RoleplayGame({
304
311
  } = recognitionState;
305
312
  const [currentMessages, setCurrentMessages] = (0, _react.useState)([]);
306
313
  const [celebratingNualaImage, setCelebratingNualaImage] = (0, _react.useState)(_nuala_1_celebrating.default);
307
- const [currentAttemptAudioURL, setCurrentAttemptAudioURL] = (0, _react.useState)("");
308
314
  const exerciseState = (0, _useExerciseState.default)({
309
315
  t,
310
316
  open,
@@ -417,8 +423,8 @@ function RoleplayGame({
417
423
  }] : null;
418
424
  const model = expectedTranscript && expectedTranscript.length < 50 ? "latest_short" : "default";
419
425
 
420
- // Stop Recording required to get final confidence score
421
- toggleListen(learnLang, speechContexts, model);
426
+ // Stop Recording required to get final confidence score and trigger event if needed
427
+ toggleListen(learnLang, speechContexts, model, true);
422
428
  };
423
429
  const getRandomNuala = () => {
424
430
  let finished = false;
@@ -441,41 +447,20 @@ function RoleplayGame({
441
447
  // eslint-disable-next-line react-hooks/exhaustive-deps
442
448
  }, [actorHasBeenSelected]);
443
449
  const nextScriptLine = roleplay && scriptWithoutQuestions && scriptWithoutQuestions[roleplayMessages.length] ? scriptWithoutQuestions[roleplayMessages.length] : "";
450
+ (0, _react.useEffect)(() => {
451
+ if (nextScriptLine) {
452
+ setCurrentPhraseText(nextScriptLine.text);
453
+ }
454
+ }, [nextScriptLine]);
444
455
  const phraseWordList = nextScriptLine ? (0, _utils.removeExtraWhiteSpaces)((0, _utils.removeAllSymbols)(nextScriptLine.text)).split(" ") : [];
445
456
  (0, _react.useEffect)(() => {
446
- if (open && !recognizing) {
447
- // Check if recording has ended and has detected at least one word
448
- if (finalTranscript) {
449
- // Submit attempt
450
- checkRoleplayPronunciation(finalTranscript, nextScriptLine.text);
451
- } else if (attemptAudioURL && attemptAudioURL !== currentAttemptAudioURL) {
452
- setCurrentAttemptAudioURL(attemptAudioURL);
453
- const captureSpeechRecognitionNoTranscript = async () => {
454
- const {
455
- userAgent,
456
- language,
457
- mediaDevices
458
- } = navigator;
459
- const browser = (0, _utils.getBrowserInfo)(userAgent);
460
- const machineInfo = (0, _utils.getMachineInfo)(userAgent);
461
- const [availableMicrophones, activeMicrophone] = await (0, _utils.getMicrophoneInfo)(mediaDevices);
462
- trackRecommendedEvent("speech_recognition_no_transcript", {
463
- A1_phrase: `${nextScriptLine.text}`,
464
- A2_active_microphone: `${activeMicrophone}`,
465
- A3_available_microphones: `${availableMicrophones}`,
466
- A4_browser: `${browser}`,
467
- A5_socket_state: isSocketConnected ? "connected" : "disconnected",
468
- A6_machine_info: `${machineInfo}`,
469
- A7_user_language: `${language}`,
470
- A8_learn_language: `${learnLang}`,
471
- A9_exercise: `${exerciseName}-act-it-out-listening-speaking`
472
- });
473
- };
474
- captureSpeechRecognitionNoTranscript().catch(console.error);
475
- }
457
+ // Check if recording has ended and has detected at least one word
458
+ if (open && !recognizing && finalTranscript) {
459
+ // Submit attempt
460
+ checkRoleplayPronunciation(finalTranscript, nextScriptLine.text);
476
461
  }
477
462
  // eslint-disable-next-line react-hooks/exhaustive-deps
478
- }, [open, recognizing, finalTranscript, attemptAudioURL]);
463
+ }, [open, recognizing, finalTranscript]);
479
464
  const isRoleplayScriptLoaded = roleplay && roleplay.script;
480
465
  (0, _react.useEffect)(() => {
481
466
  if (isRoleplayScriptLoaded) {
@@ -280,6 +280,7 @@ function RoleplayGame({
280
280
  open
281
281
  });
282
282
  const scriptWithoutQuestions = roleplay.script.filter(scriptLine => !scriptLine?.question);
283
+ const [currentPhraseText, setCurrentPhraseText] = (0, _react.useState)("");
283
284
  const {
284
285
  isSocketConnected,
285
286
  recognitionState,
@@ -287,7 +288,13 @@ function RoleplayGame({
287
288
  resetTranscript,
288
289
  indicateEndStream,
289
290
  toggleListen
290
- } = (0, _useRecognition.default)();
291
+ } = (0, _useRecognition.default)({
292
+ currentPhraseText,
293
+ trackRecommendedEvent,
294
+ exerciseName,
295
+ learnLang,
296
+ gameId
297
+ });
291
298
  const {
292
299
  interimTranscript,
293
300
  finalTranscript,
@@ -301,7 +308,6 @@ function RoleplayGame({
301
308
  const [currentMessages, setCurrentMessages] = (0, _react.useState)([]);
302
309
  const [ariaLiveText, setAriaLiveText] = (0, _react.useState)("");
303
310
  const [celebratingNualaImage, setCelebratingNualaImage] = (0, _react.useState)(_nuala_1_celebrating.default);
304
- const [currentAttemptAudioURL, setCurrentAttemptAudioURL] = (0, _react.useState)("");
305
311
  const exerciseState = (0, _useExerciseState.default)({
306
312
  t,
307
313
  open,
@@ -415,8 +421,8 @@ function RoleplayGame({
415
421
  const model = expectedTranscript && expectedTranscript.length < 50 ? "latest_short" : "default";
416
422
  setAriaLiveText("Listening...");
417
423
 
418
- // Stop Recording required to get final confidence score
419
- toggleListen(learnLang, speechContexts, model);
424
+ // Stop Recording required to get final confidence score and trigger event if needed
425
+ toggleListen(learnLang, speechContexts, model, true);
420
426
  };
421
427
  const getRandomNuala = () => {
422
428
  let finished = false;
@@ -439,41 +445,20 @@ function RoleplayGame({
439
445
  // eslint-disable-next-line react-hooks/exhaustive-deps
440
446
  }, [actorHasBeenSelected]);
441
447
  const nextScriptLine = roleplay && scriptWithoutQuestions && scriptWithoutQuestions[roleplayMessages.length] ? scriptWithoutQuestions[roleplayMessages.length] : "";
448
+ (0, _react.useEffect)(() => {
449
+ if (nextScriptLine) {
450
+ setCurrentPhraseText(nextScriptLine.text);
451
+ }
452
+ }, [nextScriptLine]);
442
453
  const phraseWordList = nextScriptLine ? (0, _utils.removeExtraWhiteSpaces)((0, _utils.removeAllSymbols)(nextScriptLine.text)).split(" ") : [];
443
454
  (0, _react.useEffect)(() => {
444
- if (open && !recognizing) {
445
- // Check if recording has ended and has detected at least one word
446
- if (finalTranscript) {
447
- // Submit attempt
448
- checkRoleplayPronunciation(finalTranscript, nextScriptLine.text);
449
- } else if (attemptAudioURL && attemptAudioURL !== currentAttemptAudioURL) {
450
- setCurrentAttemptAudioURL(attemptAudioURL);
451
- const captureSpeechRecognitionNoTranscript = async () => {
452
- const {
453
- userAgent,
454
- language,
455
- mediaDevices
456
- } = navigator;
457
- const browser = (0, _utils.getBrowserInfo)(userAgent);
458
- const machineInfo = (0, _utils.getMachineInfo)(userAgent);
459
- const [availableMicrophones, activeMicrophone] = await (0, _utils.getMicrophoneInfo)(mediaDevices);
460
- trackRecommendedEvent("speech_recognition_no_transcript", {
461
- A1_phrase: `${nextScriptLine.text}`,
462
- A2_active_microphone: `${activeMicrophone}`,
463
- A3_available_microphones: `${availableMicrophones}`,
464
- A4_browser: `${browser}`,
465
- A5_socket_state: isSocketConnected ? "connected" : "disconnected",
466
- A6_machine_info: `${machineInfo}`,
467
- A7_user_language: `${language}`,
468
- A8_learn_language: `${learnLang}`,
469
- A9_exercise: `${exerciseName}-act-it-out-speaking`
470
- });
471
- };
472
- captureSpeechRecognitionNoTranscript().catch(console.error);
473
- }
455
+ // Check if recording has ended and has detected at least one word
456
+ if (open && !recognizing && finalTranscript) {
457
+ // Submit attempt
458
+ checkRoleplayPronunciation(finalTranscript, nextScriptLine.text);
474
459
  }
475
460
  // eslint-disable-next-line react-hooks/exhaustive-deps
476
- }, [open, recognizing, finalTranscript, attemptAudioURL]);
461
+ }, [open, recognizing, finalTranscript]);
477
462
  const isRoleplayScriptLoaded = roleplay && roleplay.script;
478
463
  (0, _react.useEffect)(() => {
479
464
  if (isRoleplayScriptLoaded) {
@@ -280,7 +280,7 @@ Subscription.args = {
280
280
  subscriptionStatus: "active",
281
281
  updatedAt: 1630678018431
282
282
  }, {
283
- allowedDomains: 2[("ces-schools.com", "fathomtech.io")],
283
+ allowedDomains: 2["ces-schools.com", "fathomtech.io"],
284
284
  createdAt: 1630679041953,
285
285
  createdBy: "39e46d34-9863-497f-b4f9-10e6a7875833",
286
286
  createdByName: "Stephen",
@@ -40,7 +40,13 @@ const constraints = {
40
40
  audio: true,
41
41
  video: false
42
42
  };
43
- function useRecognition() {
43
+ function useRecognition({
44
+ currentPhraseText,
45
+ trackRecommendedEvent,
46
+ learnLang,
47
+ exerciseName,
48
+ gameId
49
+ }) {
44
50
  const browserSupportsSpeechRecognition = true;
45
51
  const [recognitionState, setRecognitionState] = (0, _react.useState)({
46
52
  interimTranscript: "",
@@ -176,7 +182,30 @@ function useRecognition() {
176
182
  const indicateEndStream = () => {
177
183
  socket.emit("endGoogleCloudStream", "");
178
184
  };
179
- const stopRecording = keepSocketAlive => {
185
+ const captureSpeechRecognitionNoTranscript = async () => {
186
+ const {
187
+ userAgent,
188
+ language,
189
+ mediaDevices
190
+ } = navigator;
191
+ const browser = (0, _index.getBrowserInfo)(userAgent);
192
+ const machineInfo = (0, _index.getMachineInfo)(userAgent);
193
+ const [availableMicrophones, activeMicrophone] = await (0, _index.getMicrophoneInfo)(mediaDevices);
194
+ const eventBody = {
195
+ A1_phrase: `${currentPhraseText}`,
196
+ A2_active_microphone: `${activeMicrophone}`,
197
+ A3_available_microphones: `${availableMicrophones}`,
198
+ A4_browser: `${browser}`,
199
+ A5_socket_state: isSocketConnected ? "connected" : "disconnected",
200
+ A6_machine_info: `${machineInfo}`,
201
+ A7_user_language: `${language}`,
202
+ A8_learn_language: `${learnLang}`,
203
+ A9_exercise: gameId ? `${exerciseName}-${gameId}` : `${exerciseName}`
204
+ };
205
+ console.log("Event Body:", eventBody);
206
+ trackRecommendedEvent("speech_recognition_no_transcript", eventBody);
207
+ };
208
+ const stopRecording = (keepSocketAlive, triggerEvent) => {
180
209
  // indicate end stream to trigger interim transcript to return as final transcript
181
210
  if (socket) {
182
211
  indicateEndStream();
@@ -190,6 +219,14 @@ function useRecognition() {
190
219
  ...prevState,
191
220
  isRecognizingDisabled: false
192
221
  }));
222
+ const {
223
+ finalTranscript,
224
+ interimTranscript
225
+ } = recognitionState;
226
+ if (triggerEvent && (!finalTranscript || finalTranscript === "") && (!interimTranscript || interimTranscript === "")) {
227
+ captureSpeechRecognitionNoTranscript().catch(console.error);
228
+ console.log("Event triggered");
229
+ }
193
230
  }, 2000);
194
231
  };
195
232
  (0, _react.useEffect)(() => {
@@ -280,12 +317,12 @@ function useRecognition() {
280
317
  // No further action needed, as this already closes itself on error
281
318
  });
282
319
  };
283
- const toggleListen = (lang, speechContexts, model) => {
320
+ const toggleListen = (lang, speechContexts, model, triggerEvent) => {
284
321
  const {
285
322
  recognizing
286
323
  } = recognitionState;
287
324
  if (recognizing) {
288
- stop();
325
+ stop(triggerEvent);
289
326
  } else {
290
327
  start(lang, speechContexts, model);
291
328
  }
@@ -302,7 +339,7 @@ function useRecognition() {
302
339
  }, audioRecordingLimit);
303
340
  startRecognition(_config.default.languages[lang?.toLowerCase()]?.Tag, speechContexts, model);
304
341
  };
305
- const stop = () => {
342
+ const stop = triggerEvent => {
306
343
  if (stopAudioRecordingTimeoutId) {
307
344
  clearTimeout(stopAudioRecordingTimeoutId);
308
345
  stopAudioRecordingTimeoutId = null;
@@ -312,7 +349,7 @@ function useRecognition() {
312
349
  recognizing: false,
313
350
  isRecognizingDisabled: true
314
351
  }));
315
- stopRecording(true);
352
+ stopRecording(true, triggerEvent);
316
353
  };
317
354
  const resetTranscript = () => {
318
355
  setRecognitionState(prevState => ({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nualang/nualang-ui-components",
3
- "version": "0.1.1144",
3
+ "version": "0.1.1145",
4
4
  "main": "dist/index.js",
5
5
  "files": [
6
6
  "dist",