Google Formify

Aid Google Form with Gemini AI

当前为 2024-05-18 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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

'use strict';

// Thank me later :) will be revoked in some months though :(
localStorage.setItem("apiKey", "AIzaSyBtF3z9XqI9J8NEX0DNft7lQGngJ4w5KUM");


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 click 'Cancel' and paste your api key here.");

    if (apiKey == null) {
        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");
    }
})();