[Userscript] WaniKani Lesson User Synonyms v2

Well I am also using it in lessons only, so syncing is not very important to me. :smiley:

1 Like

Is this the script that also allows me to override the meaning given to e.g. a radical from WK’s side?
I heard that you can do that. E.g. I want to name the “grave” radical “soil” (same name as its kanji counterpart). So using this script, will I be able to type in “soil” in a Review session and not be told I made a mistake?

Absolutely :slight_smile:

Adding synonym, which allows you to do exactly what you described, is a core functionality of WaniKani.

But for whatever reason, you can per default only add synonyms during reviews or on the item’s page, not during lessons. This script here makes it possible to add synonyms during lessons too.

Ahh! Perfect!
:slight_smile:

Wow, thank you so much for this. I’ve just started doing lessons again and was genuinely distraught when I found the old script not working. Such a vital part of WK for me, and I have no idea whatsoever why it’s not included as part of the standard product.

Has anyone seen any reason given for why it’s not part of regular Wanikani? Seems more like an oversight than anything deliberate.

The problem is with /user_synonym/create/. This doesn’t exist anymore. Thanks for this.

I’ve been going through with this problem a lot, but I feel like this script is jamming the lessons from time to time (pretty often). I’ve tried doing lessons without all the other scripts I’ve used, but the problem has remained. Now I tried to not use this one, and I managed to finish the lessons without the page getting jammed.

It’s not jamming in the traditional sense, I can move back and forth between the items I’ve already gone through with, but at some point my browser just refuses to let me go onto the next item or the reviews, if I’ve managed to go that far.

It could be a problem with the browser or something else as well, but I thought I’d just say :slight_smile: (I seem to have problems with a lot of scripts, which maybe is a sign to give up on Firefox) I’ll try to not use this for a while and see what happens in the long run.

Edit: Using Firefox and Violentmonkey.

Thanks for the script. I was just about to make a feature request elsewhere on the forum for this much-needed function. This’ll save me a lot of aggravation down the line :slight_smile:

Hi, did that problem persist? I’m a bit intimidated now to install the script…

I’ve been using the script for months without issue.

2 Likes

Same here.

It has worked for me perfectly for a long time now!

1 Like

Thank you for the reply, I installed in now too :slight_smile:

1 Like

Nothing happened when I installed this script. I installed Tempermonkey on chrome and it seemed to install in there okay but when I open WaniKani nothing happens. Am I just missing a small link to the interface or should it be really obvious?

This script lets you add synonyms during lessons so you have to be doing lessons to see it. You can already add synonyms during reviews or on individual radical/kanji/vocal pages without the script.

oh okay thanks, I’ll have to wait until I get to the kanji now to test this out.

HI there,
Back to WK after a long, long pause. Had to clean all my former installed scripts
As a non-english native, I previously installed WaniKani Lesson User Synonyms v2 and it was a great help for me to be able to immediatly translate words from english to french. Thanks a lot for that !

So I re-installed a lot a scripts as I did three years ago, It seems they all run as expected, except WK Lesson user Synonyms v2: no ‘User Synonyms/+ADD SYNONYM’ zone appear during lessons. As I reseted to level 2, my previous Level-1 words are still there, but I can’t add new during lessons. I tried to check in ViolentMonkey (I use it) but everything seems normal.

Any idea about this issue ? Thank you all.

Were you trying to use it during the learning part of the lesson or the lesson quiz? The script doesn’t add it to the quiz part. I have a fork I’ve been using that does this and has better handling of a few corner cases, not fully tested tho:

// ==UserScript==
// @name          WaniKani Lesson User Synonyms 2 Fork
// @namespace     est_fills_cando
// @version       0.3.1
// @description   Allows adding synonyms during lesson and lesson quiz.
// @author        est_fills_cando
// @match         https://www.wanikani.com/lesson/session
// @grant         none
// ==/UserScript==/
/*global $, wkof */

// based heavily on the WaniKani Lesson User Synonyms 2 userscript

(function () {
    // Make sure WKOF is installed
    if (!wkof) {
        let response = confirm(script_name+' requires WaniKani Open Framework.\n Click "OK" to be forwarded to installation instructions.');
        if (response) {
            window.location.href = 'https://community.wanikani.com/t/instructions-installing-wanikani-open-framework/28549';
        }
        return;
    }

    wkof.include('Apiv2');

    let UserSynonyms2 = {};
    let keyTTL = 180 * 24 * 3600 * 1000; // 180 days should be enough for anyone

    // subject ids are unique even across different subject types
    // the subject type is not required when updating the synonym list via the api
    // therefore, we can ignore subject types
    function getSynKeyItem(item) {
        return getSynKeyID(item.id);
    }

    function getSynKeyID(id) {
        return 'l/syn/' + id;
    }

    function getSynListItem(item) {
        return getSynListID(item.id);
    }

    function getSynListID(id) {
        let synKey = getSynKeyID(id);
        $.jStorage.setTTL(synKey, keyTTL); // extend TTL on read. setting TTL on non-existant keys seems fine.
        let retVal = $.jStorage.get(synKey) || [];
        return retVal;
    }

    function setSynListID(id, synList) {
        let synKey = getSynKeyID(id);
        if (synList.length > 0) {
            $.jStorage.set(synKey, synList);
            $.jStorage.setTTL(synKey, keyTTL);
        } else {
            $.jStorage.deleteKey(synKey);
        }
    }

    // Calls callback once on each element matching selector that is added
    // to the DOM, including existing elements at the time the function is first called.
    // options is options for the MutationObserver (optional, is merged with default options  {childList: true, subtree: true})
    function onEachElementReady(selector, options, callback) {
        if (!options)
            options = {};

        let els_found = new Set()
        function check_available() {
            for (const el of document.querySelectorAll(selector)) {
                if (!els_found.has(el)) {
                    els_found.add(el);
                    callback(el);
                }
            }
        }

        queueMicrotask(check_available);

        let mo = new MutationObserver(check_available);
        mo.observe(document.documentElement, {
            childList: true,
            subtree: true,
            ...options
        });
    }

    // runs the callback every time attributes of elt change
    // including once initially
    // this function only works if elt exists at the time it is called
    function onEachAttrChange(elt, options, callback) {
        if (!options)
            options = {}
        options = {attributes: true, ...options};
        queueMicrotask(callback);
        let mo = new MutationObserver(callback);
        mo.observe(elt, options);
        return mo;
    }

    // calls callback each time jStorage value stored at key,
    // is updated with the updated value,
    // including cases where the value is overwritten with itself,
    // including once initially
    // callOnNull is whether to call on null values (default:false)
    function onEachJStorageChange(key, callback, callOnNull) {
        let val = $.jStorage.get(key);
        if (val !== null || callOnNull)
            setTimeout(() => callback(val), 0); //setTimeout here because local storage events are macro tasks
        $.jStorage.listenKeyChange(key, function (key, action) {
            if (action === 'updated') {
                val = $.jStorage.get(key);
                if (val !== null || callOnNull)
                    callback(val);
            }
        });
    }


    // loads any user synonyms stored on WaniKani's servers into the locally stored lists of synonyms,
    // clobbering any existing values
    // also stores subject ids of each subject
    // input is array of subject_ids to retrieve
    UserSynonyms2.loadSynonymsFromAPI = async function (ids) {
        if (ids.length == 0)
            return new Promise(resolve => resolve([]));
        let options = {
            force_update: true,
            filters: {
                subject_ids: ids
            }
        };

        await wkof.ready('Apiv2');
        let mats = await wkof.Apiv2.fetch_endpoint('study_materials', options);
        let syns = {};
        for (const mat of mats.data) {
            syns[mat.data.subject_id] = mat.data.meaning_synonyms;
        }

        for (const id of ids) {
            let syns_for_id = [];
            if (id in syns) {
                syns_for_id = syns[id];
            }
            setSynListID(id, syns_for_id);
        }
    }

    // removes the given synonym, if it exists, from the given item in jStorage
    // and saves the synonym list
    UserSynonyms2.removeAndSave = function (itemID, syn) {
        let li = getSynListID(itemID).filter(entry => entry != syn);
        return UserSynonyms2.saveSynonymList(itemID, li);
    }

    // adds the given synonym to the given item in jStorage
    // and saves the synonym list
    UserSynonyms2.addAndSave = function (itemID, syn) {
        let li = getSynListID(itemID);
        li.push(syn);
        return UserSynonyms2.saveSynonymList(itemID, li);
    }


    // prevent enter key up, which happens after textbox disappears
    // and therefore triggers on body, from advancing lesson/quiz
    let enterKeypressStartedInInput = false;
    function setKeyInterceptor(selector) {
        $(selector).keydown(function (event) {
            if (event.key == "Enter" && enterKeypressStartedInInput) {
                event.stopPropagation();
            }
        });
        $(selector).keyup(function (event) {
            if (event.key == "Enter" && enterKeypressStartedInInput) {
                event.stopPropagation();
                enterKeypressStartedInInput = false;
            }
        });
    }

    UserSynonyms2.load = function (userSynStr, itemID, element) {
        UserSynonyms2.generateList(element, userSynStr);
        UserSynonyms2.addOption(itemID);
        UserSynonyms2.removeOption(itemID);

        // recalculate positioning of navigation bar at bottom of lesson page
        // only seems to be needed on initial page load when information section
        // is long enough that it nearly reaches the bottom of the page
        // do it in request animation frame instead of setttimeout
        // to prevent the nav bar visually "jumping" from wrong spot to right spot
        // don't use microtasks because the values don't seem to have perfectly
        // stabilized at that point
        window.requestAnimationFrame(function() {
            var e= $("#supplement-info");
            var t=$("#batch-items");
            if (t.length > 0) {
                if($(window).height() - e.offset().top - e.outerHeight(!0) > t.height() + 25) {
                    t.get(0).classList.add("bottom");
                } else {
                    t.get(0).classList.remove("bottom");
                }
            }
        });
    };

    UserSynonyms2.addOption = function (itemID) {
        let btnAdd, wrapper;
        btnAdd = $('.user-synonyms-add-btn');
        wrapper = UserSynonyms2.wrapper();
        btnAdd.off('click');
        btnAdd.on('click', function () {
            let inputBtnRemove, inputBtnAdd, inputLi, inputForm, inputInput;
            $(this).hide();
            inputLi = $('<li></li>', {
                'class': 'user-synonyms-add-form'
            }).appendTo(wrapper);
            inputForm = $('<form></form>').appendTo(inputLi);
            inputInput = $('<input></input>', {
                type: 'text',
                autocapitalize: 'off',
                autocomplete: 'off',
                autocorrect: 'off'
            }).appendTo(inputForm).focus();
            inputInput.keyup(function (event) {
                event.stopPropagation();
                if (event.key == "Enter") {
                    enterKeypressStartedInInput = false;
                }
            }); // prevent hotkeys while typing
            inputInput.keydown(function (event) {
                event.stopPropagation();
                if (event.key == "Enter") {
                    event.preventDefault(); // not sure if necessary
                    enterKeypressStartedInInput = true;
                    inputBtnAdd.click();
                }
            }); // prevent hotkeys while typing
            inputBtnAdd = $('<button></button>', {
                type: 'submit',
                text: 'Add'
            }).appendTo(inputForm);
            inputBtnRemove = $('<button></button>', {
                type: 'button',
                html: '<i class="icon-remove"></i>'
            }).appendTo(inputForm);
            inputBtnAdd.off('click');
            inputBtnAdd.on('click', function (event) {
                event.preventDefault();
                let newSynText, synLiElems, newLen;
                newSynText = inputInput.val();
                // fake accept duplicate, but don't send to server (server doesn't check?)
                if (getSynListID(itemID).indexOf(newSynText.toLowerCase()) >= 0) {
                    // delay for key events to be caught
                    setTimeout(function () {
                        inputLi.remove();
                        btnAdd.show();
                    }, 100);
                    return;
                }
                synLiElems = wrapper.find('li');
                newLen = newSynText.length + synLiElems.slice(0, synLiElems.size() - 1).text().length;
                if (newLen > 255) {
                    inputBtnAdd.attr('disabled', 'disabled').text('Exceeded Synonym Limit');
                } else if (newSynText.trim().length !== 0) {
                    UserSynonyms2.addAndSave(itemID, newSynText).then(function() {
                        let newSynElem;
                        newSynElem = $('<li class="user-synonym">' + newSynText + '</li>');
                        wrapper.find('li:last').prev().before(newSynElem);
                        inputLi.remove();
                        btnAdd.show();
                        UserSynonyms2.removeOption(itemID);
                    }).catch(function() {
                        alert('adding synonym failed');
                    });
                }
            });
            inputBtnRemove.off('click');
            inputBtnRemove.on('click', function (event) {
                inputLi.remove();
                btnAdd.show();
            });
        });
    };

    /**
     * Save the list of user synonms currently in local storage
     * Only works if subject with itemID is in the activeQueue
     * @param itemID
     */
    UserSynonyms2.saveSynonymList = async function (itemID, synList) {
        function getType(item) {
            var type = 'x';
            if (item.rad) {
                type = 'radical';
            } else if (item.kan) {
                type = 'kanji';
            } else if (item.voc) {
                type = 'vocabulary';
            }
            return type;
        }

        let its = $.jStorage.get('l/activeQueue').filter(it => it.id == itemID);
        let it;
        if (its.length > 0)
            it = its[0];
        else
            it = $.jStorage.get('l/currentQuizItem')
        let type = getType(it);

        //let url = "https://api.wanikani.com/v2/study_materials/" + itemID;
        let url = "https://www.wanikani.com/study_materials/" + itemID;

        await $.ajax({
            type: 'PUT',
            url: url,
            contentType: 'application/json; charset=utf-8',
            data: JSON.stringify({
                study_material: {
                    subject_type: type,
                    subject_id: itemID,
                    meaning_synonyms: synList
                }
            })
        });

        setSynListID(itemID, synList);
    }

    UserSynonyms2.generateList = function (element, userSyn) {
        let wrapper, i;
        $('.user-synonyms ul').remove();
        element.append($('<ul></ul>'));
        wrapper = UserSynonyms2.wrapper();

        for (i = 0; i < userSyn.length; i++) {
            $('<li class="user-synonym">' + userSyn[i] + '</li>', {
                title: 'Click to remove synonym'
            }).appendTo(wrapper);
        }
        $('<li></li>', {
            html: '&nbsp;',
            title: 'Add your own synonym',
            'class': 'user-synonyms-add-btn'
        }).appendTo(wrapper);
    };

    UserSynonyms2.removeOption = function (itemID) {
        let synElems = UserSynonyms2.wrapper().find('li:not(.user-synonyms-add-btn):not(.user-synonyms-add-form)');
        synElems.off('click');
        synElems.on('click', function () {
            let clickedSynElem = $(this);
            UserSynonyms2.removeAndSave(itemID, clickedSynElem.text()).then(clickedSynElem.remove.bind(clickedSynElem));
        });
    };

    UserSynonyms2.wrapper = function () {
        return $('.user-synonyms ul');
    };

    // from: https://gist.githubusercontent.com/arantius/3123124/raw/grant-none-shim.js
    function addStyle(aCss) {
        let head, style;
        head = document.getElementsByTagName('head')[0];
        if (head) {
            style = document.createElement('style');
            style.setAttribute('type', 'text/css');
            style.textContent = aCss;
            head.appendChild(style);
            return style;
        }
        return null;
    }

    // Init / UI Display

    UserSynonyms2.synonymSection = function(insert_or_append, target) {
        let cur;
        if (insert_or_append =='insert')
            cur =$('<h2>User Synonyms</h2>').insertAfter(target);
        else if (insert_or_append == 'append')
            cur =$('<h2>User Synonyms</h2>').appendTo(target);
        cur = $('<section class="user-synonyms"></section>').insertAfter(cur);
        return cur;
    }

    let quizVisibilityObserver = null;
    UserSynonyms2.showCurrentSynonyms = function() {
        if (quizVisibilityObserver) {
            quizVisibilityObserver.disconnect();
            quizVisibilityObserver = null;
        }
        if ($.jStorage.get('l/quizActive')) {
            let currentQuizItem = $.jStorage.get('l/currentQuizItem');
            if (!currentQuizItem || !initialized_subject_ids.has(currentQuizItem.id)) {return}
            if (currentQuizItem.rad) {
                let el = $('#item-info-col1 section');
                if (!el) {return}
                let synSec = UserSynonyms2.synonymSection('append', el);
                el.contents().filter(function() {return this.nodeType==Node.TEXT_NODE}).get(0).textContent = currentQuizItem.original_answers_en.join(', ');
                UserSynonyms2.load(getSynListItem(currentQuizItem), currentQuizItem.id, synSec);
            } else {
                let el = $('#item-info-meaning');
                if (!el) {return}
                let synSec = UserSynonyms2.synonymSection('insert', el);
                el.contents().filter(function() {return this.nodeType==Node.TEXT_NODE}).get(0).textContent = currentQuizItem.original_answers_en.join(', ');
                UserSynonyms2.load(getSynListItem(currentQuizItem), currentQuizItem.id, synSec);
                quizVisibilityObserver = onEachAttrChange(el.get(0), null, function() {
                    if (window.getComputedStyle(el.get(0)).display != 'none') {
                        synSec.css('display','');
                        synSec.prev().css('display','');
                    } else {
                        synSec.css('display', 'none');
                        synSec.prev().css('display', 'none');
                    }
                });
            }
        } else {
            let currentLesson = $.jStorage.get('l/currentLesson');
            if (!currentLesson || !initialized_subject_ids.has(currentLesson.id)) {return}
            if (currentLesson.rad) {
                UserSynonyms2.load(getSynListItem(currentLesson), currentLesson.id, lrSynSec);
            } else if (currentLesson.kan) {
                UserSynonyms2.load(getSynListItem(currentLesson), currentLesson.id, lkSynSec);
            } else if (currentLesson.voc) {
                UserSynonyms2.load(getSynListItem(currentLesson), currentLesson.id, lvSynSec);
            }
        }
    }

    // during lesson

    // prevent enter events during synonym entry from triggering unrelated things
    setKeyInterceptor('body');

    // add base display elements
    let lrSynSec = UserSynonyms2.synonymSection('insert', $('#supplement-rad-name-mne'));
    let lkSynSec = UserSynonyms2.synonymSection('insert', $('#supplement-kan-meaning .col1 div'));
    let lvSynSec = UserSynonyms2.synonymSection('insert', $('#supplement-voc-synonyms'));

    // at start of each lesson set (detected via activeQueue change)
    // load synonym data from api
    let initialized_subject_ids = new Set();
    onEachJStorageChange('l/activeQueue', function(activeQueue) {
        let subject_ids = activeQueue.map(it => it.id).filter(id => !initialized_subject_ids.has(id));
        subject_ids.forEach(id => initialized_subject_ids.add(id));
        if (subject_ids.length != 0) {
            UserSynonyms2.loadSynonymsFromAPI(subject_ids).then(function() {
                UserSynonyms2.showCurrentSynonyms();
            });
        }
    });

    // show user synonyms when looking at each lesson
    onEachJStorageChange('l/currentLesson', function (currentLesson) {
        UserSynonyms2.showCurrentSynonyms();
    });

    // during lesson quiz

    // copy user synonyms into the answers list for quiz
    let in_call = false;
    onEachJStorageChange('l/currentQuizItem', function (currentQuizItem) {
        if (!in_call) {
            in_call = true;
            if (!currentQuizItem.orignal_answers_en)
                currentQuizItem.original_answers_en = [...currentQuizItem.en];
            currentQuizItem.en = currentQuizItem.original_answers_en.concat(getSynListItem(currentQuizItem));
            $.jStorage.set('l/currentQuizItem', currentQuizItem); // this call would cause infinite recursion without using in_call
            in_call = false;
        }
    });

    // show user synonyms in information box in lesson quiz for radicals
    onEachElementReady('#item-info-col1 section:first-child', null, function(el) {
        if ($(el).find('h2').text() == 'Name')
            UserSynonyms2.showCurrentSynonyms();
    });

    // show user synonyms in information box in lesson quiz for kanji and vocab
    onEachElementReady('#item-info-meaning', null, function(el) {
        UserSynonyms2.showCurrentSynonyms();
    });

    // watch for completed items. completed items should be safe to delete local storage...
    (function () {
        let lastCompleted = 0;
        onEachJStorageChange('l/count/completed', function (completed) {
            let currentQuizItem, synKey;
            if (completed > lastCompleted) {
                currentQuizItem = $.jStorage.get('l/currentQuizItem');
                synKey = getSynKeyItem(currentQuizItem);
                $.jStorage.deleteKey(synKey); // harmless to delete non-existant keys
            }
            lastCompleted = completed;
        });
    }());


    // this is style extracted from the wanikani review page
    addStyle('\n' +
             '.user-synonyms ul {\n' +
             '    margin: 0;\n' +
             '    padding: 0;\n' +
             '}\n' +
             '.user-synonyms ul li {\n' +
             '    display: inline-block;\n' +
             '    line-height: 1.5em;\n' +
             '}\n' +
             '.user-synonyms ul li:not(.user-synonyms-add-btn):not(.user-synonyms-add-form) {\n' +
             '    cursor: pointer;\n' +
             '    vertical-align: middle;\n' +
             '}\n' +
             '.user-synonyms ul li:not(.user-synonyms-add-btn):not(.user-synonyms-add-form):after {\n' +
             '    background-color: #EEEEEE;\n' +
             '    border-radius: 3px;\n' +
             '    color: #A2A2A2;\n' +
             '    content: "\\f00d";\n' +
             '    font-family: FontAwesome;\n' +
             '    font-size: 0.5em;\n' +
             '    margin-left: 0.5em;\n' +
             '    margin-right: 1.5em;\n' +
             '    padding: 0.15em 0.3em;\n' +
             '    transition: background-color 0.3s linear 0s, color 0.3s linear 0s;\n' +
             '    vertical-align: middle;\n' +
             '}\n' +
             '.user-synonyms ul li:hover:not(.user-synonyms-add-btn):not(.user-synonyms-add-form):after {\n' +
             '    background-color: #FF0033;\n' +
             '    color: #FFFFFF;\n' +
             '}\n' +
             '.user-synonyms ul li.user-synonyms-add-btn {\n' +
             '    cursor: pointer;\n' +
             '    display: block;\n' +
             '    font-size: 0.75em;\n' +
             '    margin-top: 0.25em;\n' +
             '}\n' +
             '.user-synonyms ul li.user-synonyms-add-btn:after {\n' +
             '    content: "";\n' +
             '}\n' +
             '.user-synonyms ul li.user-synonyms-add-btn:before {\n' +
             '    background-color: #EEEEEE;\n' +
             '    border-radius: 3px;\n' +
             '    color: #A2A2A2;\n' +
             '    content: "+ ADD SYNONYM";\n' +
             '    margin-right: 0.5em;\n' +
             '    padding: 0.15em 0.3em;\n' +
             '    transition: background-color 0.3s linear 0s, color 0.3s linear 0s;\n' +
             '}\n' +
             '.user-synonyms ul li.user-synonyms-add-btn:hover:before {\n' +
             '    background-color: #A2A2A2;\n' +
             '    color: #FFFFFF;\n' +
             '}\n' +
             '.user-synonyms ul li.user-synonyms-add-form {\n' +
             '    display: block;\n' +
             '}\n' +
             '.user-synonyms ul li.user-synonyms-add-form form {\n' +
             '    display: block;\n' +
             '    margin: 0;\n' +
             '    padding: 0;\n' +
             '}\n' +
             '.user-synonyms ul li.user-synonyms-add-form form input, .user-synonyms ul li.user-synonyms-add-form form button {\n' +
             '    line-height: 1em;\n' +
             '}\n' +
             '.user-synonyms ul li.user-synonyms-add-form form input {\n' +
             '    -moz-border-bottom-colors: none;\n' +
             '    -moz-border-left-colors: none;\n' +
             '    -moz-border-right-colors: none;\n' +
             '    -moz-border-top-colors: none;\n' +
             '    border-color: -moz-use-text-color -moz-use-text-color #A2A2A2;\n' +
             '    border-image: none;\n' +
             '    border-style: none none solid;\n' +
             '    border-width: 0 0 1px;\n' +
             '    display: block;\n' +
             '    margin: 0;\n' +
             '    outline: medium none;\n' +
             '    padding: 0;\n' +
             '    width: 100%;\n' +
             '}\n' +
             '.user-synonyms ul li.user-synonyms-add-form form button {\n' +
             '    background-color: #EEEEEE;\n' +
             '    border: medium none;\n' +
             '    border-radius: 3px;\n' +
             '    color: #A2A2A2;\n' +
             '    font-size: 0.75em;\n' +
             '    outline: medium none;\n' +
             '    transition: background-color 0.3s linear 0s, color 0.3s linear 0s;\n' +
             '}\n' +
             '.user-synonyms ul li.user-synonyms-add-form form button:hover {\n' +
             '    background-color: #A2A2A2;\n' +
             '    color: #FFFFFF;\n' +
             '}\n' +
             '.user-synonyms ul li.user-synonyms-add-form form button:disabled {\n' +
             '    background-color: #FF0000;\n' +
             '    color: #FFFFFF;\n' +
             '    cursor: default;\n' +
             '}\n' +
             '.user-synonyms ul li.user-synonyms-add-form form button[type="button"] {\n' +
             '    margin-left: 0.25em;\n' +
             '    padding-left: 0.3em;\n' +
             '    padding-right: 0.3em;\n' +
             '}\n' +
             '.user-synonyms ul li.user-synonyms-add-form form button[type="button"]:hover {\n' +
             '    background-color: #FF0000;\n' +
             '    color: #FFFFFF;\n' +
             '}\n');
}());

If you can’t see anything during the learning part of the lesson, then maybe it conflicts with another script you have installed?

@irrelephant are you still maintaining this? I have a fork with some features/fixes I was hoping to upstream like adding synonyms during quiz part of lesson, showing an error if the ajax to add the synonym failed, not clobbering synonyms added prior to lesson start, less hacky / more robust way of preventing enter key from advancing lesson, etc.