Google Formify

Aid Google Form with Gemini AI

目前为 2024-11-07 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Google Formify
  3. // @version 2.6
  4. // @description Aid Google Form with Gemini AI
  5. // @author rohitaryal
  6. // @license MIT
  7. // @grant GM_xmlhttpRequest
  8. // @grant unsafeWindow
  9. // @grant GM_addElement
  10. // @grant GM.xmlHttpRequest
  11. // @connect googleapis.com
  12. // @namespace https://docs.google.com/
  13. // @match https://docs.google.com/forms/*
  14. // @icon https://www.google.com/s2/favicons?sz=64&domain=google.com
  15. // ==/UserScript==
  16.  
  17. "use strict";
  18.  
  19. let apiKey = localStorage.getItem("apiKey");
  20. let isOldUser = localStorage.getItem("old_user");
  21.  
  22. while (!apiKey || apiKey.length <= 10) {
  23. apiKey = window.prompt(
  24. "Please enter your API key. To get one for free goto 'https://makersuite.google.com/app/apikey' and paste your api key here."
  25. );
  26.  
  27. if (apiKey == null) {
  28. console.log(apiKey);
  29. window.open("https://makersuite.google.com/app/apikey", "_blank");
  30. } else {
  31. localStorage.setItem("apiKey", apiKey);
  32. }
  33. }
  34.  
  35. const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=${apiKey}`;
  36.  
  37. class Question {
  38. #headers = {
  39. "Content-Type": "application/json",
  40. };
  41.  
  42. #onerror = (error) => {
  43. console.warn(": Some error occured while sending request", error);
  44. };
  45.  
  46. constructor(
  47. question, // (string)
  48. questionImage, // (string)(url)
  49. options, // (Array[{}])
  50. isOptional, // (boolean)
  51. questionType, // (string) textbox, multipleChoice(same for checkbox)
  52. htmlNode // (HTMLElement)
  53. ) {
  54. this.question = question;
  55. this.questionImage = questionImage;
  56. this.options = options;
  57. this.isOptional = isOptional;
  58. this.questionType = questionType;
  59. this.aiAnswer = null;
  60.  
  61. if (!unsafeWindow.deleteNode) {
  62. this.htmlNode = htmlNode;
  63. }
  64. }
  65.  
  66. async aiAssist() {
  67. let data = null;
  68.  
  69. if (this.questionType == "multipleChoice") {
  70. let finalOptions = "";
  71. this.options.forEach((option, index) => {
  72. finalOptions += option.value + "\n";
  73. });
  74.  
  75. data = `{"contents":[{"parts":[{"text":"Choose only the one correct option for this question: Question: ${this.question} Options: ${finalOptions}.\n"}]}]}`;
  76. } else if (this.questionType == "checkbox") {
  77. let finalOptions = "";
  78. this.options.forEach((option, index) => {
  79. finalOptions += option.value + "\n";
  80. });
  81.  
  82. data = `{"contents":[{"parts":[{"text":"Choose the correct option for this question(More than one can be true): Question: ${this.question} Options: ${finalOptions}.\n"}]}]}`;
  83. } else {
  84. data = `{"contents":[{"parts":[{"text":"Write something like a human on topic: '${this.question}'.\n Start now!"}]}]}`;
  85. }
  86.  
  87. let request = await GM.xmlHttpRequest({
  88. method: "POST",
  89. url: url,
  90. headers: this.#headers,
  91. data,
  92. }).catch((error) => this.#onerror);
  93.  
  94. this.aiAnswer = this.parseJSON(request);
  95. }
  96.  
  97. async fillUp() {
  98. await this.aiAssist();
  99.  
  100. if (this.aiAnswer?.trim() == "" || !this.aiAnswer) {
  101. this.htmlNode.querySelector(".ai-answer").textContent =
  102. "😭 Failed to fetch answers from server... ";
  103. } else {
  104. this.htmlNode.querySelector(".ai-answer").textContent = this.aiAnswer;
  105. }
  106.  
  107. if (this.questionType == "multipleChoice") {
  108. let allOptions = [...this.htmlNode.querySelectorAll("label")];
  109.  
  110. this.options.forEach((option, index) => {
  111. if (this.aiAnswer?.includes(option.value)) {
  112. allOptions[index].click();
  113. }
  114. });
  115. } else if (this.questionType == "checkbox") {
  116. let allOptions = [...this.htmlNode.querySelectorAll("label")];
  117.  
  118. this.options.forEach((option, index) => {
  119. if (this.aiAnswer?.includes(option.value)) {
  120. allOptions[index].click();
  121. }
  122. });
  123. } else {
  124. let allTextboxes = [
  125. ...this.htmlNode.querySelectorAll("input[type=text], textarea"),
  126. ];
  127.  
  128. allTextboxes.forEach((element) => {
  129. element.value = this.aiAnswer;
  130. });
  131. }
  132. }
  133.  
  134. parseJSON(data) {
  135. let parsedAnswer = null;
  136.  
  137. try {
  138. let parsedData = JSON.parse(data.responseText);
  139. parsedAnswer = parsedData?.candidates?.[0]?.content?.parts?.[0]?.text;
  140. } catch (e) {
  141. console.warn("Failed to parse to JSON.", e);
  142. }
  143.  
  144. return parsedAnswer;
  145. }
  146. }
  147.  
  148. class GoogleFormParser {
  149. parse() {
  150. let finalQuestionList = [];
  151.  
  152. const googleFormTitle = document.querySelector(
  153. ".F9yp7e.ikZYwf.LgNcQe"
  154. )?.textContent;
  155. const googleFormDescription =
  156. document.querySelector(".cBGGJ.OIC90c")?.textContent;
  157. const questionCards = document.querySelectorAll("[jsmodel='CP1oW']");
  158.  
  159. if (
  160. questionCards == undefined ||
  161. questionCards == null ||
  162. questionCards.length == 0
  163. ) {
  164. throw ": No questions found. Maybe this form is empty";
  165. }
  166.  
  167. questionCards.forEach((card, index) => {
  168. let parsedDataArray = null;
  169.  
  170. let dataParams = card.getAttribute("data-params")?.replace("%.@.", "[");
  171.  
  172. if (!dataParams) {
  173. console.warn(
  174. `No data-params found for card index ${index}. So, skipping this card.`,
  175. card
  176. );
  177. return;
  178. }
  179.  
  180. try {
  181. parsedDataArray = JSON.parse(dataParams);
  182. } catch (e) {
  183. console.warn(
  184. `Failed to parse obtained data-params to JSON: ${dataParams}`,
  185. e
  186. );
  187. return;
  188. }
  189.  
  190. let questionImage = null;
  191. let question = parsedDataArray?.[0]?.[1];
  192. let subdivsInsideCard = card.querySelectorAll(".geS5n");
  193.  
  194. if (!!subdivsInsideCard.length != 0) {
  195. subdivsInsideCard = [...subdivsInsideCard[0].childNodes];
  196. }
  197. subdivsInsideCard = subdivsInsideCard.filter((item) => {
  198. return item.tagName == "DIV";
  199. });
  200.  
  201. // Length >= 4 means question might have image;
  202. if (subdivsInsideCard.length >= 4) {
  203. subdivsInsideCard.forEach((div) => {
  204. let imageTags = div.querySelectorAll("img");
  205.  
  206. // Either theres no img elements or we already found URL.
  207. if (imageTags.length == 0 || !!questionImage) {
  208. return;
  209. }
  210.  
  211. questionImage = imageTags[0]?.src;
  212. });
  213. }
  214.  
  215. let questionType = null;
  216.  
  217. if (card.querySelectorAll(".Yri8Nb").length != 0) {
  218. questionType = "checkbox";
  219. } else if (card.querySelectorAll(".ajBQVb").length != 0) {
  220. questionType = "multipleChoice";
  221. } else if (
  222. card.querySelectorAll("input[type=text], textarea").length == 1
  223. ) {
  224. questionType = "textbox";
  225. }
  226.  
  227. let options = parsedDataArray?.[0]?.[4]?.[0]?.[1];
  228.  
  229. options = options?.map((option, index) => {
  230. let image = null;
  231. if (option.length >= 6) {
  232. image = card
  233. .querySelectorAll("label")
  234. [index]?.querySelector("img")?.src;
  235. }
  236.  
  237. return {
  238. value: option[0],
  239. image,
  240. };
  241. });
  242.  
  243. let isOptional = parsedDataArray?.[0]?.[4]?.[0]?.[2];
  244.  
  245. let finalQuestionBody = new Question(
  246. question,
  247. questionImage,
  248. options,
  249. isOptional,
  250. questionType,
  251. card
  252. );
  253.  
  254. finalQuestionList.push(finalQuestionBody);
  255. });
  256.  
  257. return finalQuestionList;
  258. }
  259. }
  260.  
  261. (function () {
  262. let googleForm = new GoogleFormParser();
  263.  
  264. let questions = googleForm.parse();
  265.  
  266. console.log(questions);
  267.  
  268. let style = document.createElement("style");
  269. 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%}`;
  270. document.head.appendChild(style);
  271.  
  272. questions.forEach((question) => {
  273. const container = document.createElement("div");
  274. container.className = "ai-container";
  275.  
  276. const divHeader = document.createElement("div");
  277. divHeader.className = "ai-header";
  278.  
  279. const divTitle = document.createElement("div");
  280. divTitle.className = "ai-title";
  281. divTitle.textContent = "🦕 Gemini Pro";
  282.  
  283. const ul = document.createElement("ul");
  284.  
  285. const liSearch = document.createElement("li");
  286. liSearch.id = "ai-search";
  287. liSearch.textContent = "SEARCH";
  288.  
  289. const liCopy = document.createElement("li");
  290. liCopy.id = "ai-copy";
  291. liCopy.textContent = "COPY";
  292.  
  293. const hr = document.createElement("hr");
  294.  
  295. const pAnswer = document.createElement("p");
  296. pAnswer.className = "ai-answer";
  297. pAnswer.textContent = "I am working on it. Please wait....";
  298.  
  299. const divFooter = document.createElement("div");
  300. divFooter.className = "ai-footer";
  301.  
  302. const pWarning = document.createElement("p");
  303. pWarning.className = "ai-warning";
  304. pWarning.textContent =
  305. "*Note: Not all AI generated content are 100% accurate. Use Search feature for double check.";
  306.  
  307. const divCircle = document.createElement("div");
  308. divCircle.className = "ai-circle";
  309.  
  310. const liCircle = document.createElement("li");
  311.  
  312. divHeader.appendChild(divTitle);
  313. divHeader.appendChild(ul);
  314. ul.appendChild(liSearch);
  315. ul.appendChild(liCopy);
  316.  
  317. divFooter.appendChild(pWarning);
  318. divFooter.appendChild(divCircle);
  319. divCircle.appendChild(liCircle);
  320.  
  321. container.appendChild(divHeader);
  322. container.appendChild(hr);
  323. container.appendChild(pAnswer);
  324. container.appendChild(divFooter);
  325.  
  326. question.htmlNode.appendChild(container);
  327.  
  328. let options = "";
  329. let questionValue = question.question;
  330.  
  331. question?.options?.forEach((option) => {
  332. options += option.value + "\n";
  333. });
  334.  
  335. liSearch.addEventListener("click", (e) => {
  336. e.preventDefault();
  337.  
  338. window.open(
  339. "https://google.com/search?q=" + questionValue + options,
  340. "_blank"
  341. );
  342. });
  343.  
  344. liCopy.addEventListener("click", (e) => {
  345. setTimeout(function () {
  346. liCopy.textContent = "COPY";
  347. }, 3000);
  348.  
  349. e.preventDefault();
  350. navigator.clipboard.writeText(questionValue + options);
  351. liCopy.textContent = "COPIED";
  352. });
  353. });
  354.  
  355. questions.forEach((element) => {
  356. element.fillUp();
  357. });
  358.  
  359. // Add a keyboard shortcut.
  360.  
  361. document.addEventListener("keydown", (e) => {
  362. if (e.ctrlKey && e.altKey) {
  363. let aiElement = document.querySelectorAll(".ai-container");
  364. aiElement.forEach((container) => {
  365. if (container.style.display != "none") {
  366. container.style.display = "none";
  367. } else {
  368. container.style.display = "block";
  369. }
  370. });
  371. }
  372. });
  373.  
  374. if (!isOldUser) {
  375. alert("You can press CTRL + ALT key to hide/unhide the AI");
  376. localStorage.setItem("old_user", "true");
  377. }
  378. })();