Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mechanics 7 Added #255

Merged
merged 16 commits into from
Jan 15, 2025
Merged

Conversation

Ayansaha20
Copy link
Contributor

@Ayansaha20 Ayansaha20 commented Jan 13, 2025

Summary by CodeRabbit

Release Notes

  • New Features

    • Added advanced speech recognition and audio recording capabilities.
    • Introduced a new phonetic learning mechanism for word practice.
    • Enhanced audio playback and recording visualization.
    • Added a new component for phonetic learning exercises.
    • Implemented celebratory visual feedback for successful recordings.
    • Introduced a new SVG graphic component for visual representation.
  • Dependency Updates

    • Replaced telemetry SDK with a new version.
    • Added metaphone library for phonetic matching.
  • Performance Improvements

    • Optimized word recognition and matching algorithms.
    • Improved user interaction feedback mechanisms.
  • UI/UX Enhancements

    • Expanded visual learning resources.
    • Enhanced conditional rendering for better user feedback.
    • Introduced new properties for improved component interactivity.
    • Updated state management for dynamic content presentation.

Copy link

coderabbitai bot commented Jan 13, 2025

Warning

There were issues while running some tools. Please review the errors and either fix the tool’s configuration or disable the tool if it’s a critical failure.

🔧 eslint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

src/components/Practice/Mechanics7.jsx

Oops! Something went wrong! :(

ESLint: 7.32.0

ESLint couldn't find the config "react-app" to extend from. Please check that the name of the config is correct.

The config "react-app" was referenced from the config file in "/package.json".

If you still have problems, please stop by https://eslint.org/chat/help to chat with the team.

Walkthrough

This pull request introduces significant updates to a learning application, primarily focusing on replacing the telemetry SDK and enhancing audio recording and speech recognition capabilities. The package.json file is modified to remove the old telemetry SDK and add new dependencies. Various components are updated for improved readability and interactivity, including the addition of a new Mechanics7 component designed for phonetic learning exercises. Enhancements in state management and audio processing functionalities are implemented across multiple files.

Changes

File Change Summary
package.json - Removed @project-sunbird/telemetry-sdk v0.0.29
- Added @tekdi/all-telemetry-sdk v^0.0.32
- Added metaphone v^2.0.1
src/App.js - Updated telemetry SDK import from @project-sunbird/telemetry-sdk to @tekdi/all-telemetry-sdk
- Modified initService to accept visitorId
src/components/Assesment/* - Reformatted conditional statements for improved readability
src/components/Layouts.jsx/MainLayout.jsx - Added new props: isRecordingComplete, answer
- Introduced confetti effect for recording completion
src/components/Mechanism/WordsOrImage.jsx - Added speech recognition and audio recording functionality
- Introduced phonetic matching methods
- Enhanced state management for recording interactions
src/components/Practice/Mechanics7.jsx - New component for phonetic learning exercise
src/utils/VoiceAnalyser.js - Added logging and audio processing enhancements
- Updated prop types
src/utils/constants.js - Added WordRedCircle component
- Updated practiceSteps and levelGetContent arrays
src/views/Practice/Practice.jsx - Integrated Mechanics7 component
- Added new state variables for image and word management
src/components/Practice/Mechanics3.jsx - Added props isNextButtonCalled, setIsNextButtonCalled to Mechanics2 component
src/components/Practice/Mechanics4.jsx - Added props isNextButtonCalled, setIsNextButtonCalled to Mechanics4 component
src/utils/phoneticUtils.js - Added functions phoneticMatch, levenshtein for phonetic comparison

Sequence Diagram

sequenceDiagram
    participant User
    participant WordsOrImage
    participant SpeechRecognition
    participant AudioRecorder
    
    User->>WordsOrImage: Start Recording
    WordsOrImage->>SpeechRecognition: Initialize Recognition
    WordsOrImage->>AudioRecorder: Start Audio Recording
    User->>SpeechRecognition: Speak Word
    SpeechRecognition-->>WordsOrImage: Return Recognized Text
    AudioRecorder-->>WordsOrImage: Capture Audio Blob
    WordsOrImage->>WordsOrImage: Phonetic Matching
    WordsOrImage->>User: Provide Feedback
Loading

Possibly related PRs

Finishing Touches

  • 📝 Generate Docstrings (Beta)

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🔭 Outside diff range comments (4)
src/components/Mechanism/WordsOrImage.jsx (4)

Line range hint 547-616: Add key prop to elements in mapped list for consistent rendering

When rendering lists in React, each element should have a unique key prop to optimize rendering and prevent potential issues.

Add a key prop to the mapped elements:

{selectedWordsRef.current?.map((elem, index) => (
+   <span key={elem} onClick={() => handleWordsLogic(elem, "", true)} className={...}>
      {/* ... */}
    </span>
))}

Line range hint 734-749: Include key prop in character mapping

Each child in a list should have a unique key prop to ensure correct rendering and improve performance.

Add a key prop to the mapped characters:

{elem?.split("")?.map((char, index) => (
+   <span key={index} style={{ /* styles */ }}>
      {char}
    </span>
))}

Line range hint 759-760: Resolve duplicate cursor and height properties in style object

Duplicate style properties can cause unexpected behavior. Ensure each property is defined only once in the style object.

Proposed changes:

  <Box
    sx={{
      cursor: wIndex === 0 ? `url(${clapImage}), auto` : "auto",
      height: { xs: "30px", sm: "40px", md: "50px" },
      /* Other style properties */
-     cursor: wIndex === 0 ? `url(${clapImage}), auto` : "auto",
      /* Duplicate properties removed */
    }}
  >

Line range hint 784-785: Resolve duplicate cursor and height properties in style object

Duplicate properties can lead to unintended styling issues. Please remove the duplicate cursor and height properties.

Proposed changes:

  <Box
    sx={{
      cursor: `url(${clapImage}), auto`,
      height: { xs: "30px", sm: "40px", md: "50px" },
      /* Other style properties */
-     cursor: `url(${clapImage}), auto`,
      /* Duplicate properties removed */
    }}
  >
🧹 Nitpick comments (8)
src/components/Mechanism/WordsOrImage.jsx (1)

20-25: Remove unnecessary console.log statements for cleaner code

The console.log statements in the phoneticMatch function appear to be for debugging purposes and should be removed to keep the codebase clean.

Proposed changes:

-   //console.log('sss', str1, str2);
    const phonetic1 = metaphone(str1);
    const phonetic2 = metaphone(str2);
    const distance = levenshtein(phonetic1, phonetic2);
-   //console.log(`Phonetic 1: ${phonetic1}, Phonetic 2: ${phonetic2}`);
    const maxLength = Math.max(phonetic1.length, phonetic2.length);
src/components/Practice/Mechanics7.jsx (1)

31-38: Remove unnecessary console.log statements for cleaner code

The console.log statements in the phoneticMatch function seem to be used for debugging and should be removed to maintain clean code.

Proposed changes:

-   //console.log('sss', str1, str2);
    const phonetic1 = metaphone(str1);
    const phonetic2 = metaphone(str2);
    const distance = levenshtein(phonetic1, phonetic2);
-   //console.log(`Phonetic 1: ${phonetic1}, Phonetic 2: ${phonetic2}`);
    const maxLength = Math.max(phonetic1.length, phonetic2.length);
src/utils/constants.js (2)

1455-1469: Add prop validation and consider size optimization.

The component could benefit from:

  1. PropTypes validation for width and height props
  2. Memoization since it's a pure component
  3. Documentation for the expected props and usage

Consider this improvement:

+import PropTypes from 'prop-types';
+import { memo } from 'react';
+
-export const WordRedCircle = (props) => (
+export const WordRedCircle = memo(props => (
   <svg
     width={props.width || 411}
     height={props.height || 200}
     viewBox="0 0 411 200"
     fill="none"
     xmlns="http://www.w3.org/2000/svg"
     {...props}
   >
     <path
       d="M3.02253 149.72C39.9332 241.843 394.604 194.311 409.612 108.803C433.265 -25.9514 110.563 -13.701 64.5675 30.4006C62.864 32.0339 63.1427 35.0512 64.5675 34.4024C189.212 -22.3579 416.242 13.4951 397.009 108.803C374.83 181.408 11.4522 233.349 9.97905 133.386C9.00908 67.5673 171.698 5.57303 289.387 25.337C292.906 24.5203 290.136 20.9491 286.277 20.1918C160.077 -0.900771 -24.8327 61.8263 3.02253 149.72Z"
       fill="#FF4B4B"
     />
   </svg>
-));
+));
+
+WordRedCircle.propTypes = {
+  width: PropTypes.number,
+  height: PropTypes.number
+};

3217-3242: Extract repeated mechanism configuration to a constant.

The same mechanism configuration is duplicated across multiple entries. Consider extracting it to a constant to improve maintainability.

+const FORM_A_WORD_MECHANISM = { id: "mechanic_7", name: "formAWord2" };
+
 export const levelGetContent = {
   // ...
   {
     title: "P2",
     criteria: "word",
     template: "simple",
     tags: "CEFR_ENG",
-    mechanism: { id: "mechanic_7", name: "formAWord2" },
+    mechanism: FORM_A_WORD_MECHANISM,
   },
   // Apply similar changes to other entries
src/App.js (1)

77-79: Use optional chaining for better readability.

Replace multiple && checks with optional chaining operator for cleaner code.

-      window.telemetry &&
-        window.telemetry.syncEvents &&
-        window.telemetry.syncEvents();
+      window.telemetry?.syncEvents?.();
🧰 Tools
🪛 Biome (1.9.4)

[error] 77-79: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

src/views/Practice/Practice.jsx (3)

26-46: Standardize image formats and optimize imports.

The image imports use inconsistent file extensions (.png, .svg, .jpeg, .jpg). Consider:

  1. Standardizing image formats for better maintenance
  2. Using a CDN or optimized image service for better performance
  3. Implementing lazy loading for images

88-159: Move levels data to a separate configuration file.

The levels data structure is large and could be moved to a separate configuration file for better maintainability and code organization. This would also allow for easier updates and potential dynamic loading.

+ // src/config/levels.js
+ export const levels = {
+   L1: [...],
+   L2: [...],
+   // ...
+ };

- const levels = {
-   L1: [...],
-   L2: [...],
-   // ...
- };
+ import { levels } from '../config/levels';

1072-1115: Optimize props passing to Mechanics7.

The component receives many props through object spreading. Consider:

  1. Explicitly listing required props for better maintainability
  2. Creating a dedicated props interface/type
  3. Grouping related props into logical objects
- {...{
-   level: level,
-   header: questions[currentQuestion]?.contentType === "image"
-     ? `Guess the below image`
-     : `Speak the below word`,
-   currentImg: currentImage,
-   // ... many more props
- }}
+ level={level}
+ header={questions[currentQuestion]?.contentType === "image"
+   ? `Guess the below image`
+   : `Speak the below word`}
+ currentImg={currentImage}
+ imageProps={{
+   contentType,
+   contentId,
+   type: "word"
+ }}
+ audioProps={{
+   setVoiceText,
+   setRecordedAudio,
+   setVoiceAnimate
+ }}
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 69fce47 and 03f6827.

⛔ Files ignored due to path filters (31)
  • package-lock.json is excluded by !**/package-lock.json
  • src/assets/Basket.png is excluded by !**/*.png
  • src/assets/Button.png is excluded by !**/*.png
  • src/assets/Candle.png is excluded by !**/*.png
  • src/assets/Dinner.jpeg is excluded by !**/*.jpeg
  • src/assets/Mango.png is excluded by !**/*.png
  • src/assets/Pocket.png is excluded by !**/*.png
  • src/assets/Sunset.png is excluded by !**/*.png
  • src/assets/Tablet.png is excluded by !**/*.png
  • src/assets/Tunnel.png is excluded by !**/*.png
  • src/assets/Window.jpeg is excluded by !**/*.jpeg
  • src/assets/apple.png is excluded by !**/*.png
  • src/assets/banana.jpeg is excluded by !**/*.jpeg
  • src/assets/bulbHint.svg is excluded by !**/*.svg
  • src/assets/clap.svg is excluded by !**/*.svg
  • src/assets/frame.svg is excluded by !**/*.svg
  • src/assets/hand-icon-new.svg is excluded by !**/*.svg
  • src/assets/hint.svg is excluded by !**/*.svg
  • src/assets/jacket.png is excluded by !**/*.png
  • src/assets/kitten.png is excluded by !**/*.png
  • src/assets/lemon.png is excluded by !**/*.png
  • src/assets/orange.png is excluded by !**/*.png
  • src/assets/pencil.gif is excluded by !**/*.gif
  • src/assets/pencil.svg is excluded by !**/*.svg
  • src/assets/picnic.png is excluded by !**/*.png
  • src/assets/rabbit.png is excluded by !**/*.png
  • src/assets/rocket.png is excluded by !**/*.png
  • src/assets/table.jpg is excluded by !**/*.jpg
  • src/assets/tiger.png is excluded by !**/*.png
  • src/assets/tomato.png is excluded by !**/*.png
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (10)
  • package.json (2 hunks)
  • src/App.js (2 hunks)
  • src/components/Assesment/Assesment.jsx (2 hunks)
  • src/components/AssesmentEnd/AssesmentEnd.jsx (1 hunks)
  • src/components/Layouts.jsx/MainLayout.jsx (6 hunks)
  • src/components/Mechanism/WordsOrImage.jsx (6 hunks)
  • src/components/Practice/Mechanics7.jsx (1 hunks)
  • src/utils/VoiceAnalyser.js (4 hunks)
  • src/utils/constants.js (3 hunks)
  • src/views/Practice/Practice.jsx (7 hunks)
✅ Files skipped from review due to trivial changes (2)
  • src/components/AssesmentEnd/AssesmentEnd.jsx
  • src/components/Assesment/Assesment.jsx
🧰 Additional context used
🪛 Biome (1.9.4)
src/App.js

[error] 77-79: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

src/components/Practice/Mechanics7.jsx

[error] 118-118: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 759-759: This property value named cursor is later overwritten by an object member with the same name.

Overwritten with this value.

If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named cursor

(lint/suspicious/noDuplicateObjectKeys)


[error] 760-760: This property value named height is later overwritten by an object member with the same name.

Overwritten with this value.

If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named height

(lint/suspicious/noDuplicateObjectKeys)


[error] 784-784: This property value named cursor is later overwritten by an object member with the same name.

Overwritten with this value.

If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named cursor

(lint/suspicious/noDuplicateObjectKeys)


[error] 785-785: This property value named height is later overwritten by an object member with the same name.

Overwritten with this value.

If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named height

(lint/suspicious/noDuplicateObjectKeys)


[error] 120-120: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)


[error] 560-599: Missing key property for this element in iterable.

The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation.

(lint/correctness/useJsxKeyInIterable)


[error] 734-749: Missing key property for this element in iterable.

The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation.

(lint/correctness/useJsxKeyInIterable)

🔇 Additional comments (10)
src/components/Mechanism/WordsOrImage.jsx (1)

120-121: 🛠️ Refactor suggestion

Optimize recordingStates initialization to improve performance

Using the spread operator inside a reduce function can lead to O(n²) time complexity due to repeated copying of the accumulator object. Consider using a different approach to build the recordingStates object.

Proposed changes:

- setRecordingStates(
-   words.reduce((acc, word) => ({ ...acc, [word]: false }), {})
- );
+ const initialStates = {};
+ words.forEach((word) => {
+   initialStates[word] = false;
+ });
+ setRecordingStates(initialStates);

Likely invalid or redundant comment.

src/components/Practice/Mechanics7.jsx (1)

654-841: ⚠️ Potential issue

Add key prop to elements in mapped list for consistent rendering

When mapping over arrays in React, each element should have a unique key prop to help React identify which items have changed.

Add a key prop to the mapped elements:

{wordsRef.current?.map((elem, wIndex) => (
+ <React.Fragment key={elem}>
    {/* ... */}
  </React.Fragment>
))}

Likely invalid or redundant comment.

🧰 Tools
🪛 Biome (1.9.4)

[error] 759-759: This property value named cursor is later overwritten by an object member with the same name.

Overwritten with this value.

If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named cursor

(lint/suspicious/noDuplicateObjectKeys)


[error] 760-760: This property value named height is later overwritten by an object member with the same name.

Overwritten with this value.

If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named height

(lint/suspicious/noDuplicateObjectKeys)


[error] 784-784: This property value named cursor is later overwritten by an object member with the same name.

Overwritten with this value.

If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named cursor

(lint/suspicious/noDuplicateObjectKeys)


[error] 785-785: This property value named height is later overwritten by an object member with the same name.

Overwritten with this value.

If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named height

(lint/suspicious/noDuplicateObjectKeys)


[error] 734-749: Missing key property for this element in iterable.

The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation.

(lint/correctness/useJsxKeyInIterable)

src/utils/constants.js (2)

3178-3187: LGTM! Well-structured practice steps.

The practice steps array maintains a consistent structure with clear naming conventions for different types of content (Lessons, Practices, Showcases).


3217-3242: Verify the mechanism integration with Mechanics7 component.

The mechanism configuration references "formAWord2". Let's verify its integration with the new Mechanics7 component.

✅ Verification successful

Mechanism integration is properly implemented

The formAWord2 mechanism is correctly integrated with the Mechanics7 component, with complete implementation of syllable-based word formation, speech recognition, and progress tracking features.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for formAWord2 usage in Mechanics7 component
ast-grep --pattern 'formAWord2'

# Search for other potential formAWord variants
rg 'formAWord[0-9]+'

Length of output: 569


Script:

#!/bin/bash
# Check the implementation in Mechanics7 component
cat src/components/Practice/Mechanics7.jsx

# Verify the handling in Practice component
cat src/views/Practice/Practice.jsx

Length of output: 74758

src/App.js (1)

11-11: Verify compatibility with the new telemetry SDK.

The telemetry SDK has been changed from @project-sunbird/telemetry-sdk to @tekdi/all-telemetry-sdk. Ensure that all telemetry events are still being tracked correctly with the new SDK.

Run this script to check the SDK versions and recent changes:

src/utils/VoiceAnalyser.js (1)

774-774: LGTM! PropTypes correctly defined.

The onAudioProcessed prop type is correctly defined as optional since it's only used conditionally for the "m7" page.

src/views/Practice/Practice.jsx (1)

64-66: LGTM! State variables properly initialized.

The new state variables for handling images and words are correctly initialized and follow React naming conventions.

src/components/Layouts.jsx/MainLayout.jsx (2)

131-134: LGTM! Props are well-structured.

The new props for recording functionality are properly destructured and have clear, descriptive names.


1244-1245: LGTM! PropTypes correctly defined.

The new prop types for isRecordingComplete and answer are properly defined and match their usage in the component.

package.json (1)

33-33: LGTM! Metaphone addition aligns with speech recognition requirements.

The addition of the metaphone package (^2.0.1) is appropriate for implementing phonetic matching algorithms in the Mechanics 7 feature.

Run this script to check for any existing phonetic matching implementations that could be consolidated:

✅ Verification successful

Verified: Metaphone package serves a unique purpose without redundancy

The metaphone package is used exclusively for phonetic matching in Mechanics7 and WordsOrImage components, while homophones serves a different purpose in Mechanics3 and Mechanics6. No consolidation needed as each package has distinct functionality.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for existing phonetic matching patterns
rg -l "soundex|levenshtein|phonetic" -t js -t jsx

# Check for similar dependencies
jq '.dependencies | keys[] | select(. | contains("phon") or contains("sound"))' package.json

Length of output: 211


Script:

#!/bin/bash
# Search for phonetic patterns with correct file types
rg -l 'soundex|levenshtein|phonetic' '*.js' '*.jsx'

# Search for imports/usage of sound-related packages
rg -l "from ['\"]metaphone['\"]|from ['\"]homophones['\"]" '*.js' '*.jsx'

# Look for actual implementations using these packages
rg -C 2 "require\(['\"]metaphone['\"]\)|require\(['\"]homophones['\"]\)|import.*from ['\"]metaphone['\"]|import.*from ['\"]homophones['\"]"

Length of output: 1903

Comment on lines 120 to 123
words.reduce((acc, word) => ({ ...acc, [word]: false }), {})
);
}
}, [words]);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Optimize setRecordingStates initialization to improve performance

Using the spread operator inside a reduce can lead to O(n²) time complexity. Consider initializing the state object without using the spread operator.

Proposed changes:

- setRecordingStates(
-   words.reduce((acc, word) => ({ ...acc, [word]: false }), {})
- );
+ const initialStates = {};
+ words.forEach((word) => {
+   initialStates[word] = false;
+ });
+ setRecordingStates(initialStates);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
words.reduce((acc, word) => ({ ...acc, [word]: false }), {})
);
}
}, [words]);
const initialStates = {};
words.forEach((word) => {
initialStates[word] = false;
});
setRecordingStates(initialStates);
);
}
}, [words]);
🧰 Tools
🪛 Biome (1.9.4)

[error] 120-120: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)

Comment on lines +106 to +107
console.log("llink", audioLink);

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Remove debug console logs.

Remove console.log statements that were added for debugging purposes to keep the production code clean.

-    console.log("llink", audioLink);
-
     try {
       let audio = new Audio(
         audioLink
           ? audioLink
           : `${process.env.REACT_APP_AWS_S3_BUCKET_CONTENT_URL}/all-audio-files/${lang}/${props.contentId}.wav`
       );
-      console.log("audo", audio);

Also applies to: 114-114

Comment on lines +202 to +206
useEffect(() => {
if (isRecordingComplete && answer) {
callConfettiSnow();
}
}, []);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add dependencies to useEffect for confetti.

The effect's empty dependency array means it only runs once. Add isRecordingComplete and answer as dependencies to show confetti for all successful recordings.

- useEffect(() => {
+ useEffect(() => {
    if (isRecordingComplete && answer) {
      callConfettiSnow();
    }
- }, []);
+ }, [isRecordingComplete, answer]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
if (isRecordingComplete && answer) {
callConfettiSnow();
}
}, []);
useEffect(() => {
if (isRecordingComplete && answer) {
callConfettiSnow();
}
}, [isRecordingComplete, answer]);

package.json Outdated
"@reduxjs/toolkit": "^2.2.0",
"@tekdi/all-telemetry-sdk": "^0.0.32",
Copy link

Choose a reason for hiding this comment

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

💡 Codebase verification

Update telemetry service implementation for SDK migration

The telemetry SDK migration is incomplete. The old SDK (@project-sunbird/client-services/telemetry) is still being used in the service layer while the new SDK (@tekdi/all-telemetry-sdk) is only partially integrated.

Required updates:

  • Migrate telemetry service implementations in src/services/telementryService.js and src/services/telemetryService.js to use the new SDK
  • Ensure consistent SDK usage across the application to avoid potential conflicts
🔗 Analysis chain

Verify the telemetry SDK migration impact.

The switch from @project-sunbird/telemetry-sdk to @tekdi/all-telemetry-sdk might require updates to telemetry event tracking across the application.

Run this script to identify potential telemetry usage that needs updating:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for old telemetry SDK imports and usage
rg -l "@project-sunbird/telemetry-sdk"

# Search for telemetry event tracking patterns
rg "telemetry|TelemetryService" -t js -t jsx

Length of output: 141


Script:

#!/bin/bash
# Search for telemetry patterns with correct file extensions
rg -t js "telemetry|TelemetryService|EkTelemetry" 

# Search for both old and new SDK imports/usage
rg "@project-sunbird/telemetry-sdk|@tekdi/all-telemetry-sdk" -t js

# Search for specific telemetry method calls
rg "\.start\(|\.impression\(|\.interact\(|\.end\(|\.log\(|\.error\(" -t js -C 2

Length of output: 79061

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
src/components/Mechanism/WordsOrImage.jsx (2)

27-61: Optimize space complexity of the Levenshtein distance calculation.

The current implementation uses O(m*n) space complexity. Consider using the two-row optimization which reduces space complexity to O(min(m,n)).

Apply this diff to optimize space complexity:

 const levenshtein = (a, b) => {
-  const tmp = [];
+  const prev = Array(b.length + 1).fill(0);
+  const curr = Array(b.length + 1).fill(0);
   let i,
     j,
     alen = a.length,
     blen = b.length,
     res;

   if (alen === 0) {
     return blen;
   }
   if (blen === 0) {
     return alen;
   }

-  for (i = 0; i <= alen; i++) {
-    tmp[i] = [i];
+  for (j = 0; j <= blen; j++) {
+    prev[j] = j;
   }
-  for (j = 0; j <= blen; j++) {
-    tmp[0][j] = j;
-  }

   for (i = 1; i <= alen; i++) {
+    curr[0] = i;
     for (j = 1; j <= blen; j++) {
       res = a[i - 1] === b[j - 1] ? 0 : 1;
-      tmp[i][j] = Math.min(
-        tmp[i - 1][j] + 1,
-        tmp[i][j - 1] + 1,
-        tmp[i - 1][j - 1] + res
+      curr[j] = Math.min(
+        prev[j] + 1,
+        curr[j - 1] + 1,
+        prev[j - 1] + res
       );
     }
+    [prev, curr] = [curr, prev];
   }

-  return tmp[alen][blen];
+  return prev[blen];
 };

105-105: Consolidate audio handling logic.

The current implementation has multiple audio references and scattered audio playback logic. Consider consolidating this into a custom hook for better reusability and maintainability.

Create a custom hook to manage audio playback:

// hooks/useAudioPlayer.js
const useAudioPlayer = () => {
  const audioRef = useRef(null);
  const [isPlaying, setIsPlaying] = useState(false);

  const playAudio = useCallback((audioBlob) => {
    if (audioBlob && audioRef.current) {
      const audioBlobUrl = URL.createObjectURL(audioBlob);
      audioRef.current.src = audioBlobUrl;
      audioRef.current.play()
        .then(() => setIsPlaying(true))
        .catch((error) => {
          console.error("Error playing audio:", error);
          setIsPlaying(false);
        });
      
      // Cleanup blob URL after playback
      audioRef.current.onended = () => {
        URL.revokeObjectURL(audioBlobUrl);
        setIsPlaying(false);
      };
    }
  }, []);

  return { audioRef, isPlaying, playAudio };
};

Then use it in your component:

-const audioRefs = createRef(null);
-const audioRef = useRef(null);
+const { audioRef, isPlaying, playAudio } = useAudioPlayer();

Also applies to: 124-124, 305-313

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 03f6827 and 4562725.

📒 Files selected for processing (4)
  • src/components/Assesment/Assesment.jsx (3 hunks)
  • src/components/Layouts.jsx/MainLayout.jsx (9 hunks)
  • src/components/Mechanism/WordsOrImage.jsx (7 hunks)
  • src/components/Practice/Mechanics7.jsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/Assesment/Assesment.jsx
🧰 Additional context used
🪛 Biome (1.9.4)
src/components/Practice/Mechanics7.jsx

[error] 118-118: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 655-655: This property value named cursor is later overwritten by an object member with the same name.

Overwritten with this value.

If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named cursor

(lint/suspicious/noDuplicateObjectKeys)


[error] 656-656: This property value named height is later overwritten by an object member with the same name.

Overwritten with this value.

If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named height

(lint/suspicious/noDuplicateObjectKeys)


[error] 680-680: This property value named cursor is later overwritten by an object member with the same name.

Overwritten with this value.

If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named cursor

(lint/suspicious/noDuplicateObjectKeys)


[error] 681-681: This property value named height is later overwritten by an object member with the same name.

Overwritten with this value.

If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named height

(lint/suspicious/noDuplicateObjectKeys)


[error] 120-120: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)


[error] 509-528: Missing key property for this element in iterable.

The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation.

(lint/correctness/useJsxKeyInIterable)


[error] 630-645: Missing key property for this element in iterable.

The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation.

(lint/correctness/useJsxKeyInIterable)

🔇 Additional comments (3)
src/components/Mechanism/WordsOrImage.jsx (1)

4-4: Add unit tests for the speech recognition functionality.

The TODO comment indicates missing test coverage. Please add comprehensive tests to verify the speech recognition functionality.

Would you like me to help generate unit tests or create a GitHub issue to track this task?

src/components/Practice/Mechanics7.jsx (1)

112-123: Optimize state initialization to avoid O(n²) complexity.

Using the spread operator inside reduce leads to O(n²) time complexity. Initialize the state object without using spread operator.

Apply this diff to optimize the initialization:

-setRecordingStates(
-  words.reduce((acc, word) => ({ ...acc, [word]: false }), {})
-);
+const initialStates = {};
+words.forEach((word) => {
+  initialStates[word] = false;
+});
+setRecordingStates(initialStates);
🧰 Tools
🪛 Biome (1.9.4)

[error] 118-118: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 120-120: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)

src/components/Layouts.jsx/MainLayout.jsx (1)

197-201: Add dependencies to useEffect for confetti.

The effect's empty dependency array means it only runs once. Add isRecordingComplete and answer as dependencies to show confetti for all successful recordings.

Apply this diff:

 useEffect(() => {
   if (isRecordingComplete && answer) {
     callConfettiSnow();
   }
-}, []);
+}, [isRecordingComplete, answer]);

Comment on lines 131 to 191
const initializeRecognition = () => {
let recognitionInstance;

if ("webkitSpeechRecognition" in window) {
recognitionInstance = new window.webkitSpeechRecognition();
} else if ("SpeechRecognition" in window) {
recognitionInstance = new window.SpeechRecognition();
} else {
alert("Your browser does not support Speech Recognition.");
return;
}

if (recognitionInstance) {
recognitionInstance.continuous = true;
recognitionInstance.interimResults = true;
recognitionInstance.lang = "en-US";
recognitionInstance.maxAlternatives = 1;

recognitionInstance.onstart = () => {
startAudioRecording();
};

recognitionInstance.onresult = (event) => {
const transcript = event.results[0][0].transcript;
setRecordedText(transcript);
setIsRecording(false);
setShowStopButton(false);
setShowListenRetryButtons(true);

const matchPercentage = phoneticMatch(
currentWordRef.current,
transcript
);

if (matchPercentage < 49) {
setAnswer(false);
} else {
setAnswer(true);
}
setIsProcessing(false);
stopAudioRecording();
};

recognitionInstance.onerror = (event) => {
setIsRecording(false);
setIsProcessing(false);
console.error("Speech recognition error:", event.error);
if (event.error === "no-speech") {
} else if (event.error === "aborted") {
recognitionInstance.start();
}
};

recognitionInstance.onend = () => {
setIsProcessing(false);
stopAudioRecording();
};

setRecognition(recognitionInstance);
}
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Enhance error handling in speech recognition initialization.

The current error handling could be improved in several ways:

  1. The alert for unsupported browsers is not user-friendly.
  2. The error callback doesn't handle all error types.
  3. There's no retry mechanism for temporary failures.

Consider these improvements:

 const initializeRecognition = () => {
   let recognitionInstance;
   if ("webkitSpeechRecognition" in window) {
     recognitionInstance = new window.webkitSpeechRecognition();
   } else if ("SpeechRecognition" in window) {
     recognitionInstance = new window.SpeechRecognition();
   } else {
-    alert("Your browser does not support Speech Recognition.");
+    setOpenMessageDialog({
+      open: true,
+      message: "Speech recognition is not supported in your browser. Please try using Chrome, Edge, or Safari.",
+      type: "error"
+    });
     return;
   }

   if (recognitionInstance) {
     recognitionInstance.continuous = true;
     recognitionInstance.interimResults = true;
     recognitionInstance.lang = "en-US";
     recognitionInstance.maxAlternatives = 1;

     recognitionInstance.onerror = (event) => {
       setIsRecording(false);
       setIsProcessing(false);
-      console.error("Speech recognition error:", event.error);
+      const errorMessages = {
+        'no-speech': 'No speech was detected. Please try again.',
+        'audio-capture': 'No microphone was found or microphone is disabled.',
+        'network': 'Network error occurred. Please check your connection.',
+        'aborted': 'Speech recognition was aborted.',
+        'not-allowed': 'Microphone permission was denied.',
+        'service-not-allowed': 'Speech recognition service is not allowed.'
+      };
+      
+      setOpenMessageDialog({
+        open: true,
+        message: errorMessages[event.error] || 'An error occurred during speech recognition.',
+        type: "error"
+      });
+
       if (event.error === "no-speech") {
+        // Handle no speech detected
       } else if (event.error === "aborted") {
-        recognitionInstance.start();
+        // Implement exponential backoff retry
+        setTimeout(() => {
+          recognitionInstance.start();
+        }, 1000);
       }
     };
   }
 };

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

Comment on lines 509 to 528
<span
onClick={() => handleWordsLogic(elem, "", true)}
className={
answer === "wrong"
? `audioSelectedWrongWord ${shake ? "shakeImage" : ""}`
: ""
}
style={{
borderRadius: "12px",
padding: answer === "correct" ? "0px" : "5px 10px 5px 10px",
border: getBorder(),
color: colors,
fontWeight: type === "word" ? 600 : 700,
fontSize: "35px",
fontFamily: "Quicksand",
cursor: "pointer",
marginLeft:
type === "word" ? 0 : answer != "correct" ? "20px" : 0,
}}
>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add key props to elements in iterables.

React requires unique key props for elements in iterables to efficiently track and update elements.

Apply these diffs to add key props:

 <span
+  key={`selected-${index}-${elem}`}
   onClick={() => handleWordsLogic(elem, "", true)}
   className={...}
   style={...}
 >
 <span
+  key={`char-${index}-${char}`}
   style={{
     color: wIndex == 0 ? "#1897DE" : "#000000",
     ...
   }}
 >

Also applies to: 630-645

🧰 Tools
🪛 Biome (1.9.4)

[error] 509-528: Missing key property for this element in iterable.

The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation.

(lint/correctness/useJsxKeyInIterable)

Comment on lines 655 to 665
cursor: "pointer",
height: "38px",
marginTop: "50px",
position: "relative",
display: "flex",
justifyContent: "center",
alignItems: "center",
height: { xs: "30px", sm: "40px", md: "50px" },
minWidth: { xs: "50px", sm: "60px", md: "70px" },
cursor: `url(${clapImage}), auto`,
fontSize: {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Remove duplicate style properties.

The Box components have duplicate cursor and height properties which are being overwritten.

Apply this diff to remove duplicate properties:

 <Box
   sx={{
-    cursor: "pointer",
-    height: "38px",
     marginTop: "50px",
     position: "relative",
     display: "flex",
     justifyContent: "center",
     alignItems: "center",
     height: { xs: "30px", sm: "40px", md: "50px" },
     minWidth: { xs: "50px", sm: "60px", md: "70px" },
     cursor: `url(${clapImage}), auto`,
     fontSize: {
       xs: "20px",
       sm: "30px",
       md: "40px",
       lg: "50px",
     },
     marginLeft: getMarginLeft(wIndex),
   }}
 >

Also applies to: 680-690

🧰 Tools
🪛 Biome (1.9.4)

[error] 655-655: This property value named cursor is later overwritten by an object member with the same name.

Overwritten with this value.

If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named cursor

(lint/suspicious/noDuplicateObjectKeys)


[error] 656-656: This property value named height is later overwritten by an object member with the same name.

Overwritten with this value.

If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named height

(lint/suspicious/noDuplicateObjectKeys)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (5)
src/components/Mechanism/WordsOrImage.jsx (1)

19-25: Optimize phonetic matching for better accuracy.

The current implementation can be improved by:

  1. Caching metaphone results
  2. Adding a minimum threshold for word length

Consider this optimization:

+const phoneticCache = new Map();
+
 const phoneticMatch = (str1, str2) => {
+  if (str1.length < 2 || str2.length < 2) return 0;
+  
+  const key = `${str1}|${str2}`;
+  if (phoneticCache.has(key)) {
+    return phoneticCache.get(key);
+  }
+
   const phonetic1 = metaphone(str1);
   const phonetic2 = metaphone(str2);
   const distance = levenshtein(phonetic1, phonetic2);
   const maxLength = Math.max(phonetic1.length, phonetic2.length);
-  return ((maxLength - distance) / maxLength) * 100;
+  const result = ((maxLength - distance) / maxLength) * 100;
+  phoneticCache.set(key, result);
+  return result;
 };
src/components/Practice/Mechanics7.jsx (1)

125-135: Simplify state management and reduce ref usage.

The current implementation uses multiple refs with manual synchronization, which could lead to maintenance issues.

Consider using a reducer:

+const wordReducer = (state, action) => {
+  switch (action.type) {
+    case 'UPDATE_WORD':
+      return {
+        ...state,
+        currentWord: action.word,
+        currentIsSelected: action.isSelected
+      };
+    case 'UPDATE_WORDS':
+      return {
+        ...state,
+        words: action.words,
+        selectedWords: action.selectedWords
+      };
+    default:
+      return state;
+  }
+};

-const currentWordRef = useRef(currentWord);
-const currentIsSelectedRef = useRef(currentIsSelected);
-const wordsRef = useRef(words);
-const selectedWordsRef = useRef(selectedWords);
+const [wordState, dispatch] = useReducer(wordReducer, {
+  currentWord: '',
+  currentIsSelected: false,
+  words: [],
+  selectedWords: []
+});

-useEffect(() => {
-  currentWordRef.current = currentWord;
-  currentIsSelectedRef.current = currentIsSelected;
-  wordsRef.current = words;
-  selectedWordsRef.current = selectedWords;
-}, [currentWord, currentIsSelected, words, selectedWords]);
src/components/Layouts.jsx/MainLayout.jsx (1)

Line range hint 165-190: Enhance audio resource cleanup.

The current implementation could lead to memory leaks if the component unmounts during audio preloading.

Improve cleanup:

 useEffect(() => {
+  let mounted = true;
   const preloadAudio = async () => {
     try {
       const urls = [LevelCompleteAudio, gameLoseAudio];
       const cache = {};

       for (const url of urls) {
         const response = await fetch(url);
         const audioBlob = await response.blob();
         const audioUrl = URL.createObjectURL(audioBlob);
+        if (!mounted) {
+          URL.revokeObjectURL(audioUrl);
+          return;
+        }
         cache[url] = audioUrl;
       }

       setAudioCache(cache);
     } catch (error) {
       console.error("Error preloading audio:", error);
     }
   };

   preloadAudio();

   return () => {
+    mounted = false;
     Object.values(audioCache).forEach((audioUrl) =>
       URL.revokeObjectURL(audioUrl)
     );
   };
 }, []);
src/views/Practice/Practice.jsx (2)

85-156: Extract level data to a separate configuration file.

The levels data structure makes the component file large and harder to maintain.

Move levels data to a separate file:

+// src/config/levelData.js
+export const levels = {
+  L1: [
+    { completeWord: "Tiger", syllable: ["Ti", "ger"], img: Tiger },
+    // ... other L1 data
+  ],
+  // ... other levels
+};

-const levels = {
-  L1: [
-    { completeWord: "Tiger", syllable: ["Ti", "ger"], img: Tiger },
-    // ... rest of the levels data
-  ],
-};
+import { levels } from '../config/levelData';

Line range hint 747-855: Optimize word highlighting logic and state management.

The word highlighting implementation is complex and could be simplified.

Consider extracting the highlighting logic:

+const HighlightedWord = memo(({ word, matchedChar, color, isFirst }) => {
+  const highlightedString = [];
+  const processedWord = splitGraphemes(word.toLowerCase()).filter(
+    char => char !== "‌" && char !== "" && char !== " "
+  );
+
+  for (let i = 0; i < processedWord.length; i++) {
+    const isMatched = matchedChar.some(char => 
+      processedWord.slice(i, i + char.length).join("").includes(char)
+    );
+    
+    highlightedString.push(
+      <Typography
+        key={`${word}-${i}`}
+        variant="h5"
+        component="h4"
+        sx={{
+          fontSize: "clamp(1.6rem, 2.5vw, 3.8rem)",
+          fontWeight: 700,
+          fontFamily: "Quicksand",
+          lineHeight: "50px",
+          ...(isMatched && { background: "#FFF0BD" }),
+          color
+        }}
+      >
+        {isFirst ? processedWord[i].toUpperCase() : processedWord[i]}
+      </Typography>
+    );
+  }
+  return highlightedString;
+});

-function highlightWords(sentence, matchedChar, color) {
+const highlightWords = (sentence, matchedChar, color) => {
   const words = sentence.split(" ");
   const type = currentContentType?.toLowerCase();
   
   if (type === "char" || type === "word") {
-    // Current complex implementation
+    return <HighlightedWord 
+      word={words[0]}
+      matchedChar={matchedChar}
+      color={color}
+      isFirst={true}
+    />;
   }
   // ... rest of the implementation
 }
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4562725 and 4453aba.

📒 Files selected for processing (4)
  • src/components/Layouts.jsx/MainLayout.jsx (10 hunks)
  • src/components/Mechanism/WordsOrImage.jsx (7 hunks)
  • src/components/Practice/Mechanics7.jsx (1 hunks)
  • src/views/Practice/Practice.jsx (8 hunks)
🧰 Additional context used
🪛 Biome (1.9.4)
src/components/Practice/Mechanics7.jsx

[error] 105-105: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 107-107: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)


[error] 538-564: Missing key property for this element in iterable.

The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation.

(lint/correctness/useJsxKeyInIterable)


[error] 667-682: Missing key property for this element in iterable.

The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation.

(lint/correctness/useJsxKeyInIterable)

🔇 Additional comments (4)
src/components/Mechanism/WordsOrImage.jsx (2)

187-202: 🛠️ Refactor suggestion

Add error handling for audio recording.

The audio recording implementation lacks proper error handling and user feedback for permission denials or device issues.

Add error handling:

 navigator.mediaDevices
   .getUserMedia({ audio: true })
   .then((stream) => {
     mediaRecorder = new MediaRecorder(stream);
     mediaRecorder.ondataavailable = (event) => {
       if (event.data.size > 0) {
         recordedChunks.push(event.data);
       }
     };
     mediaRecorder.start();
+    mediaRecorder.onerror = (event) => {
+      setOpenMessageDialog({
+        open: true,
+        message: "Error recording audio. Please try again.",
+        type: "error"
+      });
+    };
   })
   .catch((error) => {
-    console.error("Error accessing audio stream:", error);
+    let message = "Error accessing microphone.";
+    if (error.name === "NotAllowedError") {
+      message = "Microphone access was denied. Please allow microphone access and try again.";
+    } else if (error.name === "NotFoundError") {
+      message = "No microphone found. Please connect a microphone and try again.";
+    }
+    setOpenMessageDialog({
+      open: true,
+      message,
+      type: "error"
+    });
   });

Likely invalid or redundant comment.


126-185: 🛠️ Refactor suggestion

Enhance error handling in speech recognition initialization.

The error handling in the speech recognition setup could be improved:

  1. Browser support alert is not user-friendly
  2. Error handling is minimal and logs to console
  3. No retry mechanism for temporary failures

Apply this diff to improve error handling:

 if (SpeechRecognition) {
   recognitionInstance = new SpeechRecognition();
 } else {
-  alert("Your browser does not support Speech Recognition.");
+  setOpenMessageDialog({
+    open: true,
+    message: "Speech recognition is not supported in your browser. Please try using Chrome, Edge, or Safari.",
+    type: "error"
+  });
   return;
 }

 recognitionInstance.onerror = (event) => {
   setIsRecording(false);
   setIsProcessing(false);
-  console.error("Speech recognition error:", event.error);
+  const errorMessages = {
+    'no-speech': 'No speech was detected. Please try again.',
+    'audio-capture': 'No microphone was found or microphone is disabled.',
+    'network': 'Network error occurred. Please check your connection.',
+    'aborted': 'Speech recognition was aborted.',
+    'not-allowed': 'Microphone permission was denied.',
+    'service-not-allowed': 'Speech recognition service is not allowed.'
+  };
+  
+  setOpenMessageDialog({
+    open: true,
+    message: errorMessages[event.error] || 'An error occurred during speech recognition.',
+    type: "error"
+  });

   if (event.error === "no-speech") {
+    // Handle no speech detected
   } else if (event.error === "aborted") {
-    recognitionInstance.start();
+    // Implement exponential backoff retry
+    setTimeout(() => {
+      recognitionInstance.start();
+    }, 1000);
   }
 };

Likely invalid or redundant comment.

src/components/Practice/Mechanics7.jsx (1)

106-110: 🛠️ Refactor suggestion

Optimize state initialization to prevent O(n²) complexity.

The current implementation using reduce with spread operator has O(n²) time complexity.

Use a more efficient approach:

-setRecordingStates(
-  words.reduce((acc, word) => ({ ...acc, [word]: false }), {})
-);
+const initialStates = {};
+words.forEach(word => {
+  initialStates[word] = false;
+});
+setRecordingStates(initialStates);

Likely invalid or redundant comment.

🧰 Tools
🪛 Biome (1.9.4)

[error] 107-107: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)

src/components/Layouts.jsx/MainLayout.jsx (1)

191-195: 🛠️ Refactor suggestion

Add missing dependencies to useEffect for confetti.

The effect's empty dependency array means it only runs once, missing subsequent successful recordings.

Add required dependencies:

-useEffect(() => {
+useEffect(() => {
   if (isRecordingComplete && answer) {
     callConfettiSnow();
   }
-}, []);
+}, [isRecordingComplete, answer]);

Likely invalid or redundant comment.

Comment on lines 538 to 581
<span
role="button"
tabIndex={0}
onClick={() => handleWordsLogic(elem, "", true)}
className={
answer === "wrong"
? `audioSelectedWrongWord ${shake ? "shakeImage" : ""}`
: ""
}
onKeyDown={(e) => {
if (e.key === " ") {
e.preventDefault();
}
}}
style={{
borderRadius: "12px",
padding: answer === "correct" ? "0px" : "5px 10px 5px 10px",
border: getBorder(),
color: colors,
fontWeight: type === "word" ? 600 : 700,
fontSize: "35px",
fontFamily: "Quicksand",
cursor: "pointer",
marginLeft:
type === "word" ? 0 : answer != "correct" ? "20px" : 0,
}}
>
{elem}
{remainingParts.length > 0 && (
<span
style={{
color: "#1897DE1A",
fontWeight: 700,
fontSize: "35px",
fontFamily: "Quicksand",
marginLeft: "5px",
role: "button",
}}
>
{remainingParts.join(" ")}
</span>
)}
</span>
);
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 accessibility and React key usage.

The component has accessibility issues and missing key props.

Apply these improvements:

 <span
+  key={`word-${index}-${elem}`}
   role="button"
   tabIndex={0}
   onClick={() => handleWordsLogic(elem, "", true)}
+  onKeyPress={(e) => {
+    if (e.key === 'Enter' || e.key === ' ') {
+      handleWordsLogic(elem, "", true);
+    }
+  }}
   className={
     answer === "wrong"
       ? `audioSelectedWrongWord ${shake ? "shakeImage" : ""}`
       : ""
   }
+  aria-label={`Select word ${elem}`}
   onKeyDown={(e) => {
     if (e.key === " ") {
       e.preventDefault();
     }
   }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<span
role="button"
tabIndex={0}
onClick={() => handleWordsLogic(elem, "", true)}
className={
answer === "wrong"
? `audioSelectedWrongWord ${shake ? "shakeImage" : ""}`
: ""
}
onKeyDown={(e) => {
if (e.key === " ") {
e.preventDefault();
}
}}
style={{
borderRadius: "12px",
padding: answer === "correct" ? "0px" : "5px 10px 5px 10px",
border: getBorder(),
color: colors,
fontWeight: type === "word" ? 600 : 700,
fontSize: "35px",
fontFamily: "Quicksand",
cursor: "pointer",
marginLeft:
type === "word" ? 0 : answer != "correct" ? "20px" : 0,
}}
>
{elem}
{remainingParts.length > 0 && (
<span
style={{
color: "#1897DE1A",
fontWeight: 700,
fontSize: "35px",
fontFamily: "Quicksand",
marginLeft: "5px",
role: "button",
}}
>
{remainingParts.join(" ")}
</span>
)}
</span>
);
<span
key={`word-${index}-${elem}`}
role="button"
tabIndex={0}
onClick={() => handleWordsLogic(elem, "", true)}
onKeyPress={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
handleWordsLogic(elem, "", true);
}
}}
className={
answer === "wrong"
? `audioSelectedWrongWord ${shake ? "shakeImage" : ""}`
: ""
}
aria-label={`Select word ${elem}`}
onKeyDown={(e) => {
if (e.key === " ") {
e.preventDefault();
}
}}
style={{
borderRadius: "12px",
padding: answer === "correct" ? "0px" : "5px 10px 5px 10px",
border: getBorder(),
color: colors,
fontWeight: type === "word" ? 600 : 700,
fontSize: "35px",
fontFamily: "Quicksand",
cursor: "pointer",
marginLeft:
type === "word" ? 0 : answer != "correct" ? "20px" : 0,
}}
>
{elem}
{remainingParts.length > 0 && (
<span
style={{
color: "#1897DE1A",
fontWeight: 700,
fontSize: "35px",
fontFamily: "Quicksand",
marginLeft: "5px",
role: "button",
}}
>
{remainingParts.join(" ")}
</span>
)}
</span>
🧰 Tools
🪛 Biome (1.9.4)

[error] 538-564: Missing key property for this element in iterable.

The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation.

(lint/correctness/useJsxKeyInIterable)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
src/components/Mechanism/WordsOrImage.jsx (1)

126-184: ⚠️ Potential issue

Enhance error handling in speech recognition initialization.

The current implementation uses a basic alert for unsupported browsers and minimal error handling.

The previous review comment about improving error handling is still valid. Additionally:

  1. Consider handling permission denied errors
  2. Add retry logic for temporary failures
  3. Implement proper cleanup in error scenarios
🧹 Nitpick comments (6)
src/components/Mechanism/WordsOrImage.jsx (4)

19-25: Consider caching metaphone results for performance.

The phoneticMatch function recalculates metaphone values for each comparison. For frequently used words, consider caching the metaphone results.

+const phoneticCache = new Map();
+
 const phoneticMatch = (str1, str2) => {
-  const phonetic1 = metaphone(str1);
-  const phonetic2 = metaphone(str2);
+  const phonetic1 = phoneticCache.get(str1) || phoneticCache.set(str1, metaphone(str1)).get(str1);
+  const phonetic2 = phoneticCache.get(str2) || phoneticCache.set(str2, metaphone(str2)).get(str2);
   const distance = levenshtein(phonetic1, phonetic2);
   const maxLength = Math.max(phonetic1.length, phonetic2.length);
   return ((maxLength - distance) / maxLength) * 100;
 };

27-61: Optimize memory usage in levenshtein function.

The current implementation stores the entire matrix, which can be memory-intensive for long strings. Consider using only two rows since we only need the previous row's values.

 const levenshtein = (a, b) => {
-  const tmp = [];
+  let prev = Array(b.length + 1).fill(0);
+  let curr = Array(b.length + 1).fill(0);
   let i, j, alen = a.length, blen = b.length;

   if (alen === 0) return blen;
   if (blen === 0) return alen;

-  for (i = 0; i <= alen; i++) {
-    tmp[i] = [i];
+  for (i = 0; i <= blen; i++) {
+    prev[i] = i;
   }

-  for (j = 0; j <= blen; j++) {
-    tmp[0][j] = j;
-  }

   for (i = 1; i <= alen; i++) {
+    curr[0] = i;
     for (j = 1; j <= blen; j++) {
       const res = a[i - 1] === b[j - 1] ? 0 : 1;
-      tmp[i][j] = Math.min(
-        tmp[i - 1][j] + 1,
-        tmp[i][j - 1] + 1,
-        tmp[i - 1][j - 1] + res
+      curr[j] = Math.min(
+        prev[j] + 1,
+        curr[j - 1] + 1,
+        prev[j - 1] + res
       );
     }
+    [prev, curr] = [curr, prev];
   }

-  return tmp[alen][blen];
+  return prev[blen];
 };

111-117: Simplify UI state management using a state machine.

The current implementation uses multiple boolean states to control UI visibility. This can lead to invalid state combinations and make the code harder to maintain.

Consider using a single state machine:

-const [showSpeakButton, setShowSpeakButton] = useState(true);
-const [showStopButton, setShowStopButton] = useState(false);
-const [showListenRetryButtons, setShowListenRetryButtons] = useState(false);
+const [uiState, setUiState] = useState('initial'); // 'initial' | 'recording' | 'recorded'

+const getUIElements = (state) => {
+  switch (state) {
+    case 'initial':
+      return { showSpeak: true, showStop: false, showControls: false };
+    case 'recording':
+      return { showSpeak: false, showStop: true, showControls: false };
+    case 'recorded':
+      return { showSpeak: false, showStop: false, showControls: true };
+    default:
+      return { showSpeak: true, showStop: false, showControls: false };
+  }
+};

469-555: Improve component organization and styling consistency.

The current implementation has several areas for improvement:

  1. Complex conditional rendering could be simplified
  2. Styles are mixed between inline and sx prop
  3. Duplicate style objects

Consider these improvements:

  1. Extract the recording controls into a separate component
  2. Move styles to a constant object
  3. Use consistent styling approach
+const STYLES = {
+  controlsContainer: {
+    display: "flex",
+    justifyContent: "center",
+    alignItems: "center",
+    margin: "0 auto",
+  },
+  visualizerContainer: {
+    marginTop: "50px",
+    marginBottom: "50px",
+  },
+};

+const RecordingControls = ({ state, onRecord, onStop, onListen, onRetry, onNext }) => {
+  return (
+    <div style={STYLES.controlsContainer}>
+      {state === 'initial' && (
+        <Box sx={{ cursor: "pointer" }} onClick={onRecord}>
+          <SpeakButton />
+        </Box>
+      )}
+      {/* ... rest of the controls */}
+    </div>
+  );
+};
src/components/Practice/Mechanics7.jsx (2)

32-66: Consider optimizing the Levenshtein distance calculation.

The current implementation could benefit from:

  1. Memoization to cache repeated calculations
  2. More descriptive variable names (e.g., tmpmatrix, rescost)
  3. Space optimization by using only two rows instead of the full matrix
 const levenshtein = (a, b) => {
-  const tmp = [];
+  const matrix = [];
   let i,
     j,
     alen = a.length,
     blen = b.length,
-    res;
+    cost;
 
   if (alen === 0) {
     return blen;
   }
   if (blen === 0) {
     return alen;
   }
 
   for (i = 0; i <= alen; i++) {
-    tmp[i] = [i];
+    matrix[i] = [i];
   }
   for (j = 0; j <= blen; j++) {
-    tmp[0][j] = j;
+    matrix[0][j] = j;
   }
 
   for (i = 1; i <= alen; i++) {
     for (j = 1; j <= blen; j++) {
-      res = a[i - 1] === b[j - 1] ? 0 : 1;
-      tmp[i][j] = Math.min(
-        tmp[i - 1][j] + 1,
-        tmp[i][j - 1] + 1,
-        tmp[i - 1][j - 1] + res
+      cost = a[i - 1] === b[j - 1] ? 0 : 1;
+      matrix[i][j] = Math.min(
+        matrix[i - 1][j] + 1,
+        matrix[i][j - 1] + 1,
+        matrix[i - 1][j - 1] + cost
       );
     }
   }
 
-  return tmp[alen][blen];
+  return matrix[alen][blen];
 };

654-666: Remove duplicate style properties.

The Box component has duplicate style properties that are being overwritten.

 <Box
   sx={{
-    height: "38px",
     marginTop: "50px",
     position: "relative",
     display: "flex",
     justifyContent: "center",
     alignItems: "center",
     height: { xs: "30px", sm: "40px", md: "50px" },
     minWidth: { xs: "50px", sm: "60px", md: "70px" },
-    cursor: "pointer",
     cursor: `url(${clapImage}), auto`,
     fontSize: {
       xs: "20px",
       sm: "30px",
       md: "40px",
       lg: "50px",
     },
   }}
 >
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4453aba and 74fa20d.

📒 Files selected for processing (2)
  • src/components/Mechanism/WordsOrImage.jsx (7 hunks)
  • src/components/Practice/Mechanics7.jsx (1 hunks)
🧰 Additional context used
🪛 Biome (1.9.4)
src/components/Practice/Mechanics7.jsx

[error] 105-105: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 107-107: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)


[error] 474-500: Missing key property for this element in iterable.

The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation.

(lint/correctness/useJsxKeyInIterable)


[error] 603-618: Missing key property for this element in iterable.

The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation.

(lint/correctness/useJsxKeyInIterable)

🔇 Additional comments (3)
src/components/Practice/Mechanics7.jsx (3)

104-110: ⚠️ Potential issue

Optimize state initialization to avoid O(n²) complexity.

The spread operator in reduce causes quadratic time complexity. Use forEach instead.

 useEffect(() => {
   if (words && words?.length) {
-    setRecordingStates(
-      words.reduce((acc, word) => ({ ...acc, [word]: false }), {})
-    );
+    const initialStates = {};
+    words.forEach((word) => {
+      initialStates[word] = false;
+    });
+    setRecordingStates(initialStates);
   }
 }, [words]);

Likely invalid or redundant comment.

🧰 Tools
🪛 Biome (1.9.4)

[error] 105-105: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 107-107: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)


602-621: ⚠️ Potential issue

Add key props to character mapping.

Add key prop to mapped characters to help React efficiently update the DOM.

 {elem?.split("")?.map((char, index) => (
   <span
+    key={`char-${index}`}
     style={{
       color: wIndex == 0 ? "#1897DE" : "#000000",
       ...
     }}
   >

Likely invalid or redundant comment.

🧰 Tools
🪛 Biome (1.9.4)

[error] 603-618: Missing key property for this element in iterable.

The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation.

(lint/correctness/useJsxKeyInIterable)


459-517: ⚠️ Potential issue

Add key props and improve accessibility.

  1. Add key prop to mapped elements
  2. Add proper ARIA labels
  3. Add keyboard interaction support
 {selectedWordsRef.current?.map((elem, index) => (
   <span
+    key={`selected-word-${index}`}
     role="button"
     tabIndex={0}
     onClick={() => handleWordsLogic(elem, "", true)}
+    onKeyPress={(e) => {
+      if (e.key === 'Enter' || e.key === ' ') {
+        handleWordsLogic(elem, "", true);
+      }
+    }}
+    aria-label={`Selected word ${elem}`}
     className={...}
     style={...}
   >

Likely invalid or redundant comment.

🧰 Tools
🪛 Biome (1.9.4)

[error] 474-500: Missing key property for this element in iterable.

The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation.

(lint/correctness/useJsxKeyInIterable)

Comment on lines +123 to +125
let mediaRecorder;
let recordedChunks = [];

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix potential memory leaks and improve state management.

Several issues in the audio recording implementation need attention:

  1. mediaRecorder and recordedChunks should be managed with useRef
  2. Media streams aren't properly cleaned up
  3. Permission denials aren't handled properly

Apply these improvements:

-let mediaRecorder;
-let recordedChunks = [];
+const mediaRecorderRef = useRef(null);
+const recordedChunksRef = useRef([]);
+const streamRef = useRef(null);

 const startAudioRecording = () => {
   navigator.mediaDevices
     .getUserMedia({ audio: true })
     .then((stream) => {
-      mediaRecorder = new MediaRecorder(stream);
-      mediaRecorder.ondataavailable = (event) => {
+      streamRef.current = stream;
+      mediaRecorderRef.current = new MediaRecorder(stream);
+      mediaRecorderRef.current.ondataavailable = (event) => {
         if (event.data.size > 0) {
-          recordedChunks.push(event.data);
+          recordedChunksRef.current.push(event.data);
         }
       };
-      mediaRecorder.start();
+      mediaRecorderRef.current.start();
     })
     .catch((error) => {
-      console.error("Error accessing audio stream:", error);
+      setOpenMessageDialog({
+        open: true,
+        message: error.name === 'NotAllowedError' 
+          ? 'Microphone permission denied. Please allow microphone access.'
+          : 'Error accessing microphone. Please try again.',
+        type: "error"
+      });
     });
 };

+useEffect(() => {
+  return () => {
+    if (streamRef.current) {
+      streamRef.current.getTracks().forEach(track => track.stop());
+    }
+  };
+}, []);

Also applies to: 186-201

Comment on lines +167 to +177
recognitionInstance.onerror = (event) => {
setIsRecording(false);
setIsProcessing(false);
setIsMicOn(false);
console.error("Speech recognition error:", event.error);
if (event.error === "no-speech") {
console.log("No Speech!");
} else if (event.error === "aborted") {
recognitionInstance.start();
}
};
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 for speech recognition.

  1. Remove console.error in production
  2. Provide more user-friendly error messages
  3. Consider adding error boundaries
 recognitionInstance.onerror = (event) => {
   setIsRecording(false);
   setIsProcessing(false);
   setIsMicOn(false);
-  console.error("Speech recognition error:", event.error);
+  // Use a proper logging service in production
+  if (process.env.NODE_ENV !== 'production') {
+    console.error("Speech recognition error:", event.error);
+  }
   if (event.error === "no-speech") {
-    console.log("No Speech!");
+    setOpenMessageDialog({
+      open: true,
+      message: "No speech detected. Please try speaking again.",
+      type: "warning"
+    });
   } else if (event.error === "aborted") {
     recognitionInstance.start();
+  } else {
+    setOpenMessageDialog({
+      open: true,
+      message: "Speech recognition failed. Please try again.",
+      type: "error"
+    });
   }
 };

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

Comment on lines 253 to 318
const handleWordsLogic = (word, transcribedText, isSelected) => {
const matchPercentage = phoneticMatch(word, transcribedText);

if (matchPercentage < 49 && !isSelected) {
setIncorrectWords((prevState) => ({ ...prevState, [word]: true }));
} else {
setIncorrectWords((prevState) => ({ ...prevState, [word]: false }));
}

setShake(true);
setTimeout(() => {
setShake(false);
}, 3000);
// audioPlay[word]();
if (
selectedWordsRef.current?.length + 1 !== wordsAfterSplit?.length ||
isSelected
) {
let audio = new Audio(isSelected ? removeSound : addSound);
audio.play();
setEnableNext(false);
}

if (isSelected) {
// Remove the word from selectedWords and add it back to words
let selectedWordsArr = [...selectedWordsRef.current];
let index = selectedWordsArr.findIndex((elem) => elem === word);
if (index !== -1) {
selectedWordsArr.splice(index, 1);
setSelectedWords(selectedWordsArr);
selectedWordsRef.current = selectedWordsArr;

// Add the word back to words only if it doesn't already exist
if (!wordsRef.current.includes(word)) {
const updatedWords = [...wordsRef.current, word];
setWords(updatedWords);
wordsRef.current = updatedWords;
}
}
} else {
// Remove the word from words and add it to selectedWords
let wordsArr = [...wordsRef.current];
let index = wordsArr.findIndex((elem) => elem === word);
if (index !== -1) {
wordsArr.splice(index, 1);
setWords(wordsArr);
wordsRef.current = wordsArr;
}

// Add the word to selectedWords only if it doesn't already exist
if (!selectedWordsRef.current.includes(word)) {
const updatedSelectedWords = [...selectedWordsRef.current, word];
setSelectedWords(updatedSelectedWords);
selectedWordsRef.current = updatedSelectedWords;
}

if (selectedWordsRef.current.length + 1 === wordsAfterSplit?.length) {
let audio = new Audio(
[...selectedWordsRef.current, word].join(" ") === parentWords
? correctSound
: wrongSound
);
audio.play();
}
}
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Refactor handleWordsLogic for better maintainability.

The function handles multiple responsibilities and could be split into smaller, focused functions:

  1. Word matching logic
  2. State management
  3. Audio feedback

Consider extracting the audio handling logic:

+const playAudioFeedback = (isSelected, selectedWords, totalWords, parentWords) => {
+  if (selectedWords.length + 1 !== totalWords || isSelected) {
+    const audio = new Audio(isSelected ? removeSound : addSound);
+    audio.play();
+    return;
+  }
+
+  if (selectedWords.length + 1 === totalWords) {
+    const audio = new Audio(
+      [...selectedWords].join(" ") === parentWords
+        ? correctSound
+        : wrongSound
+    );
+    audio.play();
+  }
+};

 const handleWordsLogic = (word, transcribedText, isSelected) => {
   const matchPercentage = phoneticMatch(word, transcribedText);

   if (matchPercentage < 49 && !isSelected) {
     setIncorrectWords((prevState) => ({ ...prevState, [word]: true }));
   } else {
     setIncorrectWords((prevState) => ({ ...prevState, [word]: false }));
   }

   setShake(true);
   setTimeout(() => {
     setShake(false);
   }, 3000);

-  if (
-    selectedWordsRef.current?.length + 1 !== wordsAfterSplit?.length ||
-    isSelected
-  ) {
-    let audio = new Audio(isSelected ? removeSound : addSound);
-    audio.play();
-    setEnableNext(false);
-  }
+  playAudioFeedback(
+    isSelected,
+    selectedWordsRef.current,
+    wordsAfterSplit?.length,
+    parentWords
+  );
+  setEnableNext(false);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleWordsLogic = (word, transcribedText, isSelected) => {
const matchPercentage = phoneticMatch(word, transcribedText);
if (matchPercentage < 49 && !isSelected) {
setIncorrectWords((prevState) => ({ ...prevState, [word]: true }));
} else {
setIncorrectWords((prevState) => ({ ...prevState, [word]: false }));
}
setShake(true);
setTimeout(() => {
setShake(false);
}, 3000);
// audioPlay[word]();
if (
selectedWordsRef.current?.length + 1 !== wordsAfterSplit?.length ||
isSelected
) {
let audio = new Audio(isSelected ? removeSound : addSound);
audio.play();
setEnableNext(false);
}
if (isSelected) {
// Remove the word from selectedWords and add it back to words
let selectedWordsArr = [...selectedWordsRef.current];
let index = selectedWordsArr.findIndex((elem) => elem === word);
if (index !== -1) {
selectedWordsArr.splice(index, 1);
setSelectedWords(selectedWordsArr);
selectedWordsRef.current = selectedWordsArr;
// Add the word back to words only if it doesn't already exist
if (!wordsRef.current.includes(word)) {
const updatedWords = [...wordsRef.current, word];
setWords(updatedWords);
wordsRef.current = updatedWords;
}
}
} else {
// Remove the word from words and add it to selectedWords
let wordsArr = [...wordsRef.current];
let index = wordsArr.findIndex((elem) => elem === word);
if (index !== -1) {
wordsArr.splice(index, 1);
setWords(wordsArr);
wordsRef.current = wordsArr;
}
// Add the word to selectedWords only if it doesn't already exist
if (!selectedWordsRef.current.includes(word)) {
const updatedSelectedWords = [...selectedWordsRef.current, word];
setSelectedWords(updatedSelectedWords);
selectedWordsRef.current = updatedSelectedWords;
}
if (selectedWordsRef.current.length + 1 === wordsAfterSplit?.length) {
let audio = new Audio(
[...selectedWordsRef.current, word].join(" ") === parentWords
? correctSound
: wrongSound
);
audio.play();
}
}
};
const playAudioFeedback = (isSelected, selectedWords, totalWords, parentWords) => {
if (selectedWords.length + 1 !== totalWords || isSelected) {
const audio = new Audio(isSelected ? removeSound : addSound);
audio.play();
return;
}
if (selectedWords.length + 1 === totalWords) {
const audio = new Audio(
[...selectedWords].join(" ") === parentWords
? correctSound
: wrongSound
);
audio.play();
}
};
const handleWordsLogic = (word, transcribedText, isSelected) => {
const matchPercentage = phoneticMatch(word, transcribedText);
if (matchPercentage < 49 && !isSelected) {
setIncorrectWords((prevState) => ({ ...prevState, [word]: true }));
} else {
setIncorrectWords((prevState) => ({ ...prevState, [word]: false }));
}
setShake(true);
setTimeout(() => {
setShake(false);
}, 3000);
playAudioFeedback(
isSelected,
selectedWordsRef.current,
wordsAfterSplit?.length,
parentWords
);
setEnableNext(false);
if (isSelected) {
// Remove the word from selectedWords and add it back to words
let selectedWordsArr = [...selectedWordsRef.current];
let index = selectedWordsArr.findIndex((elem) => elem === word);
if (index !== -1) {
selectedWordsArr.splice(index, 1);
setSelectedWords(selectedWordsArr);
selectedWordsRef.current = selectedWordsArr;
// Add the word back to words only if it doesn't already exist
if (!wordsRef.current.includes(word)) {
const updatedWords = [...wordsRef.current, word];
setWords(updatedWords);
wordsRef.current = updatedWords;
}
}
} else {
// Remove the word from words and add it to selectedWords
let wordsArr = [...wordsRef.current];
let index = wordsArr.findIndex((elem) => elem === word);
if (index !== -1) {
wordsArr.splice(index, 1);
setWords(wordsArr);
wordsRef.current = wordsArr;
}
// Add the word to selectedWords only if it doesn't already exist
if (!selectedWordsRef.current.includes(word)) {
const updatedSelectedWords = [...selectedWordsRef.current, word];
setSelectedWords(updatedSelectedWords);
selectedWordsRef.current = updatedSelectedWords;
}
if (selectedWordsRef.current.length + 1 === wordsAfterSplit?.length) {
let audio = new Audio(
[...selectedWordsRef.current, word].join(" ") === parentWords
? correctSound
: wrongSound
);
audio.play();
}
}
};

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🔭 Outside diff range comments (1)
src/components/Practice/Mechanics3.jsx (1)

Line range hint 18-18: Resolve naming inconsistency between file and component.

The file is named Mechanics3.jsx but exports a component named Mechanics2. This mismatch could lead to confusion. Consider renaming either the file or the component to maintain consistency.

♻️ Duplicate comments (5)
src/components/Mechanism/WordsOrImage.jsx (3)

77-79: ⚠️ Potential issue

Fix potential memory leaks in audio recording.

The global variables mediaRecorder and recordedChunks should be managed with useRef to prevent memory leaks and ensure proper cleanup.


80-136: ⚠️ Potential issue

Enhance error handling in speech recognition initialization.

The current error handling needs improvement:

  1. Replace browser alert with user-friendly dialog
  2. Add comprehensive error handling
  3. Implement retry mechanism

420-463: 🛠️ Refactor suggestion

Improve accessibility for interactive elements.

Add proper ARIA attributes and keyboard navigation support to interactive elements.

src/components/Practice/Mechanics7.jsx (2)

51-57: ⚠️ Potential issue

Optimize recordingStates initialization.

Using spread operator in reduce leads to O(n²) complexity. Use a more efficient approach.

🧰 Tools
🪛 Biome (1.9.4)

[error] 52-52: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 54-54: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)


405-464: ⚠️ Potential issue

Add key props to elements in iterables.

React requires unique key props for elements in iterables for efficient rendering.

🧰 Tools
🪛 Biome (1.9.4)

[error] 420-446: Missing key property for this element in iterable.

The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation.

(lint/correctness/useJsxKeyInIterable)

🧹 Nitpick comments (8)
src/components/Practice/Mechanics4.jsx (2)

51-52: Add type definitions for the new props.

Consider adding PropTypes or TypeScript definitions for isNextButtonCalled and setIsNextButtonCalled to improve type safety and documentation.

+ Mechanics4.propTypes = {
+   isNextButtonCalled: PropTypes.bool.isRequired,
+   setIsNextButtonCalled: PropTypes.func.isRequired,
+   // ... other prop types
+ };

Line range hint 1-364: Consider breaking down the component for better maintainability.

The component handles multiple responsibilities (audio, word selection, UI). Consider:

  1. Extracting word handling logic into a custom hook (e.g., useWordSelection)
  2. Creating separate components for the word selection UI and audio controls
src/components/Practice/Mechanics3.jsx (1)

59-60: Add type definitions for the new props.

Similar to Mechanics4, add PropTypes or TypeScript definitions for the new props.

+ Mechanics2.propTypes = {
+   isNextButtonCalled: PropTypes.bool.isRequired,
+   setIsNextButtonCalled: PropTypes.func.isRequired,
+   // ... other prop types
+ };
src/App.js (2)

58-61: Remove commented code

Please remove the commented-out code as it's no longer needed and can cause confusion. The new direct approach of passing visitorId to initService is cleaner.

-      // //if (!localStorage.getItem("did")) {
-      //   localStorage.setItem("did", visitorId);
-      // //}
       initService(visitorId);

69-71: Use optional chaining for cleaner null checks

Replace multiple && operators with optional chaining for better readability.

-      window.telemetry &&
-        window.telemetry.syncEvents &&
-        window.telemetry.syncEvents();
+      window.telemetry?.syncEvents?.();
🧰 Tools
🪛 Biome (1.9.4)

[error] 69-71: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

src/utils/phoneticUtils.js (1)

11-45: Optimize memory usage in Levenshtein implementation.

The current implementation uses O(m*n) space. This can be optimized to O(min(m,n)) by using two 1D arrays.

 export const levenshtein = (a, b) => {
-  const tmp = [];
+  if (a.length < b.length) {
+    [a, b] = [b, a];
+  }
+  let prev = Array(b.length + 1).fill(0);
+  let curr = Array(b.length + 1).fill(0);
   let i,
     j,
     alen = a.length,
     blen = b.length,
     res;

   if (alen === 0) return blen;
   if (blen === 0) return alen;

-  for (i = 0; i <= alen; i++) {
-    tmp[i] = [i];
+  for (j = 0; j <= blen; j++) {
+    prev[j] = j;
   }
-  for (j = 0; j <= blen; j++) {
-    tmp[0][j] = j;
-  }

   for (i = 1; i <= alen; i++) {
+    curr[0] = i;
     for (j = 1; j <= blen; j++) {
       res = a[i - 1] === b[j - 1] ? 0 : 1;
-      tmp[i][j] = Math.min(
-        tmp[i - 1][j] + 1,
-        tmp[i][j - 1] + 1,
-        tmp[i - 1][j - 1] + res
+      curr[j] = Math.min(
+        prev[j] + 1,
+        curr[j - 1] + 1,
+        prev[j - 1] + res
       );
     }
+    [prev, curr] = [curr, prev];
   }

-  return tmp[alen][blen];
+  return prev[blen];
 };
src/views/Practice/Practice.jsx (2)

85-156: Extract level data to a separate configuration file.

The levels object is large and should be moved to a separate configuration file for better maintainability.

-const levels = {
-  L1: [...],
-  L2: [...],
-  ...
-};
+import { levels } from '../../config/levels';

Line range hint 747-847: Optimize highlightWords function performance.

The function performs multiple string operations and array manipulations. Consider memoizing results for repeated inputs.

+const memoizedHighlightWords = React.useMemo(
+  () => highlightWords(sentence, matchedChar, color),
+  [sentence, matchedChar, color]
+);
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 74fa20d and 6bd5bb9.

📒 Files selected for processing (8)
  • src/App.js (2 hunks)
  • src/components/Mechanism/WordsOrImage.jsx (7 hunks)
  • src/components/Practice/Mechanics3.jsx (2 hunks)
  • src/components/Practice/Mechanics4.jsx (2 hunks)
  • src/components/Practice/Mechanics7.jsx (1 hunks)
  • src/utils/VoiceAnalyser.js (4 hunks)
  • src/utils/phoneticUtils.js (1 hunks)
  • src/views/Practice/Practice.jsx (11 hunks)
🧰 Additional context used
🪛 Biome (1.9.4)
src/App.js

[error] 69-71: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

src/components/Practice/Mechanics7.jsx

[error] 52-52: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 54-54: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)


[error] 420-446: Missing key property for this element in iterable.

The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation.

(lint/correctness/useJsxKeyInIterable)


[error] 549-564: Missing key property for this element in iterable.

The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation.

(lint/correctness/useJsxKeyInIterable)

🔇 Additional comments (6)
src/utils/VoiceAnalyser.js (2)

106-107: Remove debug console logs.

Remove console.log statements that were added for debugging purposes to keep the production code clean.

Also applies to: 114-114


773-773: LGTM! PropTypes addition.

The addition of onAudioProcessed PropType is correct and follows React best practices for type checking.

src/components/Practice/Mechanics3.jsx (1)

Line range hint 18-18: Address TODO comments about code organization.

Multiple TODOs indicate needed improvements:

  1. Update component name to match file name
  2. Move constants declaration to the top
  3. Consolidate all constant declarations

Would you like me to help implement these improvements?

Also applies to: 108-108, 127-127

src/App.js (2)

17-22: Well-structured device identification logic

Good implementation of the device identification logic with proper fallback mechanism. The changes maintain backward compatibility while introducing the new visitorId parameter.


11-11: Verify compatibility with the new telemetry SDK

The switch from @project-sunbird/telemetry-sdk to @tekdi/all-telemetry-sdk is a significant change. Please ensure all telemetry methods used in the application are compatible with the new SDK.

✅ Verification successful

Telemetry SDK change appears compatible

The new SDK (@tekdi/all-telemetry-sdk) is compatible with the current implementation as:

  • Only basic telemetry methods (init, sync) are used across the codebase
  • The initialization pattern remains consistent
  • No SDK-specific features are being utilized that could cause compatibility issues
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for all telemetry method usages to verify compatibility
rg -l "telemetry\." | xargs rg "telemetry\.[a-zA-Z]+"

Length of output: 374


Script:

#!/bin/bash
# Search for telemetry initialization
rg -A 5 "initService|initTelemetry|telemetry.*init" 

# Search for SDK configuration
ast-grep --pattern 'const $_ = {
  $$$
  telemetry: {
    $$$
  }
  $$$
}'

Length of output: 1459

src/components/Practice/Mechanics7.jsx (1)

496-652: Remove duplicate style properties.

The Box components have duplicate cursor and height properties.

🧰 Tools
🪛 Biome (1.9.4)

[error] 549-564: Missing key property for this element in iterable.

The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation.

(lint/correctness/useJsxKeyInIterable)

Comment on lines +268 to +270
if (props.pageName === "m7") {
props.onAudioProcessed(base64Data);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Refactor duplicated m7 page handling.

The same condition and action for handling the "m7" page is duplicated in multiple places. Extract this into a helper function to improve maintainability and reduce code duplication.

+ const handleM7AudioProcessing = (base64Data = "") => {
+   if (props.pageName === "m7") {
+     props.onAudioProcessed(base64Data);
+   }
+ };

- if (props.pageName === "m7") {
-   props.onAudioProcessed(base64Data);
- }

  if (props.setIsNextButtonCalled) {
    props.setIsNextButtonCalled(true);
-   if (props.pageName === "m7") {
-     props.onAudioProcessed("");
-   }
+   handleM7AudioProcessing();
  } else {
    props.handleNext();
-   if (props.pageName === "m7") {
-     props.onAudioProcessed("");
-   }
+   handleM7AudioProcessing();
  }

Also applies to: 751-753, 756-758

Comment on lines +3 to +9
export const phoneticMatch = (str1, str2) => {
const phonetic1 = metaphone(str1);
const phonetic2 = metaphone(str2);
const distance = levenshtein(phonetic1, phonetic2);
const maxLength = Math.max(phonetic1.length, phonetic2.length);
return ((maxLength - distance) / maxLength) * 100;
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add input validation and error handling.

The phoneticMatch function needs defensive programming:

  1. Validate input strings are non-null and non-empty
  2. Handle potential metaphone encoding errors
  3. Prevent division by zero when both strings are empty
 export const phoneticMatch = (str1, str2) => {
+  if (!str1 || !str2) {
+    return 0;
+  }
   const phonetic1 = metaphone(str1);
   const phonetic2 = metaphone(str2);
+  if (!phonetic1 || !phonetic2) {
+    return 0;
+  }
   const distance = levenshtein(phonetic1, phonetic2);
   const maxLength = Math.max(phonetic1.length, phonetic2.length);
+  if (maxLength === 0) {
+    return 0;
+  }
   return ((maxLength - distance) / maxLength) * 100;
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const phoneticMatch = (str1, str2) => {
const phonetic1 = metaphone(str1);
const phonetic2 = metaphone(str2);
const distance = levenshtein(phonetic1, phonetic2);
const maxLength = Math.max(phonetic1.length, phonetic2.length);
return ((maxLength - distance) / maxLength) * 100;
};
export const phoneticMatch = (str1, str2) => {
if (!str1 || !str2) {
return 0;
}
const phonetic1 = metaphone(str1);
const phonetic2 = metaphone(str2);
if (!phonetic1 || !phonetic2) {
return 0;
}
const distance = levenshtein(phonetic1, phonetic2);
const maxLength = Math.max(phonetic1.length, phonetic2.length);
if (maxLength === 0) {
return 0;
}
return ((maxLength - distance) / maxLength) * 100;
};

Comment on lines +1037 to 1078
} else if (mechanism.name === "formAWord2") {
return (
<Mechanics7
page={page}
setPage={setPage}
{...{
level: level,
header:
questions[currentQuestion]?.contentType === "image"
? `Guess the below image`
: `Speak the below word`,
//
currentImg: currentImage,
parentWords: parentWords,
contentType: currentContentType,
contentId: questions[currentQuestion]?.contentId,
setVoiceText,
setRecordedAudio,
setVoiceAnimate,
storyLine,
handleNext,
type: "word",
// image: elephant,
enableNext,
showTimer: false,
points,
steps: questions?.length,
currentStep: currentQuestion + 1,
progressData,
showProgress: true,
background:
isShowCase &&
"linear-gradient(281.02deg, #AE92FF 31.45%, #555ADA 100%)",
playTeacherAudio,
callUpdateLearner: isShowCase,
disableScreen,
isShowCase,
handleBack: !isShowCase && handleBack,
setEnableNext,
loading,
setOpenMessageDialog,
}}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error boundary for Mechanics7 component.

Wrap the new Mechanics7 component with an error boundary to gracefully handle potential rendering errors.

+import ErrorBoundary from '../../components/ErrorBoundary';

 else if (mechanism.name === "formAWord2") {
   return (
+    <ErrorBoundary fallback={<div>Something went wrong</div>}>
       <Mechanics7
         {...props}
       />
+    </ErrorBoundary>
   );
 }

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🔭 Outside diff range comments (3)
src/utils/AudioCompare.js (3)

Line range hint 236-241: Add missing PropTypes validation.

The pageName prop is used throughout the component but is not defined in PropTypes. This makes the component's API unclear and could lead to runtime errors.

 AudioRecorder.propTypes = {
   enableAfterLoad: PropTypes.bool,
   showOnlyListen: PropTypes.bool,
   recordedAudio: PropTypes.string,
   originalText: PropTypes.string,
+  pageName: PropTypes.string,
 };

Line range hint 33-49: Improve error handling for audio recording failures.

Currently, recording failures are only logged to the console without providing any feedback to the user. This could lead to a confusing user experience.

Consider adding error state handling and user feedback:

  const startRecording = async () => {
    try {
      const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
      // ... existing code ...
    } catch (err) {
      console.error("Failed to start recording:", err);
+     setStatus("error");
+     if (props.onError) {
+       props.onError("Failed to start recording. Please check your microphone permissions.");
+     }
    }
  };

Don't forget to add the new prop to PropTypes:

 AudioRecorder.propTypes = {
   // ... existing props ...
+  onError: PropTypes.func,
 };

Line range hint 65-82: Fix cleanup logic to prevent potential memory leaks.

The current implementation could lead to memory leaks if the component unmounts while the timeout is pending.

  const stopRecording = () => {
    setShowLoader(true);
+   const timeoutRef = { current: null };
    const timeoutId = setTimeout(() => {
      setShowLoader(false);
      setStatus("inactive");
      if (recorderRef.current) {
        recorderRef.current.stopRecording(() => {
          // ... existing code ...
        });
      }
    }, 500);
+   timeoutRef.current = timeoutId;
 
-   return () => clearTimeout(timeoutId);
+   return () => {
+     if (timeoutRef.current) {
+       clearTimeout(timeoutRef.current);
+     }
+   };
  };
🧹 Nitpick comments (7)
src/utils/AudioCompare.js (1)

18-18: Remove debug console.log statement.

Debug logging statements should not be committed to production code.

-  console.log("pageName", props.pageName);
src/utils/constants.js (2)

1455-1469: Add JSDoc documentation for the new component.

While the implementation is good, please add documentation explaining:

  • The component's purpose
  • The significance of the default dimensions (411x200)
  • Usage examples
+/**
+ * Renders a decorative red circle SVG, typically used for highlighting words or sections.
+ * @param {Object} props - Component props
+ * @param {number} [props.width=411] - SVG width
+ * @param {number} [props.height=200] - SVG height
+ * @returns {JSX.Element} SVG element
+ */
export const WordRedCircle = (props) => (

3217-3224: Add type definitions for the mechanism object.

Consider adding TypeScript or JSDoc type definitions to ensure consistent mechanism object structure across the codebase.

+/**
+ * @typedef {Object} Mechanism
+ * @property {string} id - The mechanism identifier (e.g., "mechanic_7")
+ * @property {string} name - The mechanism name (e.g., "formAWord2")
+ */

+/**
+ * @typedef {Object} LevelContent
+ * @property {string} title - The practice title
+ * @property {string} criteria - The evaluation criteria
+ * @property {string} template - The template type
+ * @property {string} tags - Associated tags
+ * @property {Mechanism} [mechanism] - Optional mechanism configuration
+ */

 export const levelGetContent = {
src/components/Practice/Mechanics7.jsx (2)

15-45: Consider grouping related props into configuration objects.

The component accepts many props which could be organized into logical groups for better maintainability. For example:

  • UI configuration (background, type, showTimer, etc.)
  • Audio handling (setVoiceText, setRecordedAudio, etc.)
  • Navigation (handleNext, handleBack)
  • Progress tracking (points, steps, currentStep, etc.)
 const Mechanics7 = ({
-  setVoiceText,
-  setRecordedAudio,
-  setVoiceAnimate,
-  storyLine,
-  type,
-  handleNext,
-  background,
-  parentWords = "",
-  enableNext,
-  showTimer,
-  points,
-  steps,
-  currentStep,
+  uiConfig = {
+    type,
+    background,
+    showTimer,
+    enableNext,
+  },
+  audioConfig = {
+    setVoiceText,
+    setRecordedAudio,
+    setVoiceAnimate,
+    storyLine,
+  },
+  navigationConfig = {
+    handleNext,
+    handleBack,
+  },
+  progressConfig = {
+    points,
+    steps,
+    currentStep,
+  },
   // ... remaining props
 }) => {

221-285: Refactor handleWordsLogic for better maintainability.

The function handles multiple responsibilities and could be split into smaller, focused functions:

  1. Word matching logic
  2. State management
  3. Audio feedback
+const playAudioFeedback = (isSelected, selectedWords, totalWords, parentWords) => {
+  if (selectedWords.length + 1 !== totalWords || isSelected) {
+    const audio = new Audio(isSelected ? removeSound : addSound);
+    audio.play();
+    return;
+  }
+
+  if (selectedWords.length + 1 === totalWords) {
+    const audio = new Audio(
+      [...selectedWords].join(" ") === parentWords
+        ? correctSound
+        : wrongSound
+    );
+    audio.play();
+  }
+};

+const updateSelectedWords = (word, isSelected, selectedWords, words) => {
+  if (isSelected) {
+    return selectedWords.filter(elem => elem !== word);
+  }
+  return [...selectedWords, word];
+};

 const handleWordsLogic = (word, transcribedText, isSelected) => {
   const matchPercentage = phoneticMatch(word, transcribedText);

   if (matchPercentage < 49 && !isSelected) {
     setIncorrectWords((prevState) => ({ ...prevState, [word]: true }));
   } else {
     setIncorrectWords((prevState) => ({ ...prevState, [word]: false }));
   }

   setShake(true);
   setTimeout(() => {
     setShake(false);
   }, 3000);

-  if (
-    selectedWordsRef.current?.length + 1 !== wordsAfterSplit?.length ||
-    isSelected
-  ) {
-    let audio = new Audio(isSelected ? removeSound : addSound);
-    audio.play();
-    setEnableNext(false);
-  }
+  playAudioFeedback(
+    isSelected,
+    selectedWordsRef.current,
+    wordsAfterSplit?.length,
+    parentWords
+  );
+  setEnableNext(false);

   if (isSelected) {
-    let selectedWordsArr = [...selectedWordsRef.current];
-    let index = selectedWordsArr.findIndex((elem) => elem === word);
-    if (index !== -1) {
-      selectedWordsArr.splice(index, 1);
-      setSelectedWords(selectedWordsArr);
-      selectedWordsRef.current = selectedWordsArr;
+    const updatedSelectedWords = updateSelectedWords(word, true, selectedWordsRef.current, wordsRef.current);
+    setSelectedWords(updatedSelectedWords);
+    selectedWordsRef.current = updatedSelectedWords;

-      if (!wordsRef.current.includes(word)) {
-        const updatedWords = [...wordsRef.current, word];
-        setWords(updatedWords);
-        wordsRef.current = updatedWords;
-      }
+    if (!wordsRef.current.includes(word)) {
+      const updatedWords = [...wordsRef.current, word];
+      setWords(updatedWords);
+      wordsRef.current = updatedWords;
     }
   } else {
-    let wordsArr = [...wordsRef.current];
-    let index = wordsArr.findIndex((elem) => elem === word);
-    if (index !== -1) {
-      wordsArr.splice(index, 1);
-      setWords(wordsArr);
-      wordsRef.current = wordsArr;
-    }
+    const updatedWords = wordsRef.current.filter(elem => elem !== word);
+    setWords(updatedWords);
+    wordsRef.current = updatedWords;

-    if (!selectedWordsRef.current.includes(word)) {
-      const updatedSelectedWords = [...selectedWordsRef.current, word];
-      setSelectedWords(updatedSelectedWords);
-      selectedWordsRef.current = updatedSelectedWords;
-    }
+    const updatedSelectedWords = updateSelectedWords(word, false, selectedWordsRef.current, wordsRef.current);
+    setSelectedWords(updatedSelectedWords);
+    selectedWordsRef.current = updatedSelectedWords;
   }
 };
src/views/Practice/Practice.jsx (2)

24-102: Organize imports for better maintainability.

Group related imports together:

  1. React and third-party libraries
  2. Components
  3. Image assets
  4. Audio assets
+// React and third-party libraries
 import React, { useEffect, useState } from "react";
 import { Box } from "@mui/material";

+// Components
 import Mechanics2 from "../../components/Practice/Mechanics2";
 import Mechanics3 from "../../components/Practice/Mechanics3";
 import Mechanics4 from "../../components/Practice/Mechanics4";
 import Mechanics5 from "../../components/Practice/Mechanics5";
 import Mechanics6 from "../../components/Practice/Mechanics6";
 import Mechanics7 from "../../components/Practice/Mechanics7";

+// Image assets
 import Tiger from "../../assets/tiger.png";
 import Pencil from "../../assets/pencil.svg";
 // ... other image imports

+// Audio assets
 import Ti from "../../assets/Ti.wav";
 import Ger from "../../assets/ger.wav";
 // ... other audio imports
🧰 Tools
🪛 Biome (1.9.4)

[error] 70-70: Do not shadow the global "Set" property.

Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.

(lint/suspicious/noShadowRestrictedNames)


143-394: Move levels data to a separate configuration file.

The levels data is large and could be moved to a separate configuration file for better maintainability and reusability.

+// src/config/levels.js
+export const levels = {
+  L1: [
+    {
+      completeWord: "Tiger",
+      syllable: ["Ti", "ger"],
+      img: Tiger,
+      syllablesAudio: [
+        { name: "Ti", audio: Ti },
+        { name: "Ger", audio: Ger },
+      ],
+      completeAudio: TigerS,
+    },
+    // ... other level data
+  ],
+};

-const levels = {
-  L1: [
-    // ... level data
-  ],
-};
+import { levels } from '../../config/levels';
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6bd5bb9 and 7e19bae.

⛔ Files ignored due to path filters (58)
  • src/assets/Ap.wav is excluded by !**/*.wav
  • src/assets/Apple.wav is excluded by !**/*.wav
  • src/assets/Ba.wav is excluded by !**/*.wav
  • src/assets/Banana.wav is excluded by !**/*.wav
  • src/assets/Bas.wav is excluded by !**/*.wav
  • src/assets/Basket.wav is excluded by !**/*.wav
  • src/assets/But.wav is excluded by !**/*.wav
  • src/assets/Button.wav is excluded by !**/*.wav
  • src/assets/Can.wav is excluded by !**/*.wav
  • src/assets/Candle.wav is excluded by !**/*.wav
  • src/assets/Jack.wav is excluded by !**/*.wav
  • src/assets/Jacket.wav is excluded by !**/*.wav
  • src/assets/Kit.wav is excluded by !**/*.wav
  • src/assets/Kitten.wav is excluded by !**/*.wav
  • src/assets/Le.wav is excluded by !**/*.wav
  • src/assets/Lemon.wav is excluded by !**/*.wav
  • src/assets/Na.wav is excluded by !**/*.wav
  • src/assets/Or.wav is excluded by !**/*.wav
  • src/assets/Orange.wav is excluded by !**/*.wav
  • src/assets/Pen.wav is excluded by !**/*.wav
  • src/assets/Pencil.wav is excluded by !**/*.wav
  • src/assets/Pic.wav is excluded by !**/*.wav
  • src/assets/Picnic.wav is excluded by !**/*.wav
  • src/assets/Pock.wav is excluded by !**/*.wav
  • src/assets/Pocket.wav is excluded by !**/*.wav
  • src/assets/Rab.wav is excluded by !**/*.wav
  • src/assets/Rabbit.wav is excluded by !**/*.wav
  • src/assets/Rock.wav is excluded by !**/*.wav
  • src/assets/Rocket.wav is excluded by !**/*.wav
  • src/assets/Sun.wav is excluded by !**/*.wav
  • src/assets/Sunset.wav is excluded by !**/*.wav
  • src/assets/Ta.wav is excluded by !**/*.wav
  • src/assets/Tab.wav is excluded by !**/*.wav
  • src/assets/Table.wav is excluded by !**/*.wav
  • src/assets/Tablet.wav is excluded by !**/*.wav
  • src/assets/Ti.wav is excluded by !**/*.wav
  • src/assets/Tiger.wav is excluded by !**/*.wav
  • src/assets/To.wav is excluded by !**/*.wav
  • src/assets/Tomato.wav is excluded by !**/*.wav
  • src/assets/Tun.wav is excluded by !**/*.wav
  • src/assets/Tunnel.wav is excluded by !**/*.wav
  • src/assets/ange.wav is excluded by !**/*.wav
  • src/assets/bit.wav is excluded by !**/*.wav
  • src/assets/ble.wav is excluded by !**/*.wav
  • src/assets/cil.wav is excluded by !**/*.wav
  • src/assets/dle.wav is excluded by !**/*.wav
  • src/assets/et.wav is excluded by !**/*.wav
  • src/assets/ger.wav is excluded by !**/*.wav
  • src/assets/ket.wav is excluded by !**/*.wav
  • src/assets/let.wav is excluded by !**/*.wav
  • src/assets/ma.wav is excluded by !**/*.wav
  • src/assets/mon.wav is excluded by !**/*.wav
  • src/assets/nel.wav is excluded by !**/*.wav
  • src/assets/nic.wav is excluded by !**/*.wav
  • src/assets/ple.wav is excluded by !**/*.wav
  • src/assets/set.wav is excluded by !**/*.wav
  • src/assets/ten.wav is excluded by !**/*.wav
  • src/assets/ton.wav is excluded by !**/*.wav
📒 Files selected for processing (5)
  • src/components/Practice/Mechanics7.jsx (1 hunks)
  • src/utils/AudioCompare.js (6 hunks)
  • src/utils/VoiceAnalyser.js (5 hunks)
  • src/utils/constants.js (8 hunks)
  • src/views/Practice/Practice.jsx (11 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/utils/VoiceAnalyser.js
🧰 Additional context used
🪛 Biome (1.9.4)
src/components/Practice/Mechanics7.jsx

[error] 53-53: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 55-55: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)


[error] 443-469: Missing key property for this element in iterable.

The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation.

(lint/correctness/useJsxKeyInIterable)


[error] 579-589: Missing key property for this element in iterable.

The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation.

(lint/correctness/useJsxKeyInIterable)

src/views/Practice/Practice.jsx

[error] 70-70: Do not shadow the global "Set" property.

Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.

(lint/suspicious/noShadowRestrictedNames)

🔇 Additional comments (6)
src/utils/constants.js (3)

77-78: Well-structured prop additions for button components!

The addition of width and height props with consistent default values (70) across all button components improves reusability while maintaining backward compatibility.

Also applies to: 156-157, 209-210, 253-254, 2000-2001


3178-3187: Document the title pattern convention.

The title property follows specific patterns (L1-L4, P1-P4, S1-S2). Please add documentation explaining:

  • The significance of L (Level?) vs P (Practice?) prefixes
  • How these titles relate to the learning progression
  • Why some practices use L prefix while others use P prefix

3217-3242: Verify the selective application of mechanism_7.

The mechanism property (mechanic_7/formAWord2) is only added to practices P2, P5, and P6. Please verify:

  • If this selective application is intentional
  • Why these specific practices were chosen
  • If other practices should also include this mechanism
✅ Verification successful

Selective application of mechanism_7 is verified and intentional

The implementation shows a deliberate pattern where:

  • The mechanism is properly handled by a dedicated Mechanics7 component
  • Only specific practices are configured to use this mechanism
  • Other practices intentionally omit this property
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for other uses of mechanic_7 or formAWord2 in the codebase
echo "Searching for related mechanism references..."
rg -l "mechanic_7|formAWord2" --type js

Length of output: 197


Script:

#!/bin/bash
# Check implementation in Practice.jsx
echo "Checking Practice.jsx implementation:"
rg "mechanic_7|formAWord2" src/views/Practice/Practice.jsx -B 2 -A 2

# Check for mechanism-related patterns in constants.js
echo -e "\nChecking constants.js for mechanism patterns:"
rg "mechanism.*:.*{.*id.*:.*\"mechanic_7\"" src/utils/constants.js -B 2 -A 2

Length of output: 578


Script:

#!/bin/bash
# Check for Mechanics7 component usage
echo "Checking Mechanics7 component usage:"
rg "Mechanics7" src/views/Practice/Practice.jsx -B 3 -A 3

# Check mechanism patterns in constants.js with simpler pattern
echo -e "\nChecking mechanism patterns in constants.js:"
rg "mechanism.*(mechanic_7|formAWord2)" src/utils/constants.js -B 2 -A 2

# Check for Mechanics7 component definition
echo -e "\nLooking for Mechanics7 component definition:"
fd Mechanics7

Length of output: 1707

src/components/Practice/Mechanics7.jsx (2)

87-135: 🛠️ Refactor suggestion

Improve error handling in speech recognition.

  1. Replace console.error with proper error handling
  2. Provide user-friendly error messages
  3. Add error boundaries for graceful degradation
 if (SpeechRecognition) {
   recognitionInstance = new SpeechRecognition();
 } else {
-  alert("Your browser does not support Speech Recognition.");
+  setOpenMessageDialog({
+    open: true,
+    message: "Your browser does not support Speech Recognition. Please try using a supported browser like Chrome.",
+    type: "error"
+  });
   return;
 }

 recognitionInstance.onerror = (event) => {
   setIsRecording(false);
   setIsProcessing(false);
   setIsMicOn(false);
-  console.error("Speech recognition error:", event.error);
+  // Use a proper logging service in production
+  if (process.env.NODE_ENV !== 'production') {
+    console.error("Speech recognition error:", event.error);
+  }
   if (event.error === "no-speech") {
-    console.log("No Speech!");
+    setOpenMessageDialog({
+      open: true,
+      message: "No speech detected. Please try speaking again.",
+      type: "warning"
+    });
   } else if (event.error === "aborted") {
     recognitionInstance.start();
+  } else {
+    setOpenMessageDialog({
+      open: true,
+      message: "Speech recognition failed. Please try again.",
+      type: "error"
+    });
   }
 };

Likely invalid or redundant comment.


578-592: 🛠️ Refactor suggestion

Add key props to character spans.

Add key props to elements in the character mapping iteration.

 {elem?.split("")?.map((char, index) => (
   <span
+    key={`char-${index}`}
     style={{
       color: wIndex == 0 ? "#1897DE" : "#000000",
       fontWeight: 700,
       fontSize: "30px",
       lineHeight: "87px",
       letterSpacing: "2%",
       fontFamily: "Quicksand",
       marginLeft: index > 0 ? "20px" : undefined,
     }}
   >
     {char}
   </span>
 ))}

Likely invalid or redundant comment.

🧰 Tools
🪛 Biome (1.9.4)

[error] 579-589: Missing key property for this element in iterable.

The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation.

(lint/correctness/useJsxKeyInIterable)

src/views/Practice/Practice.jsx (1)

1275-1315: 🛠️ Refactor suggestion

Add error boundary for Mechanics7 component.

Wrap the Mechanics7 component with an error boundary to gracefully handle potential rendering errors.

+import ErrorBoundary from '../../components/ErrorBoundary';

 else if (mechanism.name === "formAWord2") {
   return (
+    <ErrorBoundary
+      fallback={
+        <div role="alert">
+          Something went wrong. Please try again later.
+        </div>
+      }
+    >
       <Mechanics7
         page={page}
         setPage={setPage}
         {...{
           level: level,
           // ... other props
         }}
       />
+    </ErrorBoundary>
   );
 }

Likely invalid or redundant comment.

Comment on lines +109 to +112
<StopButton
height={props.pageName == "m7" ? 50 : 70}
width={props.pageName == "m7" ? 50 : 70}
/>
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 button sizing implementation.

The current implementation has several issues:

  1. Duplicated sizing logic across multiple components
  2. Use of loose equality (==) instead of strict equality (===)
  3. Magic numbers (50, 70) repeated throughout the code

Consider extracting the sizing logic into a constant or utility function:

+ const getButtonSize = (pageName) => pageName === "m7" ? 50 : 70;
+
  // Then use it consistently:
  <StopButton
-   height={props.pageName == "m7" ? 50 : 70}
-   width={props.pageName == "m7" ? 50 : 70}
+   height={getButtonSize(props.pageName)}
+   width={getButtonSize(props.pageName)}
  />

Also consider defining the sizes as named constants:

const BUTTON_SIZES = {
  MECHANICS_7: 50,
  DEFAULT: 70
};

Also applies to: 146-149, 159-162, 180-180, 208-218

Comment on lines 108 to 113
>
<StopButton />
<StopButton
height={props.pageName == "m7" ? 50 : 70}
width={props.pageName == "m7" ? 50 : 70}
/>
</Box>
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add accessibility attributes to interactive elements.

The interactive elements lack proper ARIA attributes and keyboard support, making them inaccessible to users relying on screen readers or keyboard navigation.

  <Box
    sx={{ cursor: "pointer", height: "38px" }}
    onClick={stopRecording}
+   role="button"
+   tabIndex={0}
+   aria-label="Stop recording"
+   onKeyPress={(e) => e.key === 'Enter' && stopRecording()}
  >
    <StopButton
      height={props.pageName == "m7" ? 50 : 70}
      width={props.pageName == "m7" ? 50 : 70}
    />
  </Box>

Apply similar changes to other interactive elements throughout the component.

Also applies to: 145-150, 179-186

Comment on lines +46 to +74
const [words, setWords] = useState(
type === "word" ? [] : ["Friend", "She is", "My"]
);
const [recordingStates, setRecordingStates] = useState({});
const [completeAudio, setCompleteAudio] = useState(null);

useEffect(() => {
if (words && words?.length) {
setRecordingStates(
words.reduce((acc, word) => ({ ...acc, [word]: false }), {})
);
setCompleteAudio(currentImg?.completeAudio);
}
}, [words]);

const [wordsAfterSplit, setWordsAfterSplit] = useState([]);
const [recAudio, setRecAudio] = useState("");

const [isRecordingComplete, setIsRecordingComplete] = useState(false);
const [isRecording, setIsRecording] = useState(false);
const [isProcessing, setIsProcessing] = useState(false);
const [currentWord, setCurrentWord] = useState("");
const [currentIsSelected, setCurrentIsSelected] = useState(false);
const [recognition, setRecognition] = useState(null);
const [selectedWords, setSelectedWords] = useState([]);
const [incorrectWords, setIncorrectWords] = useState({});
const [isMicOn, setIsMicOn] = useState(false);
const [syllAudios, setSyllAudios] = useState([]);

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Optimize state initialization and management.

  1. The state initialization using reduce with spread operator has O(n²) time complexity.
  2. Consider using useReducer for managing complex related states.
-const [recordingStates, setRecordingStates] = useState({});
-const [completeAudio, setCompleteAudio] = useState(null);
-const [wordsAfterSplit, setWordsAfterSplit] = useState([]);
-const [recAudio, setRecAudio] = useState("");
-const [isRecordingComplete, setIsRecordingComplete] = useState(false);
-const [isRecording, setIsRecording] = useState(false);
-const [isProcessing, setIsProcessing] = useState(false);
-const [currentWord, setCurrentWord] = useState("");
-const [currentIsSelected, setCurrentIsSelected] = useState(false);
-const [recognition, setRecognition] = useState(null);
-const [selectedWords, setSelectedWords] = useState([]);
-const [incorrectWords, setIncorrectWords] = useState({});
-const [isMicOn, setIsMicOn] = useState(false);
-const [syllAudios, setSyllAudios] = useState([]);

+const initialState = {
+  recordingStates: {},
+  completeAudio: null,
+  wordsAfterSplit: [],
+  recAudio: "",
+  isRecordingComplete: false,
+  isRecording: false,
+  isProcessing: false,
+  currentWord: "",
+  currentIsSelected: false,
+  recognition: null,
+  selectedWords: [],
+  incorrectWords: {},
+  isMicOn: false,
+  syllAudios: [],
+};
+
+function reducer(state, action) {
+  switch (action.type) {
+    case 'SET_RECORDING_STATES':
+      return { ...state, recordingStates: action.payload };
+    // ... other cases
+    default:
+      return state;
+  }
+}
+
+const [state, dispatch] = useReducer(reducer, initialState);

For the recordingStates initialization in useEffect:

-setRecordingStates(
-  words.reduce((acc, word) => ({ ...acc, [word]: false }), {})
-);
+const initialStates = {};
+words.forEach((word) => {
+  initialStates[word] = false;
+});
+setRecordingStates(initialStates);

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

🧰 Tools
🪛 Biome (1.9.4)

[error] 53-53: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 55-55: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)

Comment on lines +428 to +487
{selectedWordsRef.current?.map((elem, index) => {
const parentWordsArray =
typeof parentWords === "string"
? parentWords.split(" ")
: parentWords;

let remainingParts = parentWordsArray.filter(
(word) => !selectedWordsRef.current.includes(word)
);

const isIncorrect = incorrectWords[elem];

const colors = getColor(type, isIncorrect, answer);

return (
<span
role="button"
tabIndex={0}
onClick={() => handleWordsLogic(elem, "", true)}
className={
answer === "wrong"
? `audioSelectedWrongWord ${shake ? "shakeImage" : ""}`
: ""
}
onKeyDown={(e) => {
if (e.key === " ") {
e.preventDefault();
}
}}
style={{
borderRadius: "12px",
padding: answer === "correct" ? "0px" : "5px 10px 5px 10px",
border: getBorder(),
color: colors,
fontWeight: type === "word" ? 600 : 700,
fontSize: "25px",
fontFamily: "Quicksand",
cursor: "pointer",
marginLeft:
type === "word" ? 0 : answer != "correct" ? "20px" : 0,
}}
>
{elem}
{remainingParts.length > 0 && (
<span
style={{
color: "#1897DE1A",
fontWeight: 700,
fontSize: "25px",
fontFamily: "Quicksand",
marginLeft: "5px",
role: "button",
}}
>
{remainingParts.join(" ")}
</span>
)}
</span>
);
})}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add key props and improve accessibility.

  1. Add key props to elements in iterables
  2. Add proper ARIA attributes
  3. Add keyboard interaction support
 {selectedWordsRef.current?.map((elem, index) => {
   return (
     <span
+      key={`selected-word-${index}`}
       role="button"
       tabIndex={0}
       onClick={() => handleWordsLogic(elem, "", true)}
+      onKeyPress={(e) => {
+        if (e.key === 'Enter' || e.key === ' ') {
+          handleWordsLogic(elem, "", true);
+        }
+      }}
       className={
         answer === "wrong"
           ? `audioSelectedWrongWord ${shake ? "shakeImage" : ""}`
           : ""
       }
+      aria-label={`Selected word ${elem}`}
       onKeyDown={(e) => {
         if (e.key === " ") {
           e.preventDefault();
         }
       }}
     >
       {elem}
     </span>
   );
 })}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{selectedWordsRef.current?.map((elem, index) => {
const parentWordsArray =
typeof parentWords === "string"
? parentWords.split(" ")
: parentWords;
let remainingParts = parentWordsArray.filter(
(word) => !selectedWordsRef.current.includes(word)
);
const isIncorrect = incorrectWords[elem];
const colors = getColor(type, isIncorrect, answer);
return (
<span
role="button"
tabIndex={0}
onClick={() => handleWordsLogic(elem, "", true)}
className={
answer === "wrong"
? `audioSelectedWrongWord ${shake ? "shakeImage" : ""}`
: ""
}
onKeyDown={(e) => {
if (e.key === " ") {
e.preventDefault();
}
}}
style={{
borderRadius: "12px",
padding: answer === "correct" ? "0px" : "5px 10px 5px 10px",
border: getBorder(),
color: colors,
fontWeight: type === "word" ? 600 : 700,
fontSize: "25px",
fontFamily: "Quicksand",
cursor: "pointer",
marginLeft:
type === "word" ? 0 : answer != "correct" ? "20px" : 0,
}}
>
{elem}
{remainingParts.length > 0 && (
<span
style={{
color: "#1897DE1A",
fontWeight: 700,
fontSize: "25px",
fontFamily: "Quicksand",
marginLeft: "5px",
role: "button",
}}
>
{remainingParts.join(" ")}
</span>
)}
</span>
);
})}
{selectedWordsRef.current?.map((elem, index) => {
const parentWordsArray =
typeof parentWords === "string"
? parentWords.split(" ")
: parentWords;
let remainingParts = parentWordsArray.filter(
(word) => !selectedWordsRef.current.includes(word)
);
const isIncorrect = incorrectWords[elem];
const colors = getColor(type, isIncorrect, answer);
return (
<span
key={`selected-word-${index}`}
role="button"
tabIndex={0}
onClick={() => handleWordsLogic(elem, "", true)}
onKeyPress={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
handleWordsLogic(elem, "", true);
}
}}
className={
answer === "wrong"
? `audioSelectedWrongWord ${shake ? "shakeImage" : ""}`
: ""
}
aria-label={`Selected word ${elem}`}
onKeyDown={(e) => {
if (e.key === " ") {
e.preventDefault();
}
}}
style={{
borderRadius: "12px",
padding: answer === "correct" ? "0px" : "5px 10px 5px 10px",
border: getBorder(),
color: colors,
fontWeight: type === "word" ? 600 : 700,
fontSize: "25px",
fontFamily: "Quicksand",
cursor: "pointer",
marginLeft:
type === "word" ? 0 : answer != "correct" ? "20px" : 0,
}}
>
{elem}
{remainingParts.length > 0 && (
<span
style={{
color: "#1897DE1A",
fontWeight: 700,
fontSize: "25px",
fontFamily: "Quicksand",
marginLeft: "5px",
role: "button",
}}
>
{remainingParts.join(" ")}
</span>
)}
</span>
);
})}
🧰 Tools
🪛 Biome (1.9.4)

[error] 443-469: Missing key property for this element in iterable.

The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation.

(lint/correctness/useJsxKeyInIterable)

Comment on lines +396 to +413
useEffect(() => {
if (
progressData?.currentPracticeStep !== undefined &&
progressData?.currentPracticeStep !== null
) {
const currentLevel =
practiceSteps[progressData.currentPracticeStep]?.title;
const currentImage =
practiceSteps[progressData.currentPracticeStep]?.title;
const levelData = levels[currentLevel];
const levelImage = levels[currentImage];
const currentWord = levelData[currentQuestion];

setCurrentImage(levelImage[currentQuestion]);
setParentWords(currentWord.syllable.join(" "));
setLevelOneWord(levelImage[currentQuestion]?.completeWord);
}
}, [progressData]);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error handling to useEffect.

The useEffect that sets current image and words should handle potential undefined values and edge cases.

 useEffect(() => {
   if (
     progressData?.currentPracticeStep !== undefined &&
     progressData?.currentPracticeStep !== null
   ) {
     const currentLevel =
       practiceSteps[progressData.currentPracticeStep]?.title;
     const currentImage =
       practiceSteps[progressData.currentPracticeStep]?.title;
     const levelData = levels[currentLevel];
     const levelImage = levels[currentImage];
+    
+    if (!levelData || !levelImage) {
+      console.error('Invalid level data or image');
+      return;
+    }
+    
     const currentWord = levelData[currentQuestion];
+    
+    if (!currentWord) {
+      console.error('Invalid current word');
+      return;
+    }

     setCurrentImage(levelImage[currentQuestion]);
-    setParentWords(currentWord.syllable.join(" "));
+    setParentWords(currentWord.syllable?.join(" ") || "");
     setLevelOneWord(levelImage[currentQuestion]?.completeWord);
   }
 }, [progressData]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
if (
progressData?.currentPracticeStep !== undefined &&
progressData?.currentPracticeStep !== null
) {
const currentLevel =
practiceSteps[progressData.currentPracticeStep]?.title;
const currentImage =
practiceSteps[progressData.currentPracticeStep]?.title;
const levelData = levels[currentLevel];
const levelImage = levels[currentImage];
const currentWord = levelData[currentQuestion];
setCurrentImage(levelImage[currentQuestion]);
setParentWords(currentWord.syllable.join(" "));
setLevelOneWord(levelImage[currentQuestion]?.completeWord);
}
}, [progressData]);
useEffect(() => {
if (
progressData?.currentPracticeStep !== undefined &&
progressData?.currentPracticeStep !== null
) {
const currentLevel =
practiceSteps[progressData.currentPracticeStep]?.title;
const currentImage =
practiceSteps[progressData.currentPracticeStep]?.title;
const levelData = levels[currentLevel];
const levelImage = levels[currentImage];
if (!levelData || !levelImage) {
console.error('Invalid level data or image');
return;
}
const currentWord = levelData[currentQuestion];
if (!currentWord) {
console.error('Invalid current word');
return;
}
setCurrentImage(levelImage[currentQuestion]);
setParentWords(currentWord.syllable?.join(" ") || "");
setLevelOneWord(levelImage[currentQuestion]?.completeWord);
}
}, [progressData]);

@gouravmore gouravmore changed the base branch from all-1.4-dev to all-1.4.0-dev January 15, 2025 07:48
@gouravmore gouravmore merged commit ac120d9 into Sunbird-ALL:all-1.4.0-dev Jan 15, 2025
2 checks passed
This was referenced Jan 20, 2025
@coderabbitai coderabbitai bot mentioned this pull request Jan 30, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants