diff --git a/package-lock.json b/package-lock.json index 21e5c292..c3aaca29 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "axios": "^1.6.5", "canvas-confetti": "^1.9.2", "character-error-rate": "^1.1.4", + "chart.js": "^4.4.6", "classnames": "^2.3.1", "eslint-plugin-import": "^2.28.0", "eslint-plugin-jsx-a11y": "^6.7.1", @@ -37,6 +38,7 @@ "react": "^18.2.0", "react-audio-analyser": "^1.0.0", "react-audio-player": "^0.17.0", + "react-chartjs-2": "^5.2.0", "react-confetti": "^6.1.0", "react-dom": "^18.2.0", "react-infinite-scroll-component": "^6.1.0", @@ -4512,6 +4514,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -9015,6 +9023,18 @@ "integrity": "sha512-VDVylpiUdLOqY9aowYsz9M3zKdeQAdFF76PI1lv3oBQANQ6bc6V7pGEqar9nx6c37x0UMu5EHg32Sus3fQ1XxQ==", "license": "MIT" }, + "node_modules/chart.js": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.6.tgz", + "integrity": "sha512-8Y406zevUPbbIBA/HRk33khEmQPk5+cxeflWE/2rx1NJsjVWMPw/9mSP9rxHP5eqi6LNoPBVMfZHxbwLSgldYA==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/check-types": { "version": "11.2.3", "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.3.tgz", @@ -20965,6 +20985,16 @@ "react-dom": ">=16" } }, + "node_modules/react-chartjs-2": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz", + "integrity": "sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==", + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-confetti": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/react-confetti/-/react-confetti-6.1.0.tgz", diff --git a/package.json b/package.json index 16afc1c3..b2570792 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "axios": "^1.6.5", "canvas-confetti": "^1.9.2", "character-error-rate": "^1.1.4", + "chart.js": "^4.4.6", "classnames": "^2.3.1", "eslint-plugin-import": "^2.28.0", "eslint-plugin-jsx-a11y": "^6.7.1", @@ -33,6 +34,7 @@ "react": "^18.2.0", "react-audio-analyser": "^1.0.0", "react-audio-player": "^0.17.0", + "react-chartjs-2": "^5.2.0", "react-confetti": "^6.1.0", "react-dom": "^18.2.0", "react-infinite-scroll-component": "^6.1.0", diff --git a/src/assets/Hourglass.gif b/src/assets/Hourglass.gif new file mode 100644 index 00000000..4219c4f5 Binary files /dev/null and b/src/assets/Hourglass.gif differ diff --git a/src/components/Assesment/Assesment.jsx b/src/components/Assesment/Assesment.jsx index 3d07ef07..32b86ee8 100644 --- a/src/components/Assesment/Assesment.jsx +++ b/src/components/Assesment/Assesment.jsx @@ -49,6 +49,7 @@ import panda from "../../assets/images/panda.svg"; import cryPanda from "../../assets/images/cryPanda.svg"; import { uniqueId } from "../../services/utilService"; import { end } from "../../services/telementryService"; +import AudioDiagnosticTool from "./AudioDiagnosticTool"; export const LanguageModal = ({ lang, setLang, setOpenLangModal }) => { const [selectedLang, setSelectedLang] = useState(lang); @@ -216,6 +217,96 @@ export const LanguageModal = ({ lang, setLang, setOpenLangModal }) => { ); }; +export const TestModal = ({ setOpenTestModal }) => { + return ( + + + + { + setOpenTestModal(false); + }} + sx={{ + cursor: "pointer", + background: "red", + width: "25px", + height: "25px", + borderRadius: "50%", + display: "flex", + justifyContent: "center", + alignItems: "center", + border: "2px solid black", // Add black border + }} + > + + X + + + + + + + {`Audio Diagnostic`} + + + + + + + + + + ); +}; + export const MessageDialog = ({ message, closeDialog, @@ -336,6 +427,7 @@ export const MessageDialog = ({ export const ProfileHeader = ({ setOpenLangModal, + setOpenTestModal, lang, profileName, points = 0, @@ -494,36 +586,90 @@ export const ProfileHeader = ({ */} - - - setOpenLangModal - ? setOpenLangModal(true) - : setOpenMessageDialog({ - message: "go to homescreen to change language", - dontShowHeader: true, - }) - } +
- - - - + setOpenTestModal + ? setOpenTestModal(true) + : setOpenMessageDialog({ + message: "go to homescreen to change language", + dontShowHeader: true, + }) + } + > + +
{ + e.target.style.transform = "scale(1.1)"; + }} + onMouseLeave={(e) => { + e.target.style.transform = "scale(1)"; + }} + role="button" + tabIndex="0" + onKeyDown={(e) => { + if (e.key === "Enter") { + e.target.style.transform = "scale(1)"; + } }} > - {languages?.find((elem) => elem.lang === language)?.name || - "Select Language"} - + + ? + +
-
+ + setOpenLangModal + ? setOpenLangModal(true) + : setOpenMessageDialog({ + message: "go to homescreen to change language", + dontShowHeader: true, + }) + } + > + + + + + {languages?.find((elem) => elem.lang === language)?.name || + "Select Language"} + + + + +
{process.env.REACT_APP_IS_IN_APP_AUTHORISATION === "true" && ( @@ -559,6 +705,7 @@ const Assesment = ({ discoverStart }) => { const [level, setLevel] = useState(""); const dispatch = useDispatch(); const [openLangModal, setOpenLangModal] = useState(false); + const [openTestModal, setOpenTestModal] = useState(false); const [lang, setLang] = useState(getLocalData("lang") || "en"); const [points, setPoints] = useState(0); @@ -711,10 +858,18 @@ const Assesment = ({ discoverStart }) => { {openLangModal && ( )} + {openTestModal && } {level > 0 ? ( {process.env.REACT_APP_SHOW_HELP_VIDEO === "true" && ( @@ -783,6 +938,7 @@ const Assesment = ({ discoverStart }) => { backgroundImage={practicebg} {...{ setOpenLangModal, + setOpenTestModal, lang, points, }} diff --git a/src/components/Assesment/AudioDiagnosticTool.jsx b/src/components/Assesment/AudioDiagnosticTool.jsx new file mode 100644 index 00000000..790e6792 --- /dev/null +++ b/src/components/Assesment/AudioDiagnosticTool.jsx @@ -0,0 +1,547 @@ +import React, { useState, useRef } from "react"; +import loaderGif from "../.././assets/Hourglass.gif"; +import record from "../.././assets/mic.png"; +import { StopButton } from "../../utils/constants"; +import { Box } from "@mui/material"; +import { Line } from "react-chartjs-2"; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + LineElement, + PointElement, + Title, + Tooltip, + Legend, +} from "chart.js"; + +// Registering Chart.js components +ChartJS.register( + CategoryScale, + LinearScale, + LineElement, + PointElement, + Title, + Tooltip, + Legend +); + +function AudioDiagnosticTool() { + const [isRecording, setIsRecording] = useState(false); + const [audioBlob, setAudioBlob] = useState(null); + const [audioUrl, setAudioUrl] = useState(null); + const [latencyData, setLatencyData] = useState([]); + const [latency, setLatency] = useState(null); + const [testResults, setTestResults] = useState([]); + const [testIndex, setTestIndex] = useState(0); + const [loading, setLoading] = useState(false); + const mediaRecorderRef = useRef(null); + const audioChunksRef = useRef([]); + const [latencyStart, setLatencyStart] = useState(0); + + //console.log('inxxx', testIndex); + + const startRecording = () => { + //console.log('Starting recording...'); + audioChunksRef.current = []; + const stream = navigator.mediaDevices.getUserMedia({ audio: true }); + + stream + .then((mediaStream) => { + mediaRecorderRef.current = new MediaRecorder(mediaStream); + mediaRecorderRef.current.ondataavailable = (event) => { + audioChunksRef.current.push(event.data); + }; + mediaRecorderRef.current.onstop = () => { + const audioBlob = new Blob(audioChunksRef.current, { + type: "audio/wav", + }); + setAudioBlob(audioBlob); + setAudioUrl(URL.createObjectURL(audioBlob)); + //console.log('Recording stopped, audioBlob set.'); + }; + mediaRecorderRef.current.start(); + setIsRecording(true); + setLatencyStart(Date.now()); + }) + .catch((err) => { + console.error("Error accessing audio devices:", err); + }); + }; + + const stopRecording = () => { + mediaRecorderRef.current.stop(); + setIsRecording(false); + + // Capture latency time + const latency = Date.now() - latencyStart; + //console.log(`Latency for this recording: ${latency} ms`); + setLatencyData((prev) => [...prev, latency]); + setLatency(latency); + }; + + const nextTest = () => { + setLoading(true); // Start loader + + setTimeout(() => { + setLoading(false); // Stop loader + + if (testIndex === 0) { + analyzeLatencyTest(latency); + } else if (testIndex === 1) { + analyzeNoiseTest(); + } else if (testIndex === 2) { + analyzeOtherTest(); + } + }, 2500); + setTestIndex((prev) => prev + 1); + setAudioBlob(null); + setAudioUrl(null); + }; + + const analyzeLatencyTest = (latency) => { + //console.log('Analyzing latency test...'); + setTestResults((prev) => [ + ...prev, + { test: "Latency", result: `${latency} ms` }, + ]); + //setTestIndex(1); // Move to the next test (Noise Test) + }; + + const analyzeNoiseTest = () => { + //console.log('Analyzing noise test...'); + blobToAudioBuffer(audioBlob) + .then(analyzeAudioBuffer) + .then((analysisReport) => { + setTestResults((prev) => [ + ...prev, + { test: "Noise Level", result: analysisReport.noiseLevelDescription }, + ]); + //setTestIndex(2); // Move to the next test (Other Test) + }) + .catch(console.error); + }; + + const analyzeOtherTest = () => { + //console.log('Analyzing other test...'); + blobToAudioBuffer(audioBlob) + .then(analyzeAudioBuffer) + .then((analysisReport) => { + const audioQualityDescription = + getAudioQualityDescription(analysisReport); + setTestResults((prev) => [ + ...prev, + { test: "Audio Quality", result: audioQualityDescription }, + ]); + //setTestIndex(3); // All tests completed + }) + .catch(console.error); + }; + + const getAudioQualityDescription = (analysisReport) => { + const noiseLevel = analysisReport.noiseLevel; + if (noiseLevel >= 8) return "Excellent"; + if (noiseLevel >= 6) return "Good"; + if (noiseLevel >= 4) return "Average"; + return "Poor"; + }; + + const blobToAudioBuffer = (blob) => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsArrayBuffer(blob); + reader.onloadend = () => { + const audioContext = new (window.AudioContext || + window.webkitAudioContext)(); + audioContext.decodeAudioData(reader.result, resolve, reject); + }; + reader.onerror = reject; + }); + }; + const analyzeAudioBuffer = (audioBuffer) => { + const channelData = audioBuffer.getChannelData(0); // Assuming mono audio + const noiseLevel = getNoiseLevel(channelData); + const noiseLevelRating = normalizeNoiseLevel(noiseLevel); + const noiseLevelDescription = getDescription( + noiseLevelRating, + "noise level" + ); + return { + noiseLevel: noiseLevelRating, + noiseLevelDescription, + }; + }; + + const getNoiseLevel = (channelData) => { + const noise = channelData.filter((value) => Math.abs(value) < 0.01).length; + return noise / channelData.length; + }; + + const normalizeNoiseLevel = (noiseLevel) => { + return Math.min(10, ((1 - noiseLevel) / 0.2) * 10); + }; + + const getDescription = (rating, type) => { + if (type === "noise level") { + if (rating >= 8) return "Very quiet"; + if (rating >= 6) return "Quiet"; + if (rating >= 4) return "Moderate noise"; + return "Noisy"; + } + }; + + // Chart data and options for latency graph + const latencyChartData = { + labels: latencyData.map((_, index) => `Test ${index + 1}`), + datasets: [ + { + label: "Latency (ms)", + data: latencyData, + borderColor: "rgba(75, 192, 192, 1)", + fill: false, + }, + ], + }; + + const latencyChartOptions = { + responsive: true, + scales: { + y: { + beginAtZero: true, + }, + }, + }; + + // Display final results when all tests are complete + const displayFinalResults = () => { + const idealRanges = [ + { test: "Latency", idealRange: "0 - 150 ms" }, + { test: "Noise Level", idealRange: "0 - 4 (lower is better)" }, + { test: "Audio Quality", idealRange: "Excellent - Poor" }, + ]; + + if (testIndex === 3) { + return ( +
+

+ Audio Performance Test Results +

+ + Audio Quality Test + +
    + {testResults.map((result, index) => ( +
  • + + {result.test}: {result.result} + +
  • + ))} +
+ + Audio Latency Test + +
+ {latencyData.length > 0 && ( +
+ +
+ )} + + + + + + + + + {idealRanges.map((range, index) => ( + + + + + ))} + +
+ Test + + Ideal Range +
+ {range.test} + + {range.idealRange} +
+
+
+ ); + } + }; + + const TestSection = ({ + title, + isRecording, + startRecording, + stopRecording, + record, + testIndex, + currentIndex, + loading, + }) => { + if (testIndex !== currentIndex || loading) return null; + + return ( +
+ + {title} + + {!isRecording && ( + + )} + {isRecording && ( + + + + )} +
+ ); + }; + + const RecordingButton = ({ startRecording, record }) => ( +
{ + if (e.key === "Enter") { + startRecording(); + } + }} + > + Record +
+ ); + + return ( +
+ {/* Show record button or next step button based on testIndex */} + + + + + + {loading && ( +
+ Loading Animation +
+ )} + {audioUrl && ( + + )} + {!loading && audioBlob && ( + + + + {"Next"} + + + + )} + + {!loading && displayFinalResults()} +
+ ); +} + +export default AudioDiagnosticTool; diff --git a/src/components/Layouts.jsx/MainLayout.jsx b/src/components/Layouts.jsx/MainLayout.jsx index 93a17ff5..a180c1b3 100644 --- a/src/components/Layouts.jsx/MainLayout.jsx +++ b/src/components/Layouts.jsx/MainLayout.jsx @@ -114,6 +114,7 @@ const MainLayout = (props) => { progressData, showProgress, setOpenLangModal, + setOpenTestModal, lang, handleBack, disableScreen, @@ -222,7 +223,14 @@ const MainLayout = (props) => { return ( {LEVEL && ( @@ -1179,6 +1187,7 @@ MainLayout.propTypes = { isShowCase: PropTypes.bool, showProgress: PropTypes.bool, setOpenLangModal: PropTypes.func, + setOpenTestModal: PropTypes.func, points: PropTypes.number, handleNext: PropTypes.any, enableNext: PropTypes.bool, diff --git a/src/utils/VoiceAnalyser.js b/src/utils/VoiceAnalyser.js index 1884ab47..ea21d0a2 100644 --- a/src/utils/VoiceAnalyser.js +++ b/src/utils/VoiceAnalyser.js @@ -77,8 +77,6 @@ function VoiceAnalyser(props) { ); const [isMatching, setIsMatching] = useState(false); - //console.log('audio', recordedAudio, isMatching); - useEffect(() => { if (!props.enableNext) { setRecordedAudio(""); @@ -389,13 +387,17 @@ function VoiceAnalyser(props) { } } - if (responseText.toLowerCase() === originalText.toLowerCase()) { - setIsMatching(true); - } else { - setIsMatching(false); - } + //console.log('dataaaa', data); - //console.log('textss', recordedAudio, isMatching, responseText, originalText); + // if (responseText.toLowerCase() === originalText.toLowerCase()) { + // setIsMatching(true); + // } else { + // setIsMatching(false); + // } + + setIsMatching( + data?.createScoreData?.session?.count_diff?.character === 0 + ); const responseEndTime = new Date().getTime(); const responseDuration = Math.round( @@ -663,8 +665,6 @@ function VoiceAnalyser(props) { }); }; - //console.log('textss', recordedAudio, isMatching); - return (
{loader ? ( diff --git a/yarn.lock b/yarn.lock index a82e4198..67ac55d8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2439,6 +2439,11 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" +"@kurkle/color@^0.3.0": + version "0.3.4" + resolved "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz" + integrity sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w== + "@leichtgewicht/ip-codec@^2.0.1": version "2.0.5" resolved "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz" @@ -5136,6 +5141,13 @@ character-error-rate@^1.1.4: resolved "https://registry.npmjs.org/character-error-rate/-/character-error-rate-1.1.4.tgz" integrity sha512-VDVylpiUdLOqY9aowYsz9M3zKdeQAdFF76PI1lv3oBQANQ6bc6V7pGEqar9nx6c37x0UMu5EHg32Sus3fQ1XxQ== +chart.js@^4.4.6: + version "4.4.6" + resolved "https://registry.npmjs.org/chart.js/-/chart.js-4.4.6.tgz" + integrity sha512-8Y406zevUPbbIBA/HRk33khEmQPk5+cxeflWE/2rx1NJsjVWMPw/9mSP9rxHP5eqi6LNoPBVMfZHxbwLSgldYA== + dependencies: + "@kurkle/color" "^0.3.0" + check-types@^11.2.3: version "11.2.3" resolved "https://registry.npmjs.org/check-types/-/check-types-11.2.3.tgz" @@ -11332,6 +11344,11 @@ react-audio-player@^0.17.0: dependencies: prop-types "^15.7.2" +react-chartjs-2@^5.2.0: + version "5.2.0" + resolved "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz" + integrity sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA== + react-confetti@^6.1.0: version "6.1.0" resolved "https://registry.npmjs.org/react-confetti/-/react-confetti-6.1.0.tgz"