@nualang/nualang-ui-components 0.1.1422 → 0.1.1425
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.
|
@@ -119,10 +119,11 @@ function RoleplayGame({
|
|
|
119
119
|
classes
|
|
120
120
|
} = useStyles();
|
|
121
121
|
const theme = useTheme();
|
|
122
|
+
const isDisabled = isGameHidden || isRoleplayEmpty;
|
|
122
123
|
return /*#__PURE__*/_jsxs(ListItem, {
|
|
123
124
|
alignItems: "flex-start",
|
|
124
|
-
disabled:
|
|
125
|
-
onClick: authenticated ? () => handleSelectRoleplayGame(gameId) : () => handleSignIn(),
|
|
125
|
+
disabled: isDisabled,
|
|
126
|
+
onClick: isDisabled ? undefined : authenticated ? () => handleSelectRoleplayGame(gameId) : () => handleSignIn(),
|
|
126
127
|
button: true,
|
|
127
128
|
divider: true,
|
|
128
129
|
classes: {
|
|
@@ -143,7 +144,11 @@ function RoleplayGame({
|
|
|
143
144
|
},
|
|
144
145
|
"&:focus": {
|
|
145
146
|
backgroundColor: "transparent"
|
|
146
|
-
}
|
|
147
|
+
},
|
|
148
|
+
...(isDisabled && {
|
|
149
|
+
opacity: 0.5,
|
|
150
|
+
pointerEvents: "none"
|
|
151
|
+
})
|
|
147
152
|
},
|
|
148
153
|
children: [/*#__PURE__*/_jsx(ListItemAvatar, {
|
|
149
154
|
children: gameIcon ? /*#__PURE__*/_jsx(Avatar, {
|
|
@@ -9,14 +9,46 @@ const ENDPOINT = import.meta.env.REACT_APP_GCLOUD_SPEECH || "wss://speech-rec-se
|
|
|
9
9
|
const socket = io(ENDPOINT, {
|
|
10
10
|
withCredentials: true,
|
|
11
11
|
autoConnect: false,
|
|
12
|
-
transports: ["websocket", "polling"]
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
12
|
+
transports: ["websocket", "polling"],
|
|
13
|
+
// use WebSocket, fallback to polling if necessary
|
|
14
|
+
// Without this, when websocket is listed first and fails outright (e.g. a
|
|
15
|
+
// school firewall blocking the WS upgrade), engine.io does NOT fall back to
|
|
16
|
+
// polling for that attempt - it just fails the whole connect_error and retries
|
|
17
|
+
// websocket again on the next reconnection attempt, looping forever on
|
|
18
|
+
// networks that block websocket entirely.
|
|
19
|
+
tryAllTransports: true
|
|
18
20
|
});
|
|
19
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Builds a single diagnostic message for a connect_error analytics event.
|
|
24
|
+
* `error.message` for a WebSocket transport failure is (by design of the
|
|
25
|
+
* browser WebSocket API) always the generic string "websocket error" - the
|
|
26
|
+
* spec deliberately withholds the underlying reason (DNS/TLS/refused/timed
|
|
27
|
+
* out) from JS to avoid leaking network topology info to a page. `error.type`
|
|
28
|
+
* and `error.description` are the only other fields engine.io-client
|
|
29
|
+
* attaches, and for polling transport failures `description` is often the
|
|
30
|
+
* XHR object (so `description.status` gives the HTTP status code) - folded
|
|
31
|
+
* into one field here rather than several, since they're only ever useful
|
|
32
|
+
* together and only for this one event.
|
|
33
|
+
*/
|
|
34
|
+
function describeConnectError(error) {
|
|
35
|
+
const description = error?.description;
|
|
36
|
+
let descriptionDetail = "none";
|
|
37
|
+
if (description && typeof description === "object") {
|
|
38
|
+
if (typeof description.status === "number") {
|
|
39
|
+
descriptionDetail = `http_status_${description.status}`;
|
|
40
|
+
} else if (description.message) {
|
|
41
|
+
descriptionDetail = `${description.message}`;
|
|
42
|
+
}
|
|
43
|
+
} else if (description) {
|
|
44
|
+
descriptionDetail = `${description}`;
|
|
45
|
+
}
|
|
46
|
+
const transport = socket.io?.engine?.transport?.name;
|
|
47
|
+
return {
|
|
48
|
+
A4_error_message: `${error?.message} (type=${error?.type}, description=${descriptionDetail}, transport=${transport}, online=${navigator.onLine})`
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
20
52
|
// audioStream constraints
|
|
21
53
|
const constraints = {
|
|
22
54
|
audio: true,
|
|
@@ -24,6 +56,21 @@ const constraints = {
|
|
|
24
56
|
};
|
|
25
57
|
const version = ["v1", "v2"].includes(import.meta.env.REACT_APP_SPEECH_RECOGNITION_VERSION) ? import.meta.env.REACT_APP_SPEECH_RECOGNITION_VERSION : "v1";
|
|
26
58
|
const audioRecordingLimit = 60000;
|
|
59
|
+
|
|
60
|
+
// Safari (16.4+) exposes a page-global AudioSession whose category WebKit
|
|
61
|
+
// otherwise flips to playback-only when TTS/<audio> plays, after which
|
|
62
|
+
// getUserMedia throws "AudioSession category is not compatible with audio
|
|
63
|
+
// capture". Pinning it to "play-and-record" keeps mic capture available even
|
|
64
|
+
// after audio playback. No-op where the API is unsupported (Chrome/Firefox).
|
|
65
|
+
function ensurePlayAndRecordAudioSession() {
|
|
66
|
+
try {
|
|
67
|
+
if (typeof navigator !== "undefined" && navigator.audioSession) {
|
|
68
|
+
navigator.audioSession.type = "play-and-record";
|
|
69
|
+
}
|
|
70
|
+
} catch (error) {
|
|
71
|
+
logger.warn(`Failed to set audioSession type: ${error}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
27
74
|
export default function useRecognition(props = {}) {
|
|
28
75
|
const {
|
|
29
76
|
currentPhraseText,
|
|
@@ -98,6 +145,12 @@ export default function useRecognition(props = {}) {
|
|
|
98
145
|
throw e;
|
|
99
146
|
}
|
|
100
147
|
}
|
|
148
|
+
|
|
149
|
+
// Set as early as the exercise mounts, before any TTS playback can claim a
|
|
150
|
+
// playback-only session on Safari.
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
ensurePlayAndRecordAudioSession();
|
|
153
|
+
}, []);
|
|
101
154
|
useEffect(() => {
|
|
102
155
|
function onConnect() {
|
|
103
156
|
hasAlertedConnectionErrorRef.current = false;
|
|
@@ -105,9 +158,13 @@ export default function useRecognition(props = {}) {
|
|
|
105
158
|
// socket.io.engine.transport.name is "polling" here only when the
|
|
106
159
|
// websocket attempt failed and it fell back - the clearest signal we
|
|
107
160
|
// have that a student couldn't connect via websocket.
|
|
161
|
+
const transport = socket.io.engine.transport.name;
|
|
108
162
|
trackConnectionDiagnostics("speech_recognition_socket_connected", {
|
|
109
|
-
A4_transport:
|
|
163
|
+
A4_transport: transport
|
|
110
164
|
});
|
|
165
|
+
if (transport === "polling") {
|
|
166
|
+
recognitionAlert("Using a slower backup connection for speech - ask your school's IT team to allow WebSocket connections.", "warning");
|
|
167
|
+
}
|
|
111
168
|
}
|
|
112
169
|
function onDisconnect() {
|
|
113
170
|
setIsSocketConnected(false);
|
|
@@ -151,9 +208,7 @@ export default function useRecognition(props = {}) {
|
|
|
151
208
|
}
|
|
152
209
|
function onConnectError(error) {
|
|
153
210
|
console.error(error);
|
|
154
|
-
trackConnectionDiagnostics("speech_recognition_connect_error",
|
|
155
|
-
A4_error_message: `${error?.message}`
|
|
156
|
-
});
|
|
211
|
+
trackConnectionDiagnostics("speech_recognition_connect_error", describeConnectError(error));
|
|
157
212
|
// Without this, a blocked/failed connection (e.g. a school network
|
|
158
213
|
// firewall blocking the websocket) leaves the mic button silently
|
|
159
214
|
// disabled with no feedback to the student.
|
|
@@ -342,6 +397,8 @@ export default function useRecognition(props = {}) {
|
|
|
342
397
|
}
|
|
343
398
|
};
|
|
344
399
|
if (navigator && navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
|
400
|
+
// Re-assert in case TTS playback downgraded the session since mount.
|
|
401
|
+
ensurePlayAndRecordAudioSession();
|
|
345
402
|
navigator.mediaDevices.getUserMedia(constraints).then(handleSuccess).then(() => {
|
|
346
403
|
isStartingRef.current = false;
|
|
347
404
|
// socket.connected is checked directly (not the isSocketConnected
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nualang/nualang-ui-components",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1425",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"files": [
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"react-swipeable-views-utils": "^0.14.0",
|
|
84
84
|
"regenerator-runtime": "^0.14.1",
|
|
85
85
|
"rehype-raw": "^7.0.0",
|
|
86
|
-
"socket.io-client": "^4.
|
|
86
|
+
"socket.io-client": "^4.8.3",
|
|
87
87
|
"styled-components": "^6.3.8",
|
|
88
88
|
"tss-react": "^4.9.16",
|
|
89
89
|
"uifx": "^2.0",
|