dmed-voice-assistant 1.0.0

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.
@@ -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;