diff --git a/README.md b/README.md index 0cdfcc8..eca9f28 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,10 @@ # **N**otice **M**e **S**enpai Firefox addon to aid in applying to jobs. + + +## Todo + +- Add option to keep model loaded vs load each time +- model load progress monitoring in popup +- allow file dropping on options ui diff --git a/addon/manifest.json b/addon/manifest.json index 3d3abec..47e2d45 100644 --- a/addon/manifest.json +++ b/addon/manifest.json @@ -14,7 +14,9 @@ "browser_style": true }, "background": { - "scripts": ["background/background.js"] + "scripts": [ + "background/background.js" + ] }, "options_ui": { "page": "options/options.html", @@ -23,6 +25,16 @@ "permissions": [ "storage", "activeTab", - "contextMenus" - ] + "contextMenus", + "", + "webRequest", + "webRequestBlocking", + "downloads" + ], + "browser_specific_settings": { + "gecko": { + "id": "nms@cray.software", + "strict_min_version": "58.0" + } + } } diff --git a/addon/options/options.html b/addon/options/options.html index d54925a..e343a49 100644 --- a/addon/options/options.html +++ b/addon/options/options.html @@ -11,14 +11,21 @@

- - No Resume On File + +
- +
-

+
+ + +
+
+
diff --git a/background/background.js b/background/background.js index 035376a..4afa91e 100644 --- a/background/background.js +++ b/background/background.js @@ -1,3 +1,38 @@ +import { CreateMLCEngine } from "@mlc-ai/web-llm"; + +var engine = null +var resume = "" +var resourcesLoaded = false + +const loadProgress = (progress) => { + console.dir(progress) + // Trigger popup and stream progress.timeElapsed +} + +const loadResources = async () => { + let modelName = (await browser.storage.local.get("model")).model || "Llama-3.2-1B-Instruct-q4f32_1-MLC" + console.log(`Loading model: ${modelName}`) + engine = await CreateMLCEngine(modelName, { initProgressCallback: loadProgress }); + console.log("Model loaded") + resume = (await browser.storage.local.get("resume")).resume || "No resume, halt and ask for resume." + resourcesLoaded = true +} + +const runMatch = async (inputText) => { + console.log("Run Match") + if (!resourcesLoaded) await loadResources() + const query = [ + { role: "system", content: "You are a resume-job description matching assistant." }, + { role: "user", content: `This is my resume: ${resume}` }, + { role: "user", content: `This is the job description: ${inputText}` } + ] + const response = await engine.chat.completions.create({ + messages: query + }) + + console.dir(response) + +} // Add context menu items browser.contextMenus.create({ @@ -10,5 +45,6 @@ browser.contextMenus.onClicked.addListener((info, tab) => { if (info.menuItemId === "nms-cmenu" && info.selectionText) { // Handle context menu click with selected text console.log("Context menu clicked with selection:", info.selectionText); + runMatch(info.selectionText) } }) diff --git a/libs/base64.js b/libs/base64.js new file mode 100644 index 0000000..b1fba6c --- /dev/null +++ b/libs/base64.js @@ -0,0 +1,26 @@ +// Base64 UTF-8 Encode/Decode Utility Functions +// Source: https://www.digitalocean.com/community/tutorials/how-to-encode-and-decode-strings-with-base64-in-javascript + + +module.exports = { + // Function to encode a UTF-8 string to Base64 + utf8ToBase64: (str) => { + const encoder = new TextEncoder() + const data = encoder.encode(str) + + const binaryString = String.fromCharCode.apply(null, data) + return btoa(binaryString) + }, + + // Function to decode a Base64 string to UTF-8 + base64ToUtf8: (b64) => { + const binaryString = atob(b64) + // Create a Uint8Array from the binary string. + const bytes = new Uint8Array(binaryString.length) + for (let i = 0 ; i < binaryString.length ; i++) { + bytes[i] = binaryString.charCodeAt(i) + } + const decoder = new TextDecoder() + return decoder.decode(bytes) + } +} diff --git a/options/options.js b/options/options.js index 0e7a018..20068db 100644 --- a/options/options.js +++ b/options/options.js @@ -1 +1,44 @@ -import "../style.scss"; +import "../style.scss" +import * as base64 from "../libs/base64.js" + +const saveOptions = async (saveEvent) => { + saveEvent.preventDefault() + if (document.querySelector("#resume").value !== "" && (await browser.storage.local.get("resume")).resume !== base64.utf8ToBase64(document.querySelector("#resume").value)) { + const LastEdited = new Date().toLocaleString() + browser.storage.local.set({ + resume: base64.utf8ToBase64(document.querySelector("#resume").value), + lastEdited: LastEdited + }) + document.querySelector("#resumeLastUpdated").textContent = `Last Edited: ${LastEdited}` + } + + browser.storage.local.set({ + model: document.querySelector("#mlModel").value + }) +} + +const restoreOptions = async () => { + const defaultModel = "Llama-3.2-1B-Instruct-q4f32_1-MLC" + try { + let resume = await browser.storage.local.get("resume") + if (resume.resume) { + document.querySelector("#resume").textContent = base64.base64ToUtf8(resume.resume) + let lastEdited = await browser.storage.local.get("lastEdited") + if (lastEdited.lastEdited) { + document.querySelector("#resumeLastUpdated").textContent = `Last Edited: ${lastEdited.lastEdited}` + } + } + + let model = await browser.storage.local.get("model") || defaultModel + model = model === "" ? defaultModel : model + // document.querySelector("#mlModel").value = model + document.querySelectorAll("#mlModel.option").forEach((element) => { + element.selected = (element.value === model) + }) + } catch (error) { + console.error("Error restoring options:", error) + } +} + +document.addEventListener("DOMContentLoaded", restoreOptions) +document.querySelector("form").addEventListener("submit", saveOptions) diff --git a/package-lock.json b/package-lock.json index 26b2e4a..1246a97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "MPL-2.0", "dependencies": { + "@mlc-ai/web-llm": "^0.2.79", "bootstrap": "^5.3.8", "css-loader": "^7.1.2", "nord": "^0.2.1", @@ -73,6 +74,15 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mlc-ai/web-llm": { + "version": "0.2.79", + "resolved": "https://registry.npmjs.org/@mlc-ai/web-llm/-/web-llm-0.2.79.tgz", + "integrity": "sha512-Hy1ZHQ0o2bZGZoVnGK48+fts/ZSKwLe96xjvqL/6C59Mem9HoHTcFE07NC2E23mRmhd01tL655N6CPeYmwWgwQ==", + "license": "Apache-2.0", + "dependencies": { + "loglevel": "^1.9.1" + } + }, "node_modules/@parcel/watcher": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", @@ -1303,6 +1313,19 @@ "node": ">=8" } }, + "node_modules/loglevel": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", diff --git a/package.json b/package.json index 93ddfde..6b89234 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "build": "webpack" }, "dependencies": { + "@mlc-ai/web-llm": "^0.2.79", "bootstrap": "^5.3.8", "css-loader": "^7.1.2", "nord": "^0.2.1", diff --git a/webpack.config.js b/webpack.config.js index 9d7f22e..7736ef4 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -11,7 +11,7 @@ module.exports = { filename: "[name]/[name].js" }, mode: "none", - watch: true, + watch: false, watchOptions: { ignored: '**/node_modules', },