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