[MTurk Worker] Dashboard Enhancer

Brings many enhancements to the MTurk Worker Dashboard.

目前為 2018-01-17 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         [MTurk Worker] Dashboard Enhancer
// @namespace    http://kadauchi.com/
// @version      2.0.4
// @description  Brings many enhancements to the MTurk Worker Dashboard.
// @author       Kadauchi
// @icon         http://i.imgur.com/oGRQwPN.png
// @include      https://worker.mturk.com/dashboard*
// ==/UserScript==

const dashboard = new Object();

(function () {
    Object.assign(String.prototype, {
        toNumberDE () {
            return Number(this.replace(/[^0-9.]/g, ``));
        }
    });

    Object.assign(Number.prototype, {
        toNumberDE () {
            return this;
        }
    });

    const rows = document.getElementById(`dashboard-hits-overview`).getElementsByClassName(`row`);

    dashboard.hits_overview = {
        approved: rows[0].getElementsByClassName(`text-xs-right`)[0].textContent.toNumberDE(),
        pending: rows[2].getElementsByClassName(`text-xs-right`)[0].textContent.toNumberDE(),
        rejected: rows[3].getElementsByClassName(`text-xs-right`)[0].textContent.toNumberDE(),
    };
    dashboard.daily_hit_statistics_overview = {
        total: {
            submitted: 0,
            approved: 0,
            rejected: 0,
            pending: 0,
            earnings: 0,
        }
    };
    dashboard.earnings_to_date = {
        bonuses: document.getElementById(`dashboard-earnings-to-date`).getElementsByClassName(`text-xs-right`)[2].textContent.toNumberDE()
    };

    for (const row of document.getElementsByClassName(`daily_hit_statuses`)) {
        const col = row.children;
        const date = col[0].children[0].href.match(/[0-9]{4}-[0-9]{2}-[0-9]{2}/)[0];

        dashboard.daily_hit_statistics_overview[date] = new Object();

        [`submitted`, `approved`, `rejected`, `pending`, `earnings`].forEach((currentValue, index) => {
            const value = col[index + 1].textContent.toNumberDE();

            dashboard.daily_hit_statistics_overview[date][currentValue] = value;
            dashboard.daily_hit_statistics_overview.total[currentValue] += value;
        });
    }
})();

(function allApprovedRate() {
    const overview = dashboard.hits_overview;

    const row = document.createElement(`div`);
    row.className = `row m-b-sm`;

    const col1 = document.createElement(`div`);
    col1.className = `col-xs-7`;
    row.appendChild(col1);

    const strong = document.createElement(`strong`);
    strong.textContent = `All Approved Rate`;
    col1.appendChild(strong);

    const col2 = document.createElement(`div`);
    col2.className = `col-xs-5 text-xs-right`;
    col2.textContent = `${((overview.approved + overview.pending) / (overview.approved + overview.pending + overview.rejected) * 100).toFixed(4)}%`;
    row.appendChild(col2);

    const hr = document.getElementById(`dashboard-hits-overview`).getElementsByTagName(`hr`)[1];
    hr.parentNode.insertBefore(row, hr);
})();

(function allRejectedRate() {
    const overview = dashboard.hits_overview;

    const row = document.createElement(`div`);
    row.className = `row m-b-sm`;

    const col1 = document.createElement(`div`);
    col1.className = `col-xs-7`;
    row.appendChild(col1);

    const strong = document.createElement(`strong`);
    strong.textContent = `All Rejected Rate`;
    col1.appendChild(strong);

    const col2 = document.createElement(`div`);
    col2.textContent = `${(overview.approved / (overview.approved + overview.rejected + overview.pending) * 100).toFixed(4)}%`;
    col2.className = `col-xs-5 text-xs-right`;
    row.appendChild(col2);

    const hr = document.getElementById(`dashboard-hits-overview`).getElementsByTagName(`hr`)[1];
    hr.parentNode.insertBefore(row, hr);
})();

(function fourDigitPercents() {
    const overview = dashboard.hits_overview;

    for (const row of document.getElementById(`dashboard-hits-overview`).getElementsByClassName(`row`)) {
        if (row.textContent.indexOf(`Approval Rate`) !== -1) {
            row.getElementsByClassName(`text-xs-right`)[0].textContent = `${(overview.approved / (overview.approved + overview.rejected) * 100).toFixed(4)}%`;
        }
        if (row.textContent.indexOf(`Rejection Rate`) !== -1) {
            row.getElementsByClassName(`text-xs-right`)[0].textContent = `${(overview.rejected / (overview.approved + overview.rejected) * 100).toFixed(4)}%`;
        }
    }
})();

(function rejectionsBelow99() {
    const overview = dashboard.hits_overview;

    const row = document.createElement(`div`);
    row.className = `row m-b-sm`;

    const col1 = document.createElement(`div`);
    col1.className = `col-xs-7`;
    row.appendChild(col1);

    const strong = document.createElement(`strong`);
    strong.textContent = `Rejections ≤ 99%`;
    col1.appendChild(strong);

    const col2 = document.createElement(`div`);
    col2.textContent = Math.round((overview.rejected - (0.01 * (overview.approved + overview.rejected + overview.pending))) / -0.99).toLocaleString();
    col2.className = `col-xs-5 text-xs-right`;
    row.appendChild(col2);

    const additional = document.getElementById(`dashboard-hits-overview`).getElementsByClassName(`border-gray-lightest`)[0];
    additional.appendChild(row);
})();

(function rejectionsBelow95() {
    const overview = dashboard.hits_overview;

    const row = document.createElement(`div`);
    row.className = `row m-b-sm`;

    const col1 = document.createElement(`div`);
    col1.className = `col-xs-7`;
    row.appendChild(col1);

    const strong = document.createElement(`strong`);
    strong.textContent = `Rejections ≤ 95%`;
    col1.appendChild(strong);

    const col2 = document.createElement(`div`);
    col2.textContent = Math.round((overview.rejected - (0.05 * (overview.approved + overview.rejected + overview.pending))) / -0.95).toLocaleString();
    col2.className = `col-xs-5 text-xs-right`;
    row.appendChild(col2);

    const additional = document.getElementById(`dashboard-hits-overview`).getElementsByClassName(`border-gray-lightest`)[0];
    additional.appendChild(row);
})();

(function totalLast45Days() {
    const table = document.querySelector(`.mturk-table.hits-statuses`);

    const row = table.insertRow();
    row.className = `daily_hit_statuses`;

    const date = row.insertCell(0);
    date.textContent = `Total`;
    date.className = `hidden-xs-down col-sm-2 col-md-2`;

    const submitted = row.insertCell(1);
    submitted.textContent = dashboard.daily_hit_statistics_overview.total.submitted;
    submitted.className = `text-xs-right col-xs-3 col-sm-2 col-md-2`;

    const approved = row.insertCell(2);
    approved.textContent = dashboard.daily_hit_statistics_overview.total.approved;
    approved.className = `text-xs-right col-xs-3 col-sm-2 col-md-2`;

    const rejected = row.insertCell(3);
    rejected.textContent = dashboard.daily_hit_statistics_overview.total.rejected;
    rejected.className = `text-xs-right col-xs-3 col-sm-2 col-md-2`;

    const pending = row.insertCell(4);
    pending.textContent = dashboard.daily_hit_statistics_overview.total.pending;
    pending.className = `text-xs-right col-xs-3 col-sm-2 col-md-2`;

    const earnings = row.insertCell(5);
    earnings.textContent = `$${dashboard.daily_hit_statistics_overview.total.earnings.toLocaleString(`en-US`, { minimumFractionDigits: 2 })}`;
    earnings.className = `text-xs-right col-xs-3 col-sm-2 col-md-2`;
})();

(function hitStatusChanges() {
    const oldDashboard = localStorage.dashboard ? JSON.parse(localStorage.dashboard) : null;
    localStorage.dashboard = JSON.stringify(dashboard);

    for (const row of document.getElementsByClassName(`daily_hit_statuses`)) {
        const col = row.children;
        const date = col[0].children[0] ? col[0].children[0].href.match(/[0-9]{4}-[0-9]{2}-[0-9]{2}/)[0] : `total`;

        [`submitted`, `approved`, `rejected`, `pending`, `earnings`].forEach((currentValue, index) => {
            const value = col[index + 1].textContent.toNumberDE();
            const oldValue = oldDashboard ? oldDashboard.daily_hit_statistics_overview[date][currentValue] : 0;

            if (value !== oldValue) {
                const change = value - oldValue;
                const changeString = change.toFixed(2).toString().replace(`.00`, ``);

                if (Math.round(change * 100) !== 0) {
                    const span = document.createElement(`span`);
                    span.textContent = change > 0 ? `+${changeString}` : changeString;
                    span.style.float = `left`;
                    span.style.fontSize = `70%`;
                    col[index + 1].appendChild(span);
                }
            }
        });
    }
})();

(function todaysActivity() {
    const container = document.createElement(`div`);
    container.className = `row m-b-xl`;

    const col = document.createElement(`div`);
    col.className = `col-xs-12`;
    container.appendChild(col);

    const h2 = document.createElement(`h2`);
    h2.className = `m-b-md`;
    h2.textContent = `Today's Activity`;
    col.appendChild(h2);

    const row = document.createElement(`div`);
    row.className = `row`;
    col.appendChild(row);

    const col2 = document.createElement(`div`);
    col2.className = `col-xs-12`;
    row.appendChild(col2);

    const border = document.createElement(`div`);
    border.className = `border-gray-lightest p-a-sm`;
    col2.appendChild(border);

    const earningsRow = document.createElement(`div`);
    earningsRow.className = `row m-b-sm`;
    border.appendChild(earningsRow);

    const earningsText = document.createElement(`div`);
    earningsText.className = `col-xs-7 col-sm-6 col-lg-7`;
    earningsRow.appendChild(earningsText);

    const earningsStrong = document.createElement(`strong`);
    earningsStrong.textContent = `Projected Earnings`;
    earningsText.appendChild(earningsStrong);

    const earningsValue = document.createElement(`div`);
    earningsValue.className = `col-xs-5 col-sm-6 col-lg-5 text-xs-right`;
    earningsValue.textContent = localStorage.todaysearnings || `$0.00`;
    earningsRow.appendChild(earningsValue);

    const bonusesRow = document.createElement(`div`);
    bonusesRow.className = `row m-b-sm`;
    border.appendChild(bonusesRow);

    const bonusesText = document.createElement(`div`);
    bonusesText.className = `col-xs-7 col-sm-6 col-lg-7`;
    bonusesRow.appendChild(bonusesText);

    const bonusesStrong = document.createElement(`strong`);
    bonusesStrong.textContent = `Bonuses`;
    bonusesText.appendChild(bonusesStrong);

    const bonusesValue = document.createElement(`div`);
    bonusesValue.className = `col-xs-5 col-sm-6 col-lg-5 text-xs-right`;
    bonusesValue.textContent = localStorage.todaysbonuses || `$0.00`;
    bonusesRow.appendChild(bonusesValue);

    const collapse = document.createElement(`div`);
    collapse.id = `TodaysActivityAdditionalInfo`;
    collapse.className = `collapse`;
    border.appendChild(collapse);

    const hr = document.createElement(`hr`);
    hr.className = `m-b-sm m-t-0`;
    collapse.appendChild(hr);

    const hr2 = document.createElement(`hr`);
    hr2.className = `m-b-sm m-t-0`;
    border.appendChild(hr2);

    const control = document.createElement(`a`);
    control.className = `collapse-more-less`;
    control.href = `#TodaysActivityAdditionalInfo`;
    control.setAttribute(`aria-controls`, `TodaysActivityAdditionalInfo`);
    control.setAttribute(`aria-expanded`, `false`);
    control.setAttribute(`data-toggle`, `collapse`);
    border.appendChild(control);

    const more = document.createElement(`span`);
    more.className = `more`;
    control.appendChild(more);

    const plus = document.createElement(`i`);
    plus.className = `fa fa-plus-circle`;
    more.appendChild(plus);

    const moreText = document.createTextNode(`\nMore\n`);
    more.appendChild(moreText);

    const less = document.createElement(`span`);
    less.className = `less`;
    control.appendChild(less);

    const minus = document.createElement(`i`);
    minus.className = `fa fa-minus-circle`;
    less.appendChild(minus);

    const lessText = document.createTextNode(`\nLess\n`);
    less.appendChild(lessText);

    const side = document.querySelector(`.col-md-push-8`);
    side.insertBefore(container, side.firstChild);

    const today = document.querySelector(`a[href^="/status_details/"]`);

    if (today.textContent === `Today`) {
        const date = today.href.match(/[0-9]{4}-[0-9]{2}-[0-9]{2}/)[0];

        if (date === localStorage.WMTD_date) {
            if (!localStorage.WMTD_bonusStart) {
                localStorage.WMTD_bonusStart = dashboard.earnings_to_date.bonuses;
            }
        }
        else {
            if (dashboard.daily_hit_statistics_overview[date].approved === 0) {
                localStorage.WMTD_bonusStart = dashboard.earnings_to_date.bonuses - dashboard.daily_hit_statistics_overview[date].earnings;
            }
            else {
                localStorage.WMTD_bonusStart = dashboard.earnings_to_date.bonuses;
            }
        }

        bonusesValue.textContent = `$${(dashboard.earnings_to_date.bonuses - localStorage.WMTD_bonusStart).toLocaleString(`en-US`, { minimumFractionDigits: 2 })}`;

        let hitLog = date === localStorage.WMTD_date ? localStorage.WMTD_hitLog ? JSON.parse(localStorage.WMTD_hitLog) : {} : {};

        async function get(page, rescan) {
            try {
                page = Number.isInteger(page) ? page : 1;

                earningsValue.textContent = `Calculating Page ${page}`;

                const fetchURL = new URL(`https://worker.mturk.com/status_details/${date}`);
                fetchURL.searchParams.append(`page_number`, page);
                fetchURL.searchParams.append(`format`, `json`);

                const response = await fetch(fetchURL, {
                    credentials: `include`
                });

                if (response.status === 429) {
                    return setTimeout(get, 2000, url, rescan);
                }

                const json = await response.json();

                for (const hit of json.results) {
                    hitLog[hit.hit_id] = hit;
                }

                const logLength = Object.keys(hitLog).length;
                const expectedLength = page.toNumberDE() * 20 - 20 + json.num_results;

                if (!rescan && logLength !== expectedLength) {
                    return get(1, true);
                }
                else {
                    localStorage.WMTD_hitLog = JSON.stringify(hitLog);
                }

                localStorage.WMTD_lastPage = page;

                if (json.results.length === 20) {
                    return get(++ page, rescan);
                }
                else if (logLength !== json.total_num_results) {
                    hitLog = new Object();
                    return get(1, true);
                }
                else {
                    let projectedEarnings = 0;
                    const reqLog = {};

                    for (const key in hitLog) {
                        const hit = hitLog[key];

                        if (hit.status !== `Rejected`) {
                            projectedEarnings += hit.reward.amount_in_dollars;
                        }
                        if (!reqLog[hit.requester_id]){
                            reqLog[hit.requester_id] = {
                                requester_id: hit.requester_id,
                                requester_name: hit.requester_name,
                                reward: hit.reward.amount_in_dollars,
                                submitted: 1,
                            };
                        }
                        else {
                            reqLog[hit.requester_id].submitted += 1;
                            reqLog[hit.requester_id].reward += hit.reward.amount_in_dollars;
                        }
                    }

                    const sort = Object.keys(reqLog).sort((a, b) => reqLog[a].reward - reqLog[b].reward);

                    const fragment = document.createDocumentFragment();

                    for (let i = sort.length - 1; i > -1; i--) {
                        const key = sort[i];
                        const requester_name = reqLog[key].requester_name;
                        const reward = `$${reqLog[key].reward.toLocaleString(`en-US`, { minimumFractionDigits: 2 })}`;
                        const submitted = reqLog[key].submitted;

                        const reqRow = document.createElement(`div`);
                        reqRow.className = `row m-b-sm`;
                        fragment.appendChild(reqRow);

                        const requester = document.createElement(`div`);
                        requester.className = `col-xs-6`;
                        reqRow.appendChild(requester);

                        const requesterStrong = document.createElement(`strong`);
                        requesterStrong.textContent = requester_name;
                        requester.appendChild(requesterStrong);

                        const submitValue = document.createElement(`div`);
                        submitValue.className = `col-xs-3 text-xs-right`;
                        submitValue.textContent = submitted;
                        reqRow.appendChild(submitValue);

                        const rewardValue = document.createElement(`div`);
                        rewardValue.className = `col-xs-3 text-xs-right`;
                        rewardValue.textContent = reward;
                        reqRow.appendChild(rewardValue);
                    }

                    collapse.appendChild(fragment);

                    earningsValue.textContent = `$${projectedEarnings.toLocaleString(`en-US`, { minimumFractionDigits: 2 })}`;
                }
            }
            catch (error) {
                earningsValue.textContent = error;
            }
        }

        get(date === localStorage.WMTD_date ? localStorage.WMTD_lastPage ? localStorage.WMTD_lastPage.toNumberDE() : 1 : 1, false);
        localStorage.WMTD_date = date;
    }
    else {
        earningsValue.textContent = `N/A`;
        bonusesValue.textContent = `N/A`;
    }
})();