Skip to content
37 changes: 34 additions & 3 deletions src/components/Assesment/Assesment.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 { useMediaCache } from "../Hooks/useMediaCache";

export const LanguageModal = ({ lang, setLang, setOpenLangModal }) => {
const [selectedLang, setSelectedLang] = useState(lang);
Expand Down Expand Up @@ -552,6 +553,36 @@ const Assesment = ({ discoverStart }) => {
username = userDetails.student_name;
setLocalData("profileName", username);
}

const { cacheMedia } = useMediaCache();
const [mediaUrls, setMediaUrls] = useState({});
const mediaFiles = [
{ key: "desktopLevel1", url: desktopLevel1 },
{ key: "desktopLevel2", url: desktopLevel2 },
{ key: "desktopLevel3", url: desktopLevel3 },
{ key: "desktopLevel4", url: desktopLevel4 },
{ key: "desktopLevel5", url: desktopLevel5 },
{ key: "desktopLevel6", url: desktopLevel6 },
{ key: "desktopLevel7", url: desktopLevel7 },
{ key: "desktopLevel8", url: desktopLevel8 },
{ key: "desktopLevel9", url: desktopLevel9 },
{ key: "assessmentBackground", url: assessmentBackground },
{ key: "HelpLogo", url: HelpLogo },
];

useEffect(() => {
const cacheAllMedia = async () => {
const urls = {};
for (const media of mediaFiles) {
const cachedUrl = await cacheMedia(media.key, media.url);
urls[media.key] = cachedUrl;
}
setMediaUrls(urls);
};

cacheAllMedia();
}, []);

// const [searchParams, setSearchParams] = useSearchParams();
// const [profileName, setProfileName] = useState(username);
const [openMessageDialog, setOpenMessageDialog] = useState("");
Expand Down Expand Up @@ -714,7 +745,7 @@ const Assesment = ({ discoverStart }) => {
const sectionStyle = {
width: "100vw",
height: "100vh",
backgroundImage: `url(${images?.[`desktopLevel${level || 1}`]})`,
backgroundImage: `url(${mediaUrls?.desktopLevel1})`,
backgroundRepeat: "round",
backgroundSize: "auto",
position: "relative",
Expand Down Expand Up @@ -803,7 +834,7 @@ const Assesment = ({ discoverStart }) => {
<MainLayout
showNext={false}
showTimer={false}
cardBackground={assessmentBackground}
cardBackground={mediaUrls?.assessmentBackground}
backgroundImage={practicebg}
{...{
setOpenLangModal,
Expand Down Expand Up @@ -865,7 +896,7 @@ const Assesment = ({ discoverStart }) => {
textAlign: "center",
}}
>
<img src={HelpLogo} alt="help_video_link" />
<img src={mediaUrls?.HelpLogo} alt="help_video_link" />
</Box>
)}
<Box
Expand Down
37 changes: 31 additions & 6 deletions src/components/AssesmentEnd/AssesmentEnd.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { useEffect, useState } from "react";
import LevelCompleteAudio from "../../assets/audio/levelComplete.wav";
import { ProfileHeader } from "../Assesment/Assesment";
import desktopLevel5 from "../../assets/images/assesmentComplete.png";
import config from '../../utils/urlConstants.json';
import config from "../../utils/urlConstants.json";
import { uniqueId } from "../../services/utilService";

const AssesmentEnd = () => {
Expand All @@ -25,10 +25,35 @@ const AssesmentEnd = () => {
const [previousLevel, setPreviousLevel] = useState("");
const [points, setPoints] = useState(0);

const [audioSrc, setAudioSrc] = useState(null);

useEffect(() => {
const preloadAudio = async () => {
try {
const response = await fetch(LevelCompleteAudio);
const audioBlob = await response.blob();
const audioUrl = URL.createObjectURL(audioBlob);
setAudioSrc(audioUrl);
} catch (error) {
console.error("Error loading audio:", error);
}
};
preloadAudio();

return () => {
// Cleanup blob URL to prevent memory leaks
if (audioSrc) {
URL.revokeObjectURL(audioSrc);
}
};
}, []);

useEffect(() => {
(async () => {
let audio = new Audio(LevelCompleteAudio);
audio.play();
if (audioSrc) {
let audio = new Audio(audioSrc);
audio.play();
}
const virtualId = getLocalData("virtualId");
const lang = getLocalData("lang");
const previous_level = getLocalData("previous_level");
Expand All @@ -40,9 +65,9 @@ const AssesmentEnd = () => {
setLevel(data.data.milestone_level);
setLocalData("userLevel", data.data.milestone_level?.replace("m", ""));
let sessionId = getLocalData("sessionId");
if (!sessionId){
if (!sessionId) {
sessionId = uniqueId();
localStorage.setItem("sessionId", sessionId)
localStorage.setItem("sessionId", sessionId);
}
const getPointersDetails = await axios.get(
`${process.env.REACT_APP_LEARNER_AI_ORCHESTRATION_HOST}/${config.URLS.GET_POINTER}/${virtualId}/${sessionId}?language=${lang}`
Expand All @@ -52,7 +77,7 @@ const AssesmentEnd = () => {
setTimeout(() => {
setShake(false);
}, 4000);
}, []);
}, [audioSrc]);

const navigate = useNavigate();
let newLevel = level.replace("m", "");
Expand Down
47 changes: 33 additions & 14 deletions src/components/DiscoverEnd/DiscoverEnd.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,8 @@ import back from "../../assets/images/back-arrow.svg";
import discoverEndLeft from "../../assets/images/discover-end-left.svg";
import discoverEndRight from "../../assets/images/discover-end-right.svg";
import textureImage from "../../assets/images/textureImage.png";
import {
LetsStart,
getLocalData,
setLocalData,
} from "../../utils/constants";
import config from '../../utils/urlConstants.json';
import { LetsStart, getLocalData, setLocalData } from "../../utils/constants";
import config from "../../utils/urlConstants.json";

const sectionStyle = {
backgroundImage: `url(${textureImage})`,
Expand All @@ -32,12 +28,35 @@ const sectionStyle = {
const SpeakSentenceComponent = () => {
const [shake, setShake] = useState(true);
const [level, setLevel] = useState("");
const [audioSrc, setAudioSrc] = useState(null);

useEffect(() => {
const preloadAudio = async () => {
try {
const response = await fetch(LevelCompleteAudio);
const audioBlob = await response.blob();
const audioUrl = URL.createObjectURL(audioBlob);
setAudioSrc(audioUrl);
} catch (error) {
console.error("Error loading audio:", error);
}
};
preloadAudio();

return () => {
// Cleanup blob URL to prevent memory leaks
if (audioSrc) {
URL.revokeObjectURL(audioSrc);
}
};
}, []);

useEffect(() => {

(async () => {
let audio = new Audio(LevelCompleteAudio);
audio.play();
if (audioSrc) {
let audio = new Audio(audioSrc);
audio.play();
}
const virtualId = getLocalData("virtualId");
const lang = getLocalData("lang");
const getMilestoneDetails = await axios.get(
Expand All @@ -50,14 +69,14 @@ const SpeakSentenceComponent = () => {
setTimeout(() => {
setShake(false);
}, 4000);
}, []);
}, [audioSrc]);

const handleProfileBack = () => {
try {
if (process.env.REACT_APP_IS_APP_IFRAME === 'true') {
navigate("/")
if (process.env.REACT_APP_IS_APP_IFRAME === "true") {
navigate("/");
} else {
navigate("/discover-start")
navigate("/discover-start");
}
} catch (error) {
console.error("Error posting message:", error);
Expand Down Expand Up @@ -132,7 +151,7 @@ const SpeakSentenceComponent = () => {
</Typography>

<Box
onClick={() => handleProfileBack()}
onClick={() => handleProfileBack()}
sx={{
display: "flex",
justifyContent: "center",
Expand Down
29 changes: 28 additions & 1 deletion src/components/DiscoverSentance/DiscoverSentance.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,36 @@ const SpeakSentenceComponent = () => {
const [openMessageDialog, setOpenMessageDialog] = useState("");
const [totalSyllableCount, setTotalSyllableCount] = useState("");
const [isNextButtonCalled, setIsNextButtonCalled] = useState(false);
const [audioSrc, setAudioSrc] = useState(null);

useEffect(() => {
const preloadAudio = async () => {
try {
const response = await fetch(LevelCompleteAudio);
const audioBlob = await response.blob();
const audioUrl = URL.createObjectURL(audioBlob);
setAudioSrc(audioUrl);
} catch (error) {
console.error("Error loading audio:", error);
}
};
preloadAudio();

return () => {
// Cleanup blob URL to prevent memory leaks
if (audioSrc) {
URL.revokeObjectURL(audioSrc);
}
};
}, []);

const callConfettiAndPlay = () => {
let audio = new Audio(LevelCompleteAudio);
let audio;
if (audioSrc) {
audio = new Audio(audioSrc);
} else {
audio = new Audio(LevelCompleteAudio);
}
audio.play();
callConfetti();
};
Expand Down
89 changes: 89 additions & 0 deletions src/components/Hooks/useMediaCache.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { useEffect } from "react";

export function useMediaCache(dbName = "MediaCacheDB", storeName = "media") {
useEffect(() => {
initializeDB(dbName, storeName);
}, [dbName, storeName]);

const initializeDB = (dbName, storeName) => {
const openDB = indexedDB.open(dbName, 1);

openDB.onupgradeneeded = () => {
const db = openDB.result;
if (!db.objectStoreNames.contains(storeName)) {
db.createObjectStore(storeName, { keyPath: "key" });
}
};

openDB.onerror = (e) => {
console.error("IndexedDB initialization error:", e.target.error);
};
};

const saveMedia = async (key, blob) => {
const db = await openDatabase(dbName);
return await putMedia(db, storeName, { key, blob });
};

const getMedia = async (key) => {
const db = await openDatabase(dbName);
return await fetchMedia(db, storeName, key);
};

const cacheMedia = async (key, url) => {
try {
const cachedBlob = await getMedia(key);
if (cachedBlob) {
return URL.createObjectURL(cachedBlob);
}
return await fetchAndCacheMedia(key, url);
} catch (error) {
console.error(`Error caching media for key "${key}":`, error);
throw error;
}
};
Comment on lines +33 to +44
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Prevent memory leaks and add cache management

The caching implementation has potential memory leak issues and lacks important safeguards:

  1. URLs created with URL.createObjectURL are not revoked
  2. No cache size limits or cleanup strategy
  3. Missing media type validation
  4. No timeout handling for fetch operations

Apply these critical fixes:

 const cacheMedia = async (key, url) => {
   try {
     const cachedBlob = await getMedia(key);
     if (cachedBlob) {
-      return URL.createObjectURL(cachedBlob);
+      const objectUrl = URL.createObjectURL(cachedBlob);
+      // Store URL for cleanup
+      mediaUrls.set(key, objectUrl);
+      return objectUrl;
     }
-    return await fetchAndCacheMedia(key, url);
+    const controller = new AbortController();
+    const timeoutId = setTimeout(() => controller.abort(), 5000);
+    
+    try {
+      return await fetchAndCacheMedia(key, url, controller.signal);
+    } finally {
+      clearTimeout(timeoutId);
+    }
   } catch (error) {
+    if (error.name === 'AbortError') {
+      throw new Error(`Fetch timeout for media "${key}"`);
+    }
     console.error(`Error caching media for key "${key}":`, error);
     throw error;
   }
 };

Add these utility functions for proper resource management:

const mediaUrls = new Map();

const cleanupObjectUrl = (key) => {
  const url = mediaUrls.get(key);
  if (url) {
    URL.revokeObjectURL(url);
    mediaUrls.delete(key);
  }
};

// Add this to the returned object
return { 
  cacheMedia,
  cleanup: () => {
    mediaUrls.forEach((url) => URL.revokeObjectURL(url));
    mediaUrls.clear();
  }
};


const openDatabase = (dbName) => {
return new Promise((resolve, reject) => {
const openDB = indexedDB.open(dbName, 1);

openDB.onsuccess = () => resolve(openDB.result);
openDB.onerror = () => reject(openDB.error);
});
};

const putMedia = (db, storeName, media) => {
return new Promise((resolve, reject) => {
const tx = db.transaction(storeName, "readwrite");
const store = tx.objectStore(storeName);
const putRequest = store.put(media);

putRequest.onsuccess = () => resolve(true);
putRequest.onerror = () => reject(putRequest.error);

tx.oncomplete = () => db.close();
});
};

const fetchMedia = (db, storeName, key) => {
return new Promise((resolve, reject) => {
const tx = db.transaction(storeName, "readonly");
const store = tx.objectStore(storeName);
const getRequest = store.get(key);

getRequest.onsuccess = () => resolve(getRequest.result?.blob || null);
getRequest.onerror = () => reject(getRequest.error);

tx.oncomplete = () => db.close();
});
};

const fetchAndCacheMedia = async (key, url) => {
const response = await fetch(url);
const blob = await response.blob();
await saveMedia(key, blob);
return URL.createObjectURL(blob);
};
Comment on lines +81 to +86
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve error handling and add validation in fetchAndCacheMedia

The function needs better error handling and validation:

  1. No validation of response status
  2. No retry mechanism for failed fetches
  3. No size limits on cached blobs

Apply these improvements:

 const fetchAndCacheMedia = async (key, url) => {
-  const response = await fetch(url);
-  const blob = await response.blob();
-  await saveMedia(key, blob);
-  return URL.createObjectURL(blob);
+  const MAX_RETRIES = 3;
+  const MAX_BLOB_SIZE = 5 * 1024 * 1024; // 5MB
+  
+  for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
+    try {
+      const response = await fetch(url);
+      if (!response.ok) {
+        throw new Error(`HTTP error! status: ${response.status}`);
+      }
+      
+      const blob = await response.blob();
+      if (blob.size > MAX_BLOB_SIZE) {
+        throw new Error('Media file too large');
+      }
+      
+      await saveMedia(key, blob);
+      const objectUrl = URL.createObjectURL(blob);
+      mediaUrls.set(key, objectUrl);
+      return objectUrl;
+    } catch (error) {
+      if (attempt === MAX_RETRIES) throw error;
+      await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
+    }
+  }
 };

Committable suggestion skipped: line range outside the PR's diff.


return { cacheMedia };
}
Loading