[MTurk Worker] Dashboard Enhancer

Brings many enhancements to the MTurk Worker Dashboard.

您需要先安裝使用者腳本管理器擴展,如 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.2.0
// @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==

try { if (mturksuite) return; } catch (e) {}

const toNum = (string) => Number(string.replace(/[^0-9.]/g, ``));
const toDate = (string) => string.split(`T`)[0];
const toMoney = (number) => `$${number.toFixed(2)}`;
const needPlus = (number) => number > 0 ? `+` : ``;

const statusDetailsTable = document.querySelector(
  `div[data-react-class="require('reactComponents/dailyWorkerStatisticsTable/DailyWorkerStatisticsTable')['default']"]`,
);
const statusDetailsArr = JSON.parse(statusDetailsTable.dataset.reactProps).bodyData;
const statusDetailsObj = statusDetailsArr.reduce((obj, details) => ({ ...obj, [toDate(details.date)]: details }), {});

const hitsOverviewTable = document.getElementById(`dashboard-hits-overview`);
const hitsOverviewRows = hitsOverviewTable.querySelectorAll(`.col-xs-5.text-xs-right`);
const hitsOverview = {
  approved: toNum(hitsOverviewRows[0].textContent),
  pending: toNum(hitsOverviewRows[1].textContent),
  rejected: toNum(hitsOverviewRows[2].textContent),
};

function allApprovedRate() {
  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 = `${(
    ((hitsOverview.approved + hitsOverview.pending) /
     (hitsOverview.approved + hitsOverview.pending + hitsOverview.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 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 = `${(
    (hitsOverview.approved / (hitsOverview.approved + hitsOverview.rejected + hitsOverview.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() {
  for (const row of document.getElementById(`dashboard-hits-overview`).getElementsByClassName(`row`)) {
    if (row.textContent.includes(`Approval Rate`)) {
      row.getElementsByClassName(`text-xs-right`)[0].textContent = `${(
        (hitsOverview.approved / (hitsOverview.approved + hitsOverview.rejected)) *
        100
      ).toFixed(4)}%`;
    }
    if (row.textContent.includes(`Rejection Rate`)) {
      row.getElementsByClassName(`text-xs-right`)[0].textContent = `${(
        (hitsOverview.rejected / (hitsOverview.approved + hitsOverview.rejected)) *
        100
      ).toFixed(4)}%`;
    }
  }
}

function hitStatusChanges() {
  const old = localStorage.getItem(`statusDetailsObj`) ? JSON.parse(localStorage.getItem(`statusDetailsObj`)) : {};
  localStorage.setItem(`statusDetailsObj`, JSON.stringify(statusDetailsObj));

  function applyChanges(node) {
    node.querySelectorAll(`.desktop-row`).forEach((row) => {
      const date = row.querySelector(`a`).href.split(`/status_details/`)[1];

      row.querySelectorAll(`.text-xs-right`).forEach((col) => {
        const key = col.classList[2].replace(`-column`, ``).replace(`-`, `_`);
        const change = statusDetailsObj[date][key] - (old[date] ? old[date][key] : 0);

        if (change !== 0) {
          const span = document.createElement(`span`);
          span.textContent = key.includes(`rewards`) || key.includes(`earnings`) ? `${needPlus(change)}${toMoney(change)}` : `${needPlus(change)}${change}`;
          span.style.float = `left`;
          span.style.fontSize = `70%`;
          col.appendChild(span);
        }
      });
    });
  }

  const observer = new MutationObserver((mutationsList, observer) => {
    mutationsList.forEach((mutation) => {
      const addedNode = mutation.addedNodes[0];

      if (addedNode && addedNode.classList.contains(`expanded-row`)) {
        applyChanges(addedNode);
      }
    });
  });

  observer.observe(statusDetailsTable, { childList: true, subtree: true });
}

function latestActivity() {
  const latest = statusDetailsArr[0];
  const date = toDate(latest.date);

  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 = `Activity for ${date}`;
  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);

  bonusesValue.textContent = `$${latest.bonus_rewards.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, page, 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 = Number(page) * 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 = {};
        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 ? Number(localStorage.WMTD_lastPage) : 1) : 1,
    false,
  );
  localStorage.WMTD_date = date;
}

function openFirstWeek() {
  statusDetailsTable.querySelector(`.fa.expand-button.fa-plus-circle`).click();
}

function rejectionsBelow99() {
  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(
    (hitsOverview.rejected - 0.01 * (hitsOverview.approved + hitsOverview.rejected + hitsOverview.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 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(
    (hitsOverview.rejected - 0.05 * (hitsOverview.approved + hitsOverview.rejected + hitsOverview.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);
}

allApprovedRate();
allRejectedRate();
fourDigitPercents();
hitStatusChanges();
latestActivity();
openFirstWeek();
rejectionsBelow99();
rejectionsBelow95();