v1.0.0
This commit is contained in:
+341
-262
@@ -1,266 +1,345 @@
|
||||
chrome.storage.sync.get(["moodleGPT"]).then(function (storage) {
|
||||
const config = storage.moodleGPT;
|
||||
(function (factory) {
|
||||
typeof define === 'function' && define.amd ? define(factory) :
|
||||
factory();
|
||||
})((function () { 'use strict';
|
||||
|
||||
/**
|
||||
* Show some informations into the document title and remove it after 3000ms
|
||||
* @param text
|
||||
*/
|
||||
function titleIndications(text) {
|
||||
const backTitle = document.title;
|
||||
document.title = text;
|
||||
setTimeout(() => (document.title = backTitle), 3000);
|
||||
}
|
||||
|
||||
/******************************************************************************
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
if (!config) throw new Error("Please configure MoodleGPT into the extension");
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted.
|
||||
|
||||
//listening to the keys to inject moodleGPT
|
||||
const pressedKeys = [];
|
||||
const listeners = [];
|
||||
document.body.addEventListener("keydown", function (event) {
|
||||
pressedKeys.push(event.key);
|
||||
if (pressedKeys.length > config.code.length) pressedKeys.shift();
|
||||
if (pressedKeys.join("") === config.code) {
|
||||
pressedKeys.length = 0;
|
||||
setUpMoodleGpt();
|
||||
}
|
||||
});
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
PERFORMANCE OF THIS SOFTWARE.
|
||||
***************************************************************************** */
|
||||
|
||||
/**
|
||||
* Show some informations into the document title and remove it after 3000ms
|
||||
* @param {*} text
|
||||
*/
|
||||
function titleIndications(text) {
|
||||
const backTitle = document.title;
|
||||
document.title = text;
|
||||
setTimeout(() => (document.title = backTitle), 3000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup moodleGPT into the page (remove/injection)
|
||||
*/
|
||||
function setUpMoodleGpt() {
|
||||
//removing events
|
||||
if (listeners.length > 0) {
|
||||
for (const listener of listeners) {
|
||||
if (config.cursor) listener.element.style.cursor = "initial";
|
||||
listener.element.removeEventListener("click", listener.fn, {
|
||||
once: !config.infinite,
|
||||
});
|
||||
}
|
||||
if (config.title) titleIndications("Removed");
|
||||
listeners.length = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
//injection
|
||||
const inputQuery = ["checkbox", "radio", "text"]
|
||||
.map((e) => `input[type="${e}"]`)
|
||||
.join(",");
|
||||
const query = inputQuery + ", textarea, select";
|
||||
const forms = Array.from(document.querySelectorAll(".formulation"));
|
||||
|
||||
for (const form of forms) {
|
||||
const hiddenButton = form.querySelector(".qtext");
|
||||
if (config.cursor) hiddenButton.style.cursor = "pointer";
|
||||
const fn = reply.bind(null, hiddenButton, form, query);
|
||||
listeners.push({ element: hiddenButton, fn });
|
||||
hiddenButton.addEventListener("click", fn, { once: !config.infinite });
|
||||
}
|
||||
|
||||
if (config.title) titleIndications("Injected");
|
||||
}
|
||||
|
||||
/**
|
||||
* Normlize text
|
||||
* @param {*} text
|
||||
*/
|
||||
function normalizeText(text) {
|
||||
return text
|
||||
.replace(/\n+/g, "\n")
|
||||
.replace(/[ \t]+/g, " ")
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/^[a-z\d]\.\s/gi, "") //a. text, b. text, c. text, 1. text, 2. text, 3.text
|
||||
.replace(/\n[a-z\d]\.\s/gi, "\n"); //a. text, b. text, c. text, 1. text, 2. text, 3.text
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the response from chatGPT api
|
||||
* @param {*} question
|
||||
* @returns
|
||||
*/
|
||||
async function getChatGPTResponse(question) {
|
||||
const req = await fetch("https://api.openai.com/v1/chat/completions", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${config.apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model:
|
||||
config.model && config.model !== "" ? config.model : "gpt-3.5-turbo",
|
||||
messages: [{ role: "user", content: question }],
|
||||
temperature: 0.8,
|
||||
top_p: 1.0,
|
||||
presence_penalty: 1.0,
|
||||
stop: null,
|
||||
}),
|
||||
});
|
||||
const rep = await req.json();
|
||||
const response = rep.choices[0].message.content;
|
||||
return normalizeText(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handling logs into the console
|
||||
*/
|
||||
class Logs {
|
||||
static question(text) {
|
||||
const css = "color: cyan";
|
||||
console.log("%c[QUESTION]: %s", css, text);
|
||||
}
|
||||
|
||||
static responseTry(text, valide) {
|
||||
const css = "color: " + (valide ? "green" : "red");
|
||||
console.log("%c[CHECKING]: %s", css, text);
|
||||
}
|
||||
|
||||
static array(arr) {
|
||||
console.log("[CORRECTS] ", arr);
|
||||
}
|
||||
|
||||
static response(text) {
|
||||
console.log(text);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reply to the question
|
||||
* @param {*} form
|
||||
* @param {*} query
|
||||
* @returns
|
||||
*/
|
||||
async function reply(hiddenButton, form, query) {
|
||||
if (config.cursor) hiddenButton.style.cursor = "wait";
|
||||
|
||||
form.querySelector(".accesshide")?.remove();
|
||||
|
||||
const question = normalizeText(form.textContent);
|
||||
const inputList = form.querySelectorAll(query);
|
||||
|
||||
const finalQuestion = `Give a short response as possible for this question, reply in ${
|
||||
config.langage && config.langage !== ""
|
||||
? 'this langage "' + config.langage + '"'
|
||||
: "the following question langage"
|
||||
} and only show the result:
|
||||
function __awaiter(thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
}
|
||||
|
||||
class Logs {
|
||||
static question(text) {
|
||||
const css = "color: cyan";
|
||||
console.log("%c[QUESTION]: %s", css, text);
|
||||
}
|
||||
static responseTry(text, valide) {
|
||||
const css = "color: " + (valide ? "green" : "red");
|
||||
console.log("%c[CHECKING]: %s", css, text);
|
||||
}
|
||||
static array(arr) {
|
||||
console.log("[CORRECTS] ", arr);
|
||||
}
|
||||
static response(text) {
|
||||
console.log(text);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normlize text
|
||||
* @param text
|
||||
*/
|
||||
function normalizeText(text) {
|
||||
return text
|
||||
.replace(/\n+/g, "\n")
|
||||
.replace(/[ \t]+/g, " ")
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/^[a-z\d]\.\s/gi, "") //a. text, b. text, c. text, 1. text, 2. text, 3.text
|
||||
.replace(/\n[a-z\d]\.\s/gi, "\n"); //same but with new line
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the response from chatGPT api
|
||||
* @param config
|
||||
* @param question
|
||||
* @returns
|
||||
*/
|
||||
function getChatGPTResponse(config, question) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const req = yield fetch("https://api.openai.com/v1/chat/completions", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${config.apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: config.model && config.model !== "" ? config.model : "gpt-3.5-turbo",
|
||||
messages: [{ role: "user", content: question }],
|
||||
temperature: 0.8,
|
||||
top_p: 1.0,
|
||||
presence_penalty: 1.0,
|
||||
stop: null,
|
||||
}),
|
||||
});
|
||||
const rep = yield req.json();
|
||||
const response = rep.choices[0].message.content;
|
||||
return normalizeText(response);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize the question and add sub informations
|
||||
* @param langage
|
||||
* @param question
|
||||
* @returns
|
||||
*/
|
||||
function normalizeQuestion(langage, question) {
|
||||
const finalQuestion = `Give a short response as possible for this question, reply in ${langage && langage !== ""
|
||||
? 'this langage "' + langage + '"'
|
||||
: "the following question langage"} and only show the result:
|
||||
${question}
|
||||
(If you have to choose between multiple results only show the corrects one, separate them with new line and take the same text as the question)`;
|
||||
|
||||
const response = await getChatGPTResponse(finalQuestion);
|
||||
|
||||
if (config.logs) {
|
||||
Logs.question(finalQuestion);
|
||||
Logs.response(response);
|
||||
}
|
||||
|
||||
if (config.cursor)
|
||||
hiddenButton.style.cursor = config.infinite ? "pointer" : "initial";
|
||||
|
||||
//if we dont find the input we copy into the clipboard
|
||||
if (inputList.length === 0) {
|
||||
if (config.title) titleIndications("Copied to clipboard");
|
||||
navigator.clipboard.writeText(response);
|
||||
return;
|
||||
}
|
||||
|
||||
//if it's a text
|
||||
if (
|
||||
(inputList.length === 1 && inputList[0].type === "text") ||
|
||||
inputList[0].tagName === "TEXTAREA"
|
||||
) {
|
||||
if (config.typing) {
|
||||
let index = 0;
|
||||
inputList[0].addEventListener("keydown", function (event) {
|
||||
if (event.key === "Backspace") index = response.length + 1;
|
||||
if (index > response.length) return;
|
||||
event.preventDefault();
|
||||
inputList[0].value = response.slice(0, ++index);
|
||||
});
|
||||
} else {
|
||||
inputList[0].value = response;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
//if it's a select
|
||||
if (inputList[0].tagName === "SELECT") {
|
||||
let correct = response.split("\n");
|
||||
if (correct.length === 1 && correct.length !== inputList)
|
||||
correct = response.split(",");
|
||||
|
||||
if (config.logs) Logs.array(correct);
|
||||
|
||||
for (let j = 0; j < inputList.length; ++j) {
|
||||
const options = inputList[j].querySelectorAll("option");
|
||||
|
||||
for (const option of options) {
|
||||
const content = normalizeText(option.textContent);
|
||||
const valide = correct[j].includes(content);
|
||||
|
||||
//if it's a put in order
|
||||
if (!isNaN(parseInt(content))) {
|
||||
const content = normalizeText(
|
||||
option.parentNode.closest("tr").querySelector(".text").textContent
|
||||
);
|
||||
const index = correct.findIndex((c) => {
|
||||
const valide = c.includes(content);
|
||||
if (config.logs) Logs.responseTry(content, valide);
|
||||
return valide;
|
||||
});
|
||||
if (index !== -1) {
|
||||
if (config.mouseover) {
|
||||
options[index + 1].closest("select").addEventListener(
|
||||
"click",
|
||||
function () {
|
||||
options[index + 1].selected = "selected";
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
} else {
|
||||
options[index + 1].selected = "selected";
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (config.logs) Logs.responseTry(content, valide);
|
||||
if (valide) {
|
||||
if (config.mouseover) {
|
||||
option.closest("select").addEventListener(
|
||||
"click",
|
||||
function () {
|
||||
option.selected = "selected";
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
} else {
|
||||
option.selected = "selected";
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
//if it's a radio button or checkbox
|
||||
for (const input of inputList) {
|
||||
const content = normalizeText(input.parentNode.textContent);
|
||||
const valide = response.includes(content);
|
||||
if (config.logs) Logs.responseTry(content, valide);
|
||||
if (valide) {
|
||||
if (config.mouseover) {
|
||||
input.addEventListener(
|
||||
"mouseover",
|
||||
function (event) {
|
||||
event.target.checked = true;
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
} else {
|
||||
input.checked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
(If you have to choose between multiple results only show the corrects one, separate them with new line and take the same text as the question)`;
|
||||
return normalizeText(finalQuestion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle checkbox and input elements
|
||||
* @param config
|
||||
* @param inputList
|
||||
* @param response
|
||||
*/
|
||||
function handleRadioAndCheckbox(config, inputList, response) {
|
||||
const input = inputList === null || inputList === void 0 ? void 0 : inputList[0];
|
||||
if (!input || (input.type !== "checkbox" && input.type !== "radio"))
|
||||
return false;
|
||||
for (const input of inputList) {
|
||||
const content = normalizeText(input.parentNode.textContent);
|
||||
const valide = response.includes(content);
|
||||
if (config.logs)
|
||||
Logs.responseTry(content, valide);
|
||||
if (valide) {
|
||||
if (config.mouseover) {
|
||||
input.addEventListener("mouseover", () => (input.checked = true), {
|
||||
once: true,
|
||||
});
|
||||
}
|
||||
else {
|
||||
input.checked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle select elements (and put in order select)
|
||||
* @param config
|
||||
* @param inputList
|
||||
* @param response
|
||||
* @returns
|
||||
*/
|
||||
function handleSelect(config, inputList, response) {
|
||||
if (inputList.length === 0 || inputList[0].tagName !== "SELECT")
|
||||
return false;
|
||||
let correct = response.split("\n");
|
||||
if (correct.length === 1 && correct.length !== inputList.length)
|
||||
correct = response.split(",");
|
||||
if (config.logs)
|
||||
Logs.array(correct);
|
||||
for (let j = 0; j < inputList.length; ++j) {
|
||||
const options = inputList[j].querySelectorAll("option");
|
||||
for (const option of options) {
|
||||
const content = normalizeText(option.textContent);
|
||||
const valide = correct[j].includes(content);
|
||||
//if it's a put in order
|
||||
if (!isNaN(parseInt(content))) {
|
||||
const content = normalizeText(option.parentNode
|
||||
.closest("tr")
|
||||
.querySelector(".text").textContent);
|
||||
const index = correct.findIndex((c) => {
|
||||
const valide = c.includes(content);
|
||||
if (config.logs)
|
||||
Logs.responseTry(content, valide);
|
||||
return valide;
|
||||
});
|
||||
if (index !== -1) {
|
||||
if (config.mouseover) {
|
||||
options[index + 1].closest("select").addEventListener("click", function () {
|
||||
options[index + 1].selected = "selected";
|
||||
}, { once: true });
|
||||
}
|
||||
else {
|
||||
options[index + 1].selected = "selected";
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
//end put in order
|
||||
if (config.logs)
|
||||
Logs.responseTry(content, valide);
|
||||
if (valide) {
|
||||
if (config.mouseover) {
|
||||
option
|
||||
.closest("select")
|
||||
.addEventListener("click", () => (option.selected = true), {
|
||||
once: true,
|
||||
});
|
||||
}
|
||||
else {
|
||||
option.selected = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle textbox
|
||||
* @param config
|
||||
* @param inputList
|
||||
* @param response
|
||||
* @returns
|
||||
*/
|
||||
function handleTextbox(config, inputList, response) {
|
||||
const input = inputList[0];
|
||||
if (inputList.length !== 1 ||
|
||||
(input.tagName !== "TEXTAREA" && input.type !== "text"))
|
||||
return false;
|
||||
if (config.typing) {
|
||||
let index = 0;
|
||||
input.addEventListener("keydown", function (event) {
|
||||
if (event.key === "Backspace")
|
||||
index = response.length + 1;
|
||||
if (index > response.length)
|
||||
return;
|
||||
event.preventDefault();
|
||||
input.value = response.slice(0, ++index);
|
||||
});
|
||||
}
|
||||
else {
|
||||
input.value = response;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the response in the clipboard if we can automaticaly fill the question
|
||||
* @param config
|
||||
* @param inputList
|
||||
* @param response
|
||||
* @param force Force the copy to clipboard
|
||||
* @returns
|
||||
*/
|
||||
function handleClipboard(config, response) {
|
||||
if (config.title)
|
||||
titleIndications("Copied to clipboard");
|
||||
navigator.clipboard.writeText(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reply to the question
|
||||
* @param config
|
||||
* @param hiddenButton
|
||||
* @param form
|
||||
* @param query
|
||||
* @returns
|
||||
*/
|
||||
function reply(config, hiddenButton, form, query) {
|
||||
var _a;
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
if (config.cursor)
|
||||
hiddenButton.style.cursor = "wait";
|
||||
(_a = form.querySelector(".accesshide")) === null || _a === void 0 ? void 0 : _a.remove();
|
||||
const question = normalizeQuestion(config.langage, form.textContent);
|
||||
const inputList = form.querySelectorAll(query);
|
||||
const response = yield getChatGPTResponse(config, question);
|
||||
if (config.logs) {
|
||||
Logs.question(question);
|
||||
Logs.response(response);
|
||||
}
|
||||
if (config.cursor)
|
||||
hiddenButton.style.cursor = config.infinite ? "pointer" : "initial";
|
||||
const handlers = [handleTextbox, handleSelect, handleRadioAndCheckbox];
|
||||
for (const handler of handlers) {
|
||||
if (handler(config, inputList, response))
|
||||
return;
|
||||
}
|
||||
handleClipboard(config, response);
|
||||
});
|
||||
}
|
||||
|
||||
const pressedKeys = [];
|
||||
const listeners = [];
|
||||
/**
|
||||
* Create a listener on the keyboard to inject the code
|
||||
* @param config
|
||||
*/
|
||||
function codeListener(config) {
|
||||
document.body.addEventListener("keydown", function (event) {
|
||||
pressedKeys.push(event.key);
|
||||
if (pressedKeys.length > config.code.length)
|
||||
pressedKeys.shift();
|
||||
if (pressedKeys.join("") === config.code) {
|
||||
pressedKeys.length = 0;
|
||||
setUpMoodleGpt(config);
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Setup moodleGPT into the page (remove/injection)
|
||||
* @param config
|
||||
* @returns
|
||||
*/
|
||||
function setUpMoodleGpt(config) {
|
||||
//removing events
|
||||
if (listeners.length > 0) {
|
||||
for (const listener of listeners) {
|
||||
if (config.cursor)
|
||||
listener.element.style.cursor = "initial";
|
||||
listener.element.removeEventListener("click", listener.fn);
|
||||
}
|
||||
if (config.title)
|
||||
titleIndications("Removed");
|
||||
listeners.length = 0;
|
||||
return;
|
||||
}
|
||||
//injection
|
||||
const inputQuery = ["checkbox", "radio", "text"]
|
||||
.map((e) => `input[type="${e}"]`)
|
||||
.join(",");
|
||||
const query = inputQuery + ", textarea, select";
|
||||
const forms = Array.from(document.querySelectorAll(".formulation"));
|
||||
for (const form of forms) {
|
||||
const hiddenButton = form.querySelector(".qtext");
|
||||
if (config.cursor)
|
||||
hiddenButton.style.cursor = "pointer";
|
||||
const fn = reply.bind(null, config, hiddenButton, form, query);
|
||||
listeners.push({ element: hiddenButton, fn });
|
||||
hiddenButton.addEventListener("click", fn, { once: !config.infinite });
|
||||
}
|
||||
if (config.title)
|
||||
titleIndications("Injected");
|
||||
}
|
||||
|
||||
chrome.storage.sync.get(["moodleGPT"]).then(function (storage) {
|
||||
const config = storage.moodleGPT;
|
||||
if (!config)
|
||||
throw new Error("Please configure MoodleGPT into the extension");
|
||||
codeListener(config);
|
||||
});
|
||||
|
||||
}));
|
||||
//# sourceMappingURL=moodle-gpt.js.map
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "moodle-gpt",
|
||||
"version": "1.0.0",
|
||||
"description": "This extension allows you to hide CHAT-GPT in a Moodle quiz.",
|
||||
"scripts": {
|
||||
"build": "rollup -c"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/yoannchb-pro/MoodleGPT.git"
|
||||
},
|
||||
"keywords": [
|
||||
"solve",
|
||||
"moodle",
|
||||
"extension-chrome",
|
||||
"quiz-solutions",
|
||||
"chatgpt"
|
||||
],
|
||||
"author": "yoannchb",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/yoannchb-pro/MoodleGPT/issues"
|
||||
},
|
||||
"homepage": "https://github.com/yoannchb-pro/MoodleGPT#readme",
|
||||
"devDependencies": {
|
||||
"rollup": "^3.20.0",
|
||||
"rollup-plugin-ts": "^3.2.0",
|
||||
"typescript": "^5.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/chrome": "^0.0.224"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
const ts = require("rollup-plugin-ts");
|
||||
|
||||
const config = require("./tsconfig.json");
|
||||
|
||||
module.exports = {
|
||||
input: "./src/index.ts",
|
||||
output: [
|
||||
{
|
||||
file: "./extension/moodle-gpt.js",
|
||||
format: "umd",
|
||||
sourcemap: true,
|
||||
},
|
||||
],
|
||||
plugins: [ts(config)],
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
import Config from "../types/config";
|
||||
import titleIndications from "../utils/title-indications";
|
||||
import reply from "./reply";
|
||||
|
||||
const pressedKeys: string[] = [];
|
||||
const listeners: {
|
||||
element: HTMLElement;
|
||||
fn: (this: HTMLElement, ev: MouseEvent) => any;
|
||||
}[] = [];
|
||||
|
||||
/**
|
||||
* Create a listener on the keyboard to inject the code
|
||||
* @param config
|
||||
*/
|
||||
function codeListener(config: Config) {
|
||||
document.body.addEventListener("keydown", function (event) {
|
||||
pressedKeys.push(event.key);
|
||||
if (pressedKeys.length > config.code.length) pressedKeys.shift();
|
||||
if (pressedKeys.join("") === config.code) {
|
||||
pressedKeys.length = 0;
|
||||
setUpMoodleGpt(config);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup moodleGPT into the page (remove/injection)
|
||||
* @param config
|
||||
* @returns
|
||||
*/
|
||||
function setUpMoodleGpt(config: Config) {
|
||||
//removing events
|
||||
if (listeners.length > 0) {
|
||||
for (const listener of listeners) {
|
||||
if (config.cursor) listener.element.style.cursor = "initial";
|
||||
listener.element.removeEventListener("click", listener.fn);
|
||||
}
|
||||
if (config.title) titleIndications("Removed");
|
||||
listeners.length = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
//injection
|
||||
const inputQuery = ["checkbox", "radio", "text"]
|
||||
.map((e) => `input[type="${e}"]`)
|
||||
.join(",");
|
||||
const query = inputQuery + ", textarea, select";
|
||||
const forms = Array.from(document.querySelectorAll(".formulation"));
|
||||
|
||||
for (const form of forms) {
|
||||
const hiddenButton: HTMLElement = form.querySelector(".qtext");
|
||||
if (config.cursor) hiddenButton.style.cursor = "pointer";
|
||||
const fn = reply.bind(null, config, hiddenButton, form, query);
|
||||
listeners.push({ element: hiddenButton, fn });
|
||||
hiddenButton.addEventListener("click", fn, { once: !config.infinite });
|
||||
}
|
||||
|
||||
if (config.title) titleIndications("Injected");
|
||||
}
|
||||
|
||||
export default codeListener;
|
||||
@@ -0,0 +1,35 @@
|
||||
import Config from "../types/config";
|
||||
import normalizeText from "../utils/normalize-text";
|
||||
|
||||
/**
|
||||
* Get the response from chatGPT api
|
||||
* @param config
|
||||
* @param question
|
||||
* @returns
|
||||
*/
|
||||
async function getChatGPTResponse(
|
||||
config: Config,
|
||||
question: string
|
||||
): Promise<string> {
|
||||
const req = await fetch("https://api.openai.com/v1/chat/completions", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${config.apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model:
|
||||
config.model && config.model !== "" ? config.model : "gpt-3.5-turbo",
|
||||
messages: [{ role: "user", content: question }],
|
||||
temperature: 0.8,
|
||||
top_p: 1.0,
|
||||
presence_penalty: 1.0,
|
||||
stop: null,
|
||||
}),
|
||||
});
|
||||
const rep = await req.json();
|
||||
const response = rep.choices[0].message.content;
|
||||
return normalizeText(response);
|
||||
}
|
||||
|
||||
export default getChatGPTResponse;
|
||||
@@ -0,0 +1,21 @@
|
||||
import Config from "../types/config";
|
||||
import normalizeText from "../utils/normalize-text";
|
||||
|
||||
/**
|
||||
* Normalize the question and add sub informations
|
||||
* @param langage
|
||||
* @param question
|
||||
* @returns
|
||||
*/
|
||||
function normalizeQuestion(langage: Config["langage"], question: string) {
|
||||
const finalQuestion = `Give a short response as possible for this question, reply in ${
|
||||
langage && langage !== ""
|
||||
? 'this langage "' + langage + '"'
|
||||
: "the following question langage"
|
||||
} and only show the result:
|
||||
${question}
|
||||
(If you have to choose between multiple results only show the corrects one, separate them with new line and take the same text as the question)`;
|
||||
return normalizeText(finalQuestion);
|
||||
}
|
||||
|
||||
export default normalizeQuestion;
|
||||
@@ -0,0 +1,17 @@
|
||||
import Config from "../../types/config";
|
||||
import titleIndications from "../../utils/title-indications";
|
||||
|
||||
/**
|
||||
* Copy the response in the clipboard if we can automaticaly fill the question
|
||||
* @param config
|
||||
* @param inputList
|
||||
* @param response
|
||||
* @param force Force the copy to clipboard
|
||||
* @returns
|
||||
*/
|
||||
function handleClipboard(config: Config, response: string) {
|
||||
if (config.title) titleIndications("Copied to clipboard");
|
||||
navigator.clipboard.writeText(response);
|
||||
}
|
||||
|
||||
export default handleClipboard;
|
||||
@@ -0,0 +1,38 @@
|
||||
import Config from "../../types/config";
|
||||
import Logs from "../../utils/logs";
|
||||
import normalizeText from "../../utils/normalize-text";
|
||||
|
||||
/**
|
||||
* Handle checkbox and input elements
|
||||
* @param config
|
||||
* @param inputList
|
||||
* @param response
|
||||
*/
|
||||
function handleRadioAndCheckbox(
|
||||
config: Config,
|
||||
inputList: NodeListOf<HTMLElement>,
|
||||
response: string
|
||||
): boolean {
|
||||
const input = inputList?.[0] as HTMLInputElement;
|
||||
|
||||
if (!input || (input.type !== "checkbox" && input.type !== "radio"))
|
||||
return false;
|
||||
|
||||
for (const input of inputList as NodeListOf<HTMLInputElement>) {
|
||||
const content = normalizeText(input.parentNode.textContent);
|
||||
const valide = response.includes(content);
|
||||
if (config.logs) Logs.responseTry(content, valide);
|
||||
if (valide) {
|
||||
if (config.mouseover) {
|
||||
input.addEventListener("mouseover", () => (input.checked = true), {
|
||||
once: true,
|
||||
});
|
||||
} else {
|
||||
input.checked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export default handleRadioAndCheckbox;
|
||||
@@ -0,0 +1,81 @@
|
||||
import Config from "../../types/config";
|
||||
import Logs from "../../utils/logs";
|
||||
import normalizeText from "../../utils/normalize-text";
|
||||
|
||||
/**
|
||||
* Handle select elements (and put in order select)
|
||||
* @param config
|
||||
* @param inputList
|
||||
* @param response
|
||||
* @returns
|
||||
*/
|
||||
function handleSelect(
|
||||
config: Config,
|
||||
inputList: NodeListOf<HTMLElement>,
|
||||
response: string
|
||||
): boolean {
|
||||
if (inputList.length === 0 || inputList[0].tagName !== "SELECT") return false;
|
||||
|
||||
let correct = response.split("\n");
|
||||
if (correct.length === 1 && correct.length !== inputList.length)
|
||||
correct = response.split(",");
|
||||
|
||||
if (config.logs) Logs.array(correct);
|
||||
|
||||
for (let j = 0; j < inputList.length; ++j) {
|
||||
const options = inputList[j].querySelectorAll("option");
|
||||
|
||||
for (const option of options) {
|
||||
const content = normalizeText(option.textContent);
|
||||
const valide = correct[j].includes(content);
|
||||
|
||||
//if it's a put in order
|
||||
if (!isNaN(parseInt(content))) {
|
||||
const content = normalizeText(
|
||||
(option.parentNode as HTMLElement)
|
||||
.closest("tr")
|
||||
.querySelector(".text").textContent
|
||||
);
|
||||
const index = correct.findIndex((c) => {
|
||||
const valide = c.includes(content);
|
||||
if (config.logs) Logs.responseTry(content, valide);
|
||||
return valide;
|
||||
});
|
||||
if (index !== -1) {
|
||||
if (config.mouseover) {
|
||||
options[index + 1].closest("select").addEventListener(
|
||||
"click",
|
||||
function () {
|
||||
options[index + 1].selected = "selected" as any;
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
} else {
|
||||
options[index + 1].selected = "selected" as any;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
//end put in order
|
||||
|
||||
if (config.logs) Logs.responseTry(content, valide);
|
||||
|
||||
if (valide) {
|
||||
if (config.mouseover) {
|
||||
option
|
||||
.closest("select")
|
||||
.addEventListener("click", () => (option.selected = true), {
|
||||
once: true,
|
||||
});
|
||||
} else {
|
||||
option.selected = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export default handleSelect;
|
||||
@@ -0,0 +1,38 @@
|
||||
import Config from "../../types/config";
|
||||
|
||||
/**
|
||||
* Handle textbox
|
||||
* @param config
|
||||
* @param inputList
|
||||
* @param response
|
||||
* @returns
|
||||
*/
|
||||
function handleTextbox(
|
||||
config: Config,
|
||||
inputList: NodeListOf<HTMLElement>,
|
||||
response: string
|
||||
): boolean {
|
||||
const input = inputList[0] as HTMLInputElement | HTMLTextAreaElement;
|
||||
|
||||
if (
|
||||
inputList.length !== 1 ||
|
||||
(input.tagName !== "TEXTAREA" && input.type !== "text")
|
||||
)
|
||||
return false;
|
||||
|
||||
if (config.typing) {
|
||||
let index = 0;
|
||||
input.addEventListener("keydown", function (event: KeyboardEvent) {
|
||||
if (event.key === "Backspace") index = response.length + 1;
|
||||
if (index > response.length) return;
|
||||
event.preventDefault();
|
||||
input.value = response.slice(0, ++index);
|
||||
});
|
||||
} else {
|
||||
input.value = response;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export default handleTextbox;
|
||||
@@ -0,0 +1,50 @@
|
||||
import Config from "../types/config";
|
||||
import Logs from "../utils/logs";
|
||||
import getChatGPTResponse from "./get-response";
|
||||
import normalizeQuestion from "./normalize-question";
|
||||
import handleRadioAndCheckbox from "./questions/radio-checkbox";
|
||||
import handleSelect from "./questions/select";
|
||||
import handleTextbox from "./questions/textbox";
|
||||
import handleClipboard from "./questions/clipboard";
|
||||
|
||||
/**
|
||||
* Reply to the question
|
||||
* @param config
|
||||
* @param hiddenButton
|
||||
* @param form
|
||||
* @param query
|
||||
* @returns
|
||||
*/
|
||||
async function reply(
|
||||
config: Config,
|
||||
hiddenButton: HTMLElement,
|
||||
form: HTMLElement,
|
||||
query: string
|
||||
) {
|
||||
if (config.cursor) hiddenButton.style.cursor = "wait";
|
||||
|
||||
form.querySelector(".accesshide")?.remove();
|
||||
|
||||
const question = normalizeQuestion(config.langage, form.textContent);
|
||||
const inputList: NodeListOf<HTMLElement> = form.querySelectorAll(query);
|
||||
|
||||
const response = await getChatGPTResponse(config, question);
|
||||
|
||||
if (config.logs) {
|
||||
Logs.question(question);
|
||||
Logs.response(response);
|
||||
}
|
||||
|
||||
if (config.cursor)
|
||||
hiddenButton.style.cursor = config.infinite ? "pointer" : "initial";
|
||||
|
||||
const handlers = [handleTextbox, handleSelect, handleRadioAndCheckbox];
|
||||
|
||||
for (const handler of handlers) {
|
||||
if (handler(config, inputList, response)) return;
|
||||
}
|
||||
|
||||
handleClipboard(config, response);
|
||||
}
|
||||
|
||||
export default reply;
|
||||
@@ -0,0 +1,9 @@
|
||||
import codeListener from "./core/code-listener";
|
||||
|
||||
chrome.storage.sync.get(["moodleGPT"]).then(function (storage) {
|
||||
const config = storage.moodleGPT;
|
||||
|
||||
if (!config) throw new Error("Please configure MoodleGPT into the extension");
|
||||
|
||||
codeListener(config);
|
||||
});
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
type Config = {
|
||||
apiKey: string;
|
||||
code: string;
|
||||
langage?: string;
|
||||
model?: string;
|
||||
infinite?: boolean;
|
||||
typing?: boolean;
|
||||
mouseover?: boolean;
|
||||
cursor?: boolean;
|
||||
logs?: boolean;
|
||||
title?: boolean;
|
||||
};
|
||||
|
||||
export default Config;
|
||||
@@ -0,0 +1,21 @@
|
||||
class Logs {
|
||||
static question(text: string) {
|
||||
const css = "color: cyan";
|
||||
console.log("%c[QUESTION]: %s", css, text);
|
||||
}
|
||||
|
||||
static responseTry(text: string, valide: boolean) {
|
||||
const css = "color: " + (valide ? "green" : "red");
|
||||
console.log("%c[CHECKING]: %s", css, text);
|
||||
}
|
||||
|
||||
static array(arr: unknown[]) {
|
||||
console.log("[CORRECTS] ", arr);
|
||||
}
|
||||
|
||||
static response(text: string) {
|
||||
console.log(text);
|
||||
}
|
||||
}
|
||||
|
||||
export default Logs;
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Normlize text
|
||||
* @param text
|
||||
*/
|
||||
function normalizeText(text: string) {
|
||||
return text
|
||||
.replace(/\n+/g, "\n")
|
||||
.replace(/[ \t]+/g, " ")
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/^[a-z\d]\.\s/gi, "") //a. text, b. text, c. text, 1. text, 2. text, 3.text
|
||||
.replace(/\n[a-z\d]\.\s/gi, "\n"); //same but with new line
|
||||
}
|
||||
|
||||
export default normalizeText;
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Show some informations into the document title and remove it after 3000ms
|
||||
* @param text
|
||||
*/
|
||||
function titleIndications(text: string) {
|
||||
const backTitle = document.title;
|
||||
document.title = text;
|
||||
setTimeout(() => (document.title = backTitle), 3000);
|
||||
}
|
||||
|
||||
export default titleIndications;
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS",
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "ES6",
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": true,
|
||||
"outDir": "extension",
|
||||
"resolveJsonModule": true,
|
||||
"types": ["node", "chrome"],
|
||||
"typeRoots": ["node_modules/@types"]
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
Reference in New Issue
Block a user