Google Formify

Aid Google Form with Gemini AI

As of 2024-06-23. See the latest version.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Google Formify
// @version      2.6
// @description  Aid Google Form with Gemini AI
// @author       erucix
// @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/d/*
// @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");
    }
})();