Ability to save resume and model

This commit is contained in:
2025-11-12 14:33:53 -05:00
parent 9a67088031
commit b5f309b5bc
9 changed files with 164 additions and 9 deletions

View File

@@ -1,3 +1,10 @@
# **N**otice **M**e **S**enpai # **N**otice **M**e **S**enpai
Firefox addon to aid in applying to jobs. 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

View File

@@ -14,7 +14,9 @@
"browser_style": true "browser_style": true
}, },
"background": { "background": {
"scripts": ["background/background.js"] "scripts": [
"background/background.js"
]
}, },
"options_ui": { "options_ui": {
"page": "options/options.html", "page": "options/options.html",
@@ -23,6 +25,16 @@
"permissions": [ "permissions": [
"storage", "storage",
"activeTab", "activeTab",
"contextMenus" "contextMenus",
] "<all_urls>",
"webRequest",
"webRequestBlocking",
"downloads"
],
"browser_specific_settings": {
"gecko": {
"id": "nms@cray.software",
"strict_min_version": "58.0"
}
}
} }

View File

@@ -11,14 +11,21 @@
<p></p> <p></p>
<form> <form>
<div class="row"> <div class="row">
<label for="resumeFile" class="col-4 form-label">Resume</label> <label for="resume" class="col-4 form-label">Resume</label>
<em class="col-8 form-text text-end" id="resumeFileName">No Resume On File</em> <em class="col-8 form-text text-end" id="resumeLastUpdated"></em>
</div> </div>
<div class="row"> <div class="row">
<input type="file" class="col-12 form-control" id="resumeFile" name="resumeFile" accept=".pdf"/> <textarea id="resume" name="resume" class="col-12 form-control" rows="5" placeholder="Paste your resume here..."></textarea>
</div> </div>
<p></p> <div class="row p-3"></div>
<div class="row"> <div class="row">
<label for="mlModel" class="col-4 form-label">Machine Learning Model</label>
<select class="col-12 form-select" id="mlModel" name="mlModel">
<option value="Llama-3.2-1B-Instruct-q4f32_1-MLC">Llama 3.2 1B Instruct</option>
</select>
</div>
<div class="row p-3"></div>
<div class="row pb-5">
<div class="col-8"></div> <div class="col-8"></div>
<input type="submit" class="col-4 btn" value="Save"> <input type="submit" class="col-4 btn" value="Save">
</div> </div>

View File

@@ -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 // Add context menu items
browser.contextMenus.create({ browser.contextMenus.create({
@@ -10,5 +45,6 @@ browser.contextMenus.onClicked.addListener((info, tab) => {
if (info.menuItemId === "nms-cmenu" && info.selectionText) { if (info.menuItemId === "nms-cmenu" && info.selectionText) {
// Handle context menu click with selected text // Handle context menu click with selected text
console.log("Context menu clicked with selection:", info.selectionText); console.log("Context menu clicked with selection:", info.selectionText);
runMatch(info.selectionText)
} }
}) })

26
libs/base64.js Normal file
View File

@@ -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)
}
}

View File

@@ -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)

23
package-lock.json generated
View File

@@ -9,6 +9,7 @@
"version": "1.0.0", "version": "1.0.0",
"license": "MPL-2.0", "license": "MPL-2.0",
"dependencies": { "dependencies": {
"@mlc-ai/web-llm": "^0.2.79",
"bootstrap": "^5.3.8", "bootstrap": "^5.3.8",
"css-loader": "^7.1.2", "css-loader": "^7.1.2",
"nord": "^0.2.1", "nord": "^0.2.1",
@@ -73,6 +74,15 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@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": { "node_modules/@parcel/watcher": {
"version": "2.5.1", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
@@ -1303,6 +1313,19 @@
"node": ">=8" "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": { "node_modules/merge-stream": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",

View File

@@ -12,6 +12,7 @@
"build": "webpack" "build": "webpack"
}, },
"dependencies": { "dependencies": {
"@mlc-ai/web-llm": "^0.2.79",
"bootstrap": "^5.3.8", "bootstrap": "^5.3.8",
"css-loader": "^7.1.2", "css-loader": "^7.1.2",
"nord": "^0.2.1", "nord": "^0.2.1",

View File

@@ -11,7 +11,7 @@ module.exports = {
filename: "[name]/[name].js" filename: "[name]/[name].js"
}, },
mode: "none", mode: "none",
watch: true, watch: false,
watchOptions: { watchOptions: {
ignored: '**/node_modules', ignored: '**/node_modules',
}, },