Google Formify

Aid Google Form with Gemini AI

目前為 2024-05-18 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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");
    }
})();