Includes images to the question
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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>
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -132,8 +132,7 @@ a {
|
||||
|
||||
#message {
|
||||
display: none;
|
||||
margin-top: 0.75rem;
|
||||
margin-bottom: -0.25rem;
|
||||
margin-bottom: -0.5rem;
|
||||
}
|
||||
|
||||
#reloadModel {
|
||||
|
||||
@@ -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
@@ -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
@@ -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';
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user