dmed-voice-assistant 1.0.3 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,575 +0,0 @@
1
- import { useEffect, useState, useRef } from "react";
2
- import { Box, Typography, MenuItem } from "@mui/material";
3
- import Grid from '@mui/material/Grid2';
4
- import { MicIcon, StartIcon, SmallStartIcon, SettingIcon, StopIcon, RedStopIcon, LanguageIcon, BoxIcon, SmallPauseIcon } from "./components/svgs";
5
- import StyledSelect from "./components/StyledSelect";
6
- import RecordListItem from "./components/RecordListItem";
7
- import Recorder from "recorder-js";
8
-
9
- const apiUrl = 'https://api.origintechx.dev/qa/v1/diagnose/voice';
10
-
11
- const getVoiceFileName = (date) => {
12
- const year = date.getFullYear(); // Get the full year (YYYY)
13
- const month = String(date.getMonth() + 1).padStart(2, '0'); // Get the month (MM), pad with leading zero if necessary
14
- const day = String(date.getDate()).padStart(2, '0'); // Get the day (DD), pad with leading zero if necessary
15
-
16
- return `Voice${year}${month}${day}.wav`;
17
- }
18
-
19
- const getTimeValues = (totalSeconds) => {
20
- const hours = Math.floor(totalSeconds / 3600); // Get hours
21
- let minutes = Math.floor((totalSeconds % 3600) / 60); // Get minutes
22
- let seconds = totalSeconds % 60; // Get seconds
23
-
24
- minutes = minutes < 10 ? `0${minutes}` : minutes;
25
- seconds = seconds < 10 ? `0${seconds}` : seconds;
26
-
27
- return { hours, minutes, seconds }; // Return the values separately
28
- };
29
-
30
- const getAudioDuration = async (blob) => {
31
- const audioContext = new AudioContext();
32
- const arrayBuffer = await blob.arrayBuffer();
33
- const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
34
- const duration = audioBuffer.duration;
35
- return parseInt(duration);
36
- }
37
-
38
- const RecorderBox = ({
39
- mode = 'recorder',
40
- recordHistoryList,
41
- setMode,
42
- onNewRecordEvent,
43
- onRecordDataChange
44
- }) => {
45
- const [isVoiceMode, setIsVoiceMode] = useState(false);
46
- const [isStartedRecord, setIsStartedRecord] = useState(false);
47
- const [selectedVoice, setSelectedVoice] = useState("");
48
- const [voiceList, setVoiceList] = useState([]);
49
- const languageList = ['Auto-Detect', 'English', 'Chinese (Simplified)'];
50
- const [selectedLanguage, setSelectedLanguage] = useState("");
51
- const [recordList, setRecordList] = useState(recordHistoryList);
52
- const [newRecordFileName, setNewRecordFileName] = useState("");
53
- const [newRecordTime, setNewRecordTime] = useState(0);
54
- const [isRunning, setIsRunning] = useState(false);
55
- const [intervalId, setIntervalId] = useState(null);
56
-
57
- const [audioBlob, setAudioBlob] = useState(null);
58
- const [audioUrl, setAudioUrl] = useState('');
59
- const [audioSize, setAudioSize] = useState(0);
60
- const mediaRecorderRef = useRef(null);
61
-
62
- const handleVoiceChange = (event) => {
63
- setSelectedVoice(event.target.value);
64
- };
65
-
66
- const handleLanguageChange = (event) => {
67
- setSelectedLanguage(event.target.value);
68
- };
69
-
70
- const initRecorder = async () => {
71
- try {
72
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
73
- const audioContext = new (window.AudioContext || window.webkitAudioContext)();
74
- const newRecorder = new Recorder(audioContext);
75
- await newRecorder.init(stream);
76
- mediaRecorderRef.current = newRecorder;
77
- } catch (error) {
78
- console.error("Unable to access microphone", error);
79
- }
80
- };
81
-
82
- const startRecording = async () => {
83
- if (!mediaRecorderRef.current) {
84
- await initRecorder();
85
- }
86
- if (mediaRecorderRef.current) {
87
- setAudioBlob(null);
88
- mediaRecorderRef.current.start();
89
- setIsStartedRecord(true);
90
-
91
- setNewRecordFileName(getVoiceFileName(new Date()));
92
- startCounting();
93
- }
94
- };
95
-
96
- const stopRecording = () => {
97
- if (mediaRecorderRef.current) {
98
- mediaRecorderRef.current.stop().then(async ({ blob }) => {
99
- setAudioBlob(blob);
100
- setAudioUrl(URL.createObjectURL(blob));
101
- console.log(blob)
102
- let temp = [...recordList];
103
- const newVoice = {
104
- audioURL: URL.createObjectURL(blob),
105
- name: newRecordFileName,
106
- date: new Date(),
107
- size: blob.size,
108
- time: await getAudioDuration(blob)
109
- };
110
-
111
- temp.push(newVoice);
112
- setRecordList(temp);
113
-
114
- if(onNewRecordEvent) {
115
- onNewRecordEvent(newVoice);
116
- }
117
- if(onRecordDataChange) {
118
- onRecordDataChange(temp);
119
- }
120
- });
121
- setIsStartedRecord(false);
122
-
123
- stopCounting();
124
- setNewRecordTime(0);
125
- setAudioSize(0);
126
- }
127
- };
128
-
129
- const handleStartRecord = async () => {
130
- if(isVoiceMode) return;
131
-
132
- if(!isStartedRecord) {
133
- startRecording();
134
- } else {
135
- stopRecording();
136
- }
137
- };
138
-
139
- const handleModeChange = () => {
140
- if(mode === "recorder") {
141
- setMode("recognition");
142
- } else if(mode === "recognition") {
143
- setMode("recorder");
144
- }
145
- };
146
-
147
- const handleSteupChange = () => {
148
- if(isStartedRecord) return;
149
-
150
- setIsVoiceMode(!isVoiceMode);
151
- };
152
-
153
- const handleLabelChange = (id, newLabel) => {
154
- setRecordList((prevRecords) =>
155
- prevRecords.map((record, index) =>
156
- index === id ? { ...record, label: newLabel } : record
157
- )
158
- );
159
- };
160
-
161
- const handleDelete = (id) => {
162
- setRecordList((prevRecords) => prevRecords.filter((record, index) => index !== id));
163
- };
164
-
165
- const startCounting = () => {
166
- if (isRunning) return; // Prevent starting a new interval if already running
167
- const id = setInterval(async () => {
168
- setNewRecordTime((prevCount) => prevCount + 1); // Increment count every 1000ms
169
- await mediaRecorderRef.current.audioRecorder.getBuffer((blob) => {
170
- setAudioSize((blob[0].byteLength / 1024 / 1024).toFixed(2));
171
- })
172
- }, 1000);
173
- setIntervalId(id); // Store the interval ID
174
- setIsRunning(true); // Set the counter as running
175
- };
176
-
177
- const stopCounting = () => {
178
- clearInterval(intervalId); // Stop the interval using the interval ID
179
- setIsRunning(false); // Set the counter as not running
180
- };
181
-
182
- useEffect(() => {
183
- const fetchAudioInputDevices = async () => {
184
- try {
185
- // Request permission to access media devices
186
- await navigator.mediaDevices.getUserMedia({ audio: true });
187
-
188
- // Enumerate all media devices
189
- const devices = await navigator.mediaDevices.enumerateDevices();
190
-
191
- // Filter only audio input devices
192
- const audioInputs = devices.filter(device => device.kind === "audioinput");
193
- let temp = ['Auto-Detect'];
194
- audioInputs.forEach(device => {
195
- temp.push(device.label);
196
- });
197
- setVoiceList(temp);
198
- } catch (error) {
199
- console.error("Error accessing audio devices:", error);
200
- }
201
- };
202
-
203
- fetchAudioInputDevices();
204
- }, []);
205
-
206
- useEffect(() => {
207
- // uploadRecording();
208
- }, [audioBlob]);
209
-
210
- return (
211
- <Box
212
- className="bg-[#0B0B0B] rounded-[5px] border p-[20px] w-[850px]"
213
- >
214
- <Grid container spacing={2}>
215
- <Grid size={6}>
216
- <Box className="flex items-center justify-between">
217
- <Typography className="!text-[24px] !font-[600]" color="#EAE5DC">
218
- Voice assistant
219
- </Typography>
220
- <Box
221
- className="flex items-center px-[10px] py-[4px] bg-[#006FFF4D] rounded-[89.1px] cursor-pointer h-[28px]"
222
- sx={{
223
- border: '0.9px solid #006FFFB2',
224
- }}
225
- onClick={handleModeChange}
226
- >
227
- <Box>
228
- <MicIcon />
229
- </Box>
230
- <Typography className="!font-600 !text-[14px]" color="#EAE5DC">
231
- Recorder mode
232
- </Typography>
233
- </Box>
234
- </Box>
235
-
236
- <Box className="flex items-center justify-between mt-4">
237
- {
238
- !isStartedRecord ?
239
- <Box
240
- className="flex items-center space-x-1 cursor-pointer px-[9px] py-[6px] rounded-[5px] bg-[#0B0B0B]"
241
- sx={{
242
- '&:hover': {
243
- boxShadow: '0px 0px 5px 1px #44C63380',
244
- cursor: isVoiceMode && 'not-allowed'
245
- }
246
- }}
247
- onClick={handleStartRecord}
248
- >
249
- <Box>
250
- <StartIcon />
251
- </Box>
252
- <Typography
253
- className="!font-400 !text-[16px]"
254
- color="#EAE5DC"
255
- >
256
- Start
257
- </Typography>
258
- </Box>
259
- :
260
- <Box
261
- className="flex items-center space-x-1 cursor-pointer px-[9px] py-[6px] rounded-[5px] bg-[#0B0B0B]"
262
- sx={{
263
- border: '0.9px solid #F3151580',
264
- '&:hover': {
265
- boxShadow: '0px 0px 5px 1px #F3151580'
266
- }
267
- }}
268
- onClick={handleStartRecord}
269
- >
270
- <Box>
271
- <SmallPauseIcon />
272
- </Box>
273
- <Typography
274
- className="!font-400 !text-[14px]"
275
- color="#EAE5DC"
276
- >
277
- Stop
278
- </Typography>
279
- </Box>
280
- }
281
- <Box
282
- className="flex items-center space-x-1 cursor-pointer px-[9px] py-[6px] rounded-[5px] bg-[#0B0B0B]"
283
- sx={{
284
- '&:hover': {
285
- boxShadow: '0px 0px 5px 2px #58585880',
286
- cursor: isStartedRecord && 'not-allowed'
287
- }
288
- }}
289
- onClick={handleSteupChange}
290
- >
291
- <Box>
292
- <SettingIcon />
293
- </Box>
294
- <Typography className="!font-400 !text-[14px]" color="#EAE5DC">
295
- Setup
296
- </Typography>
297
- </Box>
298
- </Box>
299
-
300
- <Box className="rounded-[5px] bg-[#A3DBFE] mt-2">
301
- <Box className="flex items-center justify-between p-[4.5px]">
302
- <Box
303
- className="flex items-center space-x-1 rounded-[5px] px-[5px] py-[2px] bg-[#0B0B0B]"
304
- sx={{
305
- boxShadow: '0px 0px 1.8px 0px #00000040'
306
- }}
307
- >
308
- <Box>
309
- { !isStartedRecord ? <StopIcon /> : <RedStopIcon /> }
310
- </Box>
311
- <Typography className="!font-[600] !text-[14px]" color="#A3DBFE">
312
- { !isStartedRecord ? 'STOP' : 'REC' }
313
- </Typography>
314
- </Box>
315
- <Typography className="!font-400 !text-[20px] px-[9px]" color="#1A2123"
316
- sx={{
317
- fontFamily: "Space Grotesk !important"
318
- }}
319
- >
320
- 01.08.2024
321
- </Typography>
322
- </Box>
323
-
324
- <Box className="flex justify-between p-[9px]">
325
- <Typography className="!font-400 !text-[36px]" color="#1A2123"
326
- sx={{
327
- fontFamily: "Space Grotesk !important"
328
- }}
329
- >
330
- {getTimeValues(newRecordTime).hours}<span style={{ color: '#494A48', fontWeight: "300", fontSize: "16px"}}>h</span> {getTimeValues(newRecordTime).minutes}<span style={{ color: '#494A48', fontWeight: "300", fontSize: "16px"}}>m</span> {getTimeValues(newRecordTime).seconds}<span style={{ color: '#494A48', fontWeight: "300", fontSize: "16px"}}>s</span>
331
- </Typography>
332
- <Box className="flex flex-col space-y-3 text-right">
333
- <Typography className="!font-400 !text-[16px]" color="#494A48"
334
- sx={{
335
- fontFamily: "Space Grotesk !important"
336
- }}
337
- >
338
- {audioSize} <span style={{ fontSize: "14px", fontFamily: "Space Grotesk !important" }}>MB</span>
339
- </Typography>
340
- {
341
- isStartedRecord &&
342
- <Typography
343
- className="!font-bold !text-[16px]" color="#494A48"
344
- sx={{ fontFamily: "Space Grotesk !important" }}
345
- >
346
- {newRecordFileName}
347
- </Typography>
348
- }
349
- </Box>
350
- </Box>
351
- </Box>
352
- </Grid>
353
-
354
- <Grid size={6} className={`w-full ${isVoiceMode ? 'pr-[10px]' : ''}`}>
355
- {
356
- isVoiceMode &&
357
- <>
358
- <Box className="flex space-x-1 w-full">
359
- <Box>
360
- <MicIcon />
361
- </Box>
362
- <Box className="flex-1">
363
- <Typography className="!font-[600] !text-[16px]" color="#EAE5DC">
364
- Voice
365
- </Typography>
366
- <Typography className="!font-400 !text-[14px] pt-1" color="#EAE5DC">
367
- Input device
368
- </Typography>
369
- <StyledSelect
370
- className="mt-1"
371
- fullWidth
372
- displayEmpty
373
- value={selectedVoice}
374
- onChange={handleVoiceChange}
375
- renderValue={(selected) => {
376
- if (selected === "") {
377
- return <span style={{
378
- fontSize: '12.6px',
379
- fontWeight: '400',
380
- lineHeight: '25.2px',
381
- color: '#EAE5DC99' }}>Auto-Detect</span>;
382
- }
383
- return <span style={{
384
- fontSize: '12.6px',
385
- fontWeight: '400',
386
- lineHeight: '25.2px',
387
- color: '#EAE5DC99' }}>{voiceList[selected]}</span>;
388
- }}
389
- MenuProps={{
390
- PaperProps: {
391
- sx: {
392
- marginTop: "5px",
393
- padding: '5px',
394
- background: '#0B0B0B',
395
- border: '0.9px solid #565656',
396
- '& .MuiList-root': {
397
- padding: "unset"
398
- },
399
- '& .MuiMenuItem-root': {
400
- padding: "7.2px 9px",
401
- color: '#EAE5DC99',
402
- fontFamily: 'Reddit Sans',
403
- fontSize: '12.6px',
404
- fontWeight: '400',
405
- lineHeight: '25.2px'
406
- },
407
- '& .MuiMenuItem-root:hover': {
408
- background: "#A3DBFE99",
409
- color: '#1A2123',
410
- },
411
- '& .MuiMenuItem-root.Mui-selected': {
412
- background: "#A3DBFE",
413
- color: '#1A2123',
414
- },
415
- '& .MuiMenuItem-root.Mui-selected:hover': {
416
- background: "#A3DBFE99",
417
- color: '#1A2123',
418
- }
419
- },
420
- },
421
- }}
422
- >
423
- {
424
- voiceList.map((device, index) => {
425
- return (
426
- <MenuItem value={index} key={index}>{device}</MenuItem>
427
- )
428
- })
429
- }
430
- </StyledSelect>
431
- </Box>
432
- </Box>
433
-
434
- <Box className="flex space-x-1 w-full mt-2">
435
- <Box>
436
- <LanguageIcon />
437
- </Box>
438
- <Box className="flex-1">
439
- <Typography className="!font-[600] !text-[16px]" color="#EAE5DC">
440
- Language
441
- </Typography>
442
- <Typography className="!font-400 !text-[14px] pt-1" color="#EAE5DC">
443
- Prefer language
444
- </Typography>
445
- <StyledSelect
446
- className="mt-1"
447
- fullWidth
448
- displayEmpty
449
- value={selectedLanguage}
450
- onChange={handleLanguageChange}
451
- renderValue={(selected) => {
452
- if (selected === "") {
453
- return <span style={{
454
- fontSize: '12.6px',
455
- fontWeight: '400',
456
- lineHeight: '25.2px',
457
- color: '#EAE5DC99' }}>Auto-Detect</span>;
458
- }
459
- return <span style={{
460
- fontSize: '12.6px',
461
- fontWeight: '400',
462
- lineHeight: '25.2px',
463
- color: '#EAE5DC99' }}>{languageList[selected]}</span>;
464
- }}
465
- MenuProps={{
466
- PaperProps: {
467
- sx: {
468
- marginTop: "5px",
469
- padding: '5px',
470
- background: '#0B0B0B',
471
- border: '0.9px solid #565656',
472
- '& .MuiList-root': {
473
- padding: "unset"
474
- },
475
- '& .MuiMenuItem-root': {
476
- padding: "7.2px 9px",
477
- color: '#EAE5DC99',
478
- fontFamily: 'Reddit Sans',
479
- fontSize: '12.6px',
480
- fontWeight: '400',
481
- lineHeight: '25.2px'
482
- },
483
- '& .MuiMenuItem-root:hover': {
484
- background: "#A3DBFE99",
485
- color: '#1A2123',
486
- },
487
- '& .MuiMenuItem-root.Mui-selected': {
488
- background: "#A3DBFE",
489
- color: '#1A2123',
490
- },
491
- '& .MuiMenuItem-root.Mui-selected:hover': {
492
- background: "#A3DBFE99",
493
- color: '#1A2123',
494
- }
495
- },
496
- },
497
- }}
498
- >
499
- <MenuItem value={0}>Auto-Detect</MenuItem>
500
- <MenuItem value={1}>English</MenuItem>
501
- <MenuItem value={2}>Chinese (Simplified)</MenuItem>
502
- </StyledSelect>
503
- </Box>
504
- </Box>
505
- </>
506
- }
507
-
508
- {
509
- !isVoiceMode &&
510
- <>
511
- {
512
- recordList.length === 0 &&
513
- <>
514
- <Box className="flex flex-col items-center justify-center h-full">
515
- <Box>
516
- <BoxIcon />
517
- </Box>
518
- <Typography className="!font-[600] !text-[16px] pt-2" color="#EAE5DC"
519
- sx={{ fontFamily: "Afacad !important" }}
520
- >
521
- Record Empty
522
- </Typography>
523
- <Box className="flex items-center space-x-1 mt-1">
524
- <Typography className="!font-400 !text-[14px]" color="#EAE5DC"
525
- sx={{ fontFamily: "Afacad !important" }}
526
- >
527
- Push
528
- </Typography>
529
- <Box>
530
- <SmallStartIcon />
531
- </Box>
532
- <Typography className="!font-400 !text-[16px]" color="#EAE5DC"
533
- sx={{ fontFamily: "Afacad !important" }}
534
- >
535
- to start
536
- </Typography>
537
- </Box>
538
- </Box>
539
- </>
540
- }
541
-
542
- {
543
- recordList.length > 0 &&
544
- <Box className="flex flex-col space-y-2 p-[10px] scrollableBox"
545
- sx={{
546
- maxHeight: "225px",
547
- }}
548
- >
549
- {
550
- recordList.map((record, index) => {
551
- return (
552
- <RecordListItem
553
- audioURL={record.audioURL}
554
- label={record.name}
555
- capacity={record.size}
556
- time={record.time}
557
- createdDate={record.date}
558
- key={index}
559
- onLabelChange={(newLabel) => handleLabelChange(index, newLabel)}
560
- onDelete={() => handleDelete(index)}
561
- />
562
- )
563
- })
564
- }
565
- </Box>
566
- }
567
- </>
568
- }
569
- </Grid>
570
- </Grid>
571
- </Box>
572
- )
573
- };
574
-
575
- export default RecorderBox;