Quick Review Summary UserScript

Hi everyone.

I made a user script that shows a quick summary (meaning, reading, part-of-speech) info next to the kanji after the user’s submission. This avoids clicking on the “item info” button, scrolling, expanding sections, etc…

This is a first version, hence not yet stylized or beautified. Feedback is welcome!

GreasyFork Link Here

3 Likes

Nice idea. I’ll have to try this.

1 Like

Some thoughts for when you get around to styling:

  1. I’d consider putting the reading first, meaning second, and part of speech last.

  2. I’d make the part-of-speech info italic and smaller (it’s less important) and the meaning bold (the kana reading will stand out visually automatically since it’s necessarily a different font).

  3. It would be nicest if the reading could appear as furigana above/below the subject (using <ruby> tags). That’s hard, though.

  4. Please consider a fixed width for the added content (something like 40em) and let the text wrap automatically.

  5. Styling will obviate the need for an explicit <br> to separate things.

  6. Please take advantage of the CSS custom properties that Wanikani is now using for styling choices.

So instead of:

Something like:

If furigana is a bridge too far, just put the kana at the beginning of the meaning text.

You might also consider a different background color (or filter) for the div you’re adding. Again please use custom props so user styling overrides are automatic.

3 Likes

Thanks so for much for the feedback. These are all great suggestions. I’ll implement them in a near future (hopefully)

1 Like

Updated the script with the aforementioned changes… not perfect yet, but I believe it’s in the right direction. Any other feedback is always appreciated

1 Like

The update is awesome. Thanks!

How on earth did you get the furigana working correctly for vocabulary? That seemed hard to me. I thought the API only returned the complete readings (including any embedded kana). Very cool.

I notice that it doesn’t show the reading furigana after meaning quizzes. Not a problem, but I wonder if that’s related to how you generate the <ruby> markup?

Also, just FYI: It looks great with a userstylesheet to put the furigana below and dark theme. This is what I see during reviews (with a locally modified version of WKElementaryDarkTheme and furigana below user stylesheets):

You could optionally inject style="ruby-position: under;" on the <ruby> tag to force the furigana underneath instead of on top it you wanted.

Now to train my fingers to stop reflexively typing fe


Final edit.

This is really great!

It doesn’t seem like this would be that big of a time saver, but I’m flying through my reviews without having to manually expand everything. I notice that I ignore the script output the majority of the time (correct answers). I just hit return immediately. But not having to expand and scroll for the occasional incorrect answer really seems to remove some mental friction and speeds things up considerably.

Seriously: this is a significant UX improvement. Nice work!

Oops.

I think there’s a bug. It appears to inject the reading from a previous response both before and after giving a response. It only happens sometimes (maybe two reading quizzes in a row?):

Before answering:

After answering:

Context: I’d answered the reading for (ふう) incorrectly earlier in the same session.


Related. It’s not just with the furigana.

Saw this after answering both quizzes for (かえで) correctly:

Context: I’d answered the reading for this one (悠長(ゆうちょう)) incorrectly previously, but haven’t been quizzed on the meaning yet. This is the second time it’s quizzed me on the reading in this session.


I think it happens on repeat quizzes after answering incorrectly. I haven’t looked at the script, but I think the logic for when to display the info isn’t taking into account repeat quizzes within the same session – it thinks you’ve already answered.


Nope, it just happened with one where both quizzes for the same subject came up one after the other.

It quizzed the reading first. I answered correctly and it displayed the reading and meaning (but I might have hit return too quickly before it displayed). When the meaning quiz rendered, it showed the correct answer before I started typing.

It might be some sort of race condition.

Furigana over the kanji: definitely.

1 Like

If it’s ever below, it should be optional for sure. But the brain is weird: I tend to only read the furigana and ignore the kanji if it’s above.

Personally, I definitely prefer it below.

1 Like

Thanks for your thorough feedback @Rrwrex, really appreciated. Although have done web development before, it’s a first for me with UserScripts. I hope to eventually mature this to the point where I can add some options menu to switch between furigana above or below the kanji - thanks for the idea!

Regarding the bug, thanks for reporting, I’ll look into that. I have a short delay (Line 105) to let the DOM settle after some async calls, and the clearing of the previous content happens then (it was a quick workaround, otherwise the content would inject properly; maybe there’s a more elegant solution). Perhaps it’s a too short of an interval that sometimes it’s not executed. Definitely there can be som weird behavior. I’ve experienced the same repeated questioning upon entering a mistake, but I didn’t know if I was just too tired lol - thanks for confirming it was just not my sleepy eyes!

I’ll add this to a github repo and start tracking down these issues properly when I find some time.

1 Like

You’re correct, it does only return the full reading. What i did was to find the intersection (or better, the difference?) between the kanji str and the reading str. what’s different must be the furigana part. See @L32: function add_furigana(reading, word){ ... for the algo.

I’m not 100% happy though, because it will span the furigana across all the kanji. Luckily most of the times it lands above the right kanji when you have more than one, but sometimes it gets funky. I’ll implement a better way since we have the kanji components ids in the api response object. Stay tuned :slight_smile:

1 Like

That is by design - maybe something to add to the options menu. I feel that one would “cheat” by seeing the reading if you’re asked for the meaning first. This way you don’t see the answer for the reading. Maybe I could add a similar option to hide the meaning answer if one is asked for the reading first… I’m sure folks will have different preferences on this.

1 Like

I’ve only glanced at the code, but I strongly suggest avoiding jquery and lodash. Modern javascript has obviated the need for much of that, and pulling in heavy libraries for a userscript is kinda ugly (though WKOF itself loads jquery, I think).

I’m neck deep (and behind) in several other projects, so I’m trying hard not to debug what’s happening. I’ll continue to submit bug reports as I find them, though.

Not sure how it’s different from showing the meaning when asked the reading first, but I can understand the thought.

If you’re going to add options, it might be good to have radio-buttons for:

  • Always show both reading and meaning answers
  • Show answer only to what was quizzed

And

  • Show answer automatically after providing a response
  • Only display answer if a hotkey is pressed

A hotkey obviously wouldn’t need a delay, but I’m unsure why you have one anyway. The script shouldn’t run until the page has fully rendered (I forget the incantation to ensure that happens, though, and the whole review/lesson behaviors have changed since I last looked at it, anyway).

Please keep reporting bugs, appreciated.

Agreed that some subset of functionality can be replaced by native js, but on the other hand, those external libs are not such a big hindrance and the benefit outweighs the cost for this purpose. Convenience and quick turnaround are more valuable - I’d rather spend my time studying Japanese than implementing diff algorithms lol

1 Like

Cool! Great suggestions. I’m sure the whole implementation can be refined. I’ll look into it when time allows

1 Like

I haven’t tested extensively, but I think moving your guard clause into the main function definition fixes the bug (note the location of the if statement after the // cleanup comment):

// ==UserScript==
// @name         WaniKani Item Review Summary
// @namespace    wanikani
// @version      0.3
// @license     GPL version 3 or any later version; http://www.gnu.org/copyleft/gpl.html
// @description  A quick Review item summary plugin that will show the meaning and readings next to the kanji upon the submission of an answer.
// @author       Ricardo Neves
// @match        https://www.wanikani.com/subjects/review
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // Check if WFOF is installed
    if (!window.wkof) {
        alert('[Your script name here] script requires Wanikani Open Framework.\nYou will now be forwarded to installation instructions.');
        window.location.href = 'https://community.wanikani.com/t/instructions-installing-wanikani-open-framework/28549';
        return;
    }

    let jquery_url = "https://code.jquery.com/jquery-3.7.0.min.js";
    let lodash_url = "https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js"
    var promises = [];
    promises[0] = wkof.load_script(jquery_url, true /* use_cache */);
    promises[1] = wkof.load_script(lodash_url, true /* use_cache */);

    // Wait until all files are loaded, then do something
    Promise.all(promises).then(start_script);


    function add_furigana(reading, word){
        const furigana = _.difference(reading.split(""), word.split("")).join("")
        const kana = reading.replace(furigana, "")
        const kanji = word.replace(kana,"")
        const out = word.replace(kanji, "<ruby>"+kanji+"<rt style='font-size: var(--font-size-large);'>"+furigana+"</rt></ruby>")
        return  out
    }


    // This function is called when all the files requested above are loaded.
    function start_script() {

        $( document ).ready(function() {

            let btn = document.getElementsByClassName('quiz-input__submit-button')[0]
            $('.character-header__characters').after('<div id="WKIRS-content-right" class="WKIRS-content-right" style="border-radius: 5px;box-shadow: inset 0 -3px 1px rgba(0,0,0,0.2), inset 0 3px 1px rgba(0,0,0,0);background-color:rgb(255 255 255 / 30%);width: 20em;min-height: 6em; margin: 1em; padding: 0.5em;"></div>');
            $('#WKIRS-content-right').hide()
            function extract_subject_info(){




                let subj_url = document.getElementsByClassName("additional-content__item additional-content__item--item-info")[0].getAttribute("href")
                if(subj_url!=="/subject_info/:id"){
                    let subj_id = subj_url.replace("/subject_info/","")
                    wkof.Apiv2.fetch_endpoint('subjects/'+subj_id).then((subj)=>{

                        // cleanup
                        if($("#user-response").attr('enabled')==='true'){
                            //$('#WKIRS-content-left').html("")
                            $('#WKIRS-content-right').hide()
                            $('#WKIRS-content-right').html("")
                            return;
                        }


                        let meanings=subj['data']['meanings'];
                        let readings=subj['data']['readings'];
                        let pos=subj['data']['parts_of_speech'];
                        let sentences=subj['data']['context_sentences'];


                        // create content to display

                        var content_right = "<p style='font-weight: var(--font-weight-bold); margin-bottom:1em;'>"+meanings.map(s=>s.meaning).join(", ")+"</p>"
                        // Get readings
                        if(subj.object==='vocabulary'){
                            if($(".quiz-input__question-type")[0].innerHTML === "reading"){
                                var reading = readings.map(s=>s.reading).join("<br>")
                                var kanji_div = $(".character-header__characters")[0]
                                var kanji_content = kanji_div.innerHTML
                                var furigana = add_furigana(reading, kanji_content)
                                kanji_div.setHTML(furigana)
                            }

                            content_right += "<p  style='font-size: var(--font-size-small); font-style: italic;'>"+pos.join(", ")+"</p><br>"// + content_right
                        }

                        if(subj.object==='kanji'){
                            let _readings = _.groupBy(readings, 'type')
                            if(_.has(_readings, 'onyomi')){
                                content_right += "<p> On: "+_readings['onyomi'].map(s=>s.reading).join(', ')+"</p>"
                            }
                            if(_.has(_readings, 'kunyomi')){
                                content_right += "<p style='line-height:var(--spacing-xloose)'> Kun: "+_readings['kunyomi'].map(s=>s.reading).join(', ')+"</p>"
                            }
                        }
                        // insert data into display areas
                        $('#WKIRS-content-right').html(content_right)
                        $('#WKIRS-content-right').show()
                    });

                }
            }

            btn.addEventListener('click', ()=>setTimeout(extract_subject_info, 10))

        });

    };





    //get subject id


})();

I also added an option for whether or not to always show the readings. Note that meanings are always shown for any quiz type.

Note that your version would always show the readings for Kanji but not for Vocabulary. I made the option affect both.

This is what I’m using (but with ALWAYS_SHOW_READING set to true). I’ve still not tested extensively, but it seems to be working perfectly for me (AND I LOVE IT!):

// ==UserScript==
// @name         WaniKani Item Review Summary
// @namespace    wanikani
// @version      0.3
// @license     GPL version 3 or any later version; http://www.gnu.org/copyleft/gpl.html
// @description  A quick Review item summary plugin that will show the meaning and readings next to the kanji upon the submission of an answer.
// @author       Ricardo Neves
// @match        https://www.wanikani.com/subjects/review
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    const ALWAYS_SHOW_READING = false; // If set to false, don't show readings after meaning quizzes

    // Check if WFOF is installed
    if (!window.wkof) {
        alert('[Your script name here] script requires Wanikani Open Framework.\nYou will now be forwarded to installation instructions.');
        window.location.href = 'https://community.wanikani.com/t/instructions-installing-wanikani-open-framework/28549';
        return;
    }

    let jquery_url = "https://code.jquery.com/jquery-3.7.0.min.js";
    let lodash_url = "https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js"
    var promises = [];
    promises[0] = wkof.load_script(jquery_url, true /* use_cache */);
    promises[1] = wkof.load_script(lodash_url, true /* use_cache */);

    // Wait until all files are loaded, then do something
    Promise.all(promises).then(start_script);


    function add_furigana(reading, word){
        const furigana = _.difference(reading.split(""), word.split("")).join("")
        const kana = reading.replace(furigana, "")
        const kanji = word.replace(kana,"")
        const out = word.replace(kanji, "<ruby>"+kanji+"<rt style='font-size: var(--font-size-large);'>"+furigana+"</rt></ruby>")
        return  out
    }


    // This function is called when all the files requested above are loaded.
    function start_script() {

        $( document ).ready(function() {

            let btn = document.getElementsByClassName('quiz-input__submit-button')[0]
            $('.character-header__characters').after('<div id="WKIRS-content-right" class="WKIRS-content-right" style="border-radius: 5px;box-shadow: inset 0 -3px 1px rgba(0,0,0,0.2), inset 0 3px 1px rgba(0,0,0,0);background-color:rgb(255 255 255 / 30%);width: 20em;min-height: 6em; margin: 1em; padding: 0.5em;"></div>');
            $('#WKIRS-content-right').hide()
            function extract_subject_info(){




                let subj_url = document.getElementsByClassName("additional-content__item additional-content__item--item-info")[0].getAttribute("href")
                if(subj_url!=="/subject_info/:id"){
                    let subj_id = subj_url.replace("/subject_info/","")
                    wkof.Apiv2.fetch_endpoint('subjects/'+subj_id).then((subj)=>{

                        // cleanup
                        if($("#user-response").attr('enabled')==='true'){
                            //$('#WKIRS-content-left').html("")
                            $('#WKIRS-content-right').hide()
                            $('#WKIRS-content-right').html("")
                            return;
                        }


                        let meanings=subj['data']['meanings'];
                        let readings=subj['data']['readings'];
                        let pos=subj['data']['parts_of_speech'];
                        let sentences=subj['data']['context_sentences'];


                        // create content to display

                        var content_right = "<p style='font-weight: var(--font-weight-bold); margin-bottom:1em;'>"+meanings.map(s=>s.meaning).join(", ")+"</p>"
                        // Get readings
                        if(subj.object==='vocabulary'){
                            if($(".quiz-input__question-type")[0].innerHTML === "reading" || ALWAYS_SHOW_READING){
                                var reading = readings.map(s=>s.reading).join("<br>")
                                var kanji_div = $(".character-header__characters")[0]
                                var kanji_content = kanji_div.innerHTML
                                var furigana = add_furigana(reading, kanji_content)
                                kanji_div.setHTML(furigana)
                            }

                            content_right += "<p  style='font-size: var(--font-size-small); font-style: italic;'>"+pos.join(", ")+"</p><br>"// + content_right
                        }

                        if(subj.object==='kanji'){
                            if($(".quiz-input__question-type")[0].innerHTML === "reading" || ALWAYS_SHOW_READING){
                                let _readings = _.groupBy(readings, 'type')
                                if(_.has(_readings, 'onyomi')){
                                    content_right += "<p> On: "+_readings['onyomi'].map(s=>s.reading).join(', ')+"</p>"
                                }
                                if(_.has(_readings, 'kunyomi')){
                                    content_right += "<p style='line-height:var(--spacing-xloose)'> Kun: "+_readings['kunyomi'].map(s=>s.reading).join(', ')+"</p>"
                                }
                            }
                        }
                        // insert data into display areas
                        $('#WKIRS-content-right').html(content_right)
                        $('#WKIRS-content-right').show()
                    });

                }
            }

            btn.addEventListener('click', ()=>setTimeout(extract_subject_info, 10))

        });

    };





    //get subject id


})();

The main remaining annoyance is the one you mentioned:

This is currently rendered as:

<ruby>呆れ返る
  <rt style="font-size: var(--font-size-large);">あきかえ</rt>
</ruby>

Ideally, it should be rendered as:

<ruby>呆
  <rt style="font-size: var(--font-size-large);">あき</rt>
</ruby>れ
<ruby>返
  <rt style="font-size: var(--font-size-large);">かえ</rt>
</ruby>る

But that’s hard to accomplish programmatically.

The simpler solution would be to only use your difference algorithm if the kana was strictly appended to the end. Otherwise, use the entire reading in the rt element:

<ruby>呆れ返る
  <rt style="font-size: var(--font-size-large);">あきれかえる</rt>
</ruby>

Which would render as:

I’ll likely implement this in a few days if you don’t get to it first.

I’ll likely remove the dependency on jquery and lodash in the process if I do it, though! :smile:

Thank so much for this update! I’m creating a github so we can collaborate more efficiently instead of pasting code through here :slight_smile:

I’ll try your mods, and if i dont see any issues on my side, I’ll make them part of the next version.

Regarding the furigana, I have a new algo to implement that will solve that and put the furigana on top of each kanji properly. I’ll work on that this weekend (maybe).

1 Like

Updated the furigana algo (still not perfect; will keep working on it) and included your mods here. If you would like to contribute with further changes, could you (or anyone) please just submit a merge request?

I want to add the options menu before put it on greasyfork.

1 Like