dmed-voice-assistant 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,575 @@
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 "../shared/svgs";
5
+ import StyledSelect from "../shared/StyledSelect";
6
+ import RecordListItem from "../shared/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;