This commit is contained in:
yoannchb-pro
2023-03-21 21:59:21 -04:00
committed by GitHub
parent 5289620caa
commit 5db3cfad63
18 changed files with 817 additions and 262 deletions
+61
View File
@@ -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;
+35
View File
@@ -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;
+21
View File
@@ -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;
+17
View File
@@ -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;
+38
View File
@@ -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;
+81
View File
@@ -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;
+38
View File
@@ -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;
+50
View File
@@ -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;
+9
View File
@@ -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);
});
+14
View File
@@ -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;
+21
View File
@@ -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;
+15
View File
@@ -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;
+11
View File
@@ -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;