Includes images to the question

This commit is contained in:
yoannchb-pro
2024-02-29 11:39:33 -05:00
parent d2bc6c4c65
commit 1365ccb56d
13 changed files with 182 additions and 29 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+10 -4
View File
@@ -27,14 +27,15 @@
<body>
<main>
<div class="line center" style="margin-top: 1rem; margin-bottom: 1rem">
<div class="line center" style="margin-bottom: 0.5rem">
<a
target="_blank"
rel="noopener noreferrer"
href="https://www.flaticon.com/free-icons/mortarboard"
title="Mortarboard icons created by itim2101 - Flaticon"
><img src="../icon.png" alt="icon"
/></a>
>
<img src="../icon.png" alt="icon" />
</a>
<div class="col center title">
<h1>MoodleGPT</h1>
<p id="version"></p>
@@ -113,9 +114,14 @@
<input id="history" type="checkbox" />
<label for="history">Save history</label>
</div>
<!-- This option is only showed if the current version of the model support it -->
<div class="line" id="includeImages-line" style="display: none">
<input id="includeImages" type="checkbox" />
<label for="includeImages">Include images</label>
</div>
</div>
</div>
<p id="message">Message</p>
<p id="message">{Message}</p>
<div class="line center">
<button class="save">Save</button>
</div>
+15 -2
View File
@@ -1,5 +1,18 @@
'use strict';
const modelInput = document.querySelector('#model');
const imagesIntegrationLine = document.querySelector('#includeImages-line');
// Check if the gpt version is at least 4 to show the option 'Include images'
modelInput.addEventListener('input', function () {
const version = modelInput.value;
if (isCurrentVersionSupportingImages(version)) {
imagesIntegrationLine.style.display = 'flex';
} else {
imagesIntegrationLine.style.display = 'none';
}
});
/**
* Get the last ChatGPT version
*/
@@ -7,7 +20,7 @@ function getLastChatGPTVersion() {
const apiKeySelector = document.querySelector('#apiKey');
const reloadModel = document.querySelector('#reloadModel');
let apiKey = apiKeySelector.value;
let apiKey = apiKeySelector.value?.trim();
// If the api key is set we enable the button to get the last chatgpt version
function checkFiledApiKey() {
@@ -40,7 +53,7 @@ function getLastChatGPTVersion() {
});
const rep = await req.json();
const model = rep.data.find(model => model.id.startsWith('gpt'));
document.querySelector('#model').value = model.id;
modelInput.value = model.id;
} catch (err) {
console.error(err);
showMessage({ msg: 'Failed to fetch last ChatGPT version', error: true });
+11 -8
View File
@@ -1,6 +1,8 @@
'use strict';
const saveBtn = document.querySelector('.save');
/* inputs id */
// inputs id
const inputsText = ['apiKey', 'code', 'model'];
const inputsCheckbox = [
'logs',
@@ -10,20 +12,20 @@ const inputsCheckbox = [
'mouseover',
'infinite',
'timeout',
'history'
'history',
'includeImages'
];
/* Save the configuration */
// Save the configuration
saveBtn.addEventListener('click', function () {
const [apiKey, code, model] = inputsText.map(selector =>
document.querySelector('#' + selector).value.trim()
);
const [logs, title, cursor, typing, mouseover, infinite, timeout, history] = inputsCheckbox.map(
selector => {
const [logs, title, cursor, typing, mouseover, infinite, timeout, history, includeImages] =
inputsCheckbox.map(selector => {
const element = document.querySelector('#' + selector);
return element.checked && element.parentElement.style.display !== 'none';
}
);
});
if (!apiKey || !model) {
showMessage({ msg: 'Please complete all the form', error: true });
@@ -51,6 +53,7 @@ saveBtn.addEventListener('click', function () {
infinite,
timeout,
history,
includeImages,
mode: actualMode
}
});
@@ -58,7 +61,7 @@ saveBtn.addEventListener('click', function () {
showMessage({ msg: 'Configuration saved' });
});
/* we load back the configuration */
// we load back the configuration
chrome.storage.sync.get(['moodleGPT']).then(function (storage) {
const config = storage.moodleGPT;
+8 -3
View File
@@ -5,7 +5,10 @@ const modes = mode.querySelectorAll('button');
let actualMode = 'autocomplete';
/* inputs id that need to be disabled for a specific mode */
// input to don't take in consideration
const toExcludes = ['includeImages'];
// inputs id that need to be disabled for a specific mode
const disabledForThisMode = {
autocomplete: [],
clipboard: ['typing', 'mouseover'],
@@ -17,7 +20,9 @@ const disabledForThisMode = {
*/
function handleModeChange() {
const needDisable = disabledForThisMode[actualMode];
const dontNeedDisable = inputsCheckbox.filter(input => !needDisable.includes(input));
const dontNeedDisable = inputsCheckbox.filter(
input => !needDisable.includes(input) && !toExcludes.includes(input)
);
for (const id of needDisable) {
document.querySelector('#' + id).parentElement.style.display = 'none';
}
@@ -26,7 +31,7 @@ function handleModeChange() {
}
}
/* Mode handler */
// Mode hanlder
for (const button of modes) {
button.addEventListener('click', function () {
const value = button.value;
+13
View File
@@ -10,3 +10,16 @@ function showMessage({ msg, error, infinite }) {
message.style.display = 'block';
if (!infinite) setTimeout(() => (message.style.display = 'none'), 5000);
}
/**
* Check if the current model support images integrations
* @param {string} version
* @returns
*/
function isCurrentVersionSupportingImages(version) {
const versionNumber = version.match(/gpt-(\d+)/);
if (!versionNumber?.[1]) {
return false;
}
return Number(versionNumber[1]) >= 4;
}
+1 -2
View File
@@ -132,8 +132,7 @@ a {
#message {
display: none;
margin-top: 0.75rem;
margin-bottom: -0.25rem;
margin-bottom: -0.5rem;
}
#reloadModel {
+67 -4
View File
@@ -1,11 +1,20 @@
import type Config from '@typing/config';
import type GPTAnswer from '@typing/gptAnswer';
import imageToBase64 from '@utils/image-to-base64';
import normalizeText from '@utils/normalize-text';
import isCurrentVersionSupportingImages from '@utils/version-support-images';
type Content =
| string
| Array<{
type: CONTENT_TYPE;
content: string;
}>;
type History = {
url: string | null;
system: { role: ROLE; content: string };
history: { role: ROLE; content: string }[];
system: { role: ROLE; content: Content };
history: { role: ROLE; content: Content }[];
};
enum ROLE {
@@ -14,6 +23,11 @@ enum ROLE {
ASSISTANT = 'assistant'
}
enum CONTENT_TYPE {
TEXT = 'text',
IMAGE = 'image_url'
}
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.
@@ -37,13 +51,61 @@ const history: History = {
history: []
};
/**
* Get the content to send to ChatGPT API (it allows to includes images if supported)
* @param config
*/
async function getContent(
config: Config,
questionElement: HTMLElement,
question: string
): Promise<Content> {
const imagesElements = questionElement.querySelectorAll('img');
if (
!config.includeImages ||
!isCurrentVersionSupportingImages(config.model) ||
imagesElements.length === 0
) {
return question;
}
let content: Content = [];
const base64Images = Array.from(imagesElements).map(imgEl => imageToBase64(imgEl));
const results = await Promise.all(base64Images);
const filteredResults = results.filter(value => value !== null) as string[];
for (const result of filteredResults) {
content.push({
type: CONTENT_TYPE.IMAGE,
content: result
});
}
if (content.length > 0) {
content.push({
type: CONTENT_TYPE.TEXT,
content: question
});
} else {
content = question;
}
return content;
}
/**
* Get the response from chatGPT api
* @param config
* @param question
* @returns
*/
async function getChatGPTResponse(config: Config, question: string): Promise<GPTAnswer> {
async function getChatGPTResponse(
config: Config,
questionElement: HTMLElement,
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
@@ -55,7 +117,8 @@ async function getChatGPTResponse(config: Config, question: string): Promise<GPT
const controller = new AbortController();
const timeoutControler = setTimeout(() => controller.abort(), 15 * 1000);
const message = { role: ROLE.USER, content: question };
const content = await getContent(config, questionElement, question);
const message = { role: ROLE.USER, content };
const req = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
+5 -3
View File
@@ -25,9 +25,11 @@ async function reply(props: Props): Promise<void> {
const question = createAndNormalizeQuestion(props.form);
const inputList: NodeListOf<HTMLElement> = props.form.querySelectorAll(props.inputQuery);
const gptAnswer = await getChatGPTResponse(props.config, question).catch(error => ({
error
}));
const gptAnswer = await getChatGPTResponse(props.config, props.questionElement, question).catch(
error => ({
error
})
);
const haveError = typeof gptAnswer === 'object' && 'error' in gptAnswer;
+2 -1
View File
@@ -1,7 +1,7 @@
type Config = {
apiKey: string;
model: string;
code?: string;
model?: string;
infinite?: boolean;
typing?: boolean;
mouseover?: boolean;
@@ -10,6 +10,7 @@ type Config = {
title?: boolean;
timeout?: boolean;
history?: boolean;
includeImages?: boolean;
mode?: 'autocomplete' | 'question-to-answer' | 'clipboard';
};
+34
View File
@@ -0,0 +1,34 @@
/**
* Convert an image html element into a base64 image string
* @param imageElement
* @returns
*/
function imageToBase64(imageElement: HTMLImageElement): Promise<string | null> {
return new Promise((resolve, reject) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) {
resolve(null);
return;
}
const img = new Image();
img.crossOrigin = 'Anonymous';
img.onload = () => {
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
const base64 = canvas.toDataURL('image/png');
resolve(base64);
};
img.onerror = error => {
reject(error);
};
img.src = imageElement.src;
});
}
export default imageToBase64;
+14
View File
@@ -0,0 +1,14 @@
/**
* Check if the current ChatGPT version support image sending
* @param version
* @returns
*/
function isCurrentVersionSupportingImages(version: string): boolean {
const versionNumber = version.match(/gpt-(\d+)/);
if (!versionNumber?.[1]) {
return false;
}
return Number(versionNumber[1]) >= 4;
}
export default isCurrentVersionSupportingImages;