Google Formify

Aid Google Form with Gemini AI

目前为 2024-11-07 提交的版本。查看 最新版本

// ==UserScript==
// @name         Google Formify
// @version      2.6
// @description  Aid Google Form with Gemini AI
// @author       rohitaryal
// @license      MIT
// @grant        GM_xmlhttpRequest
// @grant        unsafeWindow
// @grant        GM_addElement
// @grant        GM.xmlHttpRequest
// @connect      googleapis.com
// @namespace    https://docs.google.com/
// @match        https://docs.google.com/forms/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=google.com
// ==/UserScript==

"use strict";

let apiKey = localStorage.getItem("apiKey");
let isOldUser = localStorage.getItem("old_user");

while (!apiKey || apiKey.length <= 10) {
  apiKey = window.prompt(
    "Please enter your API key. To get one for free goto 'https://makersuite.google.com/app/apikey' and paste your api key here."
  );

  if (apiKey == null) {
    console.log(apiKey);
    window.open("https://makersuite.google.com/app/apikey", "_blank");
  } else {
    localStorage.setItem("apiKey", apiKey);
  }
}

const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=${apiKey}`;

class Question {
  #headers = {
    "Content-Type": "application/json",
  };

  #onerror = (error) => {
    console.warn(": Some error occured while sending request", error);
  };

  constructor(
    question, // (string)
    questionImage, // (string)(url)
    options, // (Array[{}])
    isOptional, // (boolean)
    questionType, // (string) textbox, multipleChoice(same for checkbox)
    htmlNode // (HTMLElement)
  ) {
    this.question = question;
    this.questionImage = questionImage;
    this.options = options;
    this.isOptional = isOptional;
    this.questionType = questionType;
    this.aiAnswer = null;

    if (!unsafeWindow.deleteNode) {
      this.htmlNode = htmlNode;
    }
  }

  async aiAssist() {
    let data = null;

    if (this.questionType == "multipleChoice") {
      let finalOptions = "";
      this.options.forEach((option, index) => {
        finalOptions += option.value + "\n";
      });

      data = `{"contents":[{"parts":[{"text":"Choose only the one correct option for this question: Question: ${this.question} Options: ${finalOptions}.\n"}]}]}`;
    } else if (this.questionType == "checkbox") {
      let finalOptions = "";
      this.options.forEach((option, index) => {
        finalOptions += option.value + "\n";
      });

      data = `{"contents":[{"parts":[{"text":"Choose the correct option for this question(More than one can be true): Question: ${this.question} Options: ${finalOptions}.\n"}]}]}`;
    } else {
      data = `{"contents":[{"parts":[{"text":"Write something like a human on topic: '${this.question}'.\n Start now!"}]}]}`;
    }

    let request = await GM.xmlHttpRequest({
      method: "POST",
      url: url,
      headers: this.#headers,
      data,
    }).catch((error) => this.#onerror);

    this.aiAnswer = this.parseJSON(request);
  }

  async fillUp() {
    await this.aiAssist();

    if (this.aiAnswer?.trim() == "" || !this.aiAnswer) {
      this.htmlNode.querySelector(".ai-answer").textContent =
        "😭 Failed to fetch answers from server... ";
    } else {
      this.htmlNode.querySelector(".ai-answer").textContent = this.aiAnswer;
    }

    if (this.questionType == "multipleChoice") {
      let allOptions = [...this.htmlNode.querySelectorAll("label")];

      this.options.forEach((option, index) => {
        if (this.aiAnswer?.includes(option.value)) {
          allOptions[index].click();
        }
      });
    } else if (this.questionType == "checkbox") {
      let allOptions = [...this.htmlNode.querySelectorAll("label")];

      this.options.forEach((option, index) => {
        if (this.aiAnswer?.includes(option.value)) {
          allOptions[index].click();
        }
      });
    } else {
      let allTextboxes = [
        ...this.htmlNode.querySelectorAll("input[type=text], textarea"),
      ];

      allTextboxes.forEach((element) => {
        element.value = this.aiAnswer;
      });
    }
  }

  parseJSON(data) {
    let parsedAnswer = null;

    try {
      let parsedData = JSON.parse(data.responseText);
      parsedAnswer = parsedData?.candidates?.[0]?.content?.parts?.[0]?.text;
    } catch (e) {
      console.warn("Failed to parse to JSON.", e);
    }

    return parsedAnswer;
  }
}

class GoogleFormParser {
  parse() {
    let finalQuestionList = [];

    const googleFormTitle = document.querySelector(
      ".F9yp7e.ikZYwf.LgNcQe"
    )?.textContent;
    const googleFormDescription =
      document.querySelector(".cBGGJ.OIC90c")?.textContent;
    const questionCards = document.querySelectorAll("[jsmodel='CP1oW']");

    if (
      questionCards == undefined ||
      questionCards == null ||
      questionCards.length == 0
    ) {
      throw ": No questions found. Maybe this form is empty";
    }

    questionCards.forEach((card, index) => {
      let parsedDataArray = null;

      let dataParams = card.getAttribute("data-params")?.replace("%.@.", "[");

      if (!dataParams) {
        console.warn(
          `No data-params found for card index ${index}. So, skipping this card.`,
          card
        );
        return;
      }

      try {
        parsedDataArray = JSON.parse(dataParams);
      } catch (e) {
        console.warn(
          `Failed to parse obtained data-params to JSON: ${dataParams}`,
          e
        );
        return;
      }

      let questionImage = null;
      let question = parsedDataArray?.[0]?.[1];
      let subdivsInsideCard = card.querySelectorAll(".geS5n");

      if (!!subdivsInsideCard.length != 0) {
        subdivsInsideCard = [...subdivsInsideCard[0].childNodes];
      }
      subdivsInsideCard = subdivsInsideCard.filter((item) => {
        return item.tagName == "DIV";
      });

      // Length >= 4 means question might have image;
      if (subdivsInsideCard.length >= 4) {
        subdivsInsideCard.forEach((div) => {
          let imageTags = div.querySelectorAll("img");

          // Either theres no img elements or we already found URL.
          if (imageTags.length == 0 || !!questionImage) {
            return;
          }

          questionImage = imageTags[0]?.src;
        });
      }

      let questionType = null;

      if (card.querySelectorAll(".Yri8Nb").length != 0) {
        questionType = "checkbox";
      } else if (card.querySelectorAll(".ajBQVb").length != 0) {
        questionType = "multipleChoice";
      } else if (
        card.querySelectorAll("input[type=text], textarea").length == 1
      ) {
        questionType = "textbox";
      }

      let options = parsedDataArray?.[0]?.[4]?.[0]?.[1];

      options = options?.map((option, index) => {
        let image = null;
        if (option.length >= 6) {
          image = card
            .querySelectorAll("label")
            [index]?.querySelector("img")?.src;
        }

        return {
          value: option[0],
          image,
        };
      });

      let isOptional = parsedDataArray?.[0]?.[4]?.[0]?.[2];

      let finalQuestionBody = new Question(
        question,
        questionImage,
        options,
        isOptional,
        questionType,
        card
      );

      finalQuestionList.push(finalQuestionBody);
    });

    return finalQuestionList;
  }
}

(function () {
  let googleForm = new GoogleFormParser();

  let questions = googleForm.parse();

  console.log(questions);

  let style = document.createElement("style");
  style.textContent = `.ai-container *{margin:0;padding:0;box-sizing:border-box;}.ai-container{margin-bottom: 10px;width:100%;color:#343232;padding:8px 0;background-color:#fff;border-radius:10px;box-shadow:rgba(9,30,66,.25) 0 4px 8px -2px,rgba(9,30,66,.08) 0 0 0 1px}.ai-container .ai-footer,.ai-container .ai-header{padding:4px 16px 10px;display:flex;align-items:center;justify-content:space-between}.ai-container .ai-header .ai-title{font-weight:bolder;font-size:15px}.ai-container .ai-header ul{list-style-type:none;display:flex;align-items:center;justify-content:space-between}.ai-container .ai-header ul li{font-weight:bolder;font-size:small;padding:0 6px;cursor:pointer;transition-duration:.2s;border:2px solid transparent;margin-right:8px;border-radius:4px}.ai-container .ai-header ul li:hover{color:#fff;background-color:#ff4500}.ai-container hr{border:1px solid #42ea42}.ai-container .ai-answer{font-size:13px;padding:16px 16px 8px 16px;}.ai-container .ai-footer{padding:10px 0 0 8px;width:100%;color:orange}.ai-container .ai-footer .ai-circle{display:flex;align-items:center;justify-content:center}.ai-container .ai-footer .ai-circle li{width:15px;color:#42ea42}.ai-container .ai-footer .ai-warning{font-size:10px;width:100%}`;
  document.head.appendChild(style);

  questions.forEach((question) => {
    const container = document.createElement("div");
    container.className = "ai-container";

    const divHeader = document.createElement("div");
    divHeader.className = "ai-header";

    const divTitle = document.createElement("div");
    divTitle.className = "ai-title";
    divTitle.textContent = "🦕 Gemini Pro";

    const ul = document.createElement("ul");

    const liSearch = document.createElement("li");
    liSearch.id = "ai-search";
    liSearch.textContent = "SEARCH";

    const liCopy = document.createElement("li");
    liCopy.id = "ai-copy";
    liCopy.textContent = "COPY";

    const hr = document.createElement("hr");

    const pAnswer = document.createElement("p");
    pAnswer.className = "ai-answer";
    pAnswer.textContent = "I am working on it. Please wait....";

    const divFooter = document.createElement("div");
    divFooter.className = "ai-footer";

    const pWarning = document.createElement("p");
    pWarning.className = "ai-warning";
    pWarning.textContent =
      "*Note: Not all AI generated content are 100% accurate. Use Search feature for double check.";

    const divCircle = document.createElement("div");
    divCircle.className = "ai-circle";

    const liCircle = document.createElement("li");

    divHeader.appendChild(divTitle);
    divHeader.appendChild(ul);
    ul.appendChild(liSearch);
    ul.appendChild(liCopy);

    divFooter.appendChild(pWarning);
    divFooter.appendChild(divCircle);
    divCircle.appendChild(liCircle);

    container.appendChild(divHeader);
    container.appendChild(hr);
    container.appendChild(pAnswer);
    container.appendChild(divFooter);

    question.htmlNode.appendChild(container);

    let options = "";
    let questionValue = question.question;

    question?.options?.forEach((option) => {
      options += option.value + "\n";
    });

    liSearch.addEventListener("click", (e) => {
      e.preventDefault();

      window.open(
        "https://google.com/search?q=" + questionValue + options,
        "_blank"
      );
    });

    liCopy.addEventListener("click", (e) => {
      setTimeout(function () {
        liCopy.textContent = "COPY";
      }, 3000);

      e.preventDefault();
      navigator.clipboard.writeText(questionValue + options);
      liCopy.textContent = "COPIED";
    });
  });

  questions.forEach((element) => {
    element.fillUp();
  });

  // Add a keyboard shortcut.

  document.addEventListener("keydown", (e) => {
    if (e.ctrlKey && e.altKey) {
      let aiElement = document.querySelectorAll(".ai-container");
      aiElement.forEach((container) => {
        if (container.style.display != "none") {
          container.style.display = "none";
        } else {
          container.style.display = "block";
        }
      });
    }
  });

  if (!isOldUser) {
    alert("You can press CTRL + ALT key to hide/unhide the AI");
    localStorage.setItem("old_user", "true");
  }
})();