User:Nullnominal/scripts/AIT/Flow.js

Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
/*
 * @author User:Nullnominal <https://en.wikipedia.org/wiki/User:Nullnominal>
 * @license CC-BY-SA-4.0
 *
 *
 * // Imports
 * @typedef {import('@types/jquery')} $
 * @typedef {import("types-mediawiki")} mw
 * @typedef {import("@types/oojs-ui")} OO
 * @typedef {import("./wpAIToolsProvider.d.ts").default} Provider
 *
 */

/// <reference path="wpAIToolsProvider.d.ts" />

/**
 * @typedef {{
 *     provider: Provider,
 * }} AITools;
 * @typedef {{
  *         score: number,
 *         reasoning: string,
 *         quote: string,
 *         domStartId: string
 *         domEndId: string
 * }} Verdict;
 

* @typedef {{
 *      tool: string,
 *      author: string,
 *      oldID: string,
 *      dom: string,
 *      finalVerdict: {
 *          score: number,
 *          reasoning: string
 *      },
 *      verdict: Verdict[]
 * }} FlowResponse

 */

mw.loader.using("ext.aitools.Provider", "oojs-ui-windows").done(() => {
  class FlowChecker extends OO.ui.TabPanelLayout {
    /**
     * @type {Provider};
     */
    provider;
    constructor() {
      super("Flow", {
        label: "Flow",
      });

      /**
       * @type {Provider}
       */
      this.provider = AITools.provider._instance;

      this.$element.append(this.createUI().$element);

      this.provider.addTool(this);
    }

    /**
     * @argument {FlowResponse} resp;
     */
    createPopup(resp) {
      const root = new OO.ui.TabPanelLayout("Flow Response", {
        label: "Flow Response",
      });

      let body;
      if (mw.config.get("wgRevisionId") !== resp.oldID) {
        body = new OO.ui.StackLayout({
          continuous: true,
          padded: true,
          content: [
            new OO.ui.PanelLayout({
              text: "Error",
              padded: true,
              framed: true,
            }),
            new OO.ui.PanelLayout({
              padded: true,
              text: `The current page
                                  (${mw.config.get("wgPageName").replace("_", "")}
                                  @
                                  ${mw.config.get("wgRevisionId")})
                                  is not the page that this was file was generated on `,
            }),
            new OO.ui.ButtonWidget({
              href: mw.util.getUrl(`Special:PermaLink/${resp.oldID}`),
              label: "Go to Correct Page",
              title: "Go to Page",
            }),
          ],
        });
      } else {
        body = new OO.ui.StackLayout({
          continuous: true,
          padded: true,
          content: [
            new OO.ui.PanelLayout({
              text: `Author: ${resp.author}`,
            }),
            new OO.ui.PanelLayout({
              text: `Tool:   ${resp.tool}`,
            }),
            new OO.ui.StackLayout({
              continuous: true,
              padded: true,
              framed: true,
              content: [
                new OO.ui.PanelLayout({
                  text: `Overall Quality: ${resp.finalVerdict.score * 100}%`,
                }),
                new OO.ui.PanelLayout({
                  text: resp.finalVerdict.reasoning,
                }),
              ],
            }),
            new OO.ui.StackLayout({
              continuous: true,
              padded: true,
              framed: true,
              content: resp.verdict.map(this.createVerdict),
            }),
          ],
        });
      }

      root.$element.append(body.$element);

      this.provider.addTool(root);
    }

    /**
     *
     * @param {string} title
     * @returns {Promise<string>}
     */
    async getPageContents() {
      const pageRequest = await api.get({
        action: "query",
        format: "json",
        prop: "revisions",
        revisions: mw.config.get("wgCurRevisionId"),
        formatversion: "2",
        rvprop: "content",
        rvslots: "main",
      });

      const rev = pageRequest.query.pages[0].revisions[0];
      return rev.slots.main.content;
    }

    /**
     * @argument {Verdict} verdict;
     */
    createVerdict(verdict) {
      return new OO.ui.StackLayout({
        padded: true,
        framed: true,
        continuous: true,
        content: [
          new OO.ui.PanelLayout({
            text: `Quote Quality: ${verdict.score * 100}%`,
          }),
          new OO.ui.PanelLayout({
            text: verdict.reasoning,
          }),
          new OO.ui.PanelLayout({
            padded: true,
            text: `"${verdict.quote}`,
          }),
        ],
      });
    }

    callAI() {}

    /**
     * @argument {{score: number, reasoning: string, quote: string}} verdict
     * @returns {Verdict}
     */
    _getVerdictLocations(verdicts, wikitext, dom) {
      const elements = document.createTreeWalker(dom, NodeFilter.SHOW_ELEMENT);

      /**
        * @type {{
        *   [k: string]: {
        *     start: number,
        *     end: number,
        *     currentStart: number,
        *     currentEnd: number,
        *     startElement: HTMLElement
        *     endElement: HTMLElement
        *   }
        * }}
        */

      const map = {};

      verdicts.map((e) => {
        match = wikitext.match(e.quote);

        map[e.quote] = {
          start: match.index,

          end: match.index + e.quote.length,

          currentStart: 0,

          currentEnd: wikitext.length,

          startElement: elements[0],

          endElement: elements[0],
        };
      });

      let elm;

      while ((elm = elements.nextNode())) {
        let data = elm.getAttribute("data-parsoid");

        if (data === null || !data.includes("dsr")) continue;

        try {
          data = JSON.parse(data);
        } catch (e) {
          data = JSON.parse("{" + data.match(/\"dsr\":.*/));
        }

        const start = data.dsr[0];

        const end = data.dsr[1];

        for (let quote of quotes) {
          if (map[quote].start >= start && start >= map[quote].currentStart) {
            map[quote].currentStart = start;

            map[quote].startElement = elm;
          }

          if (map[quote].end <= end && end <= map[quote].currentEnd) {
            map[quote].currentEnd = end;

            map[quote].endElement = elm;
          }
        }
      }

      return verdicts.map((e) => {
          e.domStartId = map[e.quote].startElement.id;
          e.domEndId   = map[e.quote].endElement.id;
      });
    }

    /**
     * @argument {{
     *   verdicts: {
     *       score: number,
     *       reasoning: string,
     *       quote: string
     *   }[],
     *   removed: number[],
     *   added: Verdict[],
     *   finalVerdict: {
     *       score: number,
     *       reasoning: string
     *   }
     * }} aiResp
     * @argument {string} wikitext
     *
     */
    async createDataPackage(aiResp, wikitext, title) {
      const rest = new mw.Rest();

      let resp = {};
      resp["oldid"] = mw.gonfig.get("wgRevisionId");
      
      const parser = new DomParser();
      const dom = parser.parseFromString(await rest.post(`/transform/wikitext/to/html/${title}/${resp["oldid"]}`, {
        wikitext: "string",
        body_only: true,
        stash: true
      }), "text/html");

      let verdicts = aiResp.verdicts.filter((v, i) => {
        return !(i in aiResp.removed);
      });

      resp["verdicts"] = this._getVerdictLocations(verdics.concat(aiResp.added), wikitext, dom);
      resp["finalVerdict"] = aiResp.finalVerdict;
      resp["tool"] = this.provider.APIName;
      resp["oldid"] = mw.config.get("wgRevisionId");
      resp["author"] = mw.user.getName();
      resp["dom"] = dom.getElementsByClassName("mw-parser-output")[0].outerHTML;

      return resp;
    }

    createUI() {
      const callAIButton = new OO.ui.ButtonWidget({
        disabled: this.provider.canUseAI(),
        label: "Check!",
      });

      const uploadWidget = new OO.ui.SelectFileInputWidget({
        accept: "text/json",
      });

      uploadWidget.on(
        "change",
        (async (currentFiles) => {
          this.createPopup(JSON.parse(await currentFiles[0].text()));
        }).bind(this),
      );

      callAIButton.on("click", this.callAI.bind(this));

      return new OO.ui.StackLayout({
        continuous: true,
        content: [
          new OO.ui.StackLayout({
            framed: true,
            padded: false,
            continuous: true,
            content: [
              new OO.ui.PanelLayout({
                padded: true,
                $content: $("<h3>Live LLM</h3>"),
              }),
              new OO.ui.PanelLayout({
                padded: true,
                content: [
                  new OO.ui.TextInputWidget({
                    value: mw.config.get("wgPageName").replace("_", " "),
                    disabled: true,
                  }),
                ],
              }),
              new OO.ui.PanelLayout({
                padded: true,
                content: [callAIButton],
              }),
            ],
          }),
          new OO.ui.StackLayout({
            framed: true,
            padded: true,
            continuous: true,
            content: [
              new OO.ui.PanelLayout({
                $content: $("<h3>Previous Analysis</h3>"),
              }),
              uploadWidget,
            ],
          }),
        ],
      });
    }
  }

  new FlowChecker();
  
});