Ability to save resume and model
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
26
libs/base64.js
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
23
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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',
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user