CrowdSurf productivity tools

The core script for modifying the CrowdSurf transcription interface that all other related scripts build upon.

// ==UserScript==
// @author       Mobius Evalon
// @name         CrowdSurf productivity tools
// @description  The core script for modifying the CrowdSurf transcription interface that all other related scripts build upon.
// @version      1.0
// @namespace    mobiusevalon.tibbius.com
// @license      Creative Commons Attribution-ShareAlike 4.0; http://creativecommons.org/licenses/by-sa/4.0/
// @require      https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js
// @include      /^https{0,1}:\/\/ops.cielo24.com\/mediatool\/.*$/
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_listValues
// ==/UserScript==
// prevents problems when the destination pages are running their own jquery libraries.  isn't necessary when the destination page
// is not running jquery or if the script is sandboxed because of GM_ functions being granted, but it's always good to think ahead
this.$ = this.jQuery = jQuery.noConflict(true);

function cspt_hotkeys(event)
{
    if(event.ctrlKey === true)
    {
        if(event.which === 13)
        {
            // ctrl+enter was pushed.  this user wants to submit the transcript
            $("#approve_button").click();
            return false; // jquery automatically prevents the default action and stops propagation if false is returned from an event handler
        }
    }
    else if(event.altKey === true)
    {
        if(event.which === 13)
        {
            // alt+enter was pushed.  user wants to accept the job they're previwing
            window.top.postMessage("cspt-hotkey-accept","https://work.crowdsurfwork.com");
            return false;
        }
        else if(event.which === 8)
        {
            // alt+backspace was pushed.  user wants to skip in preview mode or return in transcription mode
            window.top.postMessage("cspt-hotkey-return","https://work.crowdsurfwork.com");
            return false;
        }
    }
    else // neither Ctrl nor Alt
    {
        if(event.which === 13)
        {
            // enter was pushed with no meta keys.  user wants to dismiss a modal dialog
            var $modal = $("#generic-modal"),
                $scoring = $("#confirm_first_review_submit");
            if($modal.length && $modal.css("display") === "block")
            {
                $modal.find(".accept").first().get(0).click();
                return false;
            }
            else if($scoring.length && $scoring.css("display") === "block")
            {
                $scoring.find(".accept").first().get(0).click();
                return false;
            }
        }
    }
}

function cjft_dictionary()
{
    var stored = GM_getValue("cjft-ignore-list",""),
        cjft_words = [];
    if(stored.trim().length) cjft_words = stored.split(",");
    return cjft_words;
}

function cspt_message(event)
{
    // hooks for the cielo job frame tinkerer.  i have to use dom messaging to work around
    // security protocols and sandboxing limitations
    if(event.originalEvent.origin === "https://ops.cielo24.com")
    {
        var data = event.originalEvent.data.split("-");
        if(data[0] === "cspt")
        {
            if(data[1] === "request")
            {
                switch(data[2])
                {
                    case "ignored_words_list":
                        window.postMessage(("cjft-response-ignored_words-"+GM_getValue("cjft-ignore-list","")),"https://ops.cielo24.com");
                        break;
                    case "purge_ignored_list":
                        GM_deleteValue("cjft-ignore-list");
                        break;
                    case "add_ignored_word":
                        var cjft_words = cjft_dictionary();
                        cjft_words.push(data[3]);
                        GM_setValue("cjft-ignore-list",cjft_words.join(","));
                        break;
                    case "delete_ignored_word":
                        var cjft_words = cjft_dictionary(),
                            word_index = cjft_words.indexOf(data[3]);
                        if(cjft_words.length && word_index > -1)
                        {
                            cjft_words.splice(word_index,1);
                            GM_setValue("cjft-ignore-list",cjft_words.join(","));
                        }
                        break;
                }
            }
            return false;
        }
    }
}

function ckey(s)
{
    return s.replace(/[^a-z]/ig,"-").toLowerCase();
}

function gm_db_obj(key)
{
    var obj = json_obj(GM_getValue(key));
    if(typeof obj !== "object") GM_deleteValue(key);
    return obj;
}

function json_obj(json)
{
    var obj = {volume:0,earnings:0,time:0};
    if(typeof json === "string")
    {
        try {obj = JSON.parse(json);}
        catch(e) {console.log("Malformed JSON object.  Error message from JSON library: ["+e.message+"]");}
    }
    return obj;
}

function mmss(t)
{
    // minutes:seconds display from a total number of seconds
    var m = (Math.floor(t/60) || 0);
    t %= 60;
    var s = (Math.ceil(t) || 0);
    return (m+":"+(s < 10 ? "0" : "")+s);
}

function cspt_job_display(domain,type,level)
{
    var $display = $("#cspt-display");
    if(!$display.length)
    {
        $("#right-column ul.nav-tabs").after(
            $("<div/>")
            .attr("id","cspt-display")
            .css("margin-bottom","15px")
            .append(
                $("<div/>")
                .append(
                    $("<b/>")
                    .css("font-size","125%")
                    .css("cursor","help")
                    .attr("title",level_info(level))
                    .text("L"+level+": "+type.ucfirst()),
                    $("<span/>")
                    .css("color","#775555")
                    .css("margin-left","10px")
                    .css("cursor","pointer")
                    .text("[Reset]")
                    .click(function() {
                        GM_deleteValue("cspt_"+domain+"_l"+level+"_stats");
                        cspt_job_display(domain,type,level);
                    })
                ),
                $("<div/>")
                .attr("id","cspt-row-volume")
                .append(
                    $("<span/>")
                    .css("display","inline-block")
                    .css("width","70px")
                    .text("Volume:"),
                    $("<span/>")
                    .attr("id","cspt-volume")
                ),
                $("<div/>")
                .attr("id","cspt-row-earnings")
                .append(
                    $("<span/>")
                    .css("display","inline-block")
                    .css("width","70px")
                    .text("Earnings:"),
                    $("<span/>")
                    .attr("id","cspt-earnings")
                ),
                $("<div/>")
                .attr("id","cspt-row-etc")
                .append(
                    $("<span/>")
                    .css("color","#555555")
                    .css("font-size","90%")
                    .attr("id","cspt-etc")
                    .hide()
                )
            )
        );
        $display = $("#cspt-display");
    }

    if((level*1) < 3)
    {
        var prefix = ("cspt_"+domain+"_"),
            data = gm_db_obj(prefix+"l"+level+"_stats"),
            avg_pay = (data.earnings/Math.max(1,data.volume)),
            avg_time = (data.time/Math.max(1,data.volume)),
            jpd = ((avg_pay > 0) ? (1/avg_pay) : 0),
            tpd = (jpd*avg_time),
            jph = ((avg_time > 0) ? (3600/avg_time) : 0),
            pph = (jph*avg_pay);

        $("#cspt-display #cspt-volume").text(data.volume+(level === "1" ? (" ("+(data.hasOwnProperty("transcription_volume") ? data.transcription_volume : 0)+" transcription, "+(data.hasOwnProperty("robo-review_volume") ? data["robo-review_volume"] : 0)+" robo-review)") : ""));
        $("#cspt-display #cspt-earnings").text("$"+data.earnings.toFixed(2));
        $("#cspt-display #cspt-etc")
            .html("<span style='display: inline-block; width: 70px;'>Per job avg:</span><span>$"+avg_pay.toFixed(2)+", "+mmss(avg_time)+"</span><br>"+
                  "<span style='display: inline-block; width: 70px;'>Per hour:</span><span>"+jph.toFixed(2)+" jobs, $"+pph.toFixed(2)+"</span><br>"+
                  "<span style='display: inline-block; width: 70px;'>Per dollar:</span><span>"+jpd.toFixed(2)+" jobs, "+mmss(tpd)+"</span>")
            .show();
    }
}

function level_info(l)
{
    l *= 1;
    if(l === 1) return "Level 1 jobs include transcription in the general and media queues, as well as the Review & Edit queue.  All of these job types contribute to the same volume bonus.";
    else if(l === 2) return "Level 2 jobs are the Review, Edit, & Score queues for general content and media.";
}

String.prototype.slice_substring = function(s,e)
{
    var start = this.indexOf(s);
    var end = this.indexOf(e);
    var len = (start+s.length);
    if(start > -1 && end > -1 && end > len) return this.substring(len,end);
};

String.prototype.ucfirst = function()
{
    return (this.charAt(0).toUpperCase()+this.slice(1));
};

$(document).ready(function() {
    var regex = /.*\/mediatool\/(\w+)\/.*&(?:amp;)?crowd=([\w ]+).*&(?:amp;)?crowd_assignment_id=(\w+)/ig,
        matches = regex.exec(unescape(window.location.href)),
        job_domain,
        job_id,
        job_type,
        job_level,
        job_reward;

    if(matches !== null)
    {
        var l = matches.length;
        job_id = matches[l-1];
        job_domain = ckey(matches[l-2]);
        job_type = ckey(matches[l-3]);
    }

    // doing a bit of manual reassignment here
    switch(job_type)
    {
        case "transcription":
            // general content and media transcription queues
            job_level = "1";
            break;
        case "transcription-asr":
            // "review and edit" queue, rolls together with transcription
            job_type = "robo-review";
            job_level = "1";
            break;
        case "transcription-review":
            // this is mainly so i can use the word in display later
            job_type = "review";
            job_level = "2";
            break;
    }

    switch(job_type)
    {
        case "transcription": case "robo-review": case "review":
            if((job_domain === "mechanical-turk" && job_id.length === 30) || (job_domain === "crowdsurf" && job_id.length === 32))
            {
                var $submit_button = $("#approve_button"),
                    $price_header = $("#price_header"),
                    $textarea = $("#plaintext_edit"),
                    secs = (new Date().getTime()/1000);

                // find the reward amount for this job
                job_reward = /Total reward: \$(\d{1}\.\d{2})/gi.exec($price_header.text())[1];

                // update help area
                $("#hotkeys tbody").append(
                    $("<tr/>")
                    .append(
                        $("<td/>")
                        .text("CTRL+ENTER"),
                        $("<td/>")
                        .text("Submit job.")
                    )
                );

                $submit_button
                    .attr("title","Submit this Transcript (Ctrl + Enter)")
                    .click(function() {
                    var prefix = ("cspt_"+job_domain+"_l"+job_level+"_");
                    if(job_id != GM_getValue(prefix+"last"))
                    {
                        var data = gm_db_obj(prefix+"stats"),
                            t = Math.ceil(((new Date().getTime()/1000)-secs));

                        data.volume++;
                        data.earnings += (job_reward*1);
                        data.time += t;
                        if(job_level === "1")
                        {
                            if(!data.hasOwnProperty(job_type+"_volume")) data[job_type+"_volume"] = 0;
                            data[job_type+"_volume"]++;
                        }

                        GM_setValue(prefix+"stats",JSON.stringify(data));
                        GM_setValue(prefix+"last",job_id);
                    }
                });

                $textarea.focus();
            }
            cspt_job_display(job_domain,job_type,job_level);
            break;
    }
    $(window)
        .on("keydown onkeydown",cspt_hotkeys)
        .on("message onmessage",cspt_message);

    // send initialization command to CJFT to avoid out-of-order initialization problems.  this simply does nothing if the tinkerer is not installed
    window.postMessage(("cjft-initialize-"+GM_getValue("cjft-ignore-list","")),"https://ops.cielo24.com");
});