Migrate popup UI to Preact and Tailwind CSS

This commit is contained in:
2026-05-05 19:55:09 +02:00
parent d36949b42f
commit 8b45a5d5ce
27 changed files with 2547 additions and 841 deletions
+9 -152
View File
@@ -4,6 +4,14 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SparkAssist</title> <title>SparkAssist</title>
<!-- Modern Typography -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" /> <link rel="stylesheet" href="style.css" />
<script src="./popup.js" defer></script> <script src="./popup.js" defer></script>
<link rel="icon" type="image/png" href="../icon.png" /> <link rel="icon" type="image/png" href="../icon.png" />
@@ -12,158 +20,7 @@
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
/> />
</head> </head>
<body> <body>
<main> <div id="root"></div>
<div class="header">
<img src="../icon.png" alt="SparkAssist Logo" />
<div class="header-text">
<h1>SparkAssist</h1>
<p id="version"></p>
</div>
</div>
<!-- MAIN SETTINGS -->
<div class="panel settings" id="settings">
<div class="form-group">
<label for="apiKey">Api Key<span class="required">*</span></label>
<input id="apiKey" type="text" placeholder="sk-..." />
</div>
<div class="form-group">
<label for="model">GPT Model<span class="required">*</span></label>
<div class="input-with-icon">
<input type="text" id="model" list="models" placeholder="gpt-4o" />
<datalist id="models"></datalist>
<i id="check-model" title="Test" class="fa-solid fa-play"></i>
</div>
</div>
</div>
<!-- ADVANCED SETTINGS -->
<div class="panel settings" id="advanced-settings" style="display: none">
<div class="form-group">
<label for="code">Activation Code</label>
<input id="code" type="text" placeholder="Secret key..." />
</div>
<div class="form-group">
<label for="baseURL">Base URL</label>
<input id="baseURL" type="text" placeholder="https://api.openai.com/v1" />
</div>
<div class="form-group">
<label for="projectId">Project ID</label>
<input id="projectId" type="text" placeholder="proj_..." />
</div>
<div class="form-group">
<label for="maxTokens">Max Tokens</label>
<input id="maxTokens" type="number" />
</div>
<div class="form-group">
<label for="timeoutValue">Timeout (s)</label>
<input id="timeoutValue" type="number" placeholder="20" />
</div>
</div>
<a id="switch-settings" href="#">Show Advanced Settings</a>
<div class="section-title">
<i class="fa-solid fa-bolt"></i>
<span>Operating Mode</span>
</div>
<ul id="mode">
<li><button value="autocomplete">autocomplete</button></li>
<li><button value="clipboard" class="not-selected">clipboard</button></li>
</ul>
<div class="section-title">
<i class="fa-solid fa-sliders"></i>
<span>Options</span>
</div>
<div class="panel options-grid">
<div class="toggle-row">
<label for="logs">Console logs</label>
<label class="toggle-switch">
<input id="logs" type="checkbox" />
<span class="slider"></span>
</label>
</div>
<div class="toggle-row">
<label for="title">Title hint</label>
<label class="toggle-switch">
<input id="title" type="checkbox" checked />
<span class="slider"></span>
</label>
</div>
<div class="toggle-row">
<label for="cursor">Cursor hint</label>
<label class="toggle-switch">
<input id="cursor" type="checkbox" checked />
<span class="slider"></span>
</label>
</div>
<div class="toggle-row">
<label for="timeout">Timeout</label>
<label class="toggle-switch">
<input id="timeout" type="checkbox" checked />
<span class="slider"></span>
</label>
</div>
<div class="toggle-row">
<label for="typing">Typing effect</label>
<label class="toggle-switch">
<input id="typing" type="checkbox" />
<span class="slider"></span>
</label>
</div>
<div class="toggle-row">
<label for="mouseover">Hover effect</label>
<label class="toggle-switch">
<input id="mouseover" type="checkbox" />
<span class="slider"></span>
</label>
</div>
<div class="toggle-row">
<label for="infinite">Infinite try</label>
<label class="toggle-switch">
<input id="infinite" type="checkbox" />
<span class="slider"></span>
</label>
</div>
<div class="toggle-row">
<label for="history">History</label>
<label class="toggle-switch">
<input id="history" type="checkbox" />
<span class="slider"></span>
</label>
</div>
<div class="toggle-row" id="includeImages-line" style="display: none">
<label for="includeImages">Images (GPT-4)</label>
<label class="toggle-switch">
<input id="includeImages" type="checkbox" />
<span class="slider"></span>
</label>
</div>
</div>
<p id="message">{Message}</p>
<button class="save">Save Preferences</button>
<div class="footer">
<a
class="donate"
href="https://www.buymeacoffee.com/yoannchbpro"
target="_blank"
rel="noopener noreferrer"
>Support</a
>
<a
href="https://github.com/yoannchb-pro/MoodleGPT"
target="_blank"
rel="noopener noreferrer"
>Docs</a
>
</div>
</main>
</body> </body>
</html> </html>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1029 -250
View File
File diff suppressed because it is too large Load Diff
+743 -28
View File
@@ -8,26 +8,46 @@
"name": "sparkassist", "name": "sparkassist",
"version": "2.0.0", "version": "2.0.0",
"license": "MIT", "license": "MIT",
"dependencies": {
"preact": "^10.29.1"
},
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.32.0", "@eslint/js": "^9.32.0",
"@rollup/plugin-commonjs": "^28.0.6", "@rollup/plugin-commonjs": "^28.0.6",
"@rollup/plugin-node-resolve": "^16.0.0", "@rollup/plugin-node-resolve": "^16.0.0",
"@rollup/plugin-replace": "^6.0.3",
"@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^12.1.4", "@rollup/plugin-typescript": "^12.1.4",
"@types/chrome": "^0.1.1", "@types/chrome": "^0.1.1",
"@types/node": "^24.1.0", "@types/node": "^24.1.0",
"@typescript-eslint/eslint-plugin": "^8.26.1", "@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.38.0", "@typescript-eslint/parser": "^8.38.0",
"autoprefixer": "^10.5.0",
"eslint": "^9.32.0", "eslint": "^9.32.0",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"openai": "^5.23.2", "openai": "^5.23.2",
"postcss": "^8.5.14",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"rollup": "^4.46.2", "rollup": "^4.46.2",
"rollup-plugin-ts": "^3.2.0", "rollup-plugin-ts": "^3.2.0",
"tailwindcss": "^3.4.19",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"typescript-eslint": "^8.38.0" "typescript-eslint": "^8.38.0"
} }
}, },
"node_modules/@alloc/quick-lru": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@eslint-community/eslint-utils": { "node_modules/@eslint-community/eslint-utils": {
"version": "4.7.0", "version": "4.7.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
@@ -414,6 +434,28 @@
} }
} }
}, },
"node_modules/@rollup/plugin-replace": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-6.0.3.tgz",
"integrity": "sha512-J4RZarRvQAm5IF0/LwUUg+obsm+xZhYnbMXmXROyoSE1ATJe3oXSb9L5MMppdxP2ylNSjv6zFBwKYjcKMucVfA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rollup/pluginutils": "^5.0.1",
"magic-string": "^0.30.3"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/plugin-terser": { "node_modules/@rollup/plugin-terser": {
"version": "0.4.4", "version": "0.4.4",
"resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz",
@@ -1163,6 +1205,47 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1" "url": "https://github.com/chalk/ansi-styles?sponsor=1"
} }
}, },
"node_modules/any-promise": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
"dev": true,
"license": "MIT"
},
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
"license": "ISC",
"dependencies": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/anymatch/node_modules/picomatch": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/arg": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
"dev": true,
"license": "MIT"
},
"node_modules/argparse": { "node_modules/argparse": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -1170,12 +1253,75 @@
"dev": true, "dev": true,
"license": "Python-2.0" "license": "Python-2.0"
}, },
"node_modules/autoprefixer": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz",
"integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/autoprefixer"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"browserslist": "^4.28.2",
"caniuse-lite": "^1.0.30001787",
"fraction.js": "^5.3.4",
"picocolors": "^1.1.1",
"postcss-value-parser": "^4.2.0"
},
"bin": {
"autoprefixer": "bin/autoprefixer"
},
"engines": {
"node": "^10 || ^12 || >=14"
},
"peerDependencies": {
"postcss": "^8.1.0"
}
},
"node_modules/balanced-match": { "node_modules/balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true "dev": true
}, },
"node_modules/baseline-browser-mapping": {
"version": "2.10.27",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.27.tgz",
"integrity": "sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.cjs"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
@@ -1199,9 +1345,9 @@
} }
}, },
"node_modules/browserslist": { "node_modules/browserslist": {
"version": "4.24.4", "version": "4.28.2",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
"integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -1217,11 +1363,13 @@
"url": "https://github.com/sponsors/ai" "url": "https://github.com/sponsors/ai"
} }
], ],
"license": "MIT",
"dependencies": { "dependencies": {
"caniuse-lite": "^1.0.30001688", "baseline-browser-mapping": "^2.10.12",
"electron-to-chromium": "^1.5.73", "caniuse-lite": "^1.0.30001782",
"node-releases": "^2.0.19", "electron-to-chromium": "^1.5.328",
"update-browserslist-db": "^1.1.1" "node-releases": "^2.0.36",
"update-browserslist-db": "^1.2.3"
}, },
"bin": { "bin": {
"browserslist": "cli.js" "browserslist": "cli.js"
@@ -1274,10 +1422,20 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/camelcase-css": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
}
},
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001706", "version": "1.0.30001791",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001706.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz",
"integrity": "sha512-3ZczoTApMAZwPKYWmwVbQMFpXBDds3/0VciVoUwPUbldlYyVLmRVuRs/PcUZtHpbLRpzzDvrvnFuREsGt6lUug==", "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -1292,7 +1450,8 @@
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ai" "url": "https://github.com/sponsors/ai"
} }
] ],
"license": "CC-BY-4.0"
}, },
"node_modules/chalk": { "node_modules/chalk": {
"version": "4.1.2", "version": "4.1.2",
@@ -1310,6 +1469,44 @@
"url": "https://github.com/chalk/chalk?sponsor=1" "url": "https://github.com/chalk/chalk?sponsor=1"
} }
}, },
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
},
"engines": {
"node": ">= 8.10.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/chokidar/node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/color-convert": { "node_modules/color-convert": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -1393,6 +1590,19 @@
"integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==", "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==",
"dev": true "dev": true
}, },
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
"dev": true,
"license": "MIT",
"bin": {
"cssesc": "bin/cssesc"
},
"engines": {
"node": ">=4"
}
},
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.0", "version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
@@ -1425,17 +1635,33 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/dlv": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
"dev": true,
"license": "MIT"
},
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.120", "version": "1.5.349",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.120.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.349.tgz",
"integrity": "sha512-oTUp3gfX1gZI+xfD2djr2rzQdHCwHzPQrrK0CD7WpTdF0nPdQ/INcRVjWgLdCT4a9W3jFObR9DAfsuyFQnI8CQ==", "integrity": "sha512-QsWVGyRuY07Aqb234QytTfwd5d9AJlfNIQ5wIOl1L+PZDzI9d9+Fn0FRale/QYlFxt/bUnB0/nLd1jFPGxGK1A==",
"dev": true "dev": true,
"license": "ISC"
}, },
"node_modules/escalade": { "node_modules/escalade": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=6" "node": ">=6"
} }
@@ -1732,10 +1958,14 @@
} }
}, },
"node_modules/fdir": { "node_modules/fdir": {
"version": "6.4.3", "version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true, "dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": { "peerDependencies": {
"picomatch": "^3 || ^4" "picomatch": "^3 || ^4"
}, },
@@ -1804,6 +2034,20 @@
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
"dev": true "dev": true
}, },
"node_modules/fraction.js": {
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
"integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "*"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -1924,6 +2168,19 @@
"node": ">=0.8.19" "node": ">=0.8.19"
} }
}, },
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"license": "MIT",
"dependencies": {
"binary-extensions": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/is-core-module": { "node_modules/is-core-module": {
"version": "2.16.1", "version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
@@ -1999,6 +2256,16 @@
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true "dev": true
}, },
"node_modules/jiti": {
"version": "1.21.7",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"dev": true,
"license": "MIT",
"bin": {
"jiti": "bin/jiti.js"
}
},
"node_modules/js-yaml": { "node_modules/js-yaml": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
@@ -2053,6 +2320,26 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/lilconfig": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
"integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/antonk52"
}
},
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"dev": true,
"license": "MIT"
},
"node_modules/locate-path": { "node_modules/locate-path": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -2138,6 +2425,37 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true "dev": true
}, },
"node_modules/mz": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"any-promise": "^1.0.0",
"object-assign": "^4.0.1",
"thenify-all": "^1.0.0"
}
},
"node_modules/nanoid": {
"version": "3.3.12",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/natural-compare": { "node_modules/natural-compare": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@@ -2145,10 +2463,41 @@
"dev": true "dev": true
}, },
"node_modules/node-releases": { "node_modules/node-releases": {
"version": "2.0.19", "version": "2.0.38",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz",
"integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==",
"dev": true "dev": true,
"license": "MIT"
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-hash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
}
}, },
"node_modules/object-path": { "node_modules/object-path": {
"version": "0.11.8", "version": "0.11.8",
@@ -2272,10 +2621,11 @@
"dev": true "dev": true
}, },
"node_modules/picomatch": { "node_modules/picomatch": {
"version": "4.0.2", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -2283,6 +2633,199 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/pify": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/pirates": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
"integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
}
},
"node_modules/postcss": {
"version": "8.5.14",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
"integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/postcss-import": {
"version": "15.1.0",
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
"dev": true,
"license": "MIT",
"dependencies": {
"postcss-value-parser": "^4.0.0",
"read-cache": "^1.0.0",
"resolve": "^1.1.7"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"postcss": "^8.0.0"
}
},
"node_modules/postcss-js": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
"integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"camelcase-css": "^2.0.1"
},
"engines": {
"node": "^12 || ^14 || >= 16"
},
"peerDependencies": {
"postcss": "^8.4.21"
}
},
"node_modules/postcss-load-config": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
"integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"lilconfig": "^3.1.1"
},
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"jiti": ">=1.21.0",
"postcss": ">=8.0.9",
"tsx": "^4.8.1",
"yaml": "^2.4.2"
},
"peerDependenciesMeta": {
"jiti": {
"optional": true
},
"postcss": {
"optional": true
},
"tsx": {
"optional": true
},
"yaml": {
"optional": true
}
}
},
"node_modules/postcss-nested": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
"integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"postcss-selector-parser": "^6.1.1"
},
"engines": {
"node": ">=12.0"
},
"peerDependencies": {
"postcss": "^8.2.14"
}
},
"node_modules/postcss-selector-parser": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
"dev": true,
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
},
"engines": {
"node": ">=4"
}
},
"node_modules/postcss-value-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true,
"license": "MIT"
},
"node_modules/preact": {
"version": "10.29.1",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.29.1.tgz",
"integrity": "sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
}
},
"node_modules/prelude-ls": { "node_modules/prelude-ls": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -2347,6 +2890,42 @@
"safe-buffer": "^5.1.0" "safe-buffer": "^5.1.0"
} }
}, },
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
"dev": true,
"license": "MIT",
"dependencies": {
"pify": "^2.3.0"
}
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
}
},
"node_modules/readdirp/node_modules/picomatch": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/resolve": { "node_modules/resolve": {
"version": "1.22.10", "version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
@@ -2590,6 +3169,16 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-support": { "node_modules/source-map-support": {
"version": "0.5.21", "version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
@@ -2613,6 +3202,39 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/sucrase": {
"version": "3.35.1",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
"integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.2",
"commander": "^4.0.0",
"lines-and-columns": "^1.1.6",
"mz": "^2.7.0",
"pirates": "^4.0.1",
"tinyglobby": "^0.2.11",
"ts-interface-checker": "^0.1.9"
},
"bin": {
"sucrase": "bin/sucrase",
"sucrase-node": "bin/sucrase-node"
},
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/sucrase/node_modules/commander": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
}
},
"node_modules/supports-color": { "node_modules/supports-color": {
"version": "7.2.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -2637,6 +3259,44 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/tailwindcss": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
"chokidar": "^3.6.0",
"didyoumean": "^1.2.2",
"dlv": "^1.1.3",
"fast-glob": "^3.3.2",
"glob-parent": "^6.0.2",
"is-glob": "^4.0.3",
"jiti": "^1.21.7",
"lilconfig": "^3.1.3",
"micromatch": "^4.0.8",
"normalize-path": "^3.0.0",
"object-hash": "^3.0.0",
"picocolors": "^1.1.1",
"postcss": "^8.4.47",
"postcss-import": "^15.1.0",
"postcss-js": "^4.0.1",
"postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
"postcss-nested": "^6.2.0",
"postcss-selector-parser": "^6.1.2",
"resolve": "^1.22.8",
"sucrase": "^3.35.0"
},
"bin": {
"tailwind": "lib/cli.js",
"tailwindcss": "lib/cli.js"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/terser": { "node_modules/terser": {
"version": "5.39.0", "version": "5.39.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz",
@@ -2655,6 +3315,46 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/thenify": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
"dev": true,
"license": "MIT",
"dependencies": {
"any-promise": "^1.0.0"
}
},
"node_modules/thenify-all": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
"integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
"dev": true,
"license": "MIT",
"dependencies": {
"thenify": ">= 3.1.0 < 4"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/tinyglobby": {
"version": "0.2.16",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.4"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/to-regex-range": { "node_modules/to-regex-range": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -2699,6 +3399,13 @@
"typescript": "^3.x || ^4.x || ^5.x" "typescript": "^3.x || ^4.x || ^5.x"
} }
}, },
"node_modules/ts-interface-checker": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/tslib": { "node_modules/tslib": {
"version": "2.8.1", "version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@@ -2789,9 +3496,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/update-browserslist-db": { "node_modules/update-browserslist-db": {
"version": "1.1.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
"integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -2807,6 +3514,7 @@
"url": "https://github.com/sponsors/ai" "url": "https://github.com/sponsors/ai"
} }
], ],
"license": "MIT",
"dependencies": { "dependencies": {
"escalade": "^3.2.0", "escalade": "^3.2.0",
"picocolors": "^1.1.1" "picocolors": "^1.1.1"
@@ -2828,6 +3536,13 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true,
"license": "MIT"
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+10 -2
View File
@@ -3,9 +3,10 @@
"version": "2.0.0", "version": "2.0.0",
"description": "An AI study assistant for your quizzes.", "description": "An AI study assistant for your quizzes.",
"scripts": { "scripts": {
"build": "npm run prettier && npm run lint && npm run fastBuild", "build": "npm run prettier && npm run lint && npm run build:css && npm run fastBuild",
"build:css": "tailwindcss -i ./src/popup/style.css -o ./extension/popup/style.css",
"fastBuild": "rollup -c", "fastBuild": "rollup -c",
"lint": "eslint . --ext .ts", "lint": "eslint . --ext .ts,.tsx",
"prettier": "prettier --write ." "prettier": "prettier --write ."
}, },
"repository": { "repository": {
@@ -29,19 +30,26 @@
"@eslint/js": "^9.32.0", "@eslint/js": "^9.32.0",
"@rollup/plugin-commonjs": "^28.0.6", "@rollup/plugin-commonjs": "^28.0.6",
"@rollup/plugin-node-resolve": "^16.0.0", "@rollup/plugin-node-resolve": "^16.0.0",
"@rollup/plugin-replace": "^6.0.3",
"@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^12.1.4", "@rollup/plugin-typescript": "^12.1.4",
"@types/chrome": "^0.1.1", "@types/chrome": "^0.1.1",
"@types/node": "^24.1.0", "@types/node": "^24.1.0",
"@typescript-eslint/eslint-plugin": "^8.26.1", "@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.38.0", "@typescript-eslint/parser": "^8.38.0",
"autoprefixer": "^10.5.0",
"eslint": "^9.32.0", "eslint": "^9.32.0",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"openai": "^5.23.2", "openai": "^5.23.2",
"postcss": "^8.5.14",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"rollup": "^4.46.2", "rollup": "^4.46.2",
"rollup-plugin-ts": "^3.2.0", "rollup-plugin-ts": "^3.2.0",
"tailwindcss": "^3.4.19",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"typescript-eslint": "^8.38.0" "typescript-eslint": "^8.38.0"
},
"dependencies": {
"preact": "^10.29.1"
} }
} }
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};
+11 -3
View File
@@ -1,7 +1,7 @@
const ts = require('@rollup/plugin-typescript'); const ts = require('@rollup/plugin-typescript');
const terser = require('@rollup/plugin-terser'); const terser = require('@rollup/plugin-terser');
const { nodeResolve } = require('@rollup/plugin-node-resolve'); const { nodeResolve } = require('@rollup/plugin-node-resolve');
const replace = require('@rollup/plugin-replace');
const config = require('./tsconfig.json'); const config = require('./tsconfig.json');
module.exports = [ module.exports = [
@@ -17,13 +17,21 @@ module.exports = [
}, },
{ {
input: './src/popup/index.ts', input: './src/popup/index.tsx',
output: { output: {
file: './extension/popup/popup.js', file: './extension/popup/popup.js',
format: 'umd', format: 'umd',
sourcemap: true sourcemap: true
}, },
onwarn() {}, onwarn() {},
plugins: [nodeResolve(), ts(config), terser()] plugins: [
replace({
'process.env.NODE_ENV': JSON.stringify('production'),
preventAssignment: true
}),
nodeResolve(),
ts(config),
terser()
]
} }
]; ];
@@ -0,0 +1,89 @@
import { MoodleGPTConfig } from '../hooks/useConfig';
interface Props {
config: MoodleGPTConfig;
onChange: (key: keyof MoodleGPTConfig, value: MoodleGPTConfig[keyof MoodleGPTConfig]) => void;
visible: boolean;
}
export function AdvancedSettingsPanel({ config, onChange, visible }: Props) {
if (!visible) return null;
return (
<div
class="bg-panel-bg backdrop-blur-md border border-panel-border rounded-2xl p-4 shadow-[0_8px_32px_0_rgba(0,0,0,0.3)] flex flex-col gap-4"
id="advanced-settings"
>
<div class="flex flex-col gap-2">
<label htmlFor="code" class="text-sm font-medium text-text-secondary">
Activation Code
</label>
<input
id="code"
type="text"
placeholder="Secret key..."
class="w-full px-3 py-2.5 bg-input-bg border border-input-border rounded-lg text-text-primary text-sm transition-all focus:outline-none focus:border-input-focus focus:ring-2 focus:ring-primary/20 box-border"
value={config.code || ''}
onInput={e => onChange('code', (e.target as HTMLInputElement).value)}
/>
</div>
<div class="flex flex-col gap-2">
<label htmlFor="baseURL" class="text-sm font-medium text-text-secondary">
Base URL
</label>
<input
id="baseURL"
type="text"
placeholder="https://api.openai.com/v1"
class="w-full px-3 py-2.5 bg-input-bg border border-input-border rounded-lg text-text-primary text-sm transition-all focus:outline-none focus:border-input-focus focus:ring-2 focus:ring-primary/20 box-border"
value={config.baseURL || ''}
onInput={e => onChange('baseURL', (e.target as HTMLInputElement).value)}
/>
</div>
<div class="flex flex-col gap-2">
<label htmlFor="projectId" class="text-sm font-medium text-text-secondary">
Project ID
</label>
<input
id="projectId"
type="text"
placeholder="proj_..."
class="w-full px-3 py-2.5 bg-input-bg border border-input-border rounded-lg text-text-primary text-sm transition-all focus:outline-none focus:border-input-focus focus:ring-2 focus:ring-primary/20 box-border"
value={config.projectId || ''}
onInput={e => onChange('projectId', (e.target as HTMLInputElement).value)}
/>
</div>
<div class="flex flex-col gap-2">
<label htmlFor="maxTokens" class="text-sm font-medium text-text-secondary">
Max Tokens
</label>
<input
id="maxTokens"
type="number"
class="w-full px-3 py-2.5 bg-input-bg border border-input-border rounded-lg text-text-primary text-sm transition-all focus:outline-none focus:border-input-focus focus:ring-2 focus:ring-primary/20 box-border"
value={config.maxTokens || ''}
onInput={e => {
const val = (e.target as HTMLInputElement).value;
onChange('maxTokens', val ? parseInt(val) : undefined);
}}
/>
</div>
<div class="flex flex-col gap-2">
<label htmlFor="timeoutValue" class="text-sm font-medium text-text-secondary">
Timeout (s)
</label>
<input
id="timeoutValue"
type="number"
placeholder="20"
class="w-full px-3 py-2.5 bg-input-bg border border-input-border rounded-lg text-text-primary text-sm transition-all focus:outline-none focus:border-input-focus focus:ring-2 focus:ring-primary/20 box-border"
value={config.timeoutValue || ''}
onInput={e => {
const val = (e.target as HTMLInputElement).value;
onChange('timeoutValue', val ? parseInt(val) : undefined);
}}
/>
</div>
</div>
);
}
+112
View File
@@ -0,0 +1,112 @@
import { useState } from 'preact/hooks';
import { Header } from './Header';
import { SettingsPanel } from './SettingsPanel';
import { AdvancedSettingsPanel } from './AdvancedSettingsPanel';
import { OptionsGrid } from './OptionsGrid';
import { OperatingMode } from './OperatingMode';
import { useConfig, MoodleGPTConfig } from '../hooks/useConfig';
export function App() {
const { config, loading, saveConfig, setConfig } = useConfig();
const [showAdvanced, setShowAdvanced] = useState(false);
const [message, setMessage] = useState<{ text: string; isError: boolean } | null>(null);
if (loading) return null;
const handleConfigChange = (
key: keyof MoodleGPTConfig,
value: MoodleGPTConfig[keyof MoodleGPTConfig]
) => {
setConfig(prev => ({ ...prev, [key]: value }));
};
const showMessage = (msg: string, isError = false) => {
setMessage({ text: msg, isError });
setTimeout(() => setMessage(null), 5000);
};
const handleSave = async () => {
if (!config.apiKey || !config.model) {
showMessage('Please complete all the form', true);
return;
}
if (config.code && config.code.length > 0 && config.code.length < 2) {
showMessage('The code should at least contain 2 characters', true);
return;
}
await saveConfig(config);
showMessage('Configuration saved');
};
return (
<main class="p-6 flex flex-col gap-5 bg-gradient-to-br from-gradient-start to-gradient-end text-text-primary min-h-screen font-sans antialiased overflow-x-hidden">
<Header />
<SettingsPanel
config={config}
onChange={handleConfigChange}
showMessage={showMessage}
visible={!showAdvanced}
/>
<AdvancedSettingsPanel config={config} onChange={handleConfigChange} visible={showAdvanced} />
<a
href="#"
class="block text-center text-sm text-text-secondary no-underline transition-colors hover:text-text-primary"
onClick={e => {
e.preventDefault();
setShowAdvanced(!showAdvanced);
}}
>
{showAdvanced ? 'Go back to settings' : 'Show Advanced Settings'}
</a>
<OperatingMode
mode={config.mode || 'autocomplete'}
onChange={m => handleConfigChange('mode', m)}
/>
<div class="flex items-center gap-2 text-sm font-semibold text-text-primary my-2">
<i class="fa-solid fa-sliders text-primary"></i>
<span>Options</span>
</div>
<OptionsGrid config={config} onChange={handleConfigChange} />
{message && (
<p
class={`text-center text-sm font-medium m-0 min-h-[18px] ${message.isError ? 'text-error' : 'text-success'}`}
>
{message.text}
</p>
)}
<button
class="w-full p-3 bg-gradient-to-br from-primary to-primary-hover text-white border-none rounded-xl text-base font-semibold cursor-pointer transition-all hover:-translate-y-0.5 hover:shadow-[0_4px_12px_rgba(99,102,241,0.4)] active:translate-y-0 mt-2"
onClick={handleSave}
>
Save Preferences
</button>
<div class="flex justify-center gap-6 mt-2">
<a
class="text-sm font-medium no-underline transition-colors text-amber-400 hover:text-amber-300"
href="https://www.buymeacoffee.com/yoannchbpro"
target="_blank"
rel="noopener noreferrer"
>
Support
</a>
<a
class="text-sm font-medium no-underline transition-colors text-text-secondary hover:text-text-primary"
href="https://github.com/yoannchb-pro/MoodleGPT"
target="_blank"
rel="noopener noreferrer"
>
Docs
</a>
</div>
</main>
);
}
+70
View File
@@ -0,0 +1,70 @@
import { useEffect, useState } from 'preact/hooks';
export function Header() {
const [version, setVersion] = useState('2.0.0');
const [hasUpdate, setHasUpdate] = useState(false);
useEffect(() => {
const checkVersion = async () => {
try {
const req = await fetch(
'https://raw.githubusercontent.com/yoannchb-pro/MoodleGPT/main/package.json'
);
const rep = await req.json();
const lastVersion = rep.version;
const lastVertionSplitted = lastVersion.split('.');
const currentVersionSplitted = version.split('.');
const minVersionLength = Math.min(
lastVertionSplitted.length,
currentVersionSplitted.length
);
for (let i = 0; i < minVersionLength; ++i) {
if (parseInt(lastVertionSplitted[i]) > parseInt(currentVersionSplitted[i])) {
setVersion(lastVersion);
setHasUpdate(true);
return;
} else if (parseInt(currentVersionSplitted[i]) > parseInt(lastVertionSplitted[i])) {
return;
}
}
} catch (err) {
console.error(err);
}
};
checkVersion();
}, []);
return (
<div class="flex items-center gap-4 mb-2">
<img
src="../icon.png"
alt="SparkAssist Logo"
class="w-12 h-12 drop-shadow-md animate-float"
/>
<div>
<h1 class="m-0 text-2xl font-bold bg-gradient-to-r from-indigo-300 to-indigo-400 bg-clip-text text-transparent">
SparkAssist
</h1>
<p class="m-0 mt-1 text-xs text-text-secondary">
{hasUpdate ? (
<>
<a
href="https://github.com/yoannchb-pro/MoodleGPT"
target="_blank"
rel="noopener noreferrer"
class="text-sky-400 no-underline font-medium hover:text-sky-300 transition-colors"
>
v{version}
</a>{' '}
is now available !
</>
) : (
`v${version}`
)}
</p>
</div>
</div>
);
}
+46
View File
@@ -0,0 +1,46 @@
interface Props {
mode: 'autocomplete' | 'clipboard';
onChange: (mode: 'autocomplete' | 'clipboard') => void;
}
export function OperatingMode({ mode, onChange }: Props) {
return (
<>
<div class="flex items-center gap-2 text-sm font-semibold text-text-primary my-2">
<i class="fa-solid fa-bolt text-primary"></i>
<span>Operating Mode</span>
</div>
<ul
id="mode"
class="list-none p-0 m-0 flex bg-input-bg rounded-xl border border-input-border overflow-hidden"
>
<li class="flex-1">
<button
value="autocomplete"
class={`w-full p-2.5 border-none font-sans text-sm font-semibold cursor-pointer transition-colors duration-300 ${
mode === 'autocomplete'
? 'bg-primary text-text-primary shadow-[0_2px_8px_rgba(99,102,241,0.4)]'
: 'bg-transparent text-text-secondary hover:bg-white/5 hover:text-text-primary'
}`}
onClick={() => onChange('autocomplete')}
>
autocomplete
</button>
</li>
<li class="flex-1">
<button
value="clipboard"
class={`w-full p-2.5 border-none font-sans text-sm font-semibold cursor-pointer transition-colors duration-300 ${
mode === 'clipboard'
? 'bg-primary text-text-primary shadow-[0_2px_8px_rgba(99,102,241,0.4)]'
: 'bg-transparent text-text-secondary hover:bg-white/5 hover:text-text-primary'
}`}
onClick={() => onChange('clipboard')}
>
clipboard
</button>
</li>
</ul>
</>
);
}
+47
View File
@@ -0,0 +1,47 @@
import { MoodleGPTConfig } from '../hooks/useConfig';
import { useModel } from '../hooks/useModel';
interface Props {
config: MoodleGPTConfig;
onChange: (key: keyof MoodleGPTConfig, value: MoodleGPTConfig[keyof MoodleGPTConfig]) => void;
}
export function OptionsGrid({ config, onChange }: Props) {
const { isCurrentVersionSupportingImages } = useModel();
const toggleRow = (id: keyof MoodleGPTConfig, label: string, visible = true) => {
if (!visible) return null;
return (
<div class="flex justify-between items-center">
<label htmlFor={id} class="text-sm text-text-primary">
{label}
</label>
<label class="toggle-switch">
<input
id={id}
type="checkbox"
checked={!!config[id]}
onChange={e => onChange(id, (e.target as HTMLInputElement).checked)}
/>
<span class="slider"></span>
</label>
</div>
);
};
const isClipboard = config.mode === 'clipboard';
return (
<div class="bg-panel-bg backdrop-blur-md border border-panel-border rounded-2xl p-4 shadow-[0_8px_32px_0_rgba(0,0,0,0.3)] grid grid-cols-2 gap-4">
{toggleRow('logs', 'Console logs')}
{toggleRow('title', 'Title hint')}
{toggleRow('cursor', 'Cursor hint')}
{toggleRow('timeout', 'Timeout')}
{toggleRow('typing', 'Typing effect', !isClipboard)}
{toggleRow('mouseover', 'Hover effect', !isClipboard)}
{toggleRow('infinite', 'Infinite try')}
{toggleRow('history', 'History')}
{toggleRow('includeImages', 'Images (GPT-4)', isCurrentVersionSupportingImages(config.model))}
</div>
);
}
+86
View File
@@ -0,0 +1,86 @@
import { useModel } from '../hooks/useModel';
import { MoodleGPTConfig } from '../hooks/useConfig';
import { useState } from 'preact/hooks';
interface Props {
config: MoodleGPTConfig;
onChange: (key: keyof MoodleGPTConfig, value: MoodleGPTConfig[keyof MoodleGPTConfig]) => void;
showMessage: (msg: string, isError?: boolean) => void;
visible: boolean;
}
export function SettingsPanel({ config, onChange, showMessage, visible }: Props) {
if (!visible) return null;
const { models, fetchModels, validateModel } = useModel(
config.apiKey,
config.baseURL,
config.projectId
);
const [testing, setTesting] = useState(false);
const handleTest = async () => {
if (!config.model) {
showMessage('Please select a model first', true);
return;
}
setTesting(true);
showMessage('Checking GPT version...', false);
const result = await validateModel(config.model, config.maxTokens);
setTesting(false);
if (result.success) {
showMessage(result.message || 'Valid model');
} else {
showMessage(result.error || 'Invalid model', true);
}
};
return (
<div
class="bg-panel-bg backdrop-blur-md border border-panel-border rounded-2xl p-4 shadow-[0_8px_32px_0_rgba(0,0,0,0.3)]"
id="settings"
>
<div class="flex flex-col gap-2 mb-4">
<label htmlFor="apiKey" class="text-sm font-medium text-text-secondary">
Api Key<span class="text-error ml-1">*</span>
</label>
<input
id="apiKey"
type="text"
placeholder="sk-..."
class="w-full px-3 py-2.5 bg-input-bg border border-input-border rounded-lg text-text-primary text-sm transition-all focus:outline-none focus:border-input-focus focus:ring-2 focus:ring-primary/20 box-border"
value={config.apiKey || ''}
onInput={e => onChange('apiKey', (e.target as HTMLInputElement).value)}
/>
</div>
<div class="flex flex-col gap-2">
<label htmlFor="model" class="text-sm font-medium text-text-secondary">
GPT Model<span class="text-error ml-1">*</span>
</label>
<div class="relative flex items-center">
<input
type="text"
id="model"
list="models"
placeholder="gpt-4o"
class="w-full pl-3 pr-9 py-2.5 bg-input-bg border border-input-border rounded-lg text-text-primary text-sm transition-all focus:outline-none focus:border-input-focus focus:ring-2 focus:ring-primary/20 box-border"
value={config.model || ''}
onInput={e => onChange('model', (e.target as HTMLInputElement).value)}
onFocus={fetchModels}
/>
<datalist id="models">
{models.map(m => (
<option key={m} value={m}>
{m}
</option>
))}
</datalist>
<i
class={`fa-solid ${testing ? 'fa-spinner fa-spin' : 'fa-play'} absolute right-3 text-primary text-sm transition-all hover:scale-110 hover:text-primary-hover cursor-pointer`}
onClick={handleTest}
title="Test"
></i>
</div>
</div>
</div>
);
}
-15
View File
@@ -1,15 +0,0 @@
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')!;
-142
View File
@@ -1,142 +0,0 @@
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')!;
const projectIdSelector: HTMLInputElement = document.querySelector('#projectId')!;
const maxTokensSelector: HTMLInputElement = document.querySelector('#maxTokens')!;
/**
* 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();
const projectId = projectIdSelector.value?.trim();
if (!apiKey) return;
inputModel.innerHTML = '';
try {
const client = new OpenAI({
apiKey,
baseURL,
project: projectId,
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();
const projectId = projectIdSelector.value?.trim();
const maxTokens = maxTokensSelector.value ? parseInt(maxTokensSelector.value) : undefined;
try {
showMessage({ msg: 'Checking GPT version...', isInfinite: true, isError: false });
const client = new OpenAI({
apiKey,
baseURL,
project: projectId,
dangerouslyAllowBrowser: true
});
const completion = await client.chat.completions.create({
model,
messages: [
{
role: 'user',
content:
'reply just pong, set success to true, and provide a random number between 1 and 100.'
}
],
max_completion_tokens: maxTokens || 2000,
response_format: {
type: 'json_schema',
json_schema: {
name: 'model_test',
strict: true,
schema: {
type: 'object',
properties: {
reply: { type: 'string', description: 'The text reply' },
success: { type: 'boolean', description: 'Always true' },
data: {
type: 'object',
properties: {
number: { type: 'integer' }
},
required: ['number'],
additionalProperties: false
}
},
required: ['reply', 'success', 'data'],
additionalProperties: false
}
}
}
});
const content = completion.choices[0]?.message?.content;
if (!content) {
throw new Error('No content returned from the model.');
}
const parsed = JSON.parse(content);
if (
typeof parsed.reply !== 'string' ||
typeof parsed.success !== 'boolean' ||
typeof parsed.data !== 'object' ||
typeof parsed.data.number !== 'number'
) {
throw new Error('Model did not follow the JSON schema correctly.');
}
showMessage({ msg: 'The model is valid and supports structured outputs!' });
} catch (err: any) {
showMessage({ msg: err, isError: true });
}
}
const checkModelBtn: HTMLElement = document.querySelector('#check-model')!;
checkModelBtn.addEventListener('click', checkModel);
+48
View File
@@ -0,0 +1,48 @@
import { useState, useEffect } from 'preact/hooks';
export interface MoodleGPTConfig {
apiKey?: string;
code?: string;
model?: string;
baseURL?: string;
maxTokens?: number;
projectId?: string;
timeoutValue?: number;
logs?: boolean;
title?: boolean;
cursor?: boolean;
typing?: boolean;
mouseover?: boolean;
infinite?: boolean;
timeout?: boolean;
history?: boolean;
includeImages?: boolean;
mode?: 'autocomplete' | 'clipboard';
}
export function useConfig() {
const [config, setConfig] = useState<MoodleGPTConfig>({
mode: 'autocomplete',
title: true,
cursor: true,
timeout: true
});
const [loading, setLoading] = useState(true);
useEffect(() => {
chrome.storage.sync.get(['moodleGPT']).then(storage => {
if (storage.moodleGPT) {
setConfig(prev => ({ ...prev, ...storage.moodleGPT }));
}
setLoading(false);
});
}, []);
const saveConfig = async (newConfig: MoodleGPTConfig) => {
const updated = { ...config, ...newConfig };
setConfig(updated);
await chrome.storage.sync.set({ moodleGPT: updated });
};
return { config, loading, saveConfig, setConfig };
}
+113
View File
@@ -0,0 +1,113 @@
import { useState } from 'preact/hooks';
import OpenAI from 'openai';
export function useModel(apiKey?: string, baseURL?: string, projectId?: string) {
const [models, setModels] = useState<string[]>([]);
const [error, setError] = useState<string | null>(null);
const isCurrentVersionSupportingImages = (version?: string) => {
if (!version) return false;
const versionNumber = version.match(/gpt-(\d+)/);
if (!versionNumber?.[1]) {
return false;
}
return Number(versionNumber[1]) >= 4;
};
const fetchModels = async () => {
if (!apiKey) return;
try {
const client = new OpenAI({
apiKey,
baseURL: baseURL || undefined,
project: projectId || undefined,
dangerouslyAllowBrowser: true
});
const rep = await client.models.list();
const filteredModels = rep.data.filter(
model =>
model.id.startsWith('gpt') ||
model.id.search(/^o\d+/gi) !== -1 ||
model.id.startsWith('chatgpt')
);
filteredModels.sort((a, b) => b.id.localeCompare(a.id));
setModels(filteredModels.map(m => m.id));
setError(null);
} catch (err: any) {
console.error(err);
setError(err.message || String(err));
}
};
const validateModel = async (model: string, maxTokens?: number) => {
if (!apiKey || !model) return { success: false, error: 'API Key and Model are required' };
try {
const client = new OpenAI({
apiKey,
baseURL: baseURL || undefined,
project: projectId || undefined,
dangerouslyAllowBrowser: true
});
const completion = await client.chat.completions.create({
model,
messages: [
{
role: 'user',
content:
'reply just pong, set success to true, and provide a random number between 1 and 100.'
}
],
max_completion_tokens: maxTokens || 2000,
response_format: {
type: 'json_schema',
json_schema: {
name: 'model_test',
strict: true,
schema: {
type: 'object',
properties: {
reply: { type: 'string', description: 'The text reply' },
success: { type: 'boolean', description: 'Always true' },
data: {
type: 'object',
properties: {
number: { type: 'integer' }
},
required: ['number'],
additionalProperties: false
}
},
required: ['reply', 'success', 'data'],
additionalProperties: false
}
}
}
});
const content = completion.choices[0]?.message?.content;
if (!content) {
throw new Error('No content returned from the model.');
}
const parsed = JSON.parse(content);
if (
typeof parsed.reply !== 'string' ||
typeof parsed.success !== 'boolean' ||
typeof parsed.data !== 'object' ||
typeof parsed.data.number !== 'number'
) {
throw new Error('Model did not follow the JSON schema correctly.');
}
return { success: true, message: 'The model is valid and supports structured outputs!' };
} catch (err: any) {
return { success: false, error: err.message || String(err) };
}
};
return { models, fetchModels, validateModel, error, isCurrentVersionSupportingImages };
}
-91
View File
@@ -1,91 +0,0 @@
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', 'projectId', 'timeoutValue'];
// Save the configuration
saveBtn.addEventListener('click', function () {
const [apiKey, code, model, baseURL, maxTokens, projectId, timeoutValue] = 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,
projectId,
timeoutValue: timeoutValue ? parseInt(timeoutValue) : 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();
});
+7
View File
@@ -0,0 +1,7 @@
import { render } from 'preact';
import { App } from './components/App';
const root = document.getElementById('root');
if (root) {
render(<App />, root);
}
-42
View File
@@ -1,42 +0,0 @@
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: Record<string, string[]> = {
autocomplete: [],
clipboard: ['typing', 'mouseover']
};
/**
* Handle when a mode change to show specific input or to hide them
*/
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';
}
for (const id of dontNeedDisable) {
document.querySelector('#' + id)!.parentElement!.style.display = '';
}
}
// Mode hanlder
for (const button of modes) {
button.addEventListener('click', function () {
const value = button.value;
globalData.actualMode = value;
for (const mode of modes) {
if (mode.value !== value) {
mode.classList.add('not-selected');
} else {
mode.classList.remove('not-selected');
}
}
handleModeChange();
});
}
-21
View File
@@ -1,21 +0,0 @@
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();
});
+61
View File
@@ -0,0 +1,61 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
body {
width: 380px;
min-height: 500px;
}
}
@layer components {
.toggle-switch {
position: relative;
display: inline-block;
width: 36px;
height: 20px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: theme('colors.input.bg');
border: 1px solid theme('colors.input.border');
transition: 0.4s;
border-radius: 20px;
}
.slider:before {
position: absolute;
content: '';
height: 14px;
width: 14px;
left: 2px;
bottom: 2px;
background-color: theme('colors.text.secondary');
transition: 0.4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: theme('colors.primary.DEFAULT');
border-color: theme('colors.primary.DEFAULT');
}
input:checked + .slider:before {
transform: translateX(16px);
background-color: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
}
-31
View File
@@ -1,31 +0,0 @@
/**
* 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;
}
-61
View File
@@ -1,61 +0,0 @@
const CURRENT_VERSION = '2.0.0';
const versionDisplay = document.querySelector('#version')!;
/**
* Get the last version from the github
* @returns
*/
export async function getLastVersion(): Promise<string> {
const req = await fetch(
'https://raw.githubusercontent.com/yoannchb-pro/MoodleGPT/main/package.json'
);
const rep = await req.json();
return rep.version;
}
/**
* Display the version or an update message
* @param {string} version
* @param {boolean} isCurrent
* @returns
*/
export function setVersion(version: string, isCurrent = true) {
if (isCurrent) {
versionDisplay.textContent = 'v' + version;
return;
}
const link = document.createElement('a');
link.href = 'https://github.com/yoannchb-pro/MoodleGPT';
link.rel = 'noopener noreferrer';
link.target = '_blank';
link.textContent = 'v' + version;
versionDisplay.appendChild(link);
versionDisplay.appendChild(document.createTextNode(' is now available !'));
}
/**
* Check if the extension neeed an update or not
*/
export async function notifyUpdate() {
const lastVersion = await getLastVersion().catch(err => {
console.error(err);
return CURRENT_VERSION;
});
const lastVertionSplitted = lastVersion.split('.');
const currentVersionSplitted = CURRENT_VERSION.split('.');
const minVersionLength = Math.min(lastVertionSplitted.length, currentVersionSplitted.length);
for (let i = 0; i < minVersionLength; ++i) {
if (parseInt(lastVertionSplitted[i]) > parseInt(currentVersionSplitted[i])) {
return setVersion(lastVersion, false);
} else if (parseInt(currentVersionSplitted[i]) > parseInt(lastVertionSplitted[i])) {
return setVersion(CURRENT_VERSION);
}
}
setVersion(CURRENT_VERSION);
}
notifyUpdate();
+55
View File
@@ -0,0 +1,55 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/popup/**/*.{js,jsx,ts,tsx}', './extension/popup/index.html'],
theme: {
extend: {
colors: {
primary: {
DEFAULT: '#6366f1',
hover: '#4f46e5'
},
gradient: {
start: '#0f172a',
end: '#1e1b4b'
},
panel: {
bg: 'rgba(255, 255, 255, 0.05)',
border: 'rgba(255, 255, 255, 0.1)'
},
text: {
primary: '#f8fafc',
secondary: '#94a3b8'
},
input: {
bg: 'rgba(0, 0, 0, 0.2)',
border: 'rgba(255, 255, 255, 0.15)',
focus: '#818cf8'
},
success: '#10b981',
error: '#ef4444'
},
fontFamily: {
sans: [
'Inter',
'-apple-system',
'BlinkMacSystemFont',
'Segoe UI',
'Roboto',
'Helvetica',
'Arial',
'sans-serif'
]
},
animation: {
float: 'float 3s ease-in-out infinite'
},
keyframes: {
float: {
'0%, 100%': { transform: 'translateY(0)' },
'50%': { transform: 'translateY(-4px)' }
}
}
}
},
plugins: []
};
+3 -1
View File
@@ -12,7 +12,9 @@
"outDir": "extension", "outDir": "extension",
"types": ["node", "chrome"], "types": ["node", "chrome"],
"typeRoots": ["node_modules/@types"], "typeRoots": ["node_modules/@types"],
"strictBindCallApply": true "strictBindCallApply": true,
"jsx": "react-jsx",
"jsxImportSource": "preact"
}, },
"include": ["src/**/*"] "include": ["src/**/*"]
} }