@nualang/nualang-ui-components 0.1.1412 → 0.1.1413
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.
- package/dist/Dialogs/QuickCreateChatbotDialog/QuickCreateChatbotDialog.js +146 -34
- package/dist/Dialogs/QuickCreateScenarioDialog/QuickCreateScenarioDialog.js +0 -1
- package/dist/Exercises/ListenAndSpeak/ListenAndSpeak.js +1 -18
- package/dist/Exercises/Meaning/Meaning.js +1 -18
- package/dist/Exercises/Translator/Translator.js +27 -7
- package/dist/Forms/VoiceSelector/VoiceSelector.js +713 -296
- package/dist/Forms/VoiceSelector/VoiceSelector.test.js +247 -4
- package/dist/Lists/Exercises/Exercises.js +6 -2
- package/dist/Misc/NotFound/ClassCourses.js +2 -1
- package/dist/Misc/NotFound/Courses.js +2 -1
- package/dist/hooks/useExerciseState.js +92 -26
- package/dist/utils/voices.js +26 -0
- package/package.json +1 -1
|
@@ -1,350 +1,767 @@
|
|
|
1
|
-
import { useState, useEffect, useRef,
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import
|
|
1
|
+
import { useState, useEffect, useRef, useCallback, useId } from "react";
|
|
2
|
+
import { Avatar, Box, Button, ButtonBase, Chip, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Tab, Tabs, TextField, ToggleButton, ToggleButtonGroup, Tooltip, Typography } from "@mui/material";
|
|
3
|
+
import { alpha } from "@mui/material/styles";
|
|
4
|
+
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
|
|
5
|
+
import PauseRoundedIcon from "@mui/icons-material/PauseRounded";
|
|
6
|
+
import PlayArrowRoundedIcon from "@mui/icons-material/PlayArrowRounded";
|
|
6
7
|
import WarningAmberIcon from "@mui/icons-material/WarningAmber";
|
|
7
8
|
import awsIconPath from "../../img/aws.svg";
|
|
8
9
|
import azureIconPath from "../../img/azure.svg";
|
|
10
|
+
|
|
11
|
+
// Panel surfaces from the design spec, with dark-mode equivalents. Text, accent
|
|
12
|
+
// and dividers come straight from the theme.
|
|
9
13
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
10
|
-
|
|
14
|
+
const panelTokens = theme => {
|
|
15
|
+
const light = theme.palette.mode === "light";
|
|
16
|
+
const white = theme.palette.common.white;
|
|
17
|
+
return {
|
|
18
|
+
panelBorder: light ? "#e6e6e2" : theme.palette.divider,
|
|
19
|
+
tabRule: light ? "#eeeeea" : theme.palette.divider,
|
|
20
|
+
cardBorder: light ? "#eaeae5" : alpha(white, 0.16),
|
|
21
|
+
cardBorderHover: light ? "#c9c9c2" : alpha(white, 0.32),
|
|
22
|
+
cardSelectedBg: light ? "#fbfbf9" : alpha(white, 0.06),
|
|
23
|
+
footerBg: light ? "#fcfcfb" : alpha(white, 0.03),
|
|
24
|
+
footerRule: light ? "#f0f0ec" : theme.palette.divider,
|
|
25
|
+
mutedText: light ? "#8f8f87" : alpha(white, 0.7),
|
|
26
|
+
faintText: light ? "#b3b3ac" : alpha(white, 0.45),
|
|
27
|
+
panelShadow: light ? "0 1px 2px rgba(0,0,0,0.04), 0 18px 44px -22px rgba(0,0,0,0.22)" : "0 1px 2px rgba(0,0,0,0.3), 0 18px 44px -22px rgba(0,0,0,0.7)"
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// MUI's own visuallyHidden lives in @mui/utils, which is only a transitive
|
|
32
|
+
// dependency here — declaring an import on it would make it an implicit one.
|
|
33
|
+
const visuallyHidden = {
|
|
34
|
+
position: "absolute",
|
|
35
|
+
width: 1,
|
|
36
|
+
height: 1,
|
|
37
|
+
overflow: "hidden",
|
|
38
|
+
clipPath: "inset(50%)",
|
|
39
|
+
whiteSpace: "nowrap"
|
|
40
|
+
};
|
|
41
|
+
const actionButtonSx = {
|
|
42
|
+
fontSize: "13.5px",
|
|
43
|
+
fontWeight: 600,
|
|
44
|
+
textTransform: "none"
|
|
45
|
+
};
|
|
46
|
+
const badgeSx = {
|
|
47
|
+
height: 18,
|
|
48
|
+
"& .MuiChip-icon": {
|
|
49
|
+
fontSize: 11,
|
|
50
|
+
marginLeft: 0.25,
|
|
51
|
+
marginRight: -0.5
|
|
52
|
+
},
|
|
53
|
+
"& .MuiChip-label": {
|
|
54
|
+
px: 0.75,
|
|
55
|
+
fontSize: "0.625rem",
|
|
56
|
+
fontWeight: 600
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// The speech API expresses medium as no pitch at all, and a ToggleButton can't
|
|
61
|
+
// carry null, so the buttons are keyed by label and mapped to a pitch here.
|
|
62
|
+
const PITCH_OPTIONS = [{
|
|
63
|
+
labelKey: "voice_pitch_x_low",
|
|
64
|
+
pitch: "x-low"
|
|
65
|
+
}, {
|
|
66
|
+
labelKey: "voice_pitch_low",
|
|
67
|
+
pitch: "low"
|
|
68
|
+
}, {
|
|
69
|
+
labelKey: "voice_pitch_medium",
|
|
70
|
+
pitch: null
|
|
71
|
+
}, {
|
|
72
|
+
labelKey: "voice_pitch_high",
|
|
73
|
+
pitch: "high"
|
|
74
|
+
}, {
|
|
75
|
+
labelKey: "voice_pitch_x_high",
|
|
76
|
+
pitch: "x-high"
|
|
77
|
+
}];
|
|
78
|
+
|
|
79
|
+
// Arrow keys move the selection around the voice grid.
|
|
80
|
+
const ARROW_STEPS = {
|
|
81
|
+
ArrowRight: 1,
|
|
82
|
+
ArrowDown: 1,
|
|
83
|
+
ArrowLeft: -1,
|
|
84
|
+
ArrowUp: -1
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// "Spanish, US" -> "US". Accent labels are prefixed with the language name,
|
|
88
|
+
// which the tab row doesn't need to repeat.
|
|
89
|
+
const accentTabLabel = (label = "") => {
|
|
90
|
+
const separator = label.indexOf(", ");
|
|
91
|
+
return separator === -1 ? label : label.slice(separator + 2);
|
|
92
|
+
};
|
|
93
|
+
function ChildChip({
|
|
94
|
+
t
|
|
95
|
+
}) {
|
|
96
|
+
return /*#__PURE__*/_jsx(Chip, {
|
|
97
|
+
label: t("child"),
|
|
98
|
+
size: "small",
|
|
99
|
+
variant: "outlined",
|
|
100
|
+
sx: {
|
|
101
|
+
...badgeSx,
|
|
102
|
+
textTransform: "capitalize"
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
function LegacyChip({
|
|
107
|
+
t
|
|
108
|
+
}) {
|
|
109
|
+
return /*#__PURE__*/_jsx(Tooltip, {
|
|
110
|
+
title: t("legacy_voice_tooltip_text"),
|
|
111
|
+
placement: "top",
|
|
112
|
+
children: /*#__PURE__*/_jsx(Chip, {
|
|
113
|
+
icon: /*#__PURE__*/_jsx(WarningAmberIcon, {}),
|
|
114
|
+
label: t("legacy"),
|
|
115
|
+
size: "small",
|
|
116
|
+
color: "warning",
|
|
117
|
+
variant: "outlined",
|
|
118
|
+
sx: badgeSx
|
|
119
|
+
})
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function ProviderIcon({
|
|
123
|
+
provider
|
|
124
|
+
}) {
|
|
125
|
+
return /*#__PURE__*/_jsx(Box, {
|
|
126
|
+
component: "img",
|
|
127
|
+
src: provider === "azure" ? azureIconPath : awsIconPath,
|
|
128
|
+
alt: `${provider || "aws"} Icon`,
|
|
129
|
+
sx: {
|
|
130
|
+
width: 16,
|
|
131
|
+
height: 16,
|
|
132
|
+
display: "block"
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
function VoicePitchToggle({
|
|
11
137
|
t,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
138
|
+
value,
|
|
139
|
+
disabled,
|
|
140
|
+
onChange
|
|
15
141
|
}) {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
value: 25,
|
|
22
|
-
label: t("voice_pitch_low"),
|
|
23
|
-
pitchValue: "low"
|
|
24
|
-
}, {
|
|
25
|
-
value: 50,
|
|
26
|
-
label: t("voice_pitch_medium"),
|
|
27
|
-
pitchValue: null
|
|
28
|
-
}, {
|
|
29
|
-
value: 75,
|
|
30
|
-
label: t("voice_pitch_high"),
|
|
31
|
-
pitchValue: "high"
|
|
32
|
-
}, {
|
|
33
|
-
value: 95,
|
|
34
|
-
label: t("voice_pitch_x_high"),
|
|
35
|
-
pitchValue: "x-high"
|
|
36
|
-
}];
|
|
37
|
-
const sliderValue = voicePitchMarks.find(mark => mark.pitchValue === voicePitch)?.value || 50;
|
|
38
|
-
if (selectedVoice?.isNeuralOnly) {
|
|
39
|
-
return /*#__PURE__*/_jsx(Tooltip, {
|
|
40
|
-
title: t("voice_pitch_disabled"),
|
|
41
|
-
children: /*#__PURE__*/_jsx("div", {
|
|
42
|
-
children: /*#__PURE__*/_jsx(Slider, {
|
|
43
|
-
disabled: true,
|
|
44
|
-
"aria-label": t("voice_pitch"),
|
|
45
|
-
defaultValue: 50,
|
|
46
|
-
step: null,
|
|
47
|
-
marks: voicePitchMarks,
|
|
48
|
-
value: sliderValue
|
|
49
|
-
})
|
|
50
|
-
})
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
return /*#__PURE__*/_jsx(Slider, {
|
|
142
|
+
const group = /*#__PURE__*/_jsx(ToggleButtonGroup, {
|
|
143
|
+
exclusive: true,
|
|
144
|
+
size: "small",
|
|
145
|
+
disabled: disabled,
|
|
146
|
+
value: PITCH_OPTIONS.find(option => option.pitch === (value || null))?.labelKey,
|
|
54
147
|
"aria-label": t("voice_pitch"),
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
148
|
+
onChange: (_, labelKey) => {
|
|
149
|
+
// labelKey is null when the active button is clicked again — a no-op,
|
|
150
|
+
// since one pitch is always in effect.
|
|
151
|
+
const option = PITCH_OPTIONS.find(o => o.labelKey === labelKey);
|
|
152
|
+
if (option) onChange(option.pitch);
|
|
153
|
+
},
|
|
154
|
+
sx: theme => ({
|
|
155
|
+
borderRadius: "999px",
|
|
156
|
+
"& .MuiToggleButton-root": {
|
|
157
|
+
textTransform: "none",
|
|
158
|
+
fontSize: "12px",
|
|
159
|
+
fontWeight: 600,
|
|
160
|
+
px: "10px",
|
|
161
|
+
py: "4px",
|
|
162
|
+
lineHeight: 1.4,
|
|
163
|
+
color: "text.secondary",
|
|
164
|
+
borderColor: panelTokens(theme).cardBorderHover
|
|
165
|
+
},
|
|
166
|
+
"& .MuiToggleButtonGroup-firstButton": {
|
|
167
|
+
borderTopLeftRadius: "999px",
|
|
168
|
+
borderBottomLeftRadius: "999px",
|
|
169
|
+
pl: "14px"
|
|
170
|
+
},
|
|
171
|
+
"& .MuiToggleButtonGroup-lastButton": {
|
|
172
|
+
borderTopRightRadius: "999px",
|
|
173
|
+
borderBottomRightRadius: "999px",
|
|
174
|
+
pr: "14px"
|
|
175
|
+
},
|
|
176
|
+
"& .MuiToggleButton-root.Mui-selected": {
|
|
177
|
+
backgroundColor: "primary.main",
|
|
178
|
+
color: theme.palette.primary.contrastText,
|
|
179
|
+
"&:hover": {
|
|
180
|
+
backgroundColor: "primary.dark"
|
|
181
|
+
}
|
|
69
182
|
}
|
|
70
|
-
}
|
|
183
|
+
}),
|
|
184
|
+
children: PITCH_OPTIONS.map(option => /*#__PURE__*/_jsx(ToggleButton, {
|
|
185
|
+
value: option.labelKey,
|
|
186
|
+
children: t(option.labelKey)
|
|
187
|
+
}, option.labelKey))
|
|
188
|
+
});
|
|
189
|
+
return /*#__PURE__*/_jsxs(Box, {
|
|
190
|
+
sx: {
|
|
191
|
+
display: "flex",
|
|
192
|
+
flexDirection: "column",
|
|
193
|
+
gap: "6px"
|
|
194
|
+
},
|
|
195
|
+
children: [/*#__PURE__*/_jsx(Typography, {
|
|
196
|
+
component: "span",
|
|
197
|
+
sx: theme => ({
|
|
198
|
+
fontSize: "10.5px",
|
|
199
|
+
fontWeight: 600,
|
|
200
|
+
letterSpacing: "0.08em",
|
|
201
|
+
textTransform: "uppercase",
|
|
202
|
+
color: panelTokens(theme).mutedText
|
|
203
|
+
}),
|
|
204
|
+
children: t("voice_pitch")
|
|
205
|
+
}), disabled ? /*#__PURE__*/_jsx(Tooltip, {
|
|
206
|
+
title: t("voice_pitch_disabled"),
|
|
207
|
+
children: /*#__PURE__*/_jsx("span", {
|
|
208
|
+
children: group
|
|
209
|
+
})
|
|
210
|
+
}) : group]
|
|
71
211
|
});
|
|
72
212
|
}
|
|
213
|
+
function VoiceCard({
|
|
214
|
+
t,
|
|
215
|
+
voiceOption,
|
|
216
|
+
languageCode,
|
|
217
|
+
selected,
|
|
218
|
+
tabbable,
|
|
219
|
+
playing,
|
|
220
|
+
showProviderIcons,
|
|
221
|
+
showPreview,
|
|
222
|
+
onSelect,
|
|
223
|
+
onPreview,
|
|
224
|
+
cardRef
|
|
225
|
+
}) {
|
|
226
|
+
const previewLabel = playing ? t("playing") : t("preview");
|
|
227
|
+
return (
|
|
228
|
+
/*#__PURE__*/
|
|
229
|
+
// ButtonBase over a plain Box: it synthesises Enter/Space activation for
|
|
230
|
+
// non-native components and picks up the theme's focus-visible ring. A div
|
|
231
|
+
// rather than the default button, since the preview control nests inside.
|
|
232
|
+
_jsxs(ButtonBase, {
|
|
233
|
+
component: "div"
|
|
234
|
+
// The id and data-language-code are part of the component's contract:
|
|
235
|
+
// consumers read the accent off the DOM node when a voice is committed.
|
|
236
|
+
,
|
|
237
|
+
id: `voice-${voiceOption.name}`,
|
|
238
|
+
"data-language-code": languageCode,
|
|
239
|
+
ref: cardRef,
|
|
240
|
+
role: "radio",
|
|
241
|
+
"aria-checked": selected
|
|
242
|
+
// One stop for the whole grid: the selected card, or the first one when
|
|
243
|
+
// the selection is empty or sits behind another tab.
|
|
244
|
+
,
|
|
245
|
+
tabIndex: tabbable ? 0 : -1,
|
|
246
|
+
onClick: onSelect,
|
|
247
|
+
sx: theme => {
|
|
248
|
+
const tokens = panelTokens(theme);
|
|
249
|
+
return {
|
|
250
|
+
p: "12px 14px",
|
|
251
|
+
borderRadius: "12px",
|
|
252
|
+
display: "flex",
|
|
253
|
+
alignItems: "center",
|
|
254
|
+
justifyContent: "flex-start",
|
|
255
|
+
textAlign: "left",
|
|
256
|
+
gap: "11px",
|
|
257
|
+
border: "1px solid",
|
|
258
|
+
borderColor: selected ? "primary.main" : tokens.cardBorder,
|
|
259
|
+
backgroundColor: selected ? tokens.cardSelectedBg : theme.palette.background.paper,
|
|
260
|
+
boxShadow: selected ? `inset 0 0 0 1px ${theme.palette.primary.main}` : "none",
|
|
261
|
+
"&:hover": {
|
|
262
|
+
borderColor: selected ? "primary.main" : tokens.cardBorderHover
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
},
|
|
266
|
+
children: [/*#__PURE__*/_jsx(Avatar, {
|
|
267
|
+
src: voiceOption.userImage,
|
|
268
|
+
alt: "",
|
|
269
|
+
sx: {
|
|
270
|
+
width: 40,
|
|
271
|
+
height: 40,
|
|
272
|
+
flex: "0 0 auto"
|
|
273
|
+
}
|
|
274
|
+
}), /*#__PURE__*/_jsxs(Box, {
|
|
275
|
+
sx: {
|
|
276
|
+
display: "flex",
|
|
277
|
+
flexDirection: "column",
|
|
278
|
+
alignItems: "flex-start",
|
|
279
|
+
gap: "4px",
|
|
280
|
+
minWidth: 0
|
|
281
|
+
},
|
|
282
|
+
children: [/*#__PURE__*/_jsxs(Box, {
|
|
283
|
+
sx: {
|
|
284
|
+
display: "flex",
|
|
285
|
+
alignItems: "center",
|
|
286
|
+
gap: "6px"
|
|
287
|
+
},
|
|
288
|
+
children: [/*#__PURE__*/_jsx(Typography, {
|
|
289
|
+
component: "span",
|
|
290
|
+
sx: {
|
|
291
|
+
fontSize: "15.5px",
|
|
292
|
+
fontWeight: selected ? 600 : 500,
|
|
293
|
+
color: "text.primary",
|
|
294
|
+
minWidth: 0,
|
|
295
|
+
overflow: "hidden",
|
|
296
|
+
textOverflow: "ellipsis",
|
|
297
|
+
whiteSpace: "nowrap"
|
|
298
|
+
},
|
|
299
|
+
children: voiceOption.name
|
|
300
|
+
}), showProviderIcons && /*#__PURE__*/_jsx(ProviderIcon, {
|
|
301
|
+
provider: voiceOption.provider
|
|
302
|
+
})]
|
|
303
|
+
}), (voiceOption.legacy || voiceOption.isChild) && /*#__PURE__*/_jsxs(Box, {
|
|
304
|
+
sx: {
|
|
305
|
+
display: "flex",
|
|
306
|
+
alignItems: "center",
|
|
307
|
+
gap: "4px",
|
|
308
|
+
flexWrap: "wrap"
|
|
309
|
+
},
|
|
310
|
+
children: [voiceOption.legacy && /*#__PURE__*/_jsx(LegacyChip, {
|
|
311
|
+
t: t
|
|
312
|
+
}), voiceOption.isChild && /*#__PURE__*/_jsx(ChildChip, {
|
|
313
|
+
t: t
|
|
314
|
+
})]
|
|
315
|
+
})]
|
|
316
|
+
}), showPreview && /*#__PURE__*/_jsx(IconButton, {
|
|
317
|
+
"aria-pressed": playing,
|
|
318
|
+
"aria-label": `${previewLabel} ${voiceOption.name}`,
|
|
319
|
+
onClick: event => {
|
|
320
|
+
event.stopPropagation();
|
|
321
|
+
onPreview();
|
|
322
|
+
},
|
|
323
|
+
sx: {
|
|
324
|
+
ml: "auto",
|
|
325
|
+
flex: "0 0 auto",
|
|
326
|
+
width: 36,
|
|
327
|
+
height: 36,
|
|
328
|
+
border: "1px solid",
|
|
329
|
+
borderColor: "primary.main",
|
|
330
|
+
color: playing ? "primary.contrastText" : "primary.main",
|
|
331
|
+
backgroundColor: playing ? "primary.main" : "transparent",
|
|
332
|
+
"&:hover": {
|
|
333
|
+
borderColor: "primary.main",
|
|
334
|
+
backgroundColor: playing ? "primary.dark" : "action.hover"
|
|
335
|
+
},
|
|
336
|
+
"& .MuiSvgIcon-root": {
|
|
337
|
+
fontSize: 20
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
children: playing ? /*#__PURE__*/_jsx(PauseRoundedIcon, {}) : /*#__PURE__*/_jsx(PlayArrowRoundedIcon, {})
|
|
341
|
+
})]
|
|
342
|
+
})
|
|
343
|
+
);
|
|
344
|
+
}
|
|
73
345
|
function VoiceSelector(props) {
|
|
74
346
|
const {
|
|
75
347
|
t = text => text,
|
|
76
348
|
voices,
|
|
77
349
|
value = "",
|
|
350
|
+
name,
|
|
351
|
+
label,
|
|
78
352
|
onChange,
|
|
79
353
|
onVoicePitchChange,
|
|
80
354
|
voicePitch,
|
|
81
355
|
generateAudio = false,
|
|
82
356
|
isUserInternal,
|
|
83
357
|
hidePitch = false,
|
|
358
|
+
handleSpeak,
|
|
359
|
+
learnLang,
|
|
360
|
+
previewText,
|
|
84
361
|
...otherProps
|
|
85
362
|
} = props;
|
|
86
|
-
const [
|
|
87
|
-
const [
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
const
|
|
363
|
+
const [open, setOpen] = useState(false);
|
|
364
|
+
const [activeAccentIndex, setActiveAccentIndex] = useState(0);
|
|
365
|
+
const [draftVoice, setDraftVoice] = useState(value);
|
|
366
|
+
const [draftPitch, setDraftPitch] = useState(voicePitch || null);
|
|
367
|
+
const [playingVoice, setPlayingVoice] = useState(null);
|
|
368
|
+
const audioRef = useRef(null);
|
|
369
|
+
const previewRequestRef = useRef(0);
|
|
370
|
+
const cardRefs = useRef({});
|
|
371
|
+
const titleId = useId();
|
|
372
|
+
const voiceAccents = Object.values(voices || {});
|
|
373
|
+
const selectedVoice = voiceAccents.flatMap(accent => accent.voiceOptions || []).find(option => option.name === value);
|
|
374
|
+
const activeAccent = voiceAccents[activeAccentIndex];
|
|
375
|
+
const activeVoices = activeAccent?.voiceOptions || [];
|
|
376
|
+
// The draft survives a tab change, so it is looked up across every accent
|
|
377
|
+
// rather than the active one — browsing accents must not move the selection.
|
|
378
|
+
// Nothing is highlighted while the drafted voice sits behind another tab.
|
|
379
|
+
const draftAccent = voiceAccents.find(accent => (accent.voiceOptions || []).some(option => option.name === draftVoice));
|
|
380
|
+
const draftVoiceOption = (draftAccent?.voiceOptions || []).find(option => option.name === draftVoice);
|
|
381
|
+
// A radiogroup keeps a single tab stop. With no selection on this tab that
|
|
382
|
+
// falls to the first card, so the grid stays reachable by keyboard.
|
|
383
|
+
const gridEntryName = activeVoices.some(option => option.name === draftVoiceOption?.name) ? draftVoiceOption.name : activeVoices[0]?.name;
|
|
96
384
|
const showProviderIcons = generateAudio || window.location.host === "localhost:9009" || isUserInternal;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
385
|
+
const showPreview = Boolean(handleSpeak);
|
|
386
|
+
const showPitch = !hidePitch && Boolean(onVoicePitchChange);
|
|
387
|
+
const panelTitle = label || t("voices");
|
|
388
|
+
|
|
389
|
+
// Neural-only voices don't accept a pitch, so the control is inert for them.
|
|
390
|
+
const pitchFor = voiceOption => voiceOption?.isNeuralOnly ? null : draftPitch;
|
|
391
|
+
const pitchDisabled = Boolean(draftVoiceOption?.isNeuralOnly);
|
|
392
|
+
const stopPlayback = useCallback(() => {
|
|
393
|
+
previewRequestRef.current += 1;
|
|
394
|
+
if (audioRef.current) {
|
|
395
|
+
audioRef.current.pause();
|
|
396
|
+
audioRef.current = null;
|
|
397
|
+
}
|
|
398
|
+
setPlayingVoice(null);
|
|
399
|
+
}, []);
|
|
400
|
+
useEffect(() => () => stopPlayback(), [stopPlayback]);
|
|
401
|
+
const handleOpen = () => {
|
|
402
|
+
const accentIndex = voiceAccents.findIndex(accent => (accent.voiceOptions || []).some(option => option.name === value));
|
|
403
|
+
const nextIndex = accentIndex === -1 ? 0 : accentIndex;
|
|
404
|
+
setActiveAccentIndex(nextIndex);
|
|
405
|
+
// Only an already-selected voice starts out drafted — opening the panel is
|
|
406
|
+
// browsing, so nothing is picked on the user's behalf.
|
|
407
|
+
setDraftVoice(value);
|
|
408
|
+
setDraftPitch(voicePitch || null);
|
|
409
|
+
setOpen(true);
|
|
410
|
+
};
|
|
411
|
+
const handleClose = () => {
|
|
412
|
+
stopPlayback();
|
|
413
|
+
setOpen(false);
|
|
414
|
+
};
|
|
415
|
+
const handleAccentChange = (_, index) => {
|
|
416
|
+
stopPlayback();
|
|
417
|
+
setActiveAccentIndex(index);
|
|
418
|
+
};
|
|
419
|
+
const handlePreview = async voiceOption => {
|
|
420
|
+
if (playingVoice === voiceOption.name) {
|
|
421
|
+
stopPlayback();
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
stopPlayback();
|
|
425
|
+
setDraftVoice(voiceOption.name);
|
|
426
|
+
const requestId = previewRequestRef.current += 1;
|
|
427
|
+
setPlayingVoice(voiceOption.name);
|
|
428
|
+
try {
|
|
429
|
+
const result = await handleSpeak({
|
|
430
|
+
text: previewText || voiceOption.name,
|
|
431
|
+
learnLanguage: learnLang,
|
|
432
|
+
voiceName: voiceOption.name,
|
|
433
|
+
languageCode: activeAccent?.languageCode,
|
|
434
|
+
pitch: pitchFor(voiceOption),
|
|
435
|
+
mute: true
|
|
436
|
+
});
|
|
437
|
+
// A newer preview (or a close/unmount) superseded this request.
|
|
438
|
+
if (previewRequestRef.current !== requestId) return;
|
|
439
|
+
const audioUrl = typeof result === "string" ? result : result?.audioUrl;
|
|
440
|
+
if (!audioUrl) {
|
|
441
|
+
setPlayingVoice(null);
|
|
442
|
+
return;
|
|
104
443
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
444
|
+
const audio = new Audio(audioUrl);
|
|
445
|
+
audioRef.current = audio;
|
|
446
|
+
audio.addEventListener("ended", () => setPlayingVoice(null));
|
|
447
|
+
await audio.play();
|
|
448
|
+
} catch (error) {
|
|
449
|
+
console.error(error);
|
|
450
|
+
if (previewRequestRef.current === requestId) setPlayingVoice(null);
|
|
110
451
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (
|
|
117
|
-
|
|
118
|
-
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
// Radiogroup keyboard support: selection follows focus across the grid.
|
|
455
|
+
const handleGridKeyDown = event => {
|
|
456
|
+
const step = ARROW_STEPS[event.key];
|
|
457
|
+
if (!step || activeVoices.length === 0) return;
|
|
458
|
+
event.preventDefault();
|
|
459
|
+
const currentIndex = activeVoices.findIndex(option => option.name === draftVoice);
|
|
460
|
+
// The draft may belong to another accent, in which case there is nothing to
|
|
461
|
+
// step from and arrowing into this grid enters at the first card.
|
|
462
|
+
const nextVoice = currentIndex === -1 ? activeVoices[0] : activeVoices[(currentIndex + step + activeVoices.length) % activeVoices.length];
|
|
463
|
+
setDraftVoice(nextVoice.name);
|
|
464
|
+
cardRefs.current[nextVoice.name]?.focus();
|
|
465
|
+
};
|
|
466
|
+
const handleCommit = () => {
|
|
467
|
+
if (!draftVoiceOption) {
|
|
468
|
+
handleClose();
|
|
469
|
+
return;
|
|
119
470
|
}
|
|
120
|
-
|
|
471
|
+
onChange({
|
|
472
|
+
target: {
|
|
473
|
+
name,
|
|
474
|
+
value: draftVoiceOption.name,
|
|
475
|
+
// The accent that owns the draft, not the one on screen — they differ
|
|
476
|
+
// when the panel is closed from a tab the draft was not picked in.
|
|
477
|
+
languageCode: draftAccent?.languageCode,
|
|
478
|
+
picture: draftVoiceOption.userImage || ""
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
// After onChange, since consumers reset the pitch when the voice changes.
|
|
482
|
+
// Only when it actually moved, so an unchanged pitch doesn't trigger a
|
|
483
|
+
// second sample playback in the consumer.
|
|
484
|
+
const committedPitch = pitchFor(draftVoiceOption);
|
|
485
|
+
if (showPitch && (voicePitch || null) !== committedPitch) {
|
|
486
|
+
onVoicePitchChange({
|
|
487
|
+
target: {
|
|
488
|
+
name: "voicePitch",
|
|
489
|
+
value: committedPitch
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
handleClose();
|
|
494
|
+
};
|
|
121
495
|
return /*#__PURE__*/_jsxs(_Fragment, {
|
|
122
496
|
children: [/*#__PURE__*/_jsx(TextField, {
|
|
123
|
-
ref: dropdownRef,
|
|
124
|
-
select: true,
|
|
125
|
-
value: value,
|
|
126
|
-
onClick: () => setIsDropdownOpen(true),
|
|
127
|
-
onChange: e => {
|
|
128
|
-
onChange(e);
|
|
129
|
-
setOpenAccentIndex(null);
|
|
130
|
-
},
|
|
131
497
|
...otherProps,
|
|
498
|
+
name: name,
|
|
499
|
+
label: label,
|
|
500
|
+
value: selectedVoice?.name || "",
|
|
501
|
+
onClick: handleOpen,
|
|
502
|
+
onKeyDown: event => {
|
|
503
|
+
if (["Enter", " ", "ArrowDown"].includes(event.key)) {
|
|
504
|
+
event.preventDefault();
|
|
505
|
+
handleOpen();
|
|
506
|
+
}
|
|
507
|
+
},
|
|
132
508
|
slotProps: {
|
|
133
509
|
inputLabel: {
|
|
134
510
|
shrink: Boolean(value) || undefined
|
|
135
511
|
},
|
|
136
512
|
input: {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
MenuProps: {
|
|
142
|
-
PaperProps: {
|
|
143
|
-
sx: {
|
|
144
|
-
maxHeight: "80vh",
|
|
145
|
-
width: "60vw",
|
|
146
|
-
left: "10 !important",
|
|
147
|
-
right: "10 !important",
|
|
148
|
-
top: "50% !important",
|
|
149
|
-
transform: "translateY(-50%) !important",
|
|
150
|
-
overflowY: "auto"
|
|
151
|
-
}
|
|
152
|
-
},
|
|
153
|
-
anchorOrigin: {
|
|
154
|
-
vertical: "top",
|
|
155
|
-
horizontal: "center"
|
|
156
|
-
},
|
|
157
|
-
transformOrigin: {
|
|
158
|
-
vertical: "center",
|
|
159
|
-
horizontal: "center"
|
|
160
|
-
},
|
|
161
|
-
getContentAnchorEl: null
|
|
513
|
+
readOnly: true,
|
|
514
|
+
notched: Boolean(value) || undefined,
|
|
515
|
+
sx: {
|
|
516
|
+
cursor: "pointer"
|
|
162
517
|
},
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
518
|
+
startAdornment: selectedVoice ? /*#__PURE__*/_jsx(Avatar, {
|
|
519
|
+
src: selectedVoice.userImage,
|
|
520
|
+
alt: "",
|
|
521
|
+
sx: {
|
|
522
|
+
width: 32,
|
|
523
|
+
height: 32,
|
|
524
|
+
mr: 1
|
|
525
|
+
}
|
|
526
|
+
}) : null,
|
|
527
|
+
endAdornment: /*#__PURE__*/_jsxs(Box, {
|
|
528
|
+
sx: {
|
|
529
|
+
display: "flex",
|
|
530
|
+
alignItems: "center",
|
|
531
|
+
gap: 0.5
|
|
532
|
+
},
|
|
533
|
+
children: [selectedVoice?.legacy && /*#__PURE__*/_jsx(LegacyChip, {
|
|
534
|
+
t: t
|
|
535
|
+
}), selectedVoice?.isChild && /*#__PURE__*/_jsx(ChildChip, {
|
|
536
|
+
t: t
|
|
537
|
+
}), showProviderIcons && selectedVoice && /*#__PURE__*/_jsx(ProviderIcon, {
|
|
538
|
+
provider: selectedVoice.provider
|
|
539
|
+
}), /*#__PURE__*/_jsx(ArrowDropDownIcon, {
|
|
167
540
|
sx: {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
marginLeft: 0.25,
|
|
201
|
-
marginRight: -0.5
|
|
202
|
-
},
|
|
203
|
-
"& .MuiChip-label": {
|
|
204
|
-
px: 0.75,
|
|
205
|
-
fontSize: "0.625rem",
|
|
206
|
-
fontWeight: 600
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
})
|
|
210
|
-
})]
|
|
211
|
-
})
|
|
212
|
-
}), showProviderIcons && /*#__PURE__*/_jsx(Grid, {
|
|
213
|
-
sx: {
|
|
214
|
-
mt: 1,
|
|
215
|
-
ml: "auto"
|
|
216
|
-
},
|
|
217
|
-
children: /*#__PURE__*/_jsx("img", {
|
|
218
|
-
src: selectedVoice.provider === "azure" ? azureIconPath : awsIconPath,
|
|
219
|
-
alt: `${selectedVoice.provider} Icon`
|
|
220
|
-
})
|
|
221
|
-
})]
|
|
222
|
-
});
|
|
541
|
+
color: "action.active"
|
|
542
|
+
}
|
|
543
|
+
})]
|
|
544
|
+
})
|
|
545
|
+
},
|
|
546
|
+
htmlInput: {
|
|
547
|
+
role: "combobox",
|
|
548
|
+
"aria-haspopup": "dialog",
|
|
549
|
+
"aria-expanded": open,
|
|
550
|
+
sx: {
|
|
551
|
+
cursor: "pointer"
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}), /*#__PURE__*/_jsxs(Dialog, {
|
|
556
|
+
open: open,
|
|
557
|
+
onClose: handleClose,
|
|
558
|
+
maxWidth: false,
|
|
559
|
+
"aria-labelledby": titleId,
|
|
560
|
+
slotProps: {
|
|
561
|
+
paper: {
|
|
562
|
+
elevation: 0,
|
|
563
|
+
sx: theme => {
|
|
564
|
+
const tokens = panelTokens(theme);
|
|
565
|
+
return {
|
|
566
|
+
width: "100%",
|
|
567
|
+
maxWidth: 720,
|
|
568
|
+
border: "1px solid",
|
|
569
|
+
borderColor: tokens.panelBorder,
|
|
570
|
+
borderRadius: "16px",
|
|
571
|
+
boxShadow: tokens.panelShadow
|
|
572
|
+
};
|
|
223
573
|
}
|
|
224
574
|
}
|
|
225
575
|
},
|
|
226
|
-
children:
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
"
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
576
|
+
children: [/*#__PURE__*/_jsxs(DialogTitle, {
|
|
577
|
+
id: titleId,
|
|
578
|
+
sx: {
|
|
579
|
+
p: "18px 20px 0",
|
|
580
|
+
display: "flex",
|
|
581
|
+
alignItems: "baseline",
|
|
582
|
+
gap: "10px",
|
|
583
|
+
flexWrap: "wrap",
|
|
584
|
+
fontSize: "16px",
|
|
585
|
+
fontWeight: 600
|
|
586
|
+
},
|
|
587
|
+
children: [panelTitle, showProviderIcons && /*#__PURE__*/_jsx(Typography, {
|
|
588
|
+
component: "span",
|
|
589
|
+
sx: theme => ({
|
|
590
|
+
fontSize: "11.5px",
|
|
591
|
+
color: panelTokens(theme).mutedText
|
|
592
|
+
}),
|
|
593
|
+
children: t("internal_view_provider_marks")
|
|
594
|
+
})]
|
|
595
|
+
}), /*#__PURE__*/_jsx(Tabs, {
|
|
596
|
+
value: activeAccentIndex,
|
|
597
|
+
onChange: handleAccentChange,
|
|
598
|
+
variant: "scrollable",
|
|
599
|
+
scrollButtons: "auto",
|
|
600
|
+
allowScrollButtonsMobile: true,
|
|
601
|
+
"aria-label": t("accent"),
|
|
602
|
+
sx: theme => ({
|
|
603
|
+
px: "20px",
|
|
604
|
+
pt: "16px",
|
|
605
|
+
minHeight: 0,
|
|
606
|
+
flex: "0 0 auto",
|
|
607
|
+
borderBottom: "1px solid",
|
|
608
|
+
borderColor: panelTokens(theme).tabRule,
|
|
609
|
+
// v9 renamed the flex container slot from flexContainer to list.
|
|
610
|
+
"& .MuiTabs-list": {
|
|
611
|
+
gap: "26px"
|
|
612
|
+
}
|
|
613
|
+
}),
|
|
614
|
+
children: voiceAccents.map((accent, index) => {
|
|
615
|
+
const isActive = index === activeAccentIndex;
|
|
616
|
+
return /*#__PURE__*/_jsx(Tab, {
|
|
617
|
+
disableRipple: true,
|
|
618
|
+
label: /*#__PURE__*/_jsxs(Box, {
|
|
256
619
|
sx: {
|
|
257
|
-
|
|
258
|
-
|
|
620
|
+
display: "flex",
|
|
621
|
+
flexDirection: "column",
|
|
622
|
+
alignItems: "flex-start",
|
|
623
|
+
gap: "3px"
|
|
259
624
|
},
|
|
260
|
-
children: [/*#__PURE__*/_jsx(
|
|
261
|
-
|
|
262
|
-
children: /*#__PURE__*/_jsx(Avatar, {
|
|
263
|
-
src: voiceOption.userImage,
|
|
264
|
-
alt: ""
|
|
265
|
-
})
|
|
266
|
-
})
|
|
267
|
-
}), /*#__PURE__*/_jsxs(Grid, {
|
|
625
|
+
children: [/*#__PURE__*/_jsx(Box, {
|
|
626
|
+
component: "span",
|
|
268
627
|
sx: {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
alignItems: "center",
|
|
272
|
-
gap: 1
|
|
628
|
+
fontSize: "14.5px",
|
|
629
|
+
fontWeight: isActive ? 600 : 500
|
|
273
630
|
},
|
|
274
|
-
children:
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
height: 18,
|
|
285
|
-
"& .MuiChip-icon": {
|
|
286
|
-
fontSize: 11,
|
|
287
|
-
marginLeft: 0.25,
|
|
288
|
-
marginRight: -0.5
|
|
289
|
-
},
|
|
290
|
-
"& .MuiChip-label": {
|
|
291
|
-
px: 0.75,
|
|
292
|
-
fontSize: "0.625rem",
|
|
293
|
-
fontWeight: 600
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
})
|
|
297
|
-
})]
|
|
298
|
-
}), showProviderIcons && /*#__PURE__*/_jsx(Grid, {
|
|
299
|
-
sx: {
|
|
300
|
-
mr: 2,
|
|
301
|
-
mt: 1
|
|
302
|
-
},
|
|
303
|
-
children: /*#__PURE__*/_jsx("img", {
|
|
304
|
-
src: voiceOption.provider === "azure" ? azureIconPath : awsIconPath,
|
|
305
|
-
alt: `${voiceOption.provider} Icon`
|
|
306
|
-
})
|
|
631
|
+
children: accentTabLabel(accent.label)
|
|
632
|
+
}), /*#__PURE__*/_jsxs(Box, {
|
|
633
|
+
component: "span",
|
|
634
|
+
sx: theme => ({
|
|
635
|
+
fontSize: "11.5px",
|
|
636
|
+
letterSpacing: "0.04em",
|
|
637
|
+
fontWeight: 400,
|
|
638
|
+
color: isActive ? panelTokens(theme).mutedText : panelTokens(theme).faintText
|
|
639
|
+
}),
|
|
640
|
+
children: [(accent.voiceOptions || []).length, " ", t("voices")]
|
|
307
641
|
})]
|
|
642
|
+
}),
|
|
643
|
+
sx: theme => ({
|
|
644
|
+
minWidth: 0,
|
|
645
|
+
minHeight: 0,
|
|
646
|
+
p: "0 2px 12px",
|
|
647
|
+
textTransform: "none",
|
|
648
|
+
alignItems: "flex-start",
|
|
649
|
+
"&:hover": {
|
|
650
|
+
backgroundColor: "transparent"
|
|
651
|
+
},
|
|
652
|
+
[theme.breakpoints.up("sm")]: {
|
|
653
|
+
minWidth: 0
|
|
654
|
+
}
|
|
308
655
|
})
|
|
309
|
-
},
|
|
310
|
-
})
|
|
311
|
-
},
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
px: 1,
|
|
316
|
-
my: 2
|
|
317
|
-
},
|
|
318
|
-
children: [/*#__PURE__*/_jsx(Typography, {
|
|
319
|
-
id: "input-slider",
|
|
320
|
-
gutterBottom: true,
|
|
321
|
-
children: t("voice_pitch")
|
|
322
|
-
}), /*#__PURE__*/_jsx(Grid, {
|
|
323
|
-
container: true,
|
|
324
|
-
spacing: 2,
|
|
656
|
+
}, accent.languageCode);
|
|
657
|
+
})
|
|
658
|
+
}), /*#__PURE__*/_jsx(DialogContent, {
|
|
659
|
+
role: "radiogroup",
|
|
660
|
+
"aria-label": panelTitle,
|
|
661
|
+
onKeyDown: handleGridKeyDown,
|
|
325
662
|
sx: {
|
|
326
|
-
|
|
327
|
-
|
|
663
|
+
p: "16px 16px 18px",
|
|
664
|
+
display: "grid",
|
|
665
|
+
gridTemplateColumns: {
|
|
666
|
+
xs: "1fr",
|
|
667
|
+
sm: "repeat(2, 1fr)",
|
|
668
|
+
md: "repeat(3, 1fr)"
|
|
669
|
+
},
|
|
670
|
+
gap: "10px",
|
|
671
|
+
maxHeight: 420
|
|
672
|
+
},
|
|
673
|
+
children: activeVoices.map(voiceOption => /*#__PURE__*/_jsx(VoiceCard, {
|
|
674
|
+
t: t,
|
|
675
|
+
voiceOption: voiceOption,
|
|
676
|
+
languageCode: activeAccent?.languageCode,
|
|
677
|
+
selected: draftVoiceOption?.name === voiceOption.name,
|
|
678
|
+
tabbable: gridEntryName === voiceOption.name,
|
|
679
|
+
playing: playingVoice === voiceOption.name,
|
|
680
|
+
showProviderIcons: showProviderIcons,
|
|
681
|
+
showPreview: showPreview,
|
|
682
|
+
onSelect: () => setDraftVoice(voiceOption.name),
|
|
683
|
+
onPreview: () => handlePreview(voiceOption),
|
|
684
|
+
cardRef: node => {
|
|
685
|
+
cardRefs.current[voiceOption.name] = node;
|
|
686
|
+
}
|
|
687
|
+
}, voiceOption.name))
|
|
688
|
+
}), /*#__PURE__*/_jsxs(DialogActions, {
|
|
689
|
+
sx: theme => {
|
|
690
|
+
const tokens = panelTokens(theme);
|
|
691
|
+
return {
|
|
692
|
+
p: "13px 20px",
|
|
693
|
+
justifyContent: "space-between",
|
|
694
|
+
gap: "16px",
|
|
695
|
+
borderTop: "1px solid",
|
|
696
|
+
borderColor: tokens.footerRule,
|
|
697
|
+
backgroundColor: tokens.footerBg
|
|
698
|
+
};
|
|
328
699
|
},
|
|
329
|
-
children: /*#__PURE__*/_jsx(
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
700
|
+
children: [showPitch ? /*#__PURE__*/_jsx(VoicePitchToggle, {
|
|
701
|
+
t: t,
|
|
702
|
+
value: pitchFor(draftVoiceOption),
|
|
703
|
+
disabled: pitchDisabled,
|
|
704
|
+
onChange: setDraftPitch
|
|
705
|
+
}) :
|
|
706
|
+
/*#__PURE__*/
|
|
707
|
+
// Nothing to tune, so the footer states what's about to be
|
|
708
|
+
// committed — the draft's own accent, which may not be the tab on
|
|
709
|
+
// screen. Blank until something is drafted.
|
|
710
|
+
_jsx(Box, {
|
|
711
|
+
sx: {
|
|
712
|
+
display: "flex",
|
|
713
|
+
alignItems: "center",
|
|
714
|
+
gap: "10px",
|
|
715
|
+
minWidth: 0
|
|
716
|
+
},
|
|
717
|
+
children: draftVoiceOption && /*#__PURE__*/_jsxs(_Fragment, {
|
|
718
|
+
children: [/*#__PURE__*/_jsx(Avatar, {
|
|
719
|
+
src: draftVoiceOption.userImage,
|
|
720
|
+
alt: "",
|
|
721
|
+
sx: {
|
|
722
|
+
width: 30,
|
|
723
|
+
height: 30
|
|
724
|
+
}
|
|
725
|
+
}), /*#__PURE__*/_jsxs(Typography, {
|
|
726
|
+
sx: {
|
|
727
|
+
fontSize: "13.5px",
|
|
728
|
+
fontWeight: 600,
|
|
729
|
+
color: "text.primary",
|
|
730
|
+
overflow: "hidden",
|
|
731
|
+
textOverflow: "ellipsis",
|
|
732
|
+
whiteSpace: "nowrap"
|
|
733
|
+
},
|
|
734
|
+
children: [draftVoiceOption.name, draftAccent?.label ? ` · ${draftAccent.label}` : ""]
|
|
735
|
+
})]
|
|
336
736
|
})
|
|
337
|
-
})
|
|
737
|
+
}), /*#__PURE__*/_jsxs(Box, {
|
|
738
|
+
sx: {
|
|
739
|
+
display: "flex",
|
|
740
|
+
alignItems: "center",
|
|
741
|
+
gap: "8px",
|
|
742
|
+
flex: "0 0 auto"
|
|
743
|
+
},
|
|
744
|
+
children: [/*#__PURE__*/_jsx(Button, {
|
|
745
|
+
color: "primary",
|
|
746
|
+
onClick: handleClose,
|
|
747
|
+
sx: actionButtonSx,
|
|
748
|
+
"data-cy": "close-voice-selector",
|
|
749
|
+
children: t("close")
|
|
750
|
+
}), /*#__PURE__*/_jsx(Button, {
|
|
751
|
+
variant: "contained",
|
|
752
|
+
color: "primary",
|
|
753
|
+
disabled: !draftVoiceOption,
|
|
754
|
+
onClick: handleCommit,
|
|
755
|
+
sx: actionButtonSx,
|
|
756
|
+
children: t("use_voice")
|
|
757
|
+
})]
|
|
758
|
+
})]
|
|
759
|
+
}), /*#__PURE__*/_jsx(Box, {
|
|
760
|
+
"aria-live": "polite",
|
|
761
|
+
sx: visuallyHidden,
|
|
762
|
+
children: playingVoice ? `${t("playing")} ${playingVoice}` : ""
|
|
338
763
|
})]
|
|
339
764
|
})]
|
|
340
765
|
});
|
|
341
766
|
}
|
|
342
|
-
VoiceSelector.propTypes = {
|
|
343
|
-
t: PropTypes.func.isRequired,
|
|
344
|
-
languages: PropTypes.object.isRequired,
|
|
345
|
-
value: PropTypes.string.isRequired,
|
|
346
|
-
onChange: PropTypes.func.isRequired,
|
|
347
|
-
onBlur: PropTypes.func,
|
|
348
|
-
error: PropTypes.bool
|
|
349
|
-
};
|
|
350
767
|
export default VoiceSelector;
|