Extra Practice

Practice your current level's Radicals and Kanji with standard, english -> Kanji, and combination mode!

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

(function () {
    'use strict';

    // ==UserScript==
    // @name         Extra Practice
    // @namespace    https://github.com/mrpassiontea/Extra-Practice
    // @version      2.0.0
    // @description  Practice your current level's Radicals and Kanji with standard, english -> Kanji, and combination mode!
    // @author       @mrpassiontea
    // @match        https://www.wanikani.com/
    // @match        *://*.wanikani.com/dashboard
    // @match        *://*.wanikani.com/dashboard?*
    // @copyright    2025, mrpassiontea
    // @grant        none
    // @grant        window.onurlchange
    // @require      https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js
    // @require      https://unpkg.com/[email protected]/wanakana.min.js
    // @license      MIT; http://opensource.org/licenses/MIT
    // @run-at       document-end
    // ==/UserScript==


    const SELECTORS = {
        DIV_LEVEL_PROGRESS_CONTENT: "div.wk-panel__content div.level-progress-dashboard",
        DIV_CONTENT_WRAPPER: "div.level-progress-dashboard__content",
        DIV_CONTENT_TITLE: "div.level-progress-dashboard__content-title"
    };

    const DB_VALUES = {
        DB_NAME: "wkof.file_cache",
        USER_RECORD: "Apiv2.user",
        SUBJECT_RECORD: "Apiv2.subjects",
        FILE_STORE: "files"
    };

    const DB_ERRORS = {
        OPEN: "Failed to open database",
        USER_LEVEL: "Failed to retrieve user level",
        SUBJECT_DATA: "Failed to retrieve subjects data"
    };

    const PRACTICE_MODES = {
        STANDARD: 'standard',
        ENGLISH_TO_KANJI: 'englishToKanji',
        COMBINED: 'combined'
    };

    const modalTemplate = `
    <div id="ep-practice-modal">
        <div id="ep-practice-modal-content">
            <div id="ep-practice-modal-welcome">
                <h1>Hello, <span id="username"></span></h1>
                <h2>Please select all the Radicals that you would like to include in your practice session</h2>
            </div>
            <button id="ep-practice-modal-select-all">Select All</button>
            <div id="ep-practice-modal-grid"></div>
            <div id="ep-practice-modal-footer">
                <button id="ep-practice-modal-start" disabled>Start Review (0 Selected)</button>
                <button id="ep-practice-modal-close">Exit</button>
            </div>
        </div>
    </div>
`;

    const reviewModalTemplate = `
    <div id="ep-review-modal">
        <div id="ep-review-modal-wrapper">
            <div id="ep-review-modal-header">
                <div id="ep-review-progress">
                    <span id="ep-review-progress-correct">0</span>
                </div>
                <button id="ep-review-exit">End Review</button>
            </div>

            <div id="ep-review-content">
                <div id="ep-review-character"></div>

                <div id="ep-review-input-section">
                    <input type="text" id="ep-review-answer" placeholder="Enter meaning..." tabindex="1" autofocus />
                    <button id="ep-review-submit" tabindex="2">Submit</button>
                </div>

                <div id="ep-review-result" style="display: none;">
                    <div id="ep-review-result-message"></div>
                    <button id="ep-review-show-hint" style="display: none;">Show Answer</button>
                </div>

                <div id="ep-review-explanation" style="display: none;">
                    <h3>
                        <span id="ep-review-meaning-label">Meaning:</span>
                        <span id="ep-review-meaning"></span>
                    </h3>
                    <div class="mnemonic-container">
                        <span id="ep-review-mnemonic-label">Mnemonic:</span>
                        <div id="ep-review-mnemonic"></div>
                    </div>
                </div>
            </div>
        </div>
    </div>
`;

    // Theme constants for consistent values across the application
    const theme = {
        colors: {
            radical: "#0598e4",
            kanji: "#eb019c",
            white: "#FFFFFF",
            black: "#000000",
            gray: {
                100: "#F3F4F6",
                200: "#E5E7EB",
                300: "#D1D5DB",
                400: "#9CA3AF",
                600: "#4B5563",
                700: "#374151",
                800: "#1F2937"},
            overlay: {
                dark: "rgba(0, 0, 0, 0.9)"},
            success: "#10B981",
            error: "#EF4444",
            info: "#3B82F6"
        },
        spacing: {
            xs: "0.5rem",    // 8px
            sm: "0.75rem",   // 12px
            md: "1rem",      // 16px
            lg: "1.5rem",    // 24px
            xl: "2rem"},
        typography: {
            fontSize: {
                xs: "0.875rem",    // 14px
                sm: "1rem",        // 16px
                md: "1.25rem",     // 20px
                lg: "1.5rem",      // 24px
                xl: "2rem",        // 32px
                "2xl": "6rem"      // 96px (for the big character display)
            },
            fontWeight: {
                normal: "400",
                medium: "500",
                bold: "700"
            }
        },
        borderRadius: {
            sm: "3px",
            md: "4px",
            lg: "8px"
        },
        zIndex: {
            modal: 99999
        }
    };

    // Common style mixins for reusable patterns
    const mixins = {
        modalBackdrop: {
            position: "fixed",
            top: "0",
            left: "0",
            width: "100%",
            height: "100%",
            zIndex: theme.zIndex.modal
        }};

    // Component-specific styles
    const styles = {
        layout: {
            contentTitle: {
                display: "flex",
                justifyContent: "space-between",
                alignItems: "center"
            }
        },

        buttons: {
            practice: {
                radical: {
                    marginBottom: theme.spacing.md,
                    backgroundColor: theme.colors.radical,
                    padding: theme.spacing.sm,
                    borderRadius: theme.borderRadius.sm,
                    color: theme.colors.white,
                    fontWeight: theme.typography.fontWeight.medium,
                    cursor: "pointer"
                },
                kanji: {
                    marginBottom: theme.spacing.md,
                    backgroundColor: theme.colors.kanji,
                    padding: theme.spacing.sm,
                    borderRadius: theme.borderRadius.sm,
                    color: theme.colors.white,
                    fontWeight: theme.typography.fontWeight.medium,
                    cursor: "pointer"
                }
            }
        },

        practiceModal: {
            backdrop: {
                ...mixins.modalBackdrop,
                backgroundColor: theme.colors.overlay.dark,
                display: "flex",
                flexDirection: "column",
                justifyContent: "center",
                alignItems: "center"
            },
            contentWrapper: {
                width: "100%",
                maxWidth: "800px",
                padding: `0 ${theme.spacing.xl}`,
                display: "flex",
                flexDirection: "column",
                alignItems: "center"
            },
            welcomeText: {
                container: {
                    color: theme.colors.white,
                    textAlign: "center",
                    fontSize: theme.typography.fontSize.sm,
                    marginBottom: theme.spacing.md,
                    display: "flex",
                    flexDirection: "column",
                    alignItems: "center",
                    maxWidth: "750px"
                },
                username: {
                    fontSize: theme.typography.fontSize.xl,
                    marginBottom: theme.spacing.md
                }
            },
            grid: {
                display: "grid",
                gridTemplateColumns: "repeat(5, minmax(100px, 1fr))",
                gap: theme.spacing.md,
                padding: `${theme.spacing.md} ${theme.spacing.xl}`,
                maxHeight: "50vh",
                maxWidth: "600px",
                margin: "0 auto",
                justifyContent: "center"
            },
            radical: {
                base: {
                    background: "rgba(255, 255, 255, 0.1)",
                    border: "2px solid rgba(255, 255, 255, 0.2)",
                    borderRadius: theme.borderRadius.lg,
                    padding: theme.spacing.md,
                    cursor: "pointer",
                    display: "flex",
                    flexDirection: "column",
                    alignItems: "center",
                    transition: "all 0.2s ease"
                },
                selected: {
                    background: "rgba(5, 152, 228, 0.3)",
                    border: `2px solid ${theme.colors.radical}`
                },
                character: {
                    fontSize: theme.typography.fontSize.xl,
                    color: theme.colors.white
                }},
            buttons: {
                start: {
                    base: {
                        padding: `${theme.spacing.sm} ${theme.spacing.lg}`,
                        borderRadius: theme.borderRadius.md,
                        border: "none",
                        fontWeight: theme.typography.fontWeight.medium,
                        transition: "all 0.2s ease",
                        cursor: "pointer",
                        color: theme.colors.white
                    },
                    radical: {
                        backgroundColor: theme.colors.radical,
                        '&:hover': {
                            backgroundColor: theme.colors.radical,
                            opacity: 0.9
                        }
                    },
                    kanji: {
                        backgroundColor: theme.colors.kanji,
                        '&:hover': {
                            backgroundColor: theme.colors.kanji,
                            opacity: 0.9
                        }
                    }
                },
                selectAll: {
                    color: theme.colors.white,
                    background: "transparent",
                    border: `1px solid ${theme.colors.white}`,
                    cursor: "pointer",
                    fontSize: theme.typography.fontSize.xs,
                    marginBottom: theme.spacing.md,
                    padding: theme.spacing.sm,
                    borderRadius: theme.borderRadius.sm,
                    fontWeight: theme.typography.fontWeight.bold,
                    transition: "all 0.2s ease"
                },
                exit: {
                    border: `1px solid ${theme.colors.white}`,
                    backgroundColor: "rgba(255, 255, 255, 0.9)",
                    padding: `${theme.spacing.sm} ${theme.spacing.md}`,
                    color: theme.colors.black,
                    fontWeight: theme.typography.fontWeight.medium,
                    borderRadius: theme.borderRadius.sm,
                    cursor: "pointer",
                    transition: "all 0.2s ease"
                }
            },
            footer: {
                padding: `${theme.spacing.md} ${theme.spacing.xl}`,
                display: "flex",
                justifyContent: "center",
                width: "100%",
                maxWidth: "600px",
                gap: theme.spacing.md
            },
            modeSelector: {
                container: {
                    display: "flex",
                    flexDirection: "column",
                    alignItems: "center",
                    marginBottom: theme.spacing.xl,
                    width: "100%",
                    maxWidth: "600px"
                },
                label: {
                    color: theme.colors.white,
                    fontSize: theme.typography.fontSize.md,
                    marginBottom: theme.spacing.md
                },
                options: {
                    display: "flex",
                    gap: theme.spacing.md,
                    justifyContent: "center",
                    width: "100%"
                },
                option: {
                    base: {
                        padding: `${theme.spacing.sm} ${theme.spacing.md}`,
                        borderRadius: theme.borderRadius.md,
                        border: `2px solid ${theme.colors.gray[400]}`,
                        backgroundColor: "transparent",
                        color: theme.colors.white,
                        cursor: "pointer",
                        transition: "all 0.2s ease",
                        fontSize: theme.typography.fontSize.sm,
                        fontWeight: theme.typography.fontWeight.medium,
                        '&:hover': {
                            borderColor: theme.colors.kanji,
                            backgroundColor: "rgba(235, 1, 156, 0.1)"
                        }
                    },
                    selected: {
                        borderColor: theme.colors.kanji,
                        backgroundColor: "rgba(235, 1, 156, 0.2)"
                    }
                }
            }},

        reviewModal: {
            container: {
                backgroundColor: theme.colors.white,
                borderRadius: theme.borderRadius.lg,
                boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1)",
                maxWidth: "600px",
                width: "100%",
                display: "flex",
                flexDirection: "column"
            },
            header: {
                display: "flex",
                justifyContent: "space-between",
                alignItems: "center",
                padding: theme.spacing.lg,
                borderBottom: `1px solid ${theme.colors.gray[200]}`,
                gap: theme.spacing.md
            },
            progress: {
                fontWeight: theme.typography.fontWeight.bold,
                fontSize: theme.typography.fontSize.md,
                color: theme.colors.gray[800]
            },
            content: {
                padding: theme.spacing.xl,
                display: "flex",
                flexDirection: "column",
                width: "100%",
                gap: theme.spacing.xl,
            },
            character: {
                fontSize: theme.typography.fontSize["2xl"],
                color: theme.colors.gray[800],
                marginBottom: theme.spacing.xl,
                textAlign: "center"
            },
            inputSection: {
                width: "100%",
                display: "flex",
                gap: theme.spacing.md,
                marginBottom: theme.spacing.xl
            },
            input: {
                flex: "1",
                padding: theme.spacing.sm,
                fontSize: theme.typography.fontSize.sm,
                borderRadius: theme.borderRadius.md,
                border: `1px solid ${theme.colors.gray[300]}`
            },
            buttons: {
                submit: {
                    backgroundColor: theme.colors.info,
                    color: theme.colors.white,
                    padding: `${theme.spacing.sm} ${theme.spacing.lg}`,
                    borderRadius: theme.borderRadius.md,
                    border: "none",
                    fontWeight: theme.typography.fontWeight.medium,
                    cursor: "pointer",
                    transition: "background-color 0.2s ease",
                    "&:hover": {
                        backgroundColor: "#2563EB"
                    }
                },
                exit: {
                    backgroundColor: "transparent",
                    color: theme.colors.kanji,
                    border: `1px solid ${theme.colors.kanji}`,
                    borderRadius: theme.borderRadius.md,
                    padding: `${theme.spacing.sm} ${theme.spacing.lg}`,
                    fontWeight: theme.typography.fontWeight.medium,
                    cursor: "pointer",
                    transition: "background-color 0.2s ease",
                    "&:hover": {
                        backgroundColor: theme.colors.gray[100]
                    }
                },
                hint: {
                    backgroundColor: "transparent",
                    color: theme.colors.info,
                    border: `1px solid ${theme.colors.info}`,
                    borderRadius: theme.borderRadius.md,
                    padding: `${theme.spacing.sm} ${theme.spacing.lg}`,
                    cursor: "pointer",
                    transition: "background-color 0.2s ease",
                    "&:hover": {
                        backgroundColor: theme.colors.gray[100]
                    }
                }
            },
            results: {
                message: {
                    fontSize: theme.typography.fontSize.lg,
                    fontWeight: theme.typography.fontWeight.bold,
                    marginBottom: theme.spacing.md,
                    color: theme.colors.info,
                    textAlign: "center",
                    "&.correct": {
                        color: theme.colors.success
                    },
                    "&.incorrect": {
                        color: theme.colors.error
                    }
                }
            },
            explanation: {
                lineHeight: "1.6",
                color: theme.colors.gray[600],
                fontSize: theme.typography.fontSize.md,
                meaningLabel: {
                    display: "inline-block",
                    fontWeight: theme.typography.fontWeight.normal,
                    fontSize: theme.typography.fontSize.md,
                    color: theme.colors.gray[800],
                    marginRight: theme.spacing.xs
                },
                meaningText: {
                    display: "inline-block",
                    fontWeight: theme.typography.fontWeight.bold,
                    fontSize: theme.typography.fontSize.md,
                    color: theme.colors.radical[800],
                    textDecoration: "none"
                },
                mnemonicContainer: {
                    marginTop: theme.spacing.md,
                    textAlign: "left",
                    lineHeight: "1.6"
                },
                mnemonicLabel: {
                    display: "block",
                    fontWeight: theme.typography.fontWeight.bold,
                    fontSize: theme.typography.fontSize.md,
                    color: theme.colors.gray[800],
                    marginBottom: theme.spacing.xs
                },
                mnemonic: {
                    color: theme.colors.gray[600],
                    fontSize: theme.typography.fontSize.md
                },
                mnemonicHighlight: {
                    backgroundColor: theme.colors.gray[200],
                    padding: `0 ${theme.spacing.xs}`,
                    borderRadius: theme.borderRadius.sm,
                    color: theme.colors.gray[800]
                }
            },
            kanjiOption: {
                base: {
                    padding: theme.spacing.lg,
                    borderRadius: theme.borderRadius.md,
                    border: `2px solid ${theme.colors.gray[300]}`,
                    backgroundColor: theme.colors.white,
                    display: "flex",
                    alignItems: "center",
                    justifyContent: "center",
                    transition: "all 0.2s ease",
                    '&:hover': {
                        borderColor: theme.colors.kanji,
                        backgroundColor: "rgba(235, 1, 156, 0.1)"
                    }
                },
                selected: {
                    borderColor: theme.colors.kanji,
                    backgroundColor: "rgba(235, 1, 156, 0.2)"
                }
            }
        }
    };

    const PRACTICE_TYPES = {
        RADICAL: "radical",
        KANJI: "kanji"
    };

    const MODAL_STATES$1 = {
        READY: "ready"
    };

    const EVENTS$1 = {
        CLOSE: "close",
        START_REVIEW: "startReview"
    };

    class BaseReviewSession {
        constructor(selectedItems) {
            if (new.target === BaseReviewSession) {
                throw new Error("BaseReviewSession is an abstract class and cannot be instantiated directly.");
            }
            this.originalItems = selectedItems;
            this.currentItem = null;
        }

        shuffleArray(array) {
            for (let i = array.length - 1; i > 0; i--) {
                const j = Math.floor(Math.random() * (i + 1));
                [array[i], array[j]] = [array[j], array[i]];
            }
            return array;
        }

        nextItem() {
            throw new Error("nextItem() must be implemented by derived classes");
        }

        checkAnswer(userAnswer) {
            throw new Error("checkAnswer() must be implemented by derived classes");
        }

        isComplete() {
            throw new Error("isComplete() must be implemented by derived classes");
        }

        getProgress() {
            throw new Error("getProgress() must be implemented by derived classes");
        }
    }

    class KanjiReviewSession extends BaseReviewSession {
        constructor(config) {
            super(config.items);
            this.mode = config.mode || PRACTICE_MODES.STANDARD;
            this.allUnlockedKanji = config.allUnlockedKanji || [];
            this.allCards = [];
            this.remainingItems = [];
            
            // Progress tracking
            this.correctMeanings = new Set();
            this.correctReadings = new Set();
            this.correctRecognition = new Set();
            
            // Initialize cards based on mode
            this.initializeCards();
        }

        initializeCards() {
            switch (this.mode) {
                case PRACTICE_MODES.STANDARD:
                    this.initializeStandardCards();
                    break;
                case PRACTICE_MODES.ENGLISH_TO_KANJI:
                    this.initializeRecognitionCards();
                    break;
                case PRACTICE_MODES.COMBINED:
                    this.initializeStandardCards();
                    this.initializeRecognitionCards();
                    break;
            }
            
            // Shuffle all cards together
            this.remainingItems = this.shuffleArray([...this.allCards]);
        }

        initializeStandardCards() {
            this.originalItems.forEach(kanji => {
                // Add meaning card
                this.allCards.push({
                    ...kanji,
                    type: "meaning",
                    questionType: "What is the meaning of this kanji?"
                });
                
                // Add reading card
                this.allCards.push({
                    ...kanji,
                    type: "reading",
                    questionType: "What is the reading of this kanji?"
                });
            });
        }

        initializeRecognitionCards() {
            this.originalItems.forEach(kanji => {
                const primaryMeaning = kanji.meanings.find(m => m.primary)?.meaning;
                
                // Create recognition card
                this.allCards.push({
                    ...kanji,
                    type: "recognition",
                    questionType: "Select the kanji that means",
                    meaningToMatch: primaryMeaning,
                    options: this.generateKanjiOptions(kanji)
                });
            });
        }

        generateKanjiOptions(correctKanji) {
            const numberOfOptions = 4;
            const options = [correctKanji];
            
            // Create a pool of incorrect options from the selected kanji
            const availableOptions = this.originalItems.filter(k => k.id !== correctKanji.id);

            
            // Randomly select additional options from the available pool
            while (options.length < numberOfOptions && availableOptions.length > 0) {
                const randomIndex = Math.floor(Math.random() * availableOptions.length);
                const selectedOption = availableOptions[randomIndex];
                options.push(selectedOption);
                availableOptions.splice(randomIndex, 1);
            }

            // If we still need more options (rare case when very few kanji are selected)
            // fill remaining slots with kanji from allUnlockedKanji
            if (options.length < numberOfOptions) {
                const additionalOptions = this.allUnlockedKanji.filter(k => 
                    !options.some(selected => selected.id === k.id) && 
                    !this.originalItems.some(selected => selected.id === k.id)
                );

                while (options.length < numberOfOptions && additionalOptions.length > 0) {
                    const randomIndex = Math.floor(Math.random() * additionalOptions.length);
                    const selectedOption = additionalOptions[randomIndex];
                    options.push(selectedOption);
                    additionalOptions.splice(randomIndex, 1);
                }
            }
            
            return this.shuffleArray(options);
        }

        nextItem() {
            if (this.remainingItems.length === 0) {
                // Get items that haven't been answered correctly
                const remainingUnlearned = [];
                
                this.originalItems.forEach(kanji => {
                    switch (this.mode) {
                        case PRACTICE_MODES.STANDARD:
                            if (!this.correctMeanings.has(kanji.id)) {
                                remainingUnlearned.push({
                                    ...kanji,
                                    type: "meaning",
                                    questionType: "What is the meaning of this kanji?"
                                });
                            }
                            if (!this.correctReadings.has(kanji.id)) {
                                remainingUnlearned.push({
                                    ...kanji,
                                    type: "reading",
                                    questionType: "What is the reading of this kanji?"
                                });
                            }
                            break;
                        case PRACTICE_MODES.ENGLISH_TO_KANJI:
                            if (!this.correctRecognition.has(kanji.id)) {
                                const primaryMeaning = kanji.meanings.find(m => m.primary)?.meaning;
                                remainingUnlearned.push({
                                    ...kanji,
                                    type: "recognition",
                                    questionType: "Select the kanji that means",
                                    meaningToMatch: primaryMeaning,
                                    options: this.generateKanjiOptions(kanji)
                                });
                            }
                            break;
                        case PRACTICE_MODES.COMBINED:
                            if (!this.correctMeanings.has(kanji.id)) {
                                remainingUnlearned.push({
                                    ...kanji,
                                    type: "meaning",
                                    questionType: "What is the meaning of this kanji?"
                                });
                            }
                            if (!this.correctReadings.has(kanji.id)) {
                                remainingUnlearned.push({
                                    ...kanji,
                                    type: "reading",
                                    questionType: "What is the reading of this kanji?"
                                });
                            }
                            if (!this.correctRecognition.has(kanji.id)) {
                                const primaryMeaning = kanji.meanings.find(m => m.primary)?.meaning;
                                remainingUnlearned.push({
                                    ...kanji,
                                    type: "recognition",
                                    questionType: "Select the kanji that means",
                                    meaningToMatch: primaryMeaning,
                                    options: this.generateKanjiOptions(kanji)
                                });
                            }
                            break;
                    }
                });

                // Shuffle the remaining items
                if (remainingUnlearned.length > 0) {
                    this.remainingItems = this.shuffleArray(remainingUnlearned);
                }
            }

            this.currentItem = this.remainingItems.shift();
            return this.currentItem;
        }

        checkAnswer(userAnswer) {
            if (!this.currentItem) return false;

            let isCorrect = false;

            switch (this.currentItem.type) {
                case "meaning":
                    isCorrect = this.checkMeaningAnswer(userAnswer);
                    if (isCorrect) this.correctMeanings.add(this.currentItem.id);
                    break;
                    
                case "reading":
                    isCorrect = this.checkReadingAnswer(userAnswer);
                    if (isCorrect) this.correctReadings.add(this.currentItem.id);
                    break;
                    
                case "recognition":
                    isCorrect = parseInt(userAnswer) === this.currentItem.id;
                    if (isCorrect) this.correctRecognition.add(this.currentItem.id);
                    break;
            }

            return isCorrect;
        }

        checkMeaningAnswer(userAnswer) {
            const normalizedUserAnswer = userAnswer.toLowerCase().trim();
            
            // Check primary meanings
            const isPrimaryCorrect = this.currentItem.meanings.some(m => 
                m.meaning.toLowerCase() === normalizedUserAnswer
            );
            
            if (isPrimaryCorrect) return true;
            
            // Check auxiliary meanings
            return this.currentItem.auxiliaryMeanings.some(m => 
                m.meaning.toLowerCase() === normalizedUserAnswer
            );
        }

        checkReadingAnswer(userAnswer) {
            const userReading = userAnswer.trim();
            return this.currentItem.readings.some(r => r.reading === userReading);
        }

        isComplete() {
            const progress = this.getProgress();
            return progress.current === progress.total;
        }

        getProgress() {
            const totalKanji = this.originalItems.length;
            let total, current;

            switch (this.mode) {
                case PRACTICE_MODES.STANDARD:
                    total = totalKanji * 2; // One point each for meaning and reading
                    current = this.correctMeanings.size + this.correctReadings.size;
                    return {
                        total,
                        current,
                        meaningProgress: this.correctMeanings.size,
                        readingProgress: this.correctReadings.size
                    };

                case PRACTICE_MODES.ENGLISH_TO_KANJI:
                    total = totalKanji; // One point for each recognition test
                    current = this.correctRecognition.size;
                    return {
                        total,
                        current,
                        recognitionProgress: this.correctRecognition.size
                    };

                case PRACTICE_MODES.COMBINED:
                    total = totalKanji * 3; // One point each for meaning, reading, and recognition
                    current = this.correctMeanings.size + 
                             this.correctReadings.size + 
                             this.correctRecognition.size;
                    return {
                        total,
                        current,
                        meaningProgress: this.correctMeanings.size,
                        readingProgress: this.correctReadings.size,
                        recognitionProgress: this.correctRecognition.size
                    };

                default:
                    return {
                        total: 0,
                        current: 0
                    };
            }
        }
    }

    class RadicalReviewSession extends BaseReviewSession {
        constructor(config) {
            super(config.items);
            this.remainingItems = this.shuffleArray([...config.items]);
            this.correctAnswers = new Set();
        }

        nextItem() {
            if (this.remainingItems.length === 0) {
                const remainingUnlearned = this.originalItems.filter(item => !this.correctAnswers.has(item.id));

                if (remainingUnlearned.length === 1) {
                    this.remainingItems = remainingUnlearned;
                } else {
                    this.remainingItems = this.shuffleArray(
                        remainingUnlearned.filter(item => !this.currentItem || item.id !== this.currentItem.id)
                    );
                }
            }
            this.currentItem = this.remainingItems.shift();
            return this.currentItem;
        }

        checkAnswer(userAnswer) {
            const isCorrect = this.currentItem.meaning.toLowerCase() === userAnswer.toLowerCase();
            if (isCorrect) {
                this.correctAnswers.add(this.currentItem.id);
            }
            return isCorrect;
        }

        isComplete() {
            return this.correctAnswers.size === this.originalItems.length;
        }

        getProgress() {
            const totalRadicals = this.originalItems.length;
            let current = this.correctAnswers.size;

            return {
                current,
                total: totalRadicals,
                remaining: totalRadicals - current,
                percentComplete: Math.round((current / totalRadicals) * 100)
            };
        }
    }

    function disableScroll() {
        const scrollPosition = window.scrollY || document.documentElement.scrollTop;

        $("html, body").css({
            overflow: "hidden",
            height: "100%",
            position: "fixed",
            top: `-${scrollPosition}px`,
            width: "100%",
        });
    }

    function enableScroll() {
        const scrollPosition = parseInt($("html").css("top")) * -1;

        $("html, body").css({
            overflow: "auto",
            height: "auto",
            position: "static",
            top: "auto",
            width: "auto",
        });

        window.scrollTo(0, scrollPosition);
    }

    // Cache for SVG content to avoid repeated fetches
    const svgCache = new Map();

    async function loadSvgContent(url) {
        if (svgCache.has(url)) {
            return svgCache.get(url);
        }
        
        const response = await fetch(url);
        const svgContent = await response.text();
        svgCache.set(url, svgContent);
        return svgContent;
    }

    class RadicalGrid {
        constructor(radicals, onSelectionChange) {
            this.radicals = radicals;
            this.selectedRadicals = new Set();
            this.onSelectionChange = onSelectionChange;
            this.$container = null;
        }

        updateRadicalSelection($element, radical, isSelected) {
            $element.css(
                isSelected 
                    ? { ...styles.practiceModal.radical.base, ...styles.practiceModal.radical.selected }
                    : styles.practiceModal.radical.base
            );

            if (isSelected) {
                this.selectedRadicals.add(radical.id);
            } else {
                this.selectedRadicals.delete(radical.id);
            }

            this.onSelectionChange(this.selectedRadicals);
        }

        toggleAllRadicals(shouldSelect) {
            if (shouldSelect) {
                this.radicals.forEach(radical => this.selectedRadicals.add(radical.id));
            } else {
                this.selectedRadicals.clear();
            }

            this.$container.find(".radical-selection-item").each((_, element) => {
                const $element = $(element);
                const radicalId = parseInt($element.data("radical-id"));
                this.updateRadicalSelection(
                    $element,
                    this.radicals.find(r => r.id === radicalId),
                    shouldSelect
                );
            });

            this.onSelectionChange(this.selectedRadicals);
        }

        getSelectedRadicals() {
            return Array.from(this.selectedRadicals).map(id => 
                this.radicals.find(radical => radical.id === id)
            );
        }

        async createRadicalElement(radical) {
            const $element = $("<div>")
                .addClass("radical-selection-item")
                .css(styles.practiceModal.radical.base)
                .data("radical-id", radical.id)
                .append(
                    $("<div>")
                        .addClass("radical-character")
                        .css(styles.practiceModal.radical.character)
                        .text(radical.character || "")
                )
                .on("click", () => {
                    const isCurrentlySelected = this.selectedRadicals.has(radical.id);
                    this.updateRadicalSelection($element, radical, !isCurrentlySelected);
                });

            if (!radical.character && radical.svg) {
                try {
                    const svgContent = await loadSvgContent(radical.svg);
                    $element.find(".radical-character").html(svgContent);
                    const svg = $element.find("svg")[0];
                    if (svg) {
                        svg.setAttribute("width", "100%");
                        svg.setAttribute("height", "100%");
                    }
                } catch (error) {
                    console.error("Error loading SVG:", error);
                    $element.find(".radical-character").text(radical.meaning);
                }
            }

            return $element;
        }

        async render() {
            this.$container = $("<div>")
                .css(styles.practiceModal.grid);

            // Create and append all radical elements
            const radicalElements = await Promise.all(
                this.radicals.map(radical => this.createRadicalElement(radical))
            );
            
            radicalElements.forEach($element => this.$container.append($element));
            
            return this.$container;
        }
    }

    class RadicalSelectionModal {
        constructor(radicals) {
            this.radicals = radicals;
            this.state = MODAL_STATES$1.READY;
            this.totalRadicals = radicals.length;
            this.$modal = null;
            this.radicalGrid = null;
            this.callbacks = new Map();
        }

        on(event, callback) {
            this.callbacks.set(event, callback);
            return this;
        }

        emit(event, data) {
            const callback = this.callbacks.get(event);
            if (callback) callback(data);
        }

        updateSelectAllButton(selectedCount) {
            const selectAllButton = $("#ep-practice-modal-select-all");
            const isAllSelected = selectedCount === this.totalRadicals;
            
            selectAllButton
                .text(isAllSelected ? "Deselect All" : "Select All")
                .css({
                    color: isAllSelected ? theme.colors.error : theme.colors.white,
                    borderColor: isAllSelected ? theme.colors.error : theme.colors.white
                });
        }

        updateStartButton(selectedCount) {
            const startButton = $("#ep-practice-modal-start");
            
            if (selectedCount > 0) {
                startButton
                    .prop("disabled", false)
                    .text(`Start Review (${selectedCount} Selected)`)
                    .css({
                        ...styles.practiceModal.buttons.start.base,
                        ...styles.practiceModal.buttons.start.radical
                    });
            } else {
                startButton
                    .prop("disabled", true)
                    .text("Start Review (0 Selected)")
                    .css({
                        ...styles.practiceModal.buttons.start.base,
                        ...styles.practiceModal.buttons.start.radical,
                        ...styles.practiceModal.buttons.start.disabled
                    });
            }
        }

        handleSelectionChange(selectedRadicals) {
            const selectedCount = selectedRadicals.size;
            this.updateSelectAllButton(selectedCount);
            this.updateStartButton(selectedCount);
        }

        async render() {
            this.$modal = $(modalTemplate).appendTo("body");
            
            $("#username").text($("p.user-summary__username:first").text());
            
            this.$modal.css(styles.practiceModal.backdrop);
            $("#ep-practice-modal-welcome").css(styles.practiceModal.welcomeText.container);
            $("#ep-practice-modal-welcome h1").css(styles.practiceModal.welcomeText.username);
            $("#ep-practice-modal-footer").css(styles.practiceModal.footer);
            $("#ep-practice-modal-start").css({
                ...styles.practiceModal.buttons.start.base,
                ...styles.practiceModal.buttons.start.radical,
                ...styles.practiceModal.buttons.start.disabled
            });
            $("#ep-practice-modal-select-all").css(styles.practiceModal.buttons.selectAll);
            $("#ep-practice-modal-content").css(styles.practiceModal.contentWrapper);
            $("#ep-practice-modal-close").css(styles.practiceModal.buttons.exit);

            this.radicalGrid = new RadicalGrid(
                this.radicals,
                this.handleSelectionChange.bind(this)
            );

            const $grid = await this.radicalGrid.render();
            $("#ep-practice-modal-grid").replaceWith($grid);

            this.updateStartButton(0);

            $("#ep-practice-modal-select-all").on("click", () => {
                const isSelectingAll = $("#ep-practice-modal-select-all").text() === "Select All";
                this.radicalGrid.toggleAllRadicals(isSelectingAll);
            });

            $("#ep-practice-modal-close").on("click", () => {
                this.emit(EVENTS$1.CLOSE);
            });

            $("#ep-practice-modal-start").on("click", () => {
                const selectedRadicals = this.radicalGrid.getSelectedRadicals();
                if (selectedRadicals.length > 0) {
                    this.emit(EVENTS$1.START_REVIEW, selectedRadicals);
                }
            });

            return this.$modal;
        }

        remove() {
            if (this.$modal) {
                this.$modal.remove();
                this.$modal = null;
            }
        }
    }

    const REVIEW_STATES = {
        ANSWERING: "answering",
        REVIEWING: "reviewing"};

    const REVIEW_EVENTS = {
        CLOSE: "close",
        NEXT_ITEM: "nextItem",
        COMPLETE: "complete",
        STUDY_AGAIN: "studyAgain"
    };

    class ReviewCard {
        constructor(item, state = REVIEW_STATES.ANSWERING) {
            this.item = item;
            this.state = state;
            this.$container = null;
            this.isKanji = !!this.item.readings;
            this.selectedOption = null;
            this.handleKanjiSelection = this.handleKanjiSelection.bind(this);
        }

        handleKanjiSelection(event, option) {
            const $selectedElement = $(event.currentTarget);
            
            this.$container.find('.kanji-option').css(styles.reviewModal.kanjiOption.base);
            
            $selectedElement.css({
                ...styles.reviewModal.kanjiOption.base,
                ...styles.reviewModal.kanjiOption.selected
            });
        
            this.selectedOption = option.id;
            
            const $submitButton = this.$container.find('#ep-review-submit');
            $submitButton
                .prop('disabled', false)
                .css({
                    ...styles.reviewModal.buttons.submit,
                    opacity: 1,
                    cursor: "pointer"
                });
        }


        getQuestionText() {
            if (this.item.type === "recognition") {
                return ["Select the kanji that means ", this.createEmphasisSpan(this.item.meaningToMatch)];
            }
            
            if (!this.isKanji) {
                return ["What is the meaning of this ", this.createEmphasisSpan("radical"), "?"];
            }
        
            if (this.item.type === "reading") {
                const readingType = this.item.readings.find(r => r.primary)?.type;
                const readingText = readingType === "onyomi" ? "on'yomi" : "kun'yomi";
                return ["What is the ", this.createEmphasisSpan(readingText), " reading for this kanji?"];
            }
        
            return ["What is the ", this.createEmphasisSpan("meaning"), " of this kanji?"];
        }
        
        createEmphasisSpan(text) {
            return $("<span>")
                .text(text)
                .css({
                    fontWeight: theme.typography.fontWeight.bold,
                    color: this.isKanji ? theme.colors.kanji : theme.colors.radical,
                    padding: `${theme.spacing.xs}`,
                    borderRadius: theme.borderRadius.sm,
                    backgroundColor: this.isKanji ? 
                        "rgba(235, 1, 156, 0.1)" : 
                        "rgba(5, 152, 228, 0.1)"
                });
        }

        createKanjiOption(option) {
            const $option = $("<div>")
                .addClass("kanji-option")
                .css(styles.reviewModal.kanjiOption.base)
                .data("kanji-id", option.id)
                .append(
                    $("<div>")
                        .addClass("kanji-character")
                        .css({
                            fontSize: theme.typography.fontSize["2xl"],
                            color: theme.colors.gray[800],
                            textAlign: "center"
                        })
                        .text(option.character)
                );

            $option.on("click", (event) => this.handleKanjiSelection(event, option));
            
            return $option;
        }

        async renderCharacter() {
            const $character = $("<div>")
                .addClass("ep-review-character")
                .css(styles.reviewModal.character);

            if (this.item.character) {
                $character.text(this.item.character);
            } else if (this.item.svg) {
                try {
                    const svgContent = await loadSvgContent(this.item.svg);
                    $character.html(svgContent);
                    const svg = $character.find("svg")[0];
                    if (svg) {
                        svg.setAttribute("width", "100%");
                        svg.setAttribute("height", "100%");
                    }
                } catch (error) {
                    console.error("Error loading SVG:", error);
                    $character.text(this.item.meaning);
                }
            }

            return $character;
        }

        async renderAnsweringState() {
            const $content = $("<div>").addClass("ep-review-content");
        
            if (this.item.type === "recognition") {
                return this.renderRecognitionCard($content);
            } else {
                const $character = await this.renderCharacter();
                const $question = $("<div>")
                    .addClass("ep-review-question")
                    .css({
                        fontSize: theme.typography.fontSize.lg,
                        marginBottom: theme.spacing.lg,
                        color: theme.colors.gray[700]
                    });
        
                const questionContent = this.getQuestionText();
                questionContent.forEach(content => {
                    if (content instanceof jQuery) {
                        $question.append(content);
                    } else {
                        $question.append(document.createTextNode(content));
                    }
                });
        
                const $inputSection = $("<div>")
                    .addClass("ep-review-input-section")
                    .css(styles.reviewModal.inputSection)
                    .append(
                        $("<input>")
                            .attr({
                                type: "text",
                                id: "ep-review-answer",
                                placeholder: this.item.type === "reading" ? "Enter reading..." : "Enter meaning...",
                                tabindex: "1",
                                autofocus: true
                            })
                            .css(styles.reviewModal.input),
                        $("<button>")
                            .attr("id", "ep-review-submit")
                            .text("Submit")
                            .attr("tabindex", "2")
                            .css(styles.reviewModal.buttons.submit)
                    );
        
                $content.append($character);
                $content.append($question);
                $content.append($inputSection);
                
                return $content;
            }
        }

        async renderStandardAnsweringCard($content) {
            const $character = await this.renderCharacter();
            const $question = $("<div>")
                .addClass("ep-review-question")
                .css({
                    fontSize: theme.typography.fontSize.lg,
                    marginBottom: theme.spacing.lg,
                    color: theme.colors.gray[700]
                });

            const questionContent = this.getQuestionText();
            questionContent.forEach(content => {
                if (content instanceof jQuery) {
                    $question.append(content);
                } else {
                    $question.append(document.createTextNode(content));
                }
            });

            const $inputSection = $("<div>")
                .addClass("ep-review-input-section")
                .css(styles.reviewModal.inputSection)
                .append(
                    $("<input>")
                        .attr({
                            type: "text",
                            id: "ep-review-answer",
                            placeholder: this.item.type === "reading" ? "Enter reading..." : "Enter meaning...",
                            tabindex: "1",
                            autofocus: true
                        })
                        .css(styles.reviewModal.input),
                    $("<button>")
                        .attr("id", "ep-review-submit")
                        .text("Submit")
                        .attr("tabindex", "2")
                        .css(styles.reviewModal.buttons.submit)
                );

            return $content.append($character, $question, $inputSection);
        }

        async renderRecognitionCard($content) {
            const $questionContainer = $("<div>")
                .css({
                    textAlign: "center",
                    marginBottom: theme.spacing.xl
                });

            const $question = $("<div>")
                .addClass("ep-review-question")
                .css({
                    fontSize: theme.typography.fontSize.lg,
                    color: theme.colors.gray[700],
                    marginBottom: theme.spacing.md
                });

            const questionContent = this.getQuestionText();
            questionContent.forEach(content => {
                if (content instanceof jQuery) {
                    $question.append(content);
                } else {
                    $question.append(document.createTextNode(content));
                }
            });

            $questionContainer.append($question);

            const $optionsGrid = $("<div>")
                .css({
                    display: "grid",
                    gridTemplateColumns: "repeat(2, 1fr)",
                    gap: theme.spacing.lg,
                    padding: theme.spacing.xl,
                    maxWidth: "500px",
                    margin: "0 auto"
                });

            this.item.options.forEach(option => {
                $optionsGrid.append(this.createKanjiOption(option));
            });

            const $submitButton = $("<button>")
                .attr({
                    id: "ep-review-submit",
                    disabled: true
                })
                .text("Submit")
                .css({
                    ...styles.reviewModal.buttons.submit,
                    opacity: 0.5,
                    cursor: "not-allowed"
                });

            const $submitButtonContainer = $("<div>")
                .css({
                    textAlign: "center",
                    marginTop: theme.spacing.xl
                })
                .append($submitButton);

            return $content.append($questionContainer, $optionsGrid, $submitButtonContainer);
        }

        processMnemonic(mnemonic) {
            if (!mnemonic) return "";

            if (!this.isKanji) {
                return mnemonic.replace(/<radical>(.*?)<\/radical>/g, (_, content) => 
                    `<span style="background-color: ${theme.colors.radical}; padding: 0 ${theme.spacing.xs}; border-radius: ${theme.borderRadius.sm}; color: ${theme.colors.white}">${content}</span>`
                );
            }

            return mnemonic
                .replace(/<radical>(.*?)<\/radical>/g, (_, content) => 
                    `<span style="background-color: ${theme.colors.radical}; padding: 0 ${theme.spacing.xs}; border-radius: ${theme.borderRadius.sm}; color: ${theme.colors.white}">${content}</span>`
                )
                .replace(/<kanji>(.*?)<\/kanji>/g, (_, content) => 
                    `<span style="background-color: ${theme.colors.kanji}; padding: 0 ${theme.spacing.xs}; border-radius: ${theme.borderRadius.sm}; color: ${theme.colors.white}">${content}</span>`
                )
                .replace(/<reading>(.*?)<\/reading>/g, (_, content) => 
                    `<span style="background-color: ${theme.colors.gray[200]}; padding: 0 ${theme.spacing.xs}; border-radius: ${theme.borderRadius.sm}; color: ${theme.colors.gray[800]}">${content}</span>`
                );
        }

        async renderReviewingState() {
            const $content = $("<div>").addClass("ep-review-content");
            const $character = await this.renderCharacter();
            const $explanation = $("<div>")
                .addClass("ep-review-explanation")
                .css(styles.reviewModal.explanation);

            const primaryReading = this.item.readings?.find(r => r.primary);
            const primaryMeaning = this.item.meanings?.find(m => m.primary);

            const $continueButton = $("<button>")
            .attr("id", "ep-review-continue")
            .text("Continue Review")
            .css({
                ...styles.reviewModal.buttons.submit,
                minWidth: "120px",
                display: "block",
                margin: "30px auto 0"
            });
            
            const $buttonContainer = $("<div>")
                .addClass("ep-review-buttons")
                .css({ 
                    display: "flex",
                    gap: theme.spacing.md,
                    justifyContent: "center",
                    marginTop: theme.spacing.xl
                })
                .append($continueButton);

            // Handle non-kanji (radical) review state
            if (!this.isKanji) {
                $content.append(
                    $character,
                    $explanation.append(
                        $("<h3>").append(
                            $("<span>")
                                .text("Meaning: ")
                                .css(styles.reviewModal.explanation.meaningLabel),
                            $("<a>")
                                .attr({
                                    href: this.item.documentationUrl,
                                    target: "_blank",
                                    title: `Click to learn more about: ${this.item.meaning}`
                                })
                                .text(this.item.meaning)
                                .css(styles.reviewModal.explanation.meaningText)
                        ),
                        $("<div>")
                            .addClass("ep-mnemonic-container")
                            .css(styles.reviewModal.explanation.mnemonicContainer)
                            .append(
                                $("<span>")
                                    .text("Mnemonic:")
                                    .css(styles.reviewModal.explanation.mnemonicLabel),
                                $("<div>")
                                    .addClass("ep-review-mnemonic")
                                    .html(this.processMnemonic(this.item.meaningMnemonic))
                                    .css(styles.reviewModal.explanation.mnemonic)
                            )
                    )
                );

                $content.append($buttonContainer);
                return $content;
            }

            // Handle kanji review states based on question type
            switch (this.item.type) {
                case "recognition":
                    $explanation.append(
                        this.createExplanationSection(
                            "Meaning",
                            this.item.meaningToMatch,
                            this.item.meaningMnemonic,
                            true
                        )
                    );

                    if (primaryReading) {
                        const readingType = primaryReading.type === "onyomi" ? "On'yomi" : "Kun'yomi";
                        $explanation.append(
                            this.createExplanationSection(
                                "Reading",
                                `${readingType}: ${primaryReading.reading}`,
                                this.item.readingMnemonic,
                                false
                            )
                        );
                    }
                    break;

                case "reading":
                    if (primaryReading) {
                        const readingType = primaryReading.type === "onyomi" ? "On'yomi" : "Kun'yomi";
                        $explanation.append(
                            this.createExplanationSection(
                                "Reading",
                                `${readingType}: ${primaryReading.reading}`,
                                this.item.readingMnemonic,
                                true
                            )
                        );
                    }

                    if (primaryMeaning) {
                        $explanation.append(
                            this.createExplanationSection(
                                "Meaning",
                                primaryMeaning.meaning,
                                this.item.meaningMnemonic,
                                false
                            )
                        );
                    }
                    break;

                case "meaning":
                    if (primaryMeaning) {
                        $explanation.append(
                            this.createExplanationSection(
                                "Meaning",
                                primaryMeaning.meaning,
                                this.item.meaningMnemonic,
                                true
                            )
                        );
                    }

                    if (primaryReading) {
                        const readingType = primaryReading.type === "onyomi" ? "On'yomi" : "Kun'yomi";
                        $explanation.append(
                            this.createExplanationSection(
                                "Reading",
                                `${readingType}: ${primaryReading.reading}`,
                                this.item.readingMnemonic,
                                false
                            )
                        );
                    }
                    break;
            }

            

            $content.append($character, $explanation);
            $content.append($buttonContainer);

            return $content;
        }

        createExplanationSection(title, answer, mnemonic, isExpanded) {
            const $section = $("<div>")
                .addClass("explanation-section")
                .css({
                    marginBottom: theme.spacing.md,
                    width: "100%",
                    display: "block"
                });
        
            const $header = $("<div>")
                .css({
                    display: "block",
                    padding: `${theme.spacing.sm} 0`,
                    width: "100%",
                    borderBottom: `1px solid ${theme.colors.gray[200]}`,
                });
                

            const $headerContent = $("<div>")
                .css({
                    display: "flex",
                    alignItems: "center",
                    cursor: "pointer",
                    width: "100%"
                }).append(
                    $("<span>")
                        .text(isExpanded ? "▼" : "▶")
                        .css({ 
                            color: theme.colors.gray[600],
                            marginRight: theme.spacing.sm,
                            fontSize: theme.typography.fontSize.md,
                            flexShink: 0
                        }),
                    $("<h3>")
                        .text(title)
                        .css({
                            margin: 0,
                            color: theme.colors.gray[800],
                            fontWeight: theme.typography.fontWeight.medium,
                            fontSize: theme.typography.fontSize.md,
                            flex: 1
                        })
                );
            
            $header.append($headerContent);
        
            const $content = $("<div>")
                .css({
                    display: isExpanded ? "block" : "none",
                    paddingLeft: theme.spacing.xl,
                    paddingTop: theme.spacing.md,
                    paddingBottom: theme.spacing.md
                });
        
            if (title.toLowerCase() === "reading") {
                // Extract reading type and format display
                const readingType = this.item.readings.find(r => r.primary)?.type;
                const formattedType = readingType === "onyomi" ? "On'yomi" : "Kun'yomi";
                
                $content.append(
                    $("<div>")
                        .css({
                            fontSize: theme.typography.fontSize.lg,
                            color: theme.colors.gray[800],
                            marginBottom: theme.spacing.md
                        })
                        .append(
                            $("<span>")
                                .text(`${formattedType}: `)
                                .css({
                                    color: theme.colors.gray[600],
                                    fontSize: theme.typography.fontSize.md
                                }),
                            $("<span>")
                                .text(this.item.readings.find(r => r.primary)?.reading || "")
                        )
                );
        
                if (mnemonic) {
                    $content.append(
                        $("<div>")
                            .addClass("ep-mnemonic-container")
                            .css(styles.reviewModal.explanation.mnemonicContainer)
                            .append(
                                $("<span>")
                                    .text("Mnemonic:")
                                    .css(styles.reviewModal.explanation.mnemonicLabel),
                                $("<div>")
                                    .addClass("ep-review-mnemonic")
                                    .html(this.processMnemonic(mnemonic))
                                    .css(styles.reviewModal.explanation.mnemonic)
                            )
                    );
                }
            } else {
                const meaningText = this.item.type === "recognition" 
                    ? this.item.meaningToMatch
                    : this.item.meanings.find(m => m.primary)?.meaning;
        
                $content.append(
                    $("<div>")
                        .css({
                            fontSize: theme.typography.fontSize.lg,
                            color: theme.colors.gray[800],
                            marginBottom: theme.spacing.md
                        })
                        .text(meaningText)
                );
        
                if (mnemonic) {
                    $content.append(
                        $("<div>")
                            .addClass("ep-mnemonic-container")
                            .css(styles.reviewModal.explanation.mnemonicContainer)
                            .append(
                                $("<span>")
                                    .text("Mnemonic:")
                                    .css(styles.reviewModal.explanation.mnemonicLabel),
                                $("<div>")
                                    .addClass("ep-review-mnemonic")
                                    .html(this.processMnemonic(mnemonic))
                                    .css(styles.reviewModal.explanation.mnemonic)
                            )
                    );
                }
            }
        
            $header.on("click", function() {
                const $content = $(this).siblings("div");
                const isVisible = $content.is(":visible");
                $content.slideToggle(200);
                const $arrow = $(this).find("span").first();
                $arrow.text(isVisible ? "▶" : "▼");
            });
        
            return $section.append($header, $content);
        }

        async render() {
            this.$container = $("<div>")
                .addClass("ep-review-card")
                .css({
                    padding: theme.spacing.xl,
                    display: "flex",
                    flexDirection: "column",
                    width: "100%",
                    gap: theme.spacing.xl
                });
        
            const $characterContainer = $("<div>")
                .css({
                    textAlign: "center",
                    width: "100%"
                });
        
            const $contentContainer = $("<div>")
                .css({
                    width: "100%",
                    textAlign: "left"
                });
        
            const content = await (this.state === REVIEW_STATES.ANSWERING
                ? this.renderAnsweringState()
                : this.renderReviewingState());
        
            if (this.state === REVIEW_STATES.ANSWERING) {
                const $character = content.find(".ep-review-character").detach();
                $characterContainer.append($character);
                
                $contentContainer.append(content);
            } else {
                const $character = content.find(".ep-review-character").detach();
                $characterContainer.append($character);
                
                $contentContainer.append(content.find(".ep-review-explanation"));
            }
        
            this.$container.append($characterContainer, $contentContainer);
            return this.$container;
        }

        async updateState(newState) {
            if (this.state === newState) return;
            
            this.state = newState;
            const content = this.state === REVIEW_STATES.ANSWERING
                ? await this.renderAnsweringState()
                : await this.renderReviewingState();

            this.$container.empty().append(content);
        }

        getAnswer() {
            if (this.item.type === "recognition") {
                return this.selectedOption?.toString() || "";
            }
            return $("#ep-review-answer").val()?.trim() || "";
        }

        remove() {
            if (this.$container) {
                this.$container.remove();
                this.$container = null;
            }
        }
    }

    class ReviewSessionModal {
        constructor(reviewSession) {
            this.reviewSession = reviewSession;
            this.state = REVIEW_STATES.ANSWERING;
            this.$modal = null;
            this.currentCard = null;
            this.callbacks = new Map();
            this.isKanjiSession = !!this.reviewSession.correctMeanings;

            // Session configuration for Play Again
            this.sessionConfig = {
                mode: this.reviewSession.mode,
                items: this.reviewSession.originalItems,
            };

            if (this.sessionConfig.mode !== "radical") {
                this.sessionConfig.allUnlockedKanji = this.reviewSession.allUnlockedKanji;
            }
        
            this.handlePlayAgain = this.handlePlayAgain.bind(this);
            this.handleAnswer = this.handleAnswer.bind(this);
            this.handleNextItem = this.handleNextItem.bind(this);
            this.showHint = this.showHint.bind(this);
            this.setupInput = this.setupInput.bind(this);
            this.showCurrentItem = this.showCurrentItem.bind(this);
            this.updateProgress = this.updateProgress.bind(this);
            this.showReviewInterface = this.showReviewInterface.bind(this);
            this.hideReviewInterface = this.hideReviewInterface.bind(this);
            this.showInputInterface = this.showInputInterface.bind(this);
            this.hideInputInterface = this.hideInputInterface.bind(this);
            this.showCompletionScreen = this.showCompletionScreen.bind(this);
        }

        // Setup Hiragana Keyboard
        setupInput() {
            const input = document.querySelector("#ep-review-answer");
            if (!input) return;

            const currentItem = this.reviewSession.currentItem;
            if (!currentItem) return;

            if (this.isKanjiSession && currentItem.type === "reading") {
                wanakana.bind(input, {
                    IMEMode: "toHiragana",
                    useObsoleteKana: false,
                    passRomaji: false,
                    upcaseKatakana: false,
                    convertLongVowelMark: true
                });
            }
        }

        on(event, callback) {
            this.callbacks.set(event, callback);
            return this;
        }

        emit(event, data) {
            const callback = this.callbacks.get(event);
            if (callback) callback(data);
        }

        handlePlayAgain() {
            const newSession = this.isKanjiSession ? new KanjiReviewSession({
                items: this.sessionConfig.items,
                mode: this.sessionConfig.mode,
                allUnlockedKanji: this.sessionConfig.allUnlockedKanji
            }) : new RadicalReviewSession({
                items: this.sessionConfig.items,
                mode: "radical",
            });

            // Initialize new session
            newSession.nextItem();

            // Clean up current modal
            this.remove();

            const newModal = new ReviewSessionModal(newSession);
            newModal
                .on(REVIEW_EVENTS.CLOSE, () => {
                    enableScroll();
                    newModal.remove();
                })
                .on(REVIEW_EVENTS.STUDY_AGAIN, () => {
                    newModal.remove();
                    enableScroll();
                    if (this.isKanjiSession) {
                        handleKanjiPractice();
                    } else {
                        handleRadicalPractice();
                    }
                });
            
                return newModal.render();
        }

        updateProgress() {
            const progress = this.reviewSession.getProgress();
            const mode = this.reviewSession.mode;
            let progressText;

            switch (mode) {
                case PRACTICE_MODES.ENGLISH_TO_KANJI:
                    progressText = `${progress.recognitionProgress}/${progress.total} Correct`;
                    break;
                case PRACTICE_MODES.COMBINED:
                    progressText = `Meanings: ${progress.meaningProgress}/${progress.total/3} | ` +
                                 `Readings: ${progress.readingProgress}/${progress.total/3} | ` +
                                 `Recognition: ${progress.recognitionProgress}/${progress.total/3}`;
                    break;
                case PRACTICE_MODES.STANDARD:
                    progressText = `Meanings: ${progress.meaningProgress}/${progress.total/2} | ` +
                                 `Readings: ${progress.readingProgress}/${progress.total/2}`;
                    break;
                default: // RADICAL 
                    progressText = `${progress.current}/${progress.total/1} Correct`;
            }

            $("#ep-review-progress-correct").html(progressText);

            if (mode === PRACTICE_MODES.COMBINED) {
                $("#ep-review-progress-correct").css({
                    fontSize: theme.typography.fontSize.xs
                });
            }
            
        }

        showReviewInterface() {
            $("#ep-review-result").show();
            $("#ep-review-result-message").show();
            $("#ep-review-explanation").show();
            $(".ep-review-buttons").hide();
        }

        hideReviewInterface() {
            $("#ep-review-result").hide();
            $("#ep-review-result-message").hide();
            $("#ep-review-explanation").hide();
            $("#ep-review-show-hint").hide();
            $(".ep-review-buttons").show();
        }

        showInputInterface() {
            $("#ep-review-input-section").show();
            $("#ep-review-answer").val("").prop("disabled", false);
            $("#ep-review-submit").show();
            $("#ep-review-answer").focus();

            this.setupInput();
        }

        hideInputInterface() {
            $("#ep-review-input-section").hide();
            $("#ep-review-submit").hide();
            $("#ep-review-answer").prop("disabled", true);
        }

        async showCurrentItem() {
            const currentItem = this.reviewSession.currentItem;
            
            if (this.currentCard) {
                this.currentCard.remove();
            }
        
            this.state = REVIEW_STATES.ANSWERING;
            this.hideReviewInterface();
            
            this.currentCard = new ReviewCard(currentItem, REVIEW_STATES.ANSWERING);
            const $card = await this.currentCard.render();
            
            // Clear and append the new card
            $("#ep-review-content").empty().append($card);
            
            // Ensure input is focused after rendering
            if (currentItem.type !== "recognition") {
                const $input = $("#ep-review-answer");
                if ($input.length) {
                    $input.focus();
                    this.setupInput();
                }
            }
        }

        async handleAnswer() {
            const currentCard = this.currentCard;
            if (!currentCard) return;
        
            const userAnswer = currentCard.getAnswer();
            if (!userAnswer) return;
        
            const isCorrect = this.reviewSession.checkAnswer(userAnswer);
            
            $(".ep-review-input-section, .ep-review-question, .ep-review-content, .kanji-option, #ep-review-submit").hide();
            $(".ep-review-character").css({
                marginBottom: "0"
            });
        
            // Create result container if it doesn't exist
            if ($("#ep-review-result-container").length === 0) {
                $(".ep-review-card").append(
                    $("<div>")
                        .attr("id", "ep-review-result-container")
                        .css({
                            ...styles.reviewModal.content,
                            padding: 0
                        })
                );
            }
        
            if (isCorrect) {
                $("#ep-review-result-container")
                    .empty()
                    .append(
                        $("<div>")
                            .attr("id", "ep-review-result-message")
                            .text("Correct!")
                            .css({
                                ...styles.reviewModal.results.message,
                                color: theme.colors.success,
                            })
                    );
                    
                this.updateProgress();
                setTimeout(() => this.handleNextItem(), 1000);
            } else {
                $("#ep-review-result-container")
                    .empty()
                    .append(
                        $("<div>")
                            .attr("id", "ep-review-result-message")
                            .text("Incorrect")
                            .css({
                                ...styles.reviewModal.results.message,
                                color: theme.colors.error,
                            }),
                        $("<div>")
                            .addClass("ep-review-buttons")
                            .css({ 
                                display: "flex",
                                gap: theme.spacing.md,
                                justifyContent: "center" 
                            })
                            .append(
                                $("<button>")
                                    .attr("id", "ep-review-show-hint")
                                    .text("Show Answer")
                                    .css({
                                        ...styles.reviewModal.buttons.hint,
                                        minWidth: "120px"
                                    }),
                                $("<button>")
                                    .attr("id", "ep-review-continue")
                                    .text("Continue Review")
                                    .css({
                                        ...styles.reviewModal.buttons.submit,
                                        minWidth: "120px"
                                    })
                            )
                    );
            }
        }

        async showHint() {
            await this.currentCard.updateState(REVIEW_STATES.REVIEWING);
        }

        async handleNextItem() {
            if (this.reviewSession.isComplete()) {
                this.showCompletionScreen();
                return;
            }

            this.reviewSession.nextItem();
            await this.showCurrentItem();
            this.emit(REVIEW_EVENTS.NEXT_ITEM);
        }

        showCompletionScreen() {
            const progress = this.reviewSession.getProgress();
            const mode = this.reviewSession.mode;
            
            let languageLearningQuotes;

            if (this.isKanjiSession) {
                languageLearningQuotes = [
                    "Every kanji you learn unlocks new understanding",
                    "One character a day",
                    "Continuation is power",
                    "Each review strengthens your kanji recognition",
                    "Little by little, steadily",
                    "Each character you master opens new doors to understanding",
                    "Your journey through the world of kanji grows stronger each day"
                ]; 
            } else {
                languageLearningQuotes = [
                    "Every radical mastered unlocks new understanding",
                    "Building your foundation, one radical at a time",
                    "Mastering radicals today, recognizing kanji tomorrow",
                    "Each radical review strengthens your foundation",
                    "Little by little, your radical knowledge grows",
                    "Each radical you master opens new paths of understanding",
                    "Your journey through radicals grows stronger each day",
                    "Steady progress in radicals paves the way forward",
                    "Your radical knowledge builds the bridge to comprehension"
                ];
            }
            
            const randomQuote = languageLearningQuotes[
                Math.floor(Math.random() * languageLearningQuotes.length)
            ];

            let completionMessage;
            switch (mode) {
                case PRACTICE_MODES.ENGLISH_TO_KANJI:
                    completionMessage = `Review completed!<br>${progress.recognitionProgress}/${progress.total} Correct`;
                    break;
                case PRACTICE_MODES.COMBINED:
                    completionMessage = `Review completed!<br>` +
                        `Meanings: ${progress.meaningProgress}/${progress.total/3} | ` +
                        `Readings: ${progress.readingProgress}/${progress.total/3} | ` +
                        `Recognition: ${progress.recognitionProgress}/${progress.total/3}`;
                    break;
                case PRACTICE_MODES.STANDARD:
                    completionMessage = `Review completed!<br>` +
                        `Meanings: ${progress.meaningProgress}/${progress.total/2} | ` +
                        `Readings: ${progress.readingProgress}/${progress.total/2}`;
                    break;
                default:
                    completionMessage = `Review completed!`;
                    
            }

            const $completionContent = $("<div>")
                .css({
                    textAlign: "center",
                    padding: theme.spacing.xl
                })
                .append(
                    $("<h1>")
                        .html(completionMessage)
                        .css({
                            ...styles.reviewModal.progress,
                            marginBottom: theme.spacing.lg
                        }),
                    $("<p>")
                        .text(`"${randomQuote}"`)
                        .css({
                            color: theme.colors.gray[600],
                            marginBottom: theme.spacing.xl,
                            fontStyle: "italic"
                        }),
                        $("<div>")
                        .css({
                            display: "flex",
                            gap: theme.spacing.md,
                            justifyContent: "center"
                        })
                        .append(
                            $("<button>")
                                .text("Play Again")
                                .css({
                                    ...styles.reviewModal.buttons.submit,
                                    backgroundColor: theme.colors.success,
                                    minWidth: "120px"
                                })
                                .on("click", this.handlePlayAgain),
                            $("<button>")
                                .text("Study Different Items")
                                .css({
                                    ...styles.reviewModal.buttons.submit,
                                    minWidth: "120px"
                                })
                                .on("click", () => {
                                    this.emit(REVIEW_EVENTS.STUDY_AGAIN);
                                })
                        )
                );

            $("#ep-review-content").empty().append($completionContent);
            this.emit(REVIEW_EVENTS.COMPLETE, { progress });
        }

        async render() {
            this.$modal = $(reviewModalTemplate).appendTo("body");
            
            this.$modal.css({
                position: "fixed",
                top: 0,
                left: 0,
                width: "100%",
                height: "100%",
                backgroundColor: "rgba(0, 0, 0, 0.9)",
                zIndex: theme.zIndex.modal,
                display: "flex",
                alignItems: "center",
                justifyContent: "center"
            });

            $("#ep-review-modal-wrapper").css(styles.reviewModal.container);
            $("#ep-review-modal-header").css(styles.reviewModal.header);
            $("#ep-review-progress").css(styles.reviewModal.progress);
            $("#ep-review-exit").css(styles.reviewModal.buttons.exit);

            // Set up event delegation
            this.$modal
                .on("click", "#ep-review-submit", this.handleAnswer)
                .on("keypress", "#ep-review-answer", (e) => {
                    if (e.which === 13) {
                        this.handleAnswer();
                    }
                })
                .on("click", "#ep-review-show-hint", this.showHint)
                .on("click", "#ep-review-continue", this.handleNextItem);

            $("#ep-review-exit").on("click", () => {
                this.emit(REVIEW_EVENTS.CLOSE);
            });

            this.updateProgress();
            await this.showCurrentItem();

            return this.$modal;
        }

        remove() {
            if (this.currentCard) {
                this.currentCard.remove();
            }

            const input = document.querySelector("#ep-review-answer");
            if (input) {
                wanakana.unbind(input);
            }

            if (this.$modal) {
                this.$modal.remove();
                this.$modal = null;
            }
        }
    }

    // Assumption: User has wkof.file_cache for the IndexedDB operations to work

    async function getCurrentUserLevel() {    
        return new Promise((resolve, reject) => {
            const request = indexedDB.open(DB_VALUES.DB_NAME, 1);
            
            request.onsuccess = (event) => {
                
                const db = event.target.result;
                const transaction = db.transaction([DB_VALUES.FILE_STORE], "readonly");
                const store = transaction.objectStore(DB_VALUES.FILE_STORE);
                const getUser = store.get(DB_VALUES.USER_RECORD);
                
                getUser.onsuccess = () => {
                    const userData = getUser.result;
                    resolve(userData.content.data.level);
                };
                
                getUser.onerror = () => {
                    reject(handleError("USER_LEVEL"));
                };
            };
            
            request.onerror = () => {
                reject(handleError("OPEN"));
            };
        });
    }

    async function getCurrentLevelRadicals() {
        try {
            const userLevel = await getCurrentUserLevel();
            
            return new Promise((resolve, reject) => {
                const request = indexedDB.open(DB_VALUES.DB_NAME, 1);
                
                request.onsuccess = (event) => {
                    const db = event.target.result;
                    const transaction = db.transaction([DB_VALUES.FILE_STORE], "readonly");
                    const store = transaction.objectStore(DB_VALUES.FILE_STORE);
                    const getSubjects = store.get(DB_VALUES.SUBJECT_RECORD);
                    
                    getSubjects.onsuccess = () => {
                        const subjectsData = getSubjects.result;
                        
                        const currentLevelRadicals = Object.values(subjectsData.content.data)
                            .filter(subject => 
                                subject.object === "radical" && 
                                subject.data.level === userLevel
                            )
                            .map(radical => ({
                                id: radical.id,
                                character: radical.data.characters,
                                meaning: radical.data.meanings[0].meaning,
                                documentationUrl: radical.data.document_url,
                                meaningMnemonic: radical.data.meaning_mnemonic,
                                svg: radical.data.character_images.find(img => 
                                    img.content_type === "image/svg+xml"
                                )?.url || null
                            }));
                        
                        resolve(currentLevelRadicals);
                    };
                    
                    getSubjects.onerror = () => {
                        reject(handleError("SUBJECT_DATA"));
                    };
                };
                
                request.onerror = () => {
                    reject(handleError("OPEN"));
                };
            });
        } catch (error) {
            console.error("Error in getCurrentLevelRadicals:", error);
            throw error;
        }
    }

    async function getCurrentLevelKanji() {
        return new Promise(async (resolve, reject) => {
            const userLevel = await getCurrentUserLevel();

            const request = indexedDB.open('wkof.file_cache', 1);
            
            request.onsuccess = (event) => {
                const db = event.target.result;
                const transaction = db.transaction(['files'], 'readonly');
                const store = transaction.objectStore('files');
                
                Promise.all([
                    new Promise(resolve => {
                        store.get('Apiv2.assignments').onsuccess = (e) => 
                            resolve(e.target.result.content.data);
                    }),
                    new Promise(resolve => {
                        store.get('Apiv2.subjects').onsuccess = (e) => 
                            resolve(e.target.result.content.data);
                    })
                ]).then(([assignments, subjects]) => {
                    const unlockedKanjiIds = new Set(
                        Object.values(assignments)
                            .filter(a => a.data.subject_type === "kanji")
                            .map(a => a.data.subject_id)
                    );

                    // Helper function to get radical information
                    const getRadicalInfo = (radicalId) => {
                        const radical = subjects[radicalId];
                        if (!radical) return null;
                        
                        return {
                            id: radical.id,
                            character: radical.data.characters,
                            meaning: radical.data.meanings[0].meaning,
                            svg: radical.data.character_images?.find(img => 
                                img.content_type === 'image/svg+xml'
                            )?.url || null
                        };
                    };
                    
                    const currentLevelKanji = Object.values(subjects)
                        .filter(subject => 
                            subject.object === "kanji" && 
                            subject.data.level === userLevel &&
                            unlockedKanjiIds.has(subject.id)
                        )
                        .map(kanji => ({
                            id: kanji.id,
                            character: kanji.data.characters,
                            meanings: kanji.data.meanings.filter(m => m.accepted_answer),
                            readings: kanji.data.readings.filter(r => r.accepted_answer),
                            meaningMnemonic: kanji.data.meaning_mnemonic,
                            meaningHint: kanji.data.meaning_hint,
                            readingMnemonic: kanji.data.reading_mnemonic,
                            readingHint: kanji.data.reading_hint,
                            documentUrl: kanji.data.document_url,
                            radicals: kanji.data.component_subject_ids
                                .map(getRadicalInfo)
                                .filter(Boolean),
                            auxiliaryMeanings: kanji.data.auxiliary_meanings
                                ?.filter(m => m.type === "whitelist")
                                ?? []
                        }));
                    
                    resolve(currentLevelKanji);
                });
            };

            request.onerror = (error) => reject(error);
        });
    }

    function handleError(type) {
        if (type == "OPEN") {
            return new Error(DB_ERRORS.OPEN);
        }

        if (type == "USER_LEVEL") {
            return new Error(DB_ERRORS.USER_LEVEL);
        }

        if (type == "SUBJECT_DATA") {
            return new Error(DB_ERRORS.SUBJECT_DATA);
        }
    }

    async function handleRadicalPractice() {
        try {
            disableScroll();
            const radicals = await getCurrentLevelRadicals();
            
            const selectionModal = new RadicalSelectionModal(radicals)
                .on(EVENTS$1.CLOSE, () => {
                    enableScroll();
                    selectionModal.remove();
                })
                .on(EVENTS$1.START_REVIEW, (selectedRadicals) => {
                    selectionModal.remove();
                    startRadicalReview(selectedRadicals);
                });

            await selectionModal.render();

        } catch (error) {
            console.error("Error in radical practice:", error);
            enableScroll();
        }
    }

    async function startRadicalReview(selectedRadicals) {
        try {
            const session = {
                items: selectedRadicals,
                mode: "radical",
            };

            const reviewSession = new RadicalReviewSession(session);
            reviewSession.nextItem();

            const reviewModal = new ReviewSessionModal(reviewSession);

            reviewModal
                .on(REVIEW_EVENTS.CLOSE, () => {
                    const progress = reviewSession.getProgress();
                    $("#ep-review-modal-header").remove();
                    $("#ep-review-content")
                        .empty()
                        .append(
                            $("<div>")
                                .css(styles.reviewModal.content)
                                .append([
                                    $("<p>", { 
                                        css: {
                                            ...styles.reviewModal.progress,
                                            marginBottom: 0
                                        },
                                        text: `${progress.current}/${progress.total} Correct (${progress.percentComplete}%)` 
                                    }), 
                                    $("<p>", {
                                        css: {
                                            marginTop: 0,
                                            textAlign: "center"
                                        },
                                        text: "Closing..."
                                    })
                                ])
                        );

                    setTimeout(() => {
                        enableScroll();
                        reviewModal.remove();
                    }, 1000);
                })
                .on(REVIEW_EVENTS.STUDY_AGAIN, () => {
                    reviewModal.remove();
                    enableScroll();
                    handleRadicalPractice();
                });

            await reviewModal.render();
        } catch (error) {
            console.error("Error in startRadicalReview:", error);
            enableScroll();
        }
    }

    const MODAL_STATES = {
        READY: "ready"
    };

    const EVENTS = {
        CLOSE: "close",
        START_REVIEW: "startReview"
    };

    class KanjiGrid {
        constructor(kanji, onSelectionChange) {
            this.kanji = kanji;
            this.selectedKanji = new Set();
            this.onSelectionChange = onSelectionChange;
            this.$container = null;
        }

        updateKanjiSelection($element, kanji, isSelected) {
            const baseStyles = {
                ...styles.practiceModal.radical.base,
                border: `2px solid ${isSelected ? theme.colors.kanji : 'rgba(255, 255, 255, 0.2)'}`,
                background: isSelected ? 'rgba(235, 1, 156, 0.2)' : 'rgba(255, 255, 255, 0.1)',
                transition: 'all 0.2s ease',
                '&:hover': {
                    borderColor: theme.colors.kanji,
                    background: isSelected ? 'rgba(235, 1, 156, 0.3)' : 'rgba(255, 255, 255, 0.2)'
                }
            };

            $element.css(baseStyles);

            if (isSelected) {
                this.selectedKanji.add(kanji.id);
            } else {
                this.selectedKanji.delete(kanji.id);
            }

            this.onSelectionChange(this.selectedKanji);
        }

        toggleAllKanji(shouldSelect) {
            if (shouldSelect) {
                this.kanji.forEach(kanji => this.selectedKanji.add(kanji.id));
            } else {
                this.selectedKanji.clear();
            }

            this.$container.find(".kanji-selection-item").each((_, element) => {
                const $element = $(element);
                const kanjiId = parseInt($element.data("kanji-id"));
                this.updateKanjiSelection(
                    $element,
                    this.kanji.find(k => k.id === kanjiId),
                    shouldSelect
                );
            });

            this.onSelectionChange(this.selectedKanji);
        }

        getSelectedKanji() {
            return Array.from(this.selectedKanji).map(id => 
                this.kanji.find(kanji => kanji.id === id)
            );
        }

        createKanjiElement(kanji) {
            const $element = $("<div>")
                .addClass("kanji-selection-item")
                .css({
                    ...styles.practiceModal.radical.base,
                    position: "relative"
                })
                .data("kanji-id", kanji.id)
                .append(
                    $("<div>")
                        .addClass("kanji-character")
                        .css({
                            fontSize: theme.typography.fontSize.xl,
                            color: theme.colors.white
                        })
                        .text(kanji.character)
                );

            $element
                .on("click", () => {
                    const isCurrentlySelected = this.selectedKanji.has(kanji.id);
                    this.updateKanjiSelection($element, kanji, !isCurrentlySelected);
                });

            return $element;
        }

        async render() {
            this.$container = $("<div>")
                .css({
                    ...styles.practiceModal.grid,
                    gridTemplateColumns: "repeat(auto-fill, minmax(80px, 1fr))"
                });

            this.kanji.forEach(kanji => {
                const $element = this.createKanjiElement(kanji);
                this.$container.append($element);
            });
            
            return this.$container;
        }
    }

    class KanjiSelectionModal {
        constructor(kanji, allUnlockedKanji) {
            this.kanji = kanji;
            this.allUnlockedKanji = allUnlockedKanji;
            this.selectedMode = PRACTICE_MODES.STANDARD;
            this.state = MODAL_STATES.READY;
            this.totalKanji = kanji.length;
            this.$modal = null;
            this.kanjiGrid = null;
            this.callbacks = new Map();
        }

        on(event, callback) {
            this.callbacks.set(event, callback);
            return this;
        }

        emit(event, data) {
            const callback = this.callbacks.get(event);
            if (callback) callback(data);
        }

        validateSelection(selectedCount) {
            const minRequired = {
                [PRACTICE_MODES.STANDARD]: 1,
                [PRACTICE_MODES.ENGLISH_TO_KANJI]: 4,
                [PRACTICE_MODES.COMBINED]: 4
            };

            const required = minRequired[this.selectedMode];
            const isValid = selectedCount >= required;
            const startButton = $("#ep-practice-modal-start");

            if (isValid) {
                startButton
                    .prop("disabled", false)
                    .text(`Start Review (${selectedCount} Selected)`)
                    .css({
                        ...styles.practiceModal.buttons.start.base,
                        ...styles.practiceModal.buttons.start.kanji,
                        opacity: 1,
                        cursor: "pointer"
                    });
            } else {
                startButton
                    .prop("disabled", true)
                    .text(`Select at least ${required} kanji`)
                    .css({
                        ...styles.practiceModal.buttons.start.base,
                        ...styles.practiceModal.buttons.start.kanji,
                        opacity: 0.5,
                        cursor: "not-allowed"
                    });
            }
        }

        updateSelectAllButton(selectedCount) {
            const selectAllButton = $("#ep-practice-modal-select-all");
            const isAllSelected = selectedCount === this.totalKanji;
            
            selectAllButton
                .text(isAllSelected ? "Deselect All" : "Select All")
                .css({
                    color: isAllSelected ? theme.colors.error : theme.colors.white,
                    borderColor: isAllSelected ? theme.colors.error : theme.colors.white,
                    '&:hover': {
                        borderColor: isAllSelected ? theme.colors.error : theme.colors.kanji
                    }
                });
        }

        handleSelectionChange(selectedKanji) {
            const selectedCount = selectedKanji.size;
            this.updateSelectAllButton(selectedCount);
            this.validateSelection(selectedCount);
        }

        createModeSelector() {
            const $container = $("<div>")
                .css(styles.practiceModal.modeSelector.container);

            const $label = $("<div>")
                .text("Select Practice Mode")
                .css(styles.practiceModal.modeSelector.label);

            const $options = $("<div>")
                .css(styles.practiceModal.modeSelector.options);

            const createOption = (mode, label) => {
                const $option = $("<button>")
                    .text(label)
                    .css({
                        ...styles.practiceModal.modeSelector.option.base,
                        ...(this.selectedMode === mode ? styles.practiceModal.modeSelector.option.selected : {})
                    })
                    .on("click", () => {
                        $options.find("button").css(styles.practiceModal.modeSelector.option.base);
                        $option.css({
                            ...styles.practiceModal.modeSelector.option.base,
                            ...styles.practiceModal.modeSelector.option.selected
                        });
                        
                        this.selectedMode = mode;
                        const currentSelection = this.kanjiGrid.getSelectedKanji();
                        this.validateSelection(currentSelection.length);
                    });
                return $option;
            };

            $options.append(
                createOption(PRACTICE_MODES.STANDARD, "Standard Practice"),
                createOption(PRACTICE_MODES.ENGLISH_TO_KANJI, "English → Kanji"),
                createOption(PRACTICE_MODES.COMBINED, "Combined Practice")
            );

            return $container.append($label, $options);
        }

        async render() {
            this.$modal = $(modalTemplate).appendTo("body");
            
            $("#username").text($("p.user-summary__username:first").text());
            
            this.$modal.css(styles.practiceModal.backdrop);
            $("#ep-practice-modal-welcome").css(styles.practiceModal.welcomeText.container);
            $("#ep-practice-modal-welcome h1").css(styles.practiceModal.welcomeText.username);
            $("#ep-practice-modal-welcome h2")
                .text("Please select the Kanji characters you would like to practice")
                .css({
                    color: theme.colors.white,
                    opacity: 0.9
                });

            const $modeSelector = this.createModeSelector();
            $modeSelector.insertAfter("#ep-practice-modal-welcome");

            $("#ep-practice-modal-footer").css(styles.practiceModal.footer);
            $("#ep-practice-modal-content").css(styles.practiceModal.contentWrapper);
            
            // Initial disabled state with kanji color scheme
            $("#ep-practice-modal-start").css({
                ...styles.practiceModal.buttons.start.base,
                ...styles.practiceModal.buttons.start.kanji,
                opacity: 0.5,
                cursor: "not-allowed"
            });

            $("#ep-practice-modal-select-all").css({
                ...styles.practiceModal.buttons.selectAll,
                '&:hover': {
                    borderColor: theme.colors.kanji
                }
            });

            $("#ep-practice-modal-close").css({
                ...styles.practiceModal.buttons.exit,
                '&:hover': {
                    borderColor: theme.colors.kanji,
                    color: theme.colors.kanji
                }
            });

            this.kanjiGrid = new KanjiGrid(
                this.kanji,
                this.handleSelectionChange.bind(this)
            );

            const $grid = await this.kanjiGrid.render();
            $("#ep-practice-modal-grid").replaceWith($grid);

            $("#ep-practice-modal-select-all").on("click", () => {
                const isSelectingAll = $("#ep-practice-modal-select-all").text() === "Select All";
                this.kanjiGrid.toggleAllKanji(isSelectingAll);
            });

            $("#ep-practice-modal-close").on("click", () => {
                this.emit(EVENTS.CLOSE);
            });

            $("#ep-practice-modal-start").on("click", () => {
                const selectedKanji = this.kanjiGrid.getSelectedKanji();
                const minRequired = {
                    [PRACTICE_MODES.STANDARD]: 1,
                    [PRACTICE_MODES.ENGLISH_TO_KANJI]: 4,
                    [PRACTICE_MODES.COMBINED]: 4
                };

                if (selectedKanji.length >= minRequired[this.selectedMode]) {
                    this.emit(EVENTS.START_REVIEW, {
                        kanji: selectedKanji,
                        mode: this.selectedMode,
                        allUnlockedKanji: this.allUnlockedKanji
                    });
                }
            });

            return this.$modal;
        }

        remove() {
            if (this.$modal) {
                this.$modal.remove();
                this.$modal = null;
            }
        }
    }

    async function handleKanjiPractice() {
        try {
            disableScroll();
            const kanji = await getCurrentLevelKanji();
            
            const selectionModal = new KanjiSelectionModal(kanji, kanji)  // Using current level kanji as unlocked list for now
                .on(EVENTS.CLOSE, () => {
                    enableScroll();
                    selectionModal.remove();
                })
                .on(EVENTS.START_REVIEW, (data) => {
                    selectionModal.remove();
                    startKanjiReview(data.kanji, data.mode, data.allUnlockedKanji);
                });

            await selectionModal.render();

        } catch (error) {
            console.error("Error in kanji practice:", error);
            enableScroll();
        }
    }

    async function startKanjiReview(selectedKanji, mode, allUnlockedKanji) {
        try {
            const reviewSession = new KanjiReviewSession({ 
                items: selectedKanji, 
                mode: mode,
                allUnlockedKanji: allUnlockedKanji
            });
            
            reviewSession.nextItem();

            const reviewModal = new ReviewSessionModal(reviewSession);

            reviewModal
                .on(REVIEW_EVENTS.CLOSE, () => {
                    const progress = reviewSession.getProgress();
                    $("#ep-review-modal-header").remove();

                    const closingContent = [$("<p>", {
                        css: {
                            marginTop: 0,
                            textAlign: "center"
                        },
                        text: "Closing..."
                    })];

                    $("#ep-review-content")
                        .empty()
                        .append(
                            $("<div>")
                                .css(styles.reviewModal.content)
                                .append((() => {
                                    if (reviewSession.mode === PRACTICE_MODES.STANDARD) {
                                        closingContent.unshift($("<p>", { 
                                            css: {
                                                ...styles.reviewModal.progress,
                                                marginBottom: 0
                                            },
                                            text: `Meanings: ${progress.meaningProgress}/${progress.total/2} - Readings: ${progress.readingProgress}/${progress.total/2}`
                                        }));
                                        return closingContent;
                                    } else if (reviewSession.mode === PRACTICE_MODES.ENGLISH_TO_KANJI) {
                                        closingContent.unshift($("<p>", { 
                                            css: {
                                                ...styles.reviewModal.progress,
                                                marginBottom: 0
                                            },
                                            text: `${progress.recognitionProgress}/${progress.total} Correct`
                                        }));
                                        return closingContent;
                                    } else { // COMBINATION PRACTICE_MODE
                                        closingContent.unshift($("<p>", { 
                                            css: {
                                                ...styles.reviewModal.progress,
                                                marginBottom: 0
                                            },
                                            text: `Meanings: ${progress.meaningProgress}/${progress.total/3} | ` +
                                                `Readings: ${progress.readingProgress}/${progress.total/3} | ` +
                                                `Recognition: ${progress.recognitionProgress}/${progress.total/3}`
                                        }));
                                        return closingContent;
                                    }
                                })())
                        );

                    setTimeout(() => {
                        enableScroll();
                        reviewModal.remove();
                    }, 1000);
                })
                .on(REVIEW_EVENTS.STUDY_AGAIN, () => {
                    reviewModal.remove();
                    enableScroll();
                    handleKanjiPractice();
                });

            await reviewModal.render();
        } catch (error) {
            console.error("Error in startKanjiReview:", error);
            enableScroll();
        }
    }

    class PracticeButton {
        constructor(type) {
            this.type = type;
            this.buttonStyle = this.getButtonStyle();
            this.handleClick = this.handleClick.bind(this);
        }

        getButtonStyle() {
            return this.type === PRACTICE_TYPES.RADICAL
                ? styles.buttons.practice.radical
                : styles.buttons.practice.kanji;
        }

        async handleClick() {
            try {
                if (this.type === PRACTICE_TYPES.RADICAL) {
                    await handleRadicalPractice();
                } else {
                    await handleKanjiPractice();
                }
            } catch (error) {
                console.error(`Error handling ${this.type} practice:`, error);
            }
        }

        render() {
            const $button = $("<button>")
                .attr("id", `ep-${this.type}-btn`)
                .text("Practice")
                .css(this.buttonStyle)
                .on("click", this.handleClick);

            const selector = `${SELECTORS.DIV_LEVEL_PROGRESS_CONTENT} ${SELECTORS.DIV_CONTENT_WRAPPER} ${SELECTORS.DIV_CONTENT_TITLE}`;
            
            // Doing a conditional check to add the practice button to the correct DIV.
            const targetSelector = this.type === PRACTICE_TYPES.RADICAL
                ? `${selector}:first`
                : `${selector}:last`;

            $button.appendTo(targetSelector);

            return $button;
        }
    }

    function initializePracticeButtons() {
        // First style the containers where the "PRACTICE" buttons be
        $(`${SELECTORS.DIV_LEVEL_PROGRESS_CONTENT} ${SELECTORS.DIV_CONTENT_WRAPPER} ${SELECTORS.DIV_CONTENT_TITLE}`)
            .css(styles.layout.contentTitle);

        const radicalButton = new PracticeButton(PRACTICE_TYPES.RADICAL);
        const kanjiButton = new PracticeButton(PRACTICE_TYPES.KANJI);

        radicalButton.render();
        kanjiButton.render();
    }

    $(document).ready(() => {
        initializePracticeButtons();
    });

})();
//# sourceMappingURL=extra-practice.user.js.map