better algo, added history, fixed bugs and refactoring

This commit is contained in:
yoannchb-pro
2024-02-28 15:25:01 -05:00
parent b4372ca422
commit 2ca0318c22
20 changed files with 572 additions and 276 deletions
+48 -46
View File
@@ -17,7 +17,7 @@ const listeners: Listener[] = [];
function codeListener(config: Config) {
document.body.addEventListener("keydown", function (event) {
pressedKeys.push(event.key);
if (pressedKeys.length > config.code.length) pressedKeys.shift();
if (pressedKeys.length > config.code!.length) pressedKeys.shift();
if (pressedKeys.join("") === config.code) {
pressedKeys.length = 0;
setUpMoodleGpt(config);
@@ -25,51 +25,6 @@ function codeListener(config: 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;
}
/* Code injection */
const inputQuery = ["checkbox", "radio", "text", "number"]
.map((e) => `input[type="${e}"]`)
.join(",");
const query = inputQuery + ", textarea, select, [contenteditable]";
const forms = document.querySelectorAll(".formulation");
for (const form of forms) {
const hiddenButton: HTMLElement = form.querySelector(".qtext");
if (!hiddenButton) continue;
if (config.cursor) hiddenButton.style.cursor = "pointer";
const injectionFunction = reply.bind(
null,
config,
hiddenButton,
form,
query
);
listeners.push({ element: hiddenButton, fn: injectionFunction });
hiddenButton.addEventListener("click", injectionFunction);
}
if (config.title) titleIndications("Injected");
}
/**
* Remove the event listener on a specific question
* @param element
@@ -82,4 +37,51 @@ function removeListener(element: HTMLElement) {
}
}
/**
* Setup moodleGPT into the page (remove/injection)
* @param config
* @returns
*/
function setUpMoodleGpt(config: Config) {
// Removing events if there are already declared
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;
}
// Query to find inputs and forms
const inputTypeQuery = ["checkbox", "radio", "text", "number"]
.map((e) => `input[type="${e}"]`)
.join(",");
const inputQuery = inputTypeQuery + ", textarea, select, [contenteditable]";
const forms = document.querySelectorAll(".formulation");
// For each form we inject a function on the queqtion
for (const form of forms) {
const questionElement: HTMLElement | null = form.querySelector(".qtext");
if (questionElement === null) continue;
if (config.cursor) questionElement.style.cursor = "pointer";
const injectionFunction = reply.bind(null, {
config,
questionElement,
form: form as HTMLElement,
inputQuery,
removeListener: () => removeListener(questionElement),
});
listeners.push({ element: questionElement, fn: injectionFunction });
questionElement.addEventListener("click", injectionFunction);
}
if (config.title) titleIndications("Injected");
}
export { codeListener, removeListener, setUpMoodleGpt };
+5 -6
View File
@@ -1,24 +1,23 @@
import type Config from "@typing/config";
import normalizeText from "@utils/normalize-text";
import htmlTableToString from "@utils/html-table-to-string";
/**
* Normalize the question and add sub informations
* Normalize the question as text and add sub informations
* @param langage
* @param question
* @returns
*/
function createQuestion(config: Config, questionContainer: HTMLElement) {
function createAndNormalizeQuestion(questionContainer: HTMLElement) {
let question = questionContainer.innerText;
/* We remove unnecessary information */
// We remove unnecessary information
const accesshideElements: NodeListOf<HTMLElement> =
questionContainer.querySelectorAll(".accesshide");
for (const useless of accesshideElements) {
question = question.replace(useless.innerText, "");
}
/* Make tables more readable for chat-gpt */
// Make tables more readable for chat-gpt
const tables: NodeListOf<HTMLTableElement> =
questionContainer.querySelectorAll(".qtext table");
for (const table of tables) {
@@ -31,4 +30,4 @@ function createQuestion(config: Config, questionContainer: HTMLElement) {
return normalizeText(question, false);
}
export default createQuestion;
export default createAndNormalizeQuestion;
+58 -18
View File
@@ -2,6 +2,41 @@ import type Config from "@typing/config";
import type GPTAnswer from "@typing/gptAnswer";
import normalizeText from "@utils/normalize-text";
type History = {
url: string | null;
system: { role: ROLE; content: string };
history: { role: ROLE; content: string }[];
};
enum ROLE {
SYSTEM = "system",
USER = "user",
ASSISTANT = "assistant",
}
const INSTRUCTION: string = `
Act as a quiz solver for the best notation with the following rules:
- When asked for the result of an equation, provide only the result without any other information and skip the other rules.
- If no answer(s) are given, answer the statement as usual without following the other rules, providing the most detailed, complete and precise explanation.
- For 'put in order' questions, provide the position of the answer separated by a new line (e.g., '1\n3\n2') and ignore other rules.- Always reply in this format: '<answer 1>\n<answer 2>\n...'
- Always reply in the format: '<answer 1>\n<answer 2>\n...'.
- Retain only the correct answer(s).
- Maintain the same order for the answers as in the text.
- Retain all text from the answer with its description, content or definition.
- Only provide answers that exactly match the given answer in the text.
- The question always has the correct answer(s), so you should always provide an answer.
- Always respond in the same language as the user's question.
`.trim();
const history: History = {
url: null,
system: {
role: ROLE.SYSTEM,
content: INSTRUCTION,
},
history: [],
};
/**
* Get the response from chatGPT api
* @param config
@@ -12,8 +47,19 @@ async function getChatGPTResponse(
config: Config,
question: string
): Promise<GPTAnswer> {
const URL = location.hostname + location.pathname;
// We reset the history when we enter a new moodle quiz or when it's desactivate
if (!config.history || history.url !== URL) {
history.url = URL;
history.history = [];
}
const controller = new AbortController();
const timeoutControler = setTimeout(() => controller.abort(), 15000);
const timeoutControler = setTimeout(() => controller.abort(), 15 * 1000);
const message = { role: ROLE.USER, content: question };
const req = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
@@ -23,33 +69,27 @@ async function getChatGPTResponse(
signal: config.timeout ? controller.signal : null,
body: JSON.stringify({
model: config.model,
messages: [
{
role: "system",
content: `
Follow those rules:
- Sometimes there won't be a question, so just answer the statement as you normally would without following the other rules and give the most detailled and complete answer with explication.
- For put in order question just give the good order separate by new line
- Your goal is to understand the statement and to reply to each question by giving only the answer.
- You will keep the same order for the answers like in the text.
- You will separate all the answer with new lines and only show the correctes one.
- You will only give the answers for each question and omit the questions, statement, title or other informations from the response.
- You will only give answer with exactly the same text as the gived answers.
- The question always have the good answer so you should always give an answer to the question.
- You will always respond in the same langage as the user question.`,
},
{ role: "user", content: question },
],
messages: [history.system, ...history.history, message],
temperature: 0.8,
top_p: 1.0,
presence_penalty: 1.0,
stop: null,
}),
});
clearTimeout(timeoutControler);
const rep = await req.json();
const response = rep.choices[0].message.content;
// Register the conversation
if (config.history) {
history.history.push(message);
history.history.push({ role: ROLE.ASSISTANT, content: response });
}
return {
question,
response,
normalizedResponse: normalizeText(response),
};
+45
View File
@@ -0,0 +1,45 @@
import type GPTAnswer from "@typing/gptAnswer";
import type Config from "@typing/config";
import handleClipboard from "@core/questions/clipboard";
import handleContentEditable from "@core/questions/contenteditable";
import handleNumber from "@core/questions/number";
import handleRadio from "@core/questions/radio";
import handleCheckbox from "@core/questions/checkbox";
import handleSelect from "@core/questions/select";
import handleTextbox from "@core/questions/textbox";
type Props = {
config: Config;
questionElement: HTMLElement;
inputList: NodeListOf<HTMLElement>;
gptAnswer: GPTAnswer;
removeListener: () => void;
};
/**
* Autocomplete mode:
* Autocomplete the question by checking the good answer
* @param props
* @returns
*/
function autoCompleteMode(props: Props) {
if (!props.config.infinite) props.removeListener();
const handlers = [
handleContentEditable,
handleTextbox,
handleNumber,
handleSelect,
handleRadio,
handleCheckbox,
];
for (const handler of handlers) {
if (handler(props.config, props.inputList, props.gptAnswer)) return;
}
// In the case we can't auto complete the question
handleClipboard(props.config, props.gptAnswer);
}
export default autoCompleteMode;
+22
View File
@@ -0,0 +1,22 @@
import type Config from "@typing/config";
import type GPTAnswer from "@typing/gptAnswer";
import handleClipboard from "@core/questions/clipboard";
type Props = {
config: Config;
questionElement: HTMLElement;
gptAnswer: GPTAnswer;
removeListener: () => void;
};
/**
* Clipboard mode:
* Simply copy the answer into the clipboard
* @param props
*/
function clipboardMode(props: Props) {
if (!props.config.infinite) props.removeListener();
handleClipboard(props.config, props.gptAnswer);
}
export default clipboardMode;
+36
View File
@@ -0,0 +1,36 @@
import type GPTAnswer from "@typing/gptAnswer";
type Props = {
questionElement: HTMLElement;
gptAnswer: GPTAnswer;
removeListener: () => void;
};
/**
* Question to answer mode:
* Simply turn the question into the answer by clicking on it
* @param props
*/
function questionToAnswerMode(props: Props) {
const questionElement = props.questionElement;
props.removeListener();
const questionBackup = questionElement.textContent;
questionElement.textContent = props.gptAnswer.response;
// Format the content
questionElement.style.whiteSpace = "pre-wrap";
// To go back to the question / answer
let contentIsResponse = true;
questionElement.addEventListener("click", function () {
questionElement.style.whiteSpace = contentIsResponse ? "" : "pre-warp";
questionElement.textContent = contentIsResponse
? questionBackup
: props.gptAnswer.response;
});
}
export default questionToAnswerMode;
+58
View File
@@ -0,0 +1,58 @@
import type Config from "@typing/config";
import type GPTAnswer from "@typing/gptAnswer";
import Logs from "@utils/logs";
import normalizeText from "@utils/normalize-text";
import { pickBestReponse, toPourcentage } from "@utils/pick-best-response";
/**
* Handle input checkbox elements
* @param config
* @param inputList
* @param gptAnswer
*/
function handleCheckbox(
config: Config,
inputList: NodeListOf<HTMLElement>,
gptAnswer: GPTAnswer
): boolean {
const firstInput = inputList?.[0] as HTMLInputElement;
// Handle the case the input is not a checkbox
if (!firstInput || firstInput.type !== "checkbox") {
return false;
}
const corrects = gptAnswer.normalizedResponse.split("\n");
const possibleAnswers = Array.from(inputList)
.map((inp) => ({
element: inp,
value: normalizeText(inp?.parentElement?.textContent ?? ""),
}))
.filter((obj) => obj.value !== "");
for (const correct of corrects) {
const bestAnswer = pickBestReponse(correct, possibleAnswers);
if (config.logs && bestAnswer.value) {
Logs.bestAnswer(bestAnswer.value, bestAnswer.similarity);
}
const correctInput = bestAnswer.element as HTMLInputElement;
if (config.mouseover) {
correctInput.addEventListener(
"mouseover",
() => (correctInput.checked = true),
{
once: true,
}
);
} else {
correctInput.checked = true;
}
}
return true;
}
export default handleCheckbox;
+8 -5
View File
@@ -16,10 +16,11 @@ function handleContentEditable(
const input = inputList[0];
if (
inputList.length !== 1 ||
inputList.length !== 1 || // for now we don't handle many input for editable textcontent
input.getAttribute("contenteditable") !== "true"
)
) {
return false;
}
if (config.typing) {
let index = 0;
@@ -29,14 +30,16 @@ function handleContentEditable(
event.preventDefault();
input.textContent = gptAnswer.response.slice(0, ++index);
/* Put the cursor at the end of the typed text */
// Put the cursor at the end of the typed text
input.focus();
const range = document.createRange();
range.selectNodeContents(input);
range.collapse(false);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
if (selection !== null) {
selection.removeAllRanges();
selection.addRange(range);
}
});
} else {
input.textContent = gptAnswer.response;
+10 -5
View File
@@ -15,20 +15,25 @@ function handleNumber(
): boolean {
const input = inputList[0] as HTMLInputElement | HTMLTextAreaElement;
if (inputList.length !== 1 || input.type !== "number") return false;
if (
inputList.length !== 1 || // for now we don't handle many input number
input.type !== "number"
) {
return false;
}
const number = gptAnswer.normalizedResponse
.match(/\d+([,\.]\d+)?/gi)?.[0]
?.replace(",", ".");
if (!number) return false;
if (number === undefined) return false;
if (config.typing) {
let index = 0;
input.addEventListener("keydown", function (event: KeyboardEvent) {
if (event.key === "Backspace") index = number.length + 1;
if (index > number.length) return;
input.addEventListener("keydown", function (event: Event) {
event.preventDefault();
if ((<KeyboardEvent>event).key === "Backspace") index = number.length + 1;
if (index > number.length) return;
if (number.slice(index, index + 1) === ".") ++index;
input.value = number.slice(0, ++index);
});
-39
View File
@@ -1,39 +0,0 @@
import type Config from "@typing/config";
import type GPTAnswer from "@typing/gptAnswer";
import Logs from "@utils/logs";
import normalizeText from "@utils/normalize-text";
/**
* Handle checkbox and input elements
* @param config
* @param inputList
* @param gptAnswer
*/
function handleRadioAndCheckbox(
config: Config,
inputList: NodeListOf<HTMLElement>,
gptAnswer: GPTAnswer
): 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 = gptAnswer.normalizedResponse.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;
+57
View File
@@ -0,0 +1,57 @@
import type Config from "@typing/config";
import type GPTAnswer from "@typing/gptAnswer";
import Logs from "@utils/logs";
import normalizeText from "@utils/normalize-text";
import { pickBestReponse, toPourcentage } from "@utils/pick-best-response";
/**
* Handle input radio elements
* @param config
* @param inputList
* @param gptAnswer
*/
function handleRadio(
config: Config,
inputList: NodeListOf<HTMLElement>,
gptAnswer: GPTAnswer
): boolean {
const firstInput = inputList?.[0] as HTMLInputElement;
// Handle the case the input is not a radio
if (!firstInput || firstInput.type !== "radio") {
return false;
}
const possibleAnswers = Array.from(inputList)
.map((inp) => ({
element: inp,
value: normalizeText(inp?.parentElement?.textContent ?? ""),
}))
.filter((obj) => obj.value !== "");
const bestAnswer = pickBestReponse(
gptAnswer.normalizedResponse,
possibleAnswers
);
if (config.logs && bestAnswer.value) {
Logs.bestAnswer(bestAnswer.value, bestAnswer.similarity);
}
const correctInput = bestAnswer.element as HTMLInputElement;
if (config.mouseover) {
correctInput.addEventListener(
"mouseover",
() => (correctInput.checked = true),
{
once: true,
}
);
} else {
correctInput.checked = true;
}
return true;
}
export default handleRadio;
+30 -67
View File
@@ -2,6 +2,7 @@ import type Config from "@typing/config";
import type GPTAnswer from "@typing/gptAnswer";
import Logs from "@utils/logs";
import normalizeText from "@utils/normalize-text";
import { pickBestReponse, toPourcentage } from "@utils/pick-best-response";
/**
* Handle select elements (and put in order select)
@@ -17,81 +18,43 @@ function handleSelect(
): boolean {
if (inputList.length === 0 || inputList[0].tagName !== "SELECT") return false;
let correct = gptAnswer.normalizedResponse.split("\n");
const corrects = gptAnswer.normalizedResponse.split("\n");
if (config.logs) Logs.array(correct);
if (config.logs) Logs.array(corrects);
/**
* Sometimes ChatGPT give the question so we should remove them
* Example:
* 5*5
* 25
* 10+10
* 20
* 20-10
* 10
*
* And we only want to keep answers
* 25
* 20
* 10
*/
if (correct.length === inputList.length * 2) {
correct = correct.filter((answer, index) => index % 2 === 1);
}
for (let i = 0; i < inputList.length; ++i) {
if (!corrects[i]) break;
for (let j = 0; j < inputList.length; ++j) {
const options = inputList[j].querySelectorAll("option");
const options = inputList[i].querySelectorAll("option");
for (const option of options) {
const content = normalizeText(option.textContent);
const valide = correct[j].includes(content);
const possibleAnswers = Array.from(options)
.map((opt) => ({
element: opt,
value: normalizeText(opt.textContent ?? ""),
}))
.filter((obj) => obj.value !== "");
/* Handle put in order question */
if (!/[^\d]+/gi.test(content)) {
const elementTitle = (option.parentNode as HTMLElement)
.closest("tr")
.querySelector(".text");
const content = normalizeText(elementTitle.textContent);
const bestAnswer = pickBestReponse(corrects[i], possibleAnswers);
const indexCorrectAnswer = correct.findIndex((answer) => {
const valide = answer.includes(content);
if (config.logs) Logs.responseTry(content, valide);
return valide;
});
if (config.logs && bestAnswer.value) {
Logs.bestAnswer(bestAnswer.value, bestAnswer.similarity);
}
if (indexCorrectAnswer !== -1) {
//we do + 1 because we skip the first option: Choose...
if (config.mouseover) {
options[indexCorrectAnswer + 1].closest("select").addEventListener(
"click",
function () {
options[indexCorrectAnswer + 1].selected = "selected" as any;
},
{ once: true }
);
} else {
options[indexCorrectAnswer + 1].selected = "selected" as any;
}
break;
const correctOption = bestAnswer.element as HTMLOptionElement;
const currentSelect = correctOption.closest("select");
if (currentSelect === null) continue;
if (config.mouseover) {
currentSelect.addEventListener(
"click",
() => (correctOption.selected = true),
{
once: true,
}
}
/* End */
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;
}
);
} else {
correctOption.selected = true;
}
}
+8 -5
View File
@@ -16,17 +16,20 @@ function handleTextbox(
const input = inputList[0] as HTMLInputElement | HTMLTextAreaElement;
if (
inputList.length !== 1 ||
inputList.length !== 1 || // for now we don't handle many input text
(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 = gptAnswer.response.length + 1;
if (index > gptAnswer.response.length) return;
input.addEventListener("keydown", function (event: Event) {
event.preventDefault();
if ((<KeyboardEvent>event).key === "Backspace") {
index = gptAnswer.response.length + 1;
}
if (index > gptAnswer.response.length) return;
input.value = gptAnswer.response.slice(0, ++index);
});
} else {
+50 -68
View File
@@ -1,35 +1,33 @@
import type Config from "@typing/config";
import Logs from "@utils/logs";
import getChatGPTResponse from "./get-response";
import createQuestion from "./create-question";
import handleRadioAndCheckbox from "./questions/radio-checkbox";
import handleSelect from "./questions/select";
import handleTextbox from "./questions/textbox";
import handleClipboard from "./questions/clipboard";
import handleNumber from "./questions/number";
import handleContentEditable from "./questions/contenteditable";
import { removeListener } from "./code-listener";
import createAndNormalizeQuestion from "./create-question";
import clipboardMode from "./modes/clipboard";
import questionToAnswerMode from "./modes/question-to-answer";
import autoCompleteMode from "./modes/autocomplete";
type Props = {
config: Config;
questionElement: HTMLElement;
form: HTMLElement;
inputQuery: string;
removeListener: () => void;
};
/**
* Reply to the question
* @param config
* @param hiddenButton
* @param form
* @param query
* @param props
* @returns
*/
async function reply(
config: Config,
hiddenButton: HTMLElement,
form: HTMLElement,
query: string
) {
if (config.cursor) hiddenButton.style.cursor = "wait";
async function reply(props: Props): Promise<void> {
if (props.config.cursor) props.questionElement.style.cursor = "wait";
const question = createQuestion(config, form);
const inputList: NodeListOf<HTMLElement> = form.querySelectorAll(query);
const question = createAndNormalizeQuestion(props.form);
const inputList: NodeListOf<HTMLElement> = props.form.querySelectorAll(
props.inputQuery
);
const gptAnswer = await getChatGPTResponse(config, question).catch(
const gptAnswer = await getChatGPTResponse(props.config, question).catch(
(error) => ({
error,
})
@@ -37,63 +35,47 @@ async function reply(
const haveError = typeof gptAnswer === "object" && "error" in gptAnswer;
if (config.cursor)
hiddenButton.style.cursor =
config.infinite || haveError ? "pointer" : "initial";
if (props.config.cursor) {
props.questionElement.style.cursor =
props.config.infinite || haveError ? "pointer" : "initial";
}
if (haveError) {
console.error(gptAnswer.error);
return;
}
if (config.logs) {
if (props.config.logs) {
Logs.question(question);
Logs.response(gptAnswer);
}
/* Handle clipboard mode */
if (config.mode === "clipboard") {
if (!config.infinite) removeListener(hiddenButton);
return handleClipboard(config, gptAnswer);
switch (props.config.mode) {
case "clipboard":
clipboardMode({
config: props.config,
questionElement: props.questionElement,
gptAnswer,
removeListener: props.removeListener,
});
break;
case "question-to-answer":
questionToAnswerMode({
gptAnswer,
questionElement: props.questionElement,
removeListener: props.removeListener,
});
break;
case "autocomplete":
autoCompleteMode({
config: props.config,
gptAnswer,
inputList,
questionElement: props.questionElement,
removeListener: props.removeListener,
});
break;
}
/* Handle question to answer mode */
if (config.mode === "question-to-answer") {
removeListener(hiddenButton);
const questionContainer = form.querySelector<HTMLElement>(".qtext");
const questionBackup = questionContainer.textContent;
questionContainer.textContent = gptAnswer.response;
questionContainer.style.whiteSpace = "pre-wrap";
questionContainer.addEventListener("click", function () {
const isNotResponse = questionContainer.textContent === questionBackup;
questionContainer.style.whiteSpace = isNotResponse ? "pre-wrap" : null;
questionContainer.textContent = isNotResponse
? gptAnswer.response
: questionBackup;
});
return;
}
/* Better then set once on the event because if there is an error the user can click an other time on the question */
if (!config.infinite) removeListener(hiddenButton);
const handlers = [
handleContentEditable,
handleTextbox,
handleNumber,
handleSelect,
handleRadioAndCheckbox,
];
for (const handler of handlers) {
if (handler(config, inputList, gptAnswer)) return;
}
/* In the case we can't auto complete the question */
handleClipboard(config, gptAnswer);
}
export default reply;
+1
View File
@@ -9,6 +9,7 @@ type Config = {
logs?: boolean;
title?: boolean;
timeout?: boolean;
history?: boolean;
mode?: "autocomplete" | "question-to-answer" | "clipboard";
};
+1
View File
@@ -1,4 +1,5 @@
type GPTAnswer = {
question: string;
response: string;
normalizedResponse: string;
};
+3 -3
View File
@@ -13,9 +13,9 @@ function htmlTableToString(table: HTMLTableElement) {
const content = cell.textContent?.trim();
maxColumnsLength[index] = Math.max(
maxColumnsLength[index] || 0,
content.length || 0
content?.length || 0
);
return content;
return content ?? "";
});
tab.push(cellsContent);
});
@@ -29,7 +29,7 @@ function htmlTableToString(table: HTMLTableElement) {
const mappedLine = line.map((content, index) =>
content.padEnd(
maxColumnsLength[index],
"\u00A0" /* For no matching with \s */
"\u00A0" // For no matching with \s
)
);
return "| " + mappedLine.join(" | ") + " |";
+8 -3
View File
@@ -1,4 +1,5 @@
import GPTAnswer from "@typing/gptAnswer";
import { toPourcentage } from "./pick-best-response";
class Logs {
static question(text: string) {
@@ -6,9 +7,13 @@ class Logs {
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 bestAnswer(answer: string, similarity: number) {
const css = "color: green";
console.log(
"%c[BEST ANSWER]: %s",
css,
`"${answer}" with a similarity of ${toPourcentage(similarity)}`
);
}
static array(arr: unknown[]) {
+9 -11
View File
@@ -3,20 +3,18 @@
* @param text
*/
function normalizeText(text: string, toLowerCase: boolean = true) {
if (toLowerCase) text = text.toLowerCase();
let normalizedText = text
.replace(/\n+/gi, "\n") //remove duplicate new lines
.replace(/(\n\s*\n)+/g, "\n") //remove useless white sapce from textcontent
.replace(/[ \t]+/gi, " "); //replace multiples space or tabs by a space
.replace(/(\n\s*\n)+/g, "\n") //remove useless white space from textcontent
.replace(/[ \t]+/gi, " ") //replace multiples space or tabs by a space
.trim()
// We remove the following content because sometimes ChatGPT will reply: "answer d"
.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
if (toLowerCase) normalizedText = normalizedText.toLowerCase();
return (
normalizedText
.trim()
/* We remove that because sometimes ChatGPT will reply: "answer d" */
.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
);
return normalizedText;
}
export default normalizeText;
+115
View File
@@ -0,0 +1,115 @@
type BestResponse = {
similarity: number;
value: string | null;
element: HTMLElement | null;
};
type ResponsesBySimilarity = {
similarity: number;
value: string;
element: HTMLElement;
};
/**
* Calculate the levenshtein distance between two sentence
* @param str1
* @param str2
* @returns
*/
function levenshteinDistance(str1: string, str2: string) {
if (str1.length === 0) return str2.length;
if (str2.length === 0) return str1.length;
const matrix: number[][] = [];
const str1WithoutSpaces = str1.replace(/\s+/, "");
const str2WithoutSpaces = str2.replace(/\s+/, "");
for (let i = 0; i <= str1WithoutSpaces.length; ++i) {
matrix.push([i]);
for (let j = 1; j <= str2WithoutSpaces.length; ++j) {
matrix[i][j] =
i === 0
? j
: Math.min(
matrix[i - 1][j] + 1,
matrix[i][j - 1] + 1,
matrix[i - 1][j - 1] +
(str1WithoutSpaces[i - 1] === str2WithoutSpaces[j - 1] ? 0 : 1)
);
}
}
return matrix[str1WithoutSpaces.length][str2WithoutSpaces.length];
}
/**
* Calculate the similarity between two sentences from 0 to 1 (best)
* @param str1
* @param str2
* @returns
*/
function sentenceSimilarity(str1: string, str2: string) {
const longerLength = str1.length > str2.length ? str1.length : str2.length;
if (longerLength === 0) return 1;
return (longerLength - levenshteinDistance(str1, str2)) / longerLength;
}
/**
* Pick the best sentence that correspond to the answer
* @param arr
* @param answer
* @returns
*/
export function pickBestReponse(
answer: string,
arr: { element: HTMLElement; value: string }[]
): BestResponse {
let bestResponse: BestResponse = {
element: null,
similarity: 0,
value: null,
};
for (const obj of arr) {
const similarity = sentenceSimilarity(obj.value, answer);
if (similarity === 1) {
return { element: obj.element, value: obj.value, similarity };
}
if (similarity > bestResponse.similarity) {
bestResponse = { element: obj.element, value: obj.value, similarity };
}
}
return bestResponse;
}
/**
* Return the sentences sorted by score with a score superior or equal to what is asked
* @param answer
* @param arr
* @param score
* @returns
*/
export function pickResponsesWithSimilarityGreaterThan(
answer: string,
arr: { element: HTMLElement; value: string }[],
score: number
): ResponsesBySimilarity[] {
const responses: ResponsesBySimilarity[] = [];
for (const obj of arr) {
const similarity = sentenceSimilarity(obj.value, answer);
if (similarity >= score)
responses.push({
similarity,
value: obj.value,
element: obj.element,
});
}
return responses.sort((a, b) => a.similarity - b.similarity);
}
/**
* Convert a number to a readable string pourcentage
* @param similarity
*/
export function toPourcentage(similarity: number): string {
return Math.round(similarity * 100 * 100) / 100 + "%";
}