14 Commits

Author SHA1 Message Date
yoannchb-pro 17aa8b49e0 Merge pull request #61 from yoannchb-pro/dev
Dev
2025-07-30 11:14:23 -04:00
yoannchb-pro a2849da20c v1.1.4 2025-07-30 11:12:44 -04:00
yoannchb-pro 54c9373e33 Merge pull request #58 from LuckyForce/main
Solves #57 for o3
2025-07-30 10:52:03 -04:00
Adrian Schauer e793cbd0b2 #57 addtion of o3 2025-07-10 17:01:10 +02:00
yoannchb-pro fffc0d55d6 Merge pull request #54 from yoannchb-pro/dev
v1.1.3
2025-03-19 09:28:49 -04:00
yoannchb-pro e561227b78 updated doc 2025-03-19 09:27:19 -04:00
yoannchb-pro 677e870635 gpt version msg 2025-03-19 09:09:35 -04:00
yoannchb-pro 6e69830a2e v1.1.2 -> v1.1.3 2025-03-19 09:07:52 -04:00
yoannchb-pro f01785256c Updated dependencies 2025-03-19 09:05:08 -04:00
yoannchb-pro f2e1ec8ed6 Merge pull request #46 from dmunozv04/main
Add baseURL and max Tokens config
2025-03-19 08:53:03 -04:00
yoannchb-pro 98f22e9056 Merge pull request #53 from yoannchb-pro/main
Merge main into dev
2025-03-19 08:51:25 -04:00
yoannchb-pro 79a75fee89 Create FUNDING.yml 2025-02-03 14:29:50 -05:00
dmunozv04 476559188f Add baseURL and max Tokens config 2025-02-03 20:21:29 +01:00
yoannchb-pro 16a82fe3d8 v1.1.2 2025-01-14 12:23:13 -05:00
53 changed files with 1875 additions and 2196 deletions
-18
View File
@@ -1,18 +0,0 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'prettier'
],
ignorePatterns: ['node_modules/'],
overrides: [
{
files: ['extension/popup/*.js', 'src/**/*.ts'],
rules: {}
}
]
};
+1
View File
@@ -0,0 +1 @@
buy_me_a_coffee: yoannchbpro
+17
View File
@@ -1,5 +1,22 @@
# CHANGELOG
## v1.1.4
- Support for all `o` models
- Removed `Clear my choice` in the api call
- Code dependencies update
## v1.1.3
- Added `base url` and `max token` in config (by dmunozv04)
- Code dependencies update
## v1.1.2
- Advanced settings
- Added OpenAI SDK for better support
- o1 model support
## v1.1.1
- Bugs correction
+17 -4
View File
@@ -2,7 +2,7 @@
href="https://www.flaticon.com/free-icons/mortarboard" target="_blank" rel="noopener noreferrer"
title="Mortarboard icons created by itim2101 - Flaticon" ><img src="./extension/icon.png" alt="Mortarboard icons created by itim2101 - Flaticon" width="150" style="display:block; margin:auto;"></a></p>
# MoodleGPT 1.1.1
# MoodleGPT 1.1.4
This extension allows you to hide CHAT-GPT in a Moodle quiz. You just need to click on the question you want to solve, and CHAT-GPT will automatically provide the answer. However, one needs to be careful because as we know, CHAT-GPT can make errors especially in calculations.
@@ -12,15 +12,17 @@ Find the extension on the Chrome Webstore right [here](https://chrome.google.com
## Summary
- [MoodleGPT 1.1.1](#moodlegpt-111)
- [MoodleGPT 1.1.4](#moodlegpt-114)
- [Chrome Webstore](#chrome-webstore)
- [Summary](#summary)
- [Disclaimer !](#disclaimer-)
- [Donate](#donate)
- [Update](#update)
- [Set up](#set-up)
- [Mode](#mode)
- [Settings](#settings)
- [Advanced Settings](#advanced-settings)
- [Mode](#mode)
- [Options](#options)
- [Internal other features](#internal-other-features)
- [Support table](#support-table)
- [Supported questions type](#supported-questions-type)
@@ -61,6 +63,17 @@ See the [changelog](./CHANGELOG.md) to see every updates !
Go to <b>"Manage my extensions"</b> on your browser, then click on <b>"Load unpacked extension"</b> and select the <b>"extension"</b> folder. Afterwards, click on the extension icon and enter the ApiKey obtained from [openai api](https://platform.openai.com/api-keys). Finally, select a [gpt model](https://platform.openai.com/docs/models) (ensure it work with completion api).
## Settings
- <b>API KEY\*</b>: Your openai [API KEY](https://platform.openai.com/api-keys)
- <b>GPT MODEL\*</b>: The [gpt model](https://platform.openai.com/docs/models) (you can click on the play button to ensure the model work with the extension)
## Advanced Settings
- <b>CODE</b>: A code you will need to type on your keyboard to inject/remove the extension code from the moodle page. It allow you to be more discret and control the injection so it's recommended.
- <b>BASE URL</b>: The API endpoint if you need to use your own llm.
- <b>MAX TOKENS</b>: The max tokens length you want the api to respond with.
## Mode
<p align="center">
@@ -72,7 +85,7 @@ Go to <b>"Manage my extensions"</b> on your browser, then click on <b>"Load unpa
- <b>Question to answer:</b> The question is converted to the answer and you can click on it to show back the question (or show back the answer).
<br/><img src="./assets/question-to-answer.gif" alt="Question to Answer">
## Settings
## Options
<p align="center">
<img src="./assets/settings.png" alt="Popup" width="300">
+32
View File
@@ -0,0 +1,32 @@
const js = require('@eslint/js');
const tsParser = require('@typescript-eslint/parser');
const tsPlugin = require('@typescript-eslint/eslint-plugin');
const prettierConfig = require('eslint-config-prettier');
const tseslint = require('typescript-eslint');
module.exports = [
{
ignores: ['**/node_modules/*', '**/dist/*', '**/*.js']
},
js.configs.recommended,
...tseslint.configs.recommended,
{
files: ['**/*.ts'],
languageOptions: {
parser: tsParser,
ecmaVersion: 'latest',
sourceType: 'module'
},
plugins: {
'@typescript-eslint': tsPlugin
},
rules: {
...tsPlugin.configs['eslint-recommended'].rules,
...tsPlugin.configs.recommended.rules,
...prettierConfig.rules,
'@typescript-eslint/no-explicit-any': 'off'
}
}
];
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "MoodleGPT",
"version": "1.1.1",
"version": "1.1.4",
"description": "Hidden chat-gpt for your moodle quiz",
"permissions": ["storage"],
"action": {
+38 -20
View File
@@ -7,13 +7,7 @@
<title>MoodleGPT</title>
<link rel="stylesheet" href="style.css" />
<!-- Should always be at the top -->
<script src="./js/utils.js" defer></script>
<script src="./js/index.js" defer></script>
<script src="./js/modeHandler.js" defer></script>
<script src="./js/gptVersion.js" defer></script>
<script src="./js/version.js" defer></script>
<script src="./popup.js" defer></script>
<link rel="icon" type="image/png" href="../icon.png" />
<link
@@ -41,20 +35,43 @@
<p id="version"></p>
</div>
</div>
<div class="line center">
<label for="apiKey" class="textLabel">Api Key<span class="required">*</span>:</label>
<input id="apiKey" type="text" />
<!-- SETTINGS -->
<div class="settings" id="settings">
<div class="line center">
<label for="apiKey" class="textLabel">Api Key<span class="required">*</span>:</label>
<input id="apiKey" type="text" />
</div>
<div class="line center">
<label for="model" class="textLabel">GPT Model<span class="required">*</span>:</label>
<input type="text" id="model" list="models" />
<datalist id="models"></datalist>
<i id="check-model" title="Test" style="cursor: pointer" class="fa-solid fa-play"></i>
</div>
</div>
<div class="line center">
<label for="model" class="textLabel">GPT Model<span class="required">*</span>:</label>
<input type="text" id="model" list="models" />
<datalist id="models"></datalist>
<!-- ADVANCED SETTINGS -->
<div class="settings" id="advanced-settings" style="display: none">
<div class="line center">
<label for="code" class="textLabel">Code:</label>
<input id="code" type="text" />
</div>
<div class="line center">
<label for="baseURL" class="textLabel">Base URL:</label>
<input id="baseURL" type="text" />
</div>
<div class="line center">
<label for="maxTokens" class="textLabel">Max Tokens:</label>
<input id="maxTokens" type="number" />
</div>
</div>
<div class="line center">
<label for="code" class="textLabel">Code:</label>
<input id="code" type="text" />
<!-- SWITCH SETTINGS MODE -->
<div class="line center mt">
<a id="switch-settings" href="#">Advanced settings</a>
</div>
<div class="line mt">
<div class="line center-y mt">
<i class="fa-solid fa-robot"></i>
<p>Mode:</p>
</div>
@@ -69,9 +86,9 @@
</li>
</ul>
</div>
<div class="line mt">
<div class="line mt center-y">
<i class="fa-solid fa-gear"></i>
<p>Settings:</p>
<p>Options:</p>
</div>
<div class="line center" style="gap: 2rem">
<div class="col">
@@ -122,6 +139,7 @@
</div>
<div class="line center">
<a
class="donate"
href="https://www.buymeacoffee.com/yoannchbpro"
target="_blank"
rel="noopener noreferrer"
-54
View File
@@ -1,54 +0,0 @@
'use strict';
const apiKeySelector = document.querySelector('#apiKey');
const inputModel = document.querySelector('#model');
const modelsList = document.querySelector('#models');
const imagesIntegrationLine = document.querySelector('#includeImages-line');
/**
* Check if the gpt version is at least 4 to show the option 'Include images'
*/
function checkCanIncludeImages() {
const version = inputModel.value;
if (isCurrentVersionSupportingImages(version)) {
imagesIntegrationLine.style.display = 'flex';
} else {
imagesIntegrationLine.style.display = 'none';
}
}
inputModel.addEventListener('input', checkCanIncludeImages);
// We populate the datalist of the chatgpt model
async function populateDatalistWithGptVersions() {
const apiKey = apiKeySelector.value?.trim();
if (!apiKey) return;
inputModel.innerHTML = '';
try {
const req = await fetch('https://api.openai.com/v1/models', {
headers: {
Authorization: `Bearer ${apiKey}`
}
});
const rep = await req.json();
rep.data.sort((a, b) => b.id.localeCompare(a.id)); // we sort the model to get the best chatgpt version first
const models = rep.data.filter(model => model.id.startsWith('gpt'));
for (const model of models) {
const opt = document.createElement('option');
opt.value = model.id;
opt.textContent = model.id;
modelsList.appendChild(opt);
}
checkCanIncludeImages();
} catch (err) {
console.error(err);
showMessage({ msg: 'Failed to fetch last ChatGPT versions', error: true });
}
}
inputModel.addEventListener('focus', populateDatalistWithGptVersions);
-25
View File
@@ -1,25 +0,0 @@
'use strict';
/**
* Show message into the popup
*/
function showMessage({ msg, error, infinite }) {
const message = document.querySelector('#message');
message.style.color = error ? 'red' : 'limegreen';
message.textContent = msg;
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;
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+44
View File
@@ -35,6 +35,15 @@ main {
width: 22rem;
}
.settings {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.4rem;
text-align: center;
width: 100%;
}
img {
width: 5rem;
}
@@ -56,6 +65,10 @@ a {
align-items: center;
}
.center-y {
align-items: center;
}
.line .textLabel {
width: 5rem;
text-align: left;
@@ -69,6 +82,7 @@ a {
.line input[type='text'],
.line input[type='password'],
.line input[type='number'],
.line select {
flex: 1 1;
border: thin solid var(--color);
@@ -148,3 +162,33 @@ a {
cursor: not-allowed;
opacity: 0.75;
}
.donate {
color: white;
animation: infinite donate 5s linear;
font-weight: bold;
}
@keyframes donate {
0% {
transform: translateX(0);
}
3.57% {
transform: translateY(-9px);
}
7.14% {
transform: translateY(-9px) rotate(17deg);
}
10.78% {
transform: translateY(-9px) rotate(-17deg);
}
14% {
transform: translateY(-9px) rotate(17deg);
}
18% {
transform: translateY(-9px) rotate(-17deg);
}
22% {
transform: translateY(0) rotate(0);
}
}
+1382 -1925
View File
File diff suppressed because it is too large Load Diff
+16 -9
View File
@@ -1,6 +1,6 @@
{
"name": "moodlegpt",
"version": "1.1.1",
"version": "1.1.4",
"description": "This extension allows you to hide CHAT-GPT in a Moodle quiz.",
"scripts": {
"build": "npm run prettier && npm run lint && npm run fastBuild",
@@ -26,15 +26,22 @@
},
"homepage": "https://github.com/yoannchb-pro/MoodleGPT#readme",
"devDependencies": {
"@eslint/js": "^9.32.0",
"@rollup/plugin-commonjs": "^28.0.6",
"@rollup/plugin-node-resolve": "^16.0.0",
"@rollup/plugin-terser": "^0.4.4",
"@types/chrome": "^0.0.263",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"prettier": "^3.2.5",
"rollup": "^4.13.0",
"@rollup/plugin-typescript": "^12.1.4",
"@types/chrome": "^0.1.1",
"@types/node": "^24.1.0",
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.38.0",
"eslint": "^9.32.0",
"eslint-config-prettier": "^10.1.8",
"openai": "^5.11.0",
"prettier": "^3.6.2",
"rollup": "^4.46.2",
"rollup-plugin-ts": "^3.2.0",
"typescript": "^5.4.2"
"typescript": "^5.8.3",
"typescript-eslint": "^8.38.0"
}
}
+22 -9
View File
@@ -1,16 +1,29 @@
const ts = require('rollup-plugin-ts');
const ts = require('@rollup/plugin-typescript');
const terser = require('@rollup/plugin-terser');
const { nodeResolve } = require('@rollup/plugin-node-resolve');
const config = require('./tsconfig.json');
module.exports = {
input: './src/index.ts',
output: [
{
module.exports = [
{
input: './src/background/index.ts',
output: {
file: './extension/MoodleGPT.js',
format: 'umd',
sourcemap: true
}
],
plugins: [ts(config), terser()]
};
},
onwarn() {},
plugins: [nodeResolve(), ts(config), terser()]
},
{
input: './src/popup/index.ts',
output: {
file: './extension/popup/popup.js',
format: 'umd',
sourcemap: true
},
onwarn() {},
plugins: [nodeResolve(), ts(config), terser()]
}
];
@@ -1,5 +1,5 @@
import type Config from '@typing/config';
import titleIndications from '@utils/title-indications';
import type Config from '../types/config';
import titleIndications from 'background/utils/title-indications';
import reply from './reply';
type Listener = {
@@ -1,5 +1,5 @@
import normalizeText from '@utils/normalize-text';
import htmlTableToString from '@utils/html-table-to-string';
import normalizeText from 'background/utils/normalize-text';
import htmlTableToString from 'background/utils/html-table-to-string';
/**
* Normalize the question as text and add sub informations
@@ -20,6 +20,8 @@ function createAndNormalizeQuestion(questionContainer: HTMLElement) {
if (attoText) {
question = question.replace((attoText as HTMLElement).innerText, '');
}
const clearMyChoice = questionContainer.querySelector('[role="button"]');
if (clearMyChoice) question = question.replace((clearMyChoice as HTMLElement).innerText, '');
// Make tables more readable for chat-gpt
const tables: NodeListOf<HTMLTableElement> = questionContainer.querySelectorAll('.qtext table');
@@ -1,14 +1,14 @@
import type Config from '@typing/config';
import { ROLE, CONTENT_TYPE, type MessageContent, type Message } from '@typing/message';
import imageToBase64 from '@utils/image-to-base64';
import isGPTModelGreaterOrEqualTo4 from '@utils/version-support-images';
import type Config from '../types/config';
import imageToBase64 from 'background/utils/image-to-base64';
import isGPTModelGreaterOrEqualTo4 from 'background/utils/version-support-images';
import { ChatCompletionMessageParam, ChatCompletionUserMessageParam } from 'openai/resources';
// The attempt and the cmid allow us to identify a quiz
type History = {
host: string;
cmid: string; // The id of the quiz
attempt: string; // The attempt of the current quiz
history: { role: ROLE; content: MessageContent }[];
history: ChatCompletionMessageParam[];
};
const INSTRUCTION: string = `
@@ -26,9 +26,9 @@ Act as a quiz solver for the best notation with the following rules:
`.trim();
const SYSTEM_INSTRUCTION_MESSAGE = {
role: ROLE.SYSTEM,
role: 'system',
content: INSTRUCTION
} as const satisfies Message;
} as const satisfies ChatCompletionMessageParam;
/**
* Get the content to send to ChatGPT API (it allows to includes images if supported)
@@ -38,7 +38,7 @@ async function getContent(
config: Config,
questionElement: HTMLElement,
question: string
): Promise<MessageContent> {
): Promise<ChatCompletionUserMessageParam['content']> {
const imagesElements = questionElement.querySelectorAll('img');
if (
@@ -49,7 +49,7 @@ async function getContent(
return question;
}
const contentWithImages: MessageContent = [];
const contentWithImages: ChatCompletionUserMessageParam['content'] = [];
const base64Images = Array.from(imagesElements).map(imgEl => imageToBase64(imgEl));
const base64ImagesResolved = await Promise.allSettled(base64Images);
@@ -57,7 +57,7 @@ async function getContent(
for (const result of base64ImagesResolved) {
if (result.status === 'fulfilled') {
contentWithImages.push({
type: CONTENT_TYPE.IMAGE,
type: 'image_url',
image_url: { url: result.value }
});
} else if (config.logs) {
@@ -66,7 +66,7 @@ async function getContent(
}
contentWithImages.push({
type: CONTENT_TYPE.TEXT,
type: 'text',
text: question
});
@@ -124,11 +124,11 @@ async function getContentWithHistory(
questionElement: HTMLElement,
question: string
): Promise<{
messages: [typeof SYSTEM_INSTRUCTION_MESSAGE, ...Message[]];
messages: [typeof SYSTEM_INSTRUCTION_MESSAGE, ...ChatCompletionMessageParam[]];
saveResponse?: (response: string) => void;
}> {
const content = await getContent(config, questionElement, question);
const message = { role: ROLE.USER, content };
const message: ChatCompletionMessageParam = { role: 'user', content };
if (!config.history) return { messages: [SYSTEM_INSTRUCTION_MESSAGE, message] };
@@ -149,7 +149,7 @@ async function getContentWithHistory(
// Register the conversation
if (config.history) {
history.history.push(message);
history.history.push({ role: ROLE.ASSISTANT, content: response });
history.history.push({ role: 'assistant', content: response });
sessionStorage.moodleGPTHistory = JSON.stringify(history);
}
}
@@ -1,7 +1,9 @@
import type Config from '@typing/config';
import type GPTAnswer from '@typing/gpt-answer';
import normalizeText from '@utils/normalize-text';
import type Config from '../types/config';
import type GPTAnswer from '../types/gpt-answer';
import normalizeText from 'background/utils/normalize-text';
import getContentWithHistory from './get-content-with-history';
import OpenAI from 'openai';
import { fixeO } from '../utils/fixe-o';
/**
* Get the response from chatGPT api
@@ -21,28 +23,28 @@ async function getChatGPTResponse(
// Including the instructions to the AI, the images as base64 if needed, the question and the past conversation if history is set to true
const contentHandler = await getContentWithHistory(config, questionElement, question);
const req = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${config.apiKey}`
},
signal: config.timeout ? controller.signal : null,
body: JSON.stringify({
const client = new OpenAI({
apiKey: config.apiKey,
baseURL: config.baseURL,
dangerouslyAllowBrowser: true
});
const req = await client.chat.completions.create(
fixeO(config.model, {
model: config.model,
messages: contentHandler.messages,
temperature: 0.1, // Controls the randomness of the generated responses, with lower values producing more deterministic and predictable outputs. With set to 0.1 instead of 0 for more creativity.
top_p: 0.6, // Determines the diversity of the generated responses
presence_penalty: 0, // Encourages the model to introduce new concepts by penalizing words that have already appeared in the text.
max_tokens: 2000 // Maximum length of the response
})
});
max_tokens: config.maxTokens || 2000 // Maximum length of the response,
}),
{ signal: config.timeout ? controller.signal : null }
);
clearTimeout(timeoutControler);
const rep = await req.json();
const response = rep.choices[0].message.content;
const response = req.choices[0].message.content ?? '';
// Save the response into the history
if (typeof contentHandler.saveResponse === 'function') contentHandler.saveResponse(response);
@@ -1,13 +1,13 @@
import type GPTAnswer from '@typing/gpt-answer';
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';
import handleAtto from '@core/questions/atto';
import type GPTAnswer from '../../types/gpt-answer';
import type Config from '../../types/config';
import handleClipboard from 'background/core/questions/clipboard';
import handleContentEditable from 'background/core/questions/contenteditable';
import handleNumber from 'background/core/questions/number';
import handleRadio from 'background/core/questions/radio';
import handleCheckbox from 'background/core/questions/checkbox';
import handleSelect from 'background/core/questions/select';
import handleTextbox from 'background/core/questions/textbox';
import handleAtto from 'background/core/questions/atto';
type Props = {
config: Config;
@@ -1,6 +1,6 @@
import type Config from '@typing/config';
import type GPTAnswer from '@typing/gpt-answer';
import handleClipboard from '@core/questions/clipboard';
import type Config from '../../types/config';
import type GPTAnswer from '../../types/gpt-answer';
import handleClipboard from 'background/core/questions/clipboard';
type Props = {
config: Config;
@@ -1,4 +1,4 @@
import type GPTAnswer from '@typing/gpt-answer';
import type GPTAnswer from '../../types/gpt-answer';
type Props = {
questionElement: HTMLElement;
@@ -1,5 +1,5 @@
import type Config from '@typing/config';
import type GPTAnswer from '@typing/gpt-answer';
import type Config from '../../types/config';
import type GPTAnswer from '../../types/gpt-answer';
/**
* Hanlde atto editor
@@ -1,8 +1,8 @@
import type Config from '@typing/config';
import type GPTAnswer from '@typing/gpt-answer';
import Logs from '@utils/logs';
import normalizeText from '@utils/normalize-text';
import { pickBestReponse } from '@utils/pick-best-response';
import type Config from '../../types/config';
import type GPTAnswer from '../../types/gpt-answer';
import Logs from 'background/utils/logs';
import normalizeText from 'background/utils/normalize-text';
import { pickBestReponse } from 'background/utils/pick-best-response';
/**
* Handle input checkbox elements
@@ -1,6 +1,6 @@
import type Config from '@typing/config';
import type GPTAnswer from '@typing/gpt-answer';
import titleIndications from '@utils/title-indications';
import type Config from '../../types/config';
import type GPTAnswer from '../../types/gpt-answer';
import titleIndications from 'background/utils/title-indications';
/**
* Copy the response in the clipboard if we can automaticaly fill the question
@@ -1,5 +1,5 @@
import type Config from '@typing/config';
import type GPTAnswer from '@typing/gpt-answer';
import type Config from '../../types/config';
import type GPTAnswer from '../../types/gpt-answer';
function isContentEditable(element: HTMLElement) {
const contenteditable = element.getAttribute('contenteditable');
@@ -1,5 +1,5 @@
import type Config from '@typing/config';
import type GPTAnswer from '@typing/gpt-answer';
import type Config from '../../types/config';
import type GPTAnswer from '../../types/gpt-answer';
/**
* Handle number input
@@ -1,8 +1,8 @@
import type Config from '@typing/config';
import type GPTAnswer from '@typing/gpt-answer';
import Logs from '@utils/logs';
import normalizeText from '@utils/normalize-text';
import { pickBestReponse } from '@utils/pick-best-response';
import type Config from '../../types/config';
import type GPTAnswer from '../../types/gpt-answer';
import Logs from 'background/utils/logs';
import normalizeText from 'background/utils/normalize-text';
import { pickBestReponse } from 'background/utils/pick-best-response';
/**
* Handle input radio elements
@@ -1,8 +1,8 @@
import type Config from '@typing/config';
import type GPTAnswer from '@typing/gpt-answer';
import Logs from '@utils/logs';
import normalizeText from '@utils/normalize-text';
import { pickBestReponse } from '@utils/pick-best-response';
import type Config from '../../types/config';
import type GPTAnswer from '../../types/gpt-answer';
import Logs from 'background/utils/logs';
import normalizeText from 'background/utils/normalize-text';
import { pickBestReponse } from 'background/utils/pick-best-response';
/**
* Handle select elements (and put in order select)
@@ -1,5 +1,5 @@
import type Config from '@typing/config';
import type GPTAnswer from '@typing/gpt-answer';
import type Config from '../../types/config';
import type GPTAnswer from '../../types/gpt-answer';
/**
* Handle textbox
@@ -1,5 +1,5 @@
import type Config from '@typing/config';
import Logs from '@utils/logs';
import type Config from '../types/config';
import Logs from 'background/utils/logs';
import getChatGPTResponse from './get-response';
import createAndNormalizeQuestion from './create-question';
import clipboardMode from './modes/clipboard';
+1 -1
View File
@@ -1,4 +1,4 @@
import type Config from '@typing/config';
import type Config from './types/config';
import { codeListener, setUpMoodleGpt } from './core/code-listener';
chrome.storage.sync.get(['moodleGPT']).then(function (storage) {
@@ -12,6 +12,8 @@ type Config = {
history?: boolean;
includeImages?: boolean;
mode?: 'autocomplete' | 'question-to-answer' | 'clipboard';
baseURL?: string;
maxTokens?: number;
};
export default Config;
+26
View File
@@ -0,0 +1,26 @@
import { ChatCompletionCreateParamsNonStreaming } from 'openai/resources/chat/completions';
/**
* Fixe request body for "o1" model
* @param model
* @param data
* @returns
*/
export function fixeO(model: string, data: ChatCompletionCreateParamsNonStreaming) {
if (model.search(/^o\d+/gi) === -1) return data;
if (data.max_tokens) {
data.max_completion_tokens = data.max_tokens;
delete data.max_tokens;
}
if (data.temperature) delete data.temperature;
if (data.top_p) delete data.top_p;
for (const message of data.messages) {
if (message.role === 'system') message.role = 'user' as any;
}
return data;
}
@@ -1,4 +1,4 @@
import GPTAnswer from '@typing/gpt-answer';
import GPTAnswer from '../types/gpt-answer';
import { toPourcentage } from './pick-best-response';
class Logs {
+15
View File
@@ -0,0 +1,15 @@
export const globalData = { actualMode: 'autocomplete' };
export const inputsCheckbox = [
'logs',
'title',
'cursor',
'typing',
'mouseover',
'infinite',
'timeout',
'history',
'includeImages'
];
export const mode = document.querySelector('#mode')!;
export const modes = mode.querySelectorAll('button')!;
+84
View File
@@ -0,0 +1,84 @@
import OpenAI from 'openai';
import { isCurrentVersionSupportingImages, showMessage } from './utils';
const apiKeySelector: HTMLInputElement = document.querySelector('#apiKey')!;
const inputModel: HTMLInputElement = document.querySelector('#model')!;
const modelsList: HTMLElement = document.querySelector('#models')!;
const imagesIntegrationLine: HTMLInputElement = document.querySelector('#includeImages-line')!;
const baseURLSelector: HTMLInputElement = document.querySelector('#baseURL')!;
/**
* Check if the gpt version is at least 4 to show the option 'Include images'
*/
export function checkCanIncludeImages() {
const version = inputModel.value;
if (isCurrentVersionSupportingImages(version)) {
imagesIntegrationLine.style.display = 'flex';
} else {
imagesIntegrationLine.style.display = 'none';
}
}
inputModel.addEventListener('input', checkCanIncludeImages);
// We populate the datalist of the chatgpt model
export async function populateDatalistWithGptVersions() {
const apiKey = apiKeySelector.value?.trim();
const baseURL = baseURLSelector.value?.trim();
if (!apiKey) return;
inputModel.innerHTML = '';
try {
const client = new OpenAI({
apiKey,
baseURL,
dangerouslyAllowBrowser: true
});
const rep = await client.models.list();
const models = rep.data.filter(
model =>
model.id.startsWith('gpt') ||
model.id.search(/^o\d+/gi) !== -1 ||
model.id.startsWith('chatgpt')
);
models.sort((a, b) => b.id.localeCompare(a.id)); // we sort the model to get the best chatgpt version first
for (const model of models) {
const opt = document.createElement('option');
opt.value = model.id;
opt.textContent = model.id;
modelsList.appendChild(opt);
}
checkCanIncludeImages();
} catch (err: any) {
console.error(err);
showMessage({ msg: err, isError: true });
}
}
inputModel.addEventListener('focus', populateDatalistWithGptVersions);
export async function checkModel() {
const model = inputModel.value?.trim();
const apiKey = apiKeySelector.value?.trim();
const baseURL = baseURLSelector.value?.trim();
try {
showMessage({ msg: 'Checking GPT version...', isInfinite: true, isError: false });
const client = new OpenAI({ apiKey, baseURL, dangerouslyAllowBrowser: true });
await client.chat.completions.create({
model,
messages: [{ role: 'user', content: 'reply just pong' }]
});
showMessage({ msg: 'The model is valid!' });
} catch (err: any) {
showMessage({ msg: err, isError: true });
}
}
const checkModelBtn: HTMLElement = document.querySelector('#check-model')!;
checkModelBtn.addEventListener('click', checkModel);
@@ -1,41 +1,36 @@
'use strict';
import { globalData, inputsCheckbox, modes } from './data';
import { checkCanIncludeImages } from './gpt-version';
import { handleModeChange } from './mode-handler';
import './version';
import './settings';
const saveBtn = document.querySelector('.save');
import { showMessage } from './utils';
const saveBtn = document.querySelector('.save')!;
// inputs id
const inputsText = ['apiKey', 'code', 'model'];
const inputsCheckbox = [
'logs',
'title',
'cursor',
'typing',
'mouseover',
'infinite',
'timeout',
'history',
'includeImages'
];
const inputsText = ['apiKey', 'code', 'model', 'baseURL', 'maxTokens'];
// Save the configuration
saveBtn.addEventListener('click', function () {
const [apiKey, code, model] = inputsText.map(selector =>
document.querySelector('#' + selector).value.trim()
const [apiKey, code, model, baseURL, maxTokens] = inputsText.map(selector =>
(document.querySelector('#' + selector) as HTMLInputElement).value.trim()
);
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';
const element: HTMLInputElement = document.querySelector('#' + selector)!;
return element.checked && element.parentElement!.style.display !== 'none';
});
if (!apiKey || !model) {
showMessage({ msg: 'Please complete all the form', error: true });
showMessage({ msg: 'Please complete all the form', isError: true });
return;
}
if (code.length > 0 && code.length < 2) {
showMessage({
msg: 'The code should at least contain 2 characters',
error: true
isError: true
});
return;
}
@@ -45,6 +40,8 @@ saveBtn.addEventListener('click', function () {
apiKey,
code,
model,
baseURL,
maxTokens: maxTokens ? parseInt(maxTokens) : undefined,
logs,
title,
cursor,
@@ -54,7 +51,7 @@ saveBtn.addEventListener('click', function () {
timeout,
history,
includeImages,
mode: actualMode
mode: globalData.actualMode
}
});
@@ -67,7 +64,7 @@ chrome.storage.sync.get(['moodleGPT']).then(function (storage) {
if (config) {
if (config.mode) {
actualMode = config.mode;
globalData.actualMode = config.mode;
for (const mode of modes) {
if (mode.value === config.mode) {
mode.classList.remove('not-selected');
@@ -78,9 +75,13 @@ chrome.storage.sync.get(['moodleGPT']).then(function (storage) {
}
inputsText.forEach(key =>
config[key] ? (document.querySelector('#' + key).value = config[key]) : null
config[key]
? ((document.querySelector('#' + key) as HTMLInputElement).value = config[key])
: null
);
inputsCheckbox.forEach(
key => ((document.querySelector('#' + key) as HTMLInputElement).checked = config[key] || '')
);
inputsCheckbox.forEach(key => (document.querySelector('#' + key).checked = config[key] || ''));
}
handleModeChange();
@@ -1,15 +1,10 @@
'use strict';
const mode = document.querySelector('#mode');
const modes = mode.querySelectorAll('button');
let actualMode = 'autocomplete';
import { globalData, inputsCheckbox, modes } from './data';
// input to don't take in consideration
const toExcludes = ['includeImages'];
// inputs id that need to be disabled for a specific mode
const disabledForThisMode = {
const disabledForThisMode: Record<string, string[]> = {
autocomplete: [],
clipboard: ['typing', 'mouseover'],
'question-to-answer': ['typing', 'infinite', 'mouseover']
@@ -18,16 +13,16 @@ const disabledForThisMode = {
/**
* Handle when a mode change to show specific input or to hide them
*/
function handleModeChange() {
const needDisable = disabledForThisMode[actualMode];
export function handleModeChange() {
const needDisable = disabledForThisMode[globalData.actualMode];
const dontNeedDisable = inputsCheckbox.filter(
input => !needDisable.includes(input) && !toExcludes.includes(input)
);
for (const id of needDisable) {
document.querySelector('#' + id).parentElement.style.display = 'none';
document.querySelector('#' + id)!.parentElement!.style.display = 'none';
}
for (const id of dontNeedDisable) {
document.querySelector('#' + id).parentElement.style.display = null;
document.querySelector('#' + id)!.parentElement!.style.display = '';
}
}
@@ -35,7 +30,7 @@ function handleModeChange() {
for (const button of modes) {
button.addEventListener('click', function () {
const value = button.value;
actualMode = value;
globalData.actualMode = value;
for (const mode of modes) {
if (mode.value !== value) {
mode.classList.add('not-selected');
+21
View File
@@ -0,0 +1,21 @@
const settings: HTMLElement = document.querySelector('#settings')!;
const advencedSettings: HTMLElement = document.querySelector('#advanced-settings')!;
const switchSettings: HTMLLinkElement = document.querySelector('#switch-settings')!;
export function switchSettingsMode() {
const isAdvancedSettings = advencedSettings.style.display === 'flex';
if (isAdvancedSettings) {
settings.style.display = 'flex';
advencedSettings.style.display = 'none';
switchSettings.textContent = 'Advanced settings';
} else {
settings.style.display = 'none';
advencedSettings.style.display = 'flex';
switchSettings.textContent = 'Go back to settings';
}
}
switchSettings.addEventListener('click', function (event) {
event.preventDefault();
switchSettingsMode();
});
+31
View File
@@ -0,0 +1,31 @@
/**
* Show message into the popup
*/
export function showMessage({
msg,
isError,
isInfinite
}: {
msg: string;
isError?: boolean;
isInfinite?: boolean;
}) {
const message: HTMLElement = document.querySelector('#message')!;
message.style.color = isError ? 'red' : 'limegreen';
message.textContent = msg;
message.style.display = 'block';
if (!isInfinite) setTimeout(() => (message.style.display = 'none'), 5000);
}
/**
* Check if the current model support images integrations
* @param {string} version
* @returns
*/
export function isCurrentVersionSupportingImages(version: string) {
const versionNumber = version.match(/gpt-(\d+)/);
if (!versionNumber?.[1]) {
return false;
}
return Number(versionNumber[1]) >= 4;
}
@@ -1,13 +1,11 @@
'use strict';
const CURRENT_VERSION = '1.1.1';
const versionDisplay = document.querySelector('#version');
const CURRENT_VERSION = '1.1.4';
const versionDisplay = document.querySelector('#version')!;
/**
* Get the last version from the github
* @returns
*/
async function getLastVersion() {
export async function getLastVersion(): Promise<string> {
const req = await fetch(
'https://raw.githubusercontent.com/yoannchb-pro/MoodleGPT/main/package.json'
);
@@ -21,7 +19,7 @@ async function getLastVersion() {
* @param {boolean} isCurrent
* @returns
*/
function setVersion(version, isCurrent = true) {
export function setVersion(version: string, isCurrent = true) {
if (isCurrent) {
versionDisplay.textContent = 'v' + version;
return;
@@ -39,7 +37,7 @@ function setVersion(version, isCurrent = true) {
/**
* Check if the extension neeed an update or not
*/
async function notifyUpdate() {
export async function notifyUpdate() {
const lastVersion = await getLastVersion().catch(err => {
console.error(err);
return CURRENT_VERSION;
+2 -8
View File
@@ -2,7 +2,7 @@
"compilerOptions": {
"strict": true,
"baseUrl": "src",
"module": "CommonJS",
"module": "ESNext",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"target": "ES6",
@@ -13,13 +13,7 @@
"resolveJsonModule": true,
"types": ["node", "chrome"],
"typeRoots": ["node_modules/@types"],
"strictBindCallApply": true,
"paths": {
"@typing/*": ["types/*"],
"@utils/*": ["utils/*"],
"@core/*": ["core/*"],
"@questions/*": ["core/question/*"]
}
"strictBindCallApply": true
},
"include": ["src/**/*"]
}