Google Formify

Aid Google Form with Gemini AI

18.05.2024 itibariyledir. En son verisyonu görün.

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

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

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

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.

(Zateb bir user-style yöneticim var, yükleyeyim!)

// ==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");
    }
})();