Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 921e2bba4e | |||
| 3591aceba1 | |||
| 17aa8b49e0 | |||
| a2849da20c | |||
| 54c9373e33 | |||
| e793cbd0b2 | |||
| fffc0d55d6 | |||
| e561227b78 | |||
| 677e870635 | |||
| 6e69830a2e | |||
| f01785256c | |||
| f2e1ec8ed6 | |||
| 98f22e9056 | |||
| 79a75fee89 | |||
| 476559188f | |||
| 16a82fe3d8 | |||
| ea57a315b7 | |||
| 8303dffe99 | |||
| ac2ca56a54 | |||
| 3dcc6dc6eb | |||
| f5679dd825 | |||
| dcfe8f3320 | |||
| ea5cd3763d | |||
| 71f43590db | |||
| eb58ac44ca | |||
| b60b3106a0 | |||
| 292d00e664 |
@@ -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: {}
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
buy_me_a_coffee: yoannchbpro
|
||||
@@ -1,3 +1,11 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: Report a bug or issue with the software.
|
||||
title: '[BUG]'
|
||||
labels: 'bug'
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
## Bug Report
|
||||
|
||||
**Description of the Bug**
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: Suggest a new feature or enhancement for the project.
|
||||
title: '[FEATURE]'
|
||||
labels: 'enhancement'
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
## Feature Request
|
||||
|
||||
**Description of the Feature**
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
---
|
||||
name: Help Request
|
||||
about: Use this template to request help or support.
|
||||
title: '[HELP]'
|
||||
labels: help
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
## Help Request
|
||||
|
||||
**Issue Summary**
|
||||
|
||||
@@ -1,5 +1,31 @@
|
||||
# CHANGELOG
|
||||
|
||||
## v1.1.5
|
||||
|
||||
- Support for gpt-5
|
||||
|
||||
## 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
|
||||
- Support for Atto editor
|
||||
|
||||
## v1.1.0
|
||||
|
||||
- Bugs correction
|
||||
|
||||
@@ -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.0
|
||||
# MoodleGPT 1.1.5
|
||||
|
||||
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.0](#moodlegpt-110)
|
||||
- [MoodleGPT 1.1.5](#moodlegpt-115)
|
||||
- [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)
|
||||
@@ -32,6 +34,7 @@ Find the extension on the Chrome Webstore right [here](https://chrome.google.com
|
||||
- [True or false](#true-or-false)
|
||||
- [Number](#number)
|
||||
- [Text](#text)
|
||||
- [Atto](#atto)
|
||||
- [What about if the question can't be autocompleted ?](#what-about-if-the-question-cant-be-autocompleted-)
|
||||
- [Test](#test)
|
||||
- [Beta version with advanced features](#beta-version-with-advanced-features)
|
||||
@@ -60,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">
|
||||
@@ -71,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">
|
||||
@@ -145,6 +159,10 @@ Person 2 | Yann | 19/01/2000 | no
|
||||
|
||||

|
||||
|
||||
### Atto
|
||||
|
||||

|
||||
|
||||
## What about if the question can't be autocompleted ?
|
||||
|
||||
To know if the answer has been copied to the clipboard, you can look at the title of the page which will become <b>"Copied to clipboard"</b> for 3 seconds if `Title indication` is on.
|
||||
@@ -153,7 +171,7 @@ To know if the answer has been copied to the clipboard, you can look at the titl
|
||||
|
||||
## Test
|
||||
|
||||
- <b>Solution 1</b>: Go on [this moodle test page](https://school.moodledemo.net/login/index.php) (username: `student`, password: `moodle`) and choose any quiz.
|
||||
- <b>Solution 1</b>: Go on this [moodle demo page](https://moodle.org/demo).
|
||||
- <b>Solution 2</b>: Run the `index.html` file located in the `test/fake-moodle` folder.
|
||||
|
||||
## Beta version with advanced features
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
@@ -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,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "MoodleGPT",
|
||||
"version": "1.1.0",
|
||||
"version": "1.1.5",
|
||||
"description": "Hidden chat-gpt for your moodle quiz",
|
||||
"permissions": ["storage"],
|
||||
"action": {
|
||||
|
||||
+38
-20
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
@@ -1,88 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const saveBtn = document.querySelector('.save');
|
||||
|
||||
// inputs id
|
||||
const inputsText = ['apiKey', 'code', 'model'];
|
||||
const inputsCheckbox = [
|
||||
'logs',
|
||||
'title',
|
||||
'cursor',
|
||||
'typing',
|
||||
'mouseover',
|
||||
'infinite',
|
||||
'timeout',
|
||||
'history',
|
||||
'includeImages'
|
||||
];
|
||||
|
||||
// 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, 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 });
|
||||
return;
|
||||
}
|
||||
|
||||
if (code.length > 0 && code.length < 3) {
|
||||
showMessage({
|
||||
msg: 'The code should at least contain 3 characters',
|
||||
error: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
chrome.storage.sync.set({
|
||||
moodleGPT: {
|
||||
apiKey,
|
||||
code,
|
||||
model,
|
||||
logs,
|
||||
title,
|
||||
cursor,
|
||||
typing,
|
||||
mouseover,
|
||||
infinite,
|
||||
timeout,
|
||||
history,
|
||||
includeImages,
|
||||
mode: actualMode
|
||||
}
|
||||
});
|
||||
|
||||
showMessage({ msg: 'Configuration saved' });
|
||||
});
|
||||
|
||||
// we load back the configuration
|
||||
chrome.storage.sync.get(['moodleGPT']).then(function (storage) {
|
||||
const config = storage.moodleGPT;
|
||||
|
||||
if (config) {
|
||||
if (config.mode) {
|
||||
actualMode = config.mode;
|
||||
for (const mode of modes) {
|
||||
if (mode.value === config.mode) {
|
||||
mode.classList.remove('not-selected');
|
||||
} else {
|
||||
mode.classList.add('not-selected');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inputsText.forEach(key =>
|
||||
config[key] ? (document.querySelector('#' + key).value = config[key]) : null
|
||||
);
|
||||
inputsCheckbox.forEach(key => (document.querySelector('#' + key).checked = config[key] || ''));
|
||||
}
|
||||
|
||||
handleModeChange();
|
||||
checkCanIncludeImages();
|
||||
});
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+1382
-1925
File diff suppressed because it is too large
Load Diff
+18
-10
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"name": "moodlegpt",
|
||||
"version": "1.1.0",
|
||||
"version": "1.1.5",
|
||||
"description": "This extension allows you to hide CHAT-GPT in a Moodle quiz.",
|
||||
"scripts": {
|
||||
"build": "npm run prettier && npm run lint && rollup -c",
|
||||
"build": "npm run prettier && npm run lint && npm run fastBuild",
|
||||
"fastBuild": "rollup -c",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"prettier": "prettier --write ."
|
||||
},
|
||||
@@ -25,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.23.2",
|
||||
"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
@@ -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 = {
|
||||
@@ -58,7 +58,7 @@ function setUpMoodleGpt(config: Config) {
|
||||
const inputTypeQuery = ['checkbox', 'radio', 'text', 'number']
|
||||
.map(e => `input[type="${e}"]`)
|
||||
.join(',');
|
||||
const inputQuery = inputTypeQuery + ', textarea, select, [contenteditable]';
|
||||
const inputQuery = inputTypeQuery + ', textarea, select, [contenteditable], .qtype_essay_editor';
|
||||
const forms = document.querySelectorAll('.formulation');
|
||||
|
||||
// For each form we inject a function on the queqtion
|
||||
@@ -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
|
||||
@@ -16,6 +16,12 @@ function createAndNormalizeQuestion(questionContainer: HTMLElement) {
|
||||
for (const useless of accesshideElements) {
|
||||
question = question.replace(useless.innerText, '');
|
||||
}
|
||||
const attoText = questionContainer.querySelector('.qtype_essay_editor');
|
||||
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');
|
||||
+14
-14
@@ -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,25 @@ 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_completion_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,12 +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 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;
|
||||
@@ -26,6 +27,7 @@ function autoCompleteMode(props: Props) {
|
||||
if (!props.config.infinite) props.removeListener();
|
||||
|
||||
const handlers = [
|
||||
handleAtto,
|
||||
handleContentEditable,
|
||||
handleTextbox,
|
||||
handleNumber,
|
||||
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
import type GPTAnswer from '@typing/gpt-answer';
|
||||
import type GPTAnswer from '../../types/gpt-answer';
|
||||
|
||||
type Props = {
|
||||
questionElement: HTMLElement;
|
||||
@@ -0,0 +1,67 @@
|
||||
import type Config from '../../types/config';
|
||||
import type GPTAnswer from '../../types/gpt-answer';
|
||||
|
||||
/**
|
||||
* Hanlde atto editor
|
||||
* See: https://docs.moodle.org/404/en/Atto_editor#Atto_accessibility
|
||||
* @param config
|
||||
* @param inputList
|
||||
* @param gptAnswer
|
||||
* @returns
|
||||
*/
|
||||
function handleAtto(
|
||||
config: Config,
|
||||
inputList: NodeListOf<HTMLElement>,
|
||||
gptAnswer: GPTAnswer
|
||||
): boolean {
|
||||
const input = inputList[0];
|
||||
|
||||
if (!input.classList.contains('qtype_essay_editor')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const iframe = input.querySelector('iframe');
|
||||
if (!iframe || !iframe.contentDocument || !iframe.contentDocument.body || !iframe.contentWindow) {
|
||||
return false;
|
||||
}
|
||||
const iframeBody = iframe.contentDocument.body;
|
||||
|
||||
const textContainer = iframeBody.querySelector('p');
|
||||
if (!textContainer) return false;
|
||||
|
||||
if (config.typing) {
|
||||
let index = 0;
|
||||
const eventHandler = function (event: KeyboardEvent) {
|
||||
event.preventDefault();
|
||||
|
||||
if (event.key === 'Backspace' || index >= gptAnswer.response.length) {
|
||||
iframe.contentWindow!.removeEventListener('keydown', eventHandler);
|
||||
return;
|
||||
}
|
||||
|
||||
// Append text one character at a time
|
||||
const textNode = document.createTextNode(gptAnswer.response.charAt(index++));
|
||||
textContainer.appendChild(textNode);
|
||||
|
||||
// Move the cursor after the last character
|
||||
const range = iframe.contentDocument!.createRange();
|
||||
range.selectNodeContents(textContainer);
|
||||
range.collapse(false); // Collapse the range to the end point
|
||||
const selection = iframe.contentWindow!.getSelection();
|
||||
if (selection) {
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
|
||||
iframe.contentWindow!.focus(); // Focus the iframe window to see cursor
|
||||
};
|
||||
|
||||
iframe.contentWindow.addEventListener('keydown', eventHandler);
|
||||
} else {
|
||||
textContainer.textContent += gptAnswer.response;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export default handleAtto;
|
||||
@@ -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
|
||||
@@ -26,11 +26,13 @@ function handleCheckbox(
|
||||
|
||||
const possibleAnswers = Array.from(inputList)
|
||||
.map(inp => ({
|
||||
element: inp,
|
||||
element: inp as HTMLInputElement,
|
||||
value: normalizeText(inp?.parentElement?.textContent ?? '')
|
||||
}))
|
||||
.filter(obj => obj.value !== '');
|
||||
|
||||
// Find the best answers elements
|
||||
const correctElements: Set<HTMLInputElement> = new Set();
|
||||
for (const correct of corrects) {
|
||||
const bestAnswer = pickBestReponse(correct, possibleAnswers);
|
||||
|
||||
@@ -38,13 +40,23 @@ function handleCheckbox(
|
||||
Logs.bestAnswer(bestAnswer.value, bestAnswer.similarity);
|
||||
}
|
||||
|
||||
const correctInput = bestAnswer.element as HTMLInputElement;
|
||||
correctElements.add(bestAnswer.element as HTMLInputElement);
|
||||
}
|
||||
|
||||
// Check if it should be checked or not
|
||||
for (const element of possibleAnswers.map(e => e.element)) {
|
||||
const needAction =
|
||||
(element.checked && !correctElements.has(element)) ||
|
||||
(!element.checked && correctElements.has(element));
|
||||
|
||||
const action = () => needAction && element.click();
|
||||
|
||||
if (config.mouseover) {
|
||||
correctInput.addEventListener('mouseover', () => correctInput.click(), {
|
||||
element.addEventListener('mouseover', action, {
|
||||
once: true
|
||||
});
|
||||
} else {
|
||||
correctInput.click();
|
||||
action();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
+19
-7
@@ -1,5 +1,10 @@
|
||||
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');
|
||||
return typeof contenteditable === 'string' && contenteditable !== 'false';
|
||||
}
|
||||
|
||||
/**
|
||||
* Hanlde contenteditable elements
|
||||
@@ -17,17 +22,22 @@ function handleContentEditable(
|
||||
|
||||
if (
|
||||
inputList.length !== 1 || // for now we don't handle many input for editable textcontent
|
||||
input.getAttribute('contenteditable') !== 'true'
|
||||
!isContentEditable(input)
|
||||
) {
|
||||
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;
|
||||
|
||||
const eventHandler = function (event: KeyboardEvent) {
|
||||
event.preventDefault();
|
||||
|
||||
if (event.key === 'Backspace' || index >= gptAnswer.response.length) {
|
||||
input.removeEventListener('keydown', eventHandler);
|
||||
return;
|
||||
}
|
||||
|
||||
input.textContent = gptAnswer.response.slice(0, ++index);
|
||||
|
||||
// Put the cursor at the end of the typed text
|
||||
@@ -40,7 +50,9 @@ function handleContentEditable(
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
input.addEventListener('keydown', eventHandler);
|
||||
} else {
|
||||
input.textContent = gptAnswer.response;
|
||||
}
|
||||
@@ -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
|
||||
@@ -28,13 +28,20 @@ function handleNumber(
|
||||
|
||||
if (config.typing) {
|
||||
let index = 0;
|
||||
input.addEventListener('keydown', function (event: Event) {
|
||||
|
||||
const eventHanlder = function (event: Event) {
|
||||
event.preventDefault();
|
||||
if ((<KeyboardEvent>event).key === 'Backspace') index = number.length + 1;
|
||||
if (index > number.length) return;
|
||||
if ((<KeyboardEvent>event).key === 'Backspace' || index >= number.length) {
|
||||
input.removeEventListener('keydown', eventHanlder);
|
||||
return;
|
||||
}
|
||||
|
||||
if (number.slice(index, index + 1) === '.') ++index;
|
||||
|
||||
input.value = number.slice(0, ++index);
|
||||
});
|
||||
};
|
||||
|
||||
input.addEventListener('keydown', eventHanlder);
|
||||
} else {
|
||||
input.value = number;
|
||||
}
|
||||
@@ -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
|
||||
@@ -24,14 +24,19 @@ function handleTextbox(
|
||||
|
||||
if (config.typing) {
|
||||
let index = 0;
|
||||
input.addEventListener('keydown', function (event: Event) {
|
||||
|
||||
const eventHandler = function (event: Event) {
|
||||
event.preventDefault();
|
||||
if ((<KeyboardEvent>event).key === 'Backspace') {
|
||||
index = gptAnswer.response.length + 1;
|
||||
|
||||
if ((<KeyboardEvent>event).key === 'Backspace' || index >= gptAnswer.response.length) {
|
||||
input.removeEventListener('keydown', eventHandler);
|
||||
return;
|
||||
}
|
||||
if (index > gptAnswer.response.length) return;
|
||||
|
||||
input.value = gptAnswer.response.slice(0, ++index);
|
||||
});
|
||||
};
|
||||
|
||||
input.addEventListener('keydown', eventHandler);
|
||||
} else {
|
||||
input.value = gptAnswer.response;
|
||||
}
|
||||
@@ -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,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;
|
||||
@@ -0,0 +1,21 @@
|
||||
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.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 {
|
||||
@@ -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')!;
|
||||
@@ -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);
|
||||
@@ -0,0 +1,89 @@
|
||||
import { globalData, inputsCheckbox, modes } from './data';
|
||||
import { checkCanIncludeImages } from './gpt-version';
|
||||
import { handleModeChange } from './mode-handler';
|
||||
import './version';
|
||||
import './settings';
|
||||
|
||||
import { showMessage } from './utils';
|
||||
|
||||
const saveBtn = document.querySelector('.save')!;
|
||||
|
||||
// inputs id
|
||||
const inputsText = ['apiKey', 'code', 'model', 'baseURL', 'maxTokens'];
|
||||
|
||||
// Save the configuration
|
||||
saveBtn.addEventListener('click', function () {
|
||||
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: HTMLInputElement = document.querySelector('#' + selector)!;
|
||||
return element.checked && element.parentElement!.style.display !== 'none';
|
||||
});
|
||||
|
||||
if (!apiKey || !model) {
|
||||
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',
|
||||
isError: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
chrome.storage.sync.set({
|
||||
moodleGPT: {
|
||||
apiKey,
|
||||
code,
|
||||
model,
|
||||
baseURL,
|
||||
maxTokens: maxTokens ? parseInt(maxTokens) : undefined,
|
||||
logs,
|
||||
title,
|
||||
cursor,
|
||||
typing,
|
||||
mouseover,
|
||||
infinite,
|
||||
timeout,
|
||||
history,
|
||||
includeImages,
|
||||
mode: globalData.actualMode
|
||||
}
|
||||
});
|
||||
|
||||
showMessage({ msg: 'Configuration saved' });
|
||||
});
|
||||
|
||||
// we load back the configuration
|
||||
chrome.storage.sync.get(['moodleGPT']).then(function (storage) {
|
||||
const config = storage.moodleGPT;
|
||||
|
||||
if (config) {
|
||||
if (config.mode) {
|
||||
globalData.actualMode = config.mode;
|
||||
for (const mode of modes) {
|
||||
if (mode.value === config.mode) {
|
||||
mode.classList.remove('not-selected');
|
||||
} else {
|
||||
mode.classList.add('not-selected');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inputsText.forEach(key =>
|
||||
config[key]
|
||||
? ((document.querySelector('#' + key) as HTMLInputElement).value = config[key])
|
||||
: null
|
||||
);
|
||||
inputsCheckbox.forEach(
|
||||
key => ((document.querySelector('#' + key) as HTMLInputElement).checked = config[key] || '')
|
||||
);
|
||||
}
|
||||
|
||||
handleModeChange();
|
||||
checkCanIncludeImages();
|
||||
});
|
||||
@@ -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');
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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.0';
|
||||
const versionDisplay = document.querySelector('#version');
|
||||
const CURRENT_VERSION = '1.1.5';
|
||||
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;
|
||||
@@ -104,6 +104,16 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Textbox -->
|
||||
<section class="formulation">
|
||||
<div class="qtext">
|
||||
<p>What is the name of the USA president ?</p>
|
||||
</div>
|
||||
<div>
|
||||
<input type="text" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Select -->
|
||||
<section class="formulation">
|
||||
<div class="qtext">
|
||||
|
||||
+3
-11
@@ -2,24 +2,16 @@
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"baseUrl": "src",
|
||||
"module": "CommonJS",
|
||||
"module": "esnext",
|
||||
"target": "ES6",
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "ES6",
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": true,
|
||||
"outDir": "extension",
|
||||
"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/**/*"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user