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