From eb702551449fe36fe24b4aa6f2e02f5f9a6b9051 Mon Sep 17 00:00:00 2001 From: Peisong Xiao Date: Fri, 16 Jan 2026 19:44:31 -0500 Subject: [PATCH] initial commit: wwcompanion v1 --- .gitignore | 1 + background.js | 189 ++++++++++++++++++++++++++++++++++++++++++ content.js | 48 +++++++++++ manifest.json | 26 ++++++ popup.css | 161 ++++++++++++++++++++++++++++++++++++ popup.html | 42 ++++++++++ popup.js | 223 ++++++++++++++++++++++++++++++++++++++++++++++++++ settings.css | 143 ++++++++++++++++++++++++++++++++ settings.html | 54 ++++++++++++ settings.js | 148 +++++++++++++++++++++++++++++++++ 10 files changed, 1035 insertions(+) create mode 100644 .gitignore create mode 100644 background.js create mode 100644 content.js create mode 100644 manifest.json create mode 100644 popup.css create mode 100644 popup.html create mode 100644 popup.js create mode 100644 settings.css create mode 100644 settings.html create mode 100644 settings.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c317064 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +AGENTS.md diff --git a/background.js b/background.js new file mode 100644 index 0000000..f770200 --- /dev/null +++ b/background.js @@ -0,0 +1,189 @@ +const DEFAULT_TASKS = [ + { + id: "task-fit-summary", + name: "Fit Summary", + text: + "Summarize the role, highlight key requirements, and assess my fit using the resume. Note any gaps and what to emphasize." + } +]; + +const DEFAULT_SETTINGS = { + apiKey: "", + model: "gpt-4o-mini", + systemPrompt: + "You are a precise, honest assistant. Be concise, highlight uncertainties, and avoid inventing details.", + resume: "", + tasks: DEFAULT_TASKS +}; + +chrome.runtime.onInstalled.addListener(async () => { + const stored = await chrome.storage.local.get(Object.keys(DEFAULT_SETTINGS)); + const updates = {}; + + for (const [key, value] of Object.entries(DEFAULT_SETTINGS)) { + const existing = stored[key]; + const missing = + existing === undefined || + existing === null || + (key === "tasks" && !Array.isArray(existing)); + + if (missing) updates[key] = value; + } + + if (Object.keys(updates).length) { + await chrome.storage.local.set(updates); + } +}); + +chrome.runtime.onConnect.addListener((port) => { + if (port.name !== "analysis") return; + + let abortController = null; + + const resetAbort = () => { + if (abortController) abortController.abort(); + abortController = null; + }; + + port.onMessage.addListener((message) => { + if (message?.type === "START_ANALYSIS") { + resetAbort(); + abortController = new AbortController(); + void handleAnalysisRequest(port, message.payload, abortController.signal).catch( + (error) => { + if (error?.name === "AbortError") { + port.postMessage({ type: "ABORTED" }); + return; + } + port.postMessage({ + type: "ERROR", + message: error?.message || "Unknown error during analysis." + }); + } + ); + return; + } + + if (message?.type === "ABORT_ANALYSIS") { + resetAbort(); + } + }); + + port.onDisconnect.addListener(() => { + resetAbort(); + }); +}); + +function buildUserMessage(resume, task, posting) { + return [ + "=== RESUME ===", + resume || "", + "", + "=== TASK ===", + task || "", + "", + "=== JOB POSTING ===", + posting || "" + ].join("\n"); +} + +async function handleAnalysisRequest(port, payload, signal) { + const { apiKey, model, systemPrompt, resume, taskText, postingText } = payload || {}; + + if (!apiKey) { + port.postMessage({ type: "ERROR", message: "Missing OpenAI API key." }); + return; + } + + if (!model) { + port.postMessage({ type: "ERROR", message: "Missing model name." }); + return; + } + + if (!postingText) { + port.postMessage({ type: "ERROR", message: "No job posting text provided." }); + return; + } + + if (!taskText) { + port.postMessage({ type: "ERROR", message: "No task prompt selected." }); + return; + } + + const userMessage = buildUserMessage(resume, taskText, postingText); + + await streamChatCompletion({ + apiKey, + model, + systemPrompt: systemPrompt || "", + userMessage, + signal, + onDelta: (text) => port.postMessage({ type: "DELTA", text }) + }); + + port.postMessage({ type: "DONE" }); +} + +async function streamChatCompletion({ + apiKey, + model, + systemPrompt, + userMessage, + signal, + onDelta +}) { + const response = await fetch("https://api.openai.com/v1/chat/completions", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}` + }, + body: JSON.stringify({ + model, + stream: true, + messages: [ + { role: "system", content: systemPrompt }, + { role: "user", content: userMessage } + ] + }), + signal + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`OpenAI API error ${response.status}: ${errorText}`); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ""; + + // OpenAI streams Server-Sent Events; parse incremental deltas from data lines. + while (true) { + const { value, done } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split("\n"); + buffer = lines.pop() || ""; + + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed.startsWith("data:")) continue; + + const data = trimmed.slice(5).trim(); + if (!data) continue; + if (data === "[DONE]") return; + + let parsed; + try { + parsed = JSON.parse(data); + } catch { + continue; + } + + const delta = parsed?.choices?.[0]?.delta?.content; + if (delta) onDelta(delta); + } + } +} diff --git a/content.js b/content.js new file mode 100644 index 0000000..e3dcbdc --- /dev/null +++ b/content.js @@ -0,0 +1,48 @@ +const HEADER_LINES = new Set([ + "OVERVIEW", + "PRE-SCREENING", + "WORK TERM RATINGS", + "JOB POSTING INFORMATION", + "APPLICATION INFORMATION", + "COMPANY INFORMATION", + "SERVICE TEAM" +]); + +function sanitizePostingText(text) { + let cleaned = text.replaceAll("fiber_manual_record", ""); + const lines = cleaned.split(/\r?\n/); + const filtered = lines.filter((line) => { + const trimmed = line.trim(); + if (!trimmed) return true; + return !HEADER_LINES.has(trimmed.toUpperCase()); + }); + + cleaned = filtered.join("\n"); + cleaned = cleaned.replace(/[ \t]+/g, " "); + cleaned = cleaned.replace(/\n{3,}/g, "\n\n"); + return cleaned.trim(); +} + +function extractPostingText() { + const contents = [...document.getElementsByClassName("modal__content")]; + if (!contents.length) { + return { ok: false, error: "No modal content found on this page." }; + } + + // WaterlooWorks renders multiple modal containers; choose the longest visible text block. + const el = contents.reduce((best, cur) => + cur.innerText.length > best.innerText.length ? cur : best + ); + + const rawText = el.innerText; + const sanitized = sanitizePostingText(rawText); + + return { ok: true, rawText, sanitized }; +} + +chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { + if (message?.type !== "EXTRACT_POSTING") return; + + const result = extractPostingText(); + sendResponse(result); +}); diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..7de09d6 --- /dev/null +++ b/manifest.json @@ -0,0 +1,26 @@ +{ + "manifest_version": 3, + "name": "WWCompanion", + "version": "0.1.0", + "description": "Manual reasoning companion for WaterlooWorks job postings.", + "permissions": ["storage", "activeTab"], + "host_permissions": ["https://waterlooworks.uwaterloo.ca/*"], + "action": { + "default_title": "WWCompanion", + "default_popup": "popup.html" + }, + "background": { + "service_worker": "background.js", + "type": "module" + }, + "content_scripts": [ + { + "matches": ["https://waterlooworks.uwaterloo.ca/*"], + "js": ["content.js"] + } + ], + "options_ui": { + "page": "settings.html", + "open_in_tab": true + } +} diff --git a/popup.css b/popup.css new file mode 100644 index 0000000..ab185a6 --- /dev/null +++ b/popup.css @@ -0,0 +1,161 @@ +:root { + --ink: #1f1a17; + --muted: #6b5f55; + --accent: #b14d2b; + --accent-deep: #7d321b; + --panel: #fff7ec; + --border: #e4d6c5; + --glow: rgba(177, 77, 43, 0.18); +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + padding: 16px; + width: 360px; + font-family: "Iowan Old Style", "Palatino Linotype", "Book Antiqua", Palatino, + "Times New Roman", serif; + color: var(--ink); + background: radial-gradient(circle at top, #fdf2df, #f7ead6 60%, #f1dcc6 100%); +} + +.title-block { + margin-bottom: 12px; +} + +.title { + font-size: 20px; + font-weight: 700; + letter-spacing: 0.3px; +} + +.subtitle { + font-size: 12px; + color: var(--muted); +} + +.panel { + padding: 12px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--panel); + box-shadow: 0 12px 30px rgba(122, 80, 47, 0.12); +} + +.field { + margin-top: 10px; + display: grid; + gap: 6px; +} + +label { + font-size: 12px; + text-transform: uppercase; + letter-spacing: 1px; + color: var(--muted); +} + +select { + width: 100%; + padding: 8px 10px; + border-radius: 10px; + border: 1px solid var(--border); + background: #fffdf9; + font-size: 13px; +} + +.actions { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; + margin-top: 12px; +} + +button { + font-family: inherit; + border: none; + border-radius: 10px; + padding: 8px 12px; + cursor: pointer; + transition: transform 0.15s ease, box-shadow 0.15s ease; +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; + box-shadow: none; +} + +button:active { + transform: translateY(1px); +} + +.primary { + width: 100%; + background: #fffdf9; + border: 1px solid var(--border); + box-shadow: 0 6px 16px rgba(120, 85, 55, 0.12); +} + +.accent { + background: var(--accent); + color: #fff9f3; + box-shadow: 0 8px 20px var(--glow); +} + +.ghost { + background: transparent; + border: 1px solid var(--border); +} + +.meta { + display: flex; + justify-content: space-between; + font-size: 12px; + color: var(--muted); + margin-top: 10px; +} + +.status { + margin-top: 10px; + font-size: 12px; + color: var(--accent-deep); +} + +.output { + margin-top: 12px; + border: 1px dashed var(--border); + border-radius: 12px; + padding: 10px; + background: rgba(255, 255, 255, 0.7); + min-height: 160px; + max-height: 240px; + overflow: hidden; +} + +pre { + margin: 0; + white-space: pre-wrap; + word-break: break-word; + max-height: 220px; + overflow-y: auto; + font-size: 12px; + line-height: 1.4; +} + +.footer { + display: flex; + justify-content: flex-end; + margin-top: 10px; +} + +.link { + padding: 4px 6px; + background: none; + border-radius: 8px; + color: var(--accent-deep); + font-size: 12px; +} diff --git a/popup.html b/popup.html new file mode 100644 index 0000000..02ed168 --- /dev/null +++ b/popup.html @@ -0,0 +1,42 @@ + + + + + + WWCompanion + + + +
+
WWCompanion
+
Manual reasoning for WaterlooWorks
+
+ +
+ +
+ + +
+
+ + +
+
+ Posting: 0 chars + Prompt: 0 chars +
+
Idle
+
+ +
+

+    
+ + + + + + diff --git a/popup.js b/popup.js new file mode 100644 index 0000000..87b0a36 --- /dev/null +++ b/popup.js @@ -0,0 +1,223 @@ +const extractBtn = document.getElementById("extractBtn"); +const analyzeBtn = document.getElementById("analyzeBtn"); +const abortBtn = document.getElementById("abortBtn"); +const taskSelect = document.getElementById("taskSelect"); +const outputEl = document.getElementById("output"); +const statusEl = document.getElementById("status"); +const postingCountEl = document.getElementById("postingCount"); +const promptCountEl = document.getElementById("promptCount"); +const settingsBtn = document.getElementById("settingsBtn"); + +const state = { + postingText: "", + tasks: [], + port: null, + isAnalyzing: false +}; + +function getStorage(keys) { + return new Promise((resolve) => chrome.storage.local.get(keys, resolve)); +} + +function buildUserMessage(resume, task, posting) { + return [ + "=== RESUME ===", + resume || "", + "", + "=== TASK ===", + task || "", + "", + "=== JOB POSTING ===", + posting || "" + ].join("\n"); +} + +function setStatus(message) { + statusEl.textContent = message; +} + +function setAnalyzing(isAnalyzing) { + state.isAnalyzing = isAnalyzing; + analyzeBtn.disabled = isAnalyzing; + abortBtn.disabled = !isAnalyzing; + extractBtn.disabled = isAnalyzing; +} + +function updatePostingCount() { + postingCountEl.textContent = `Posting: ${state.postingText.length} chars`; +} + +function updatePromptCount(count) { + promptCountEl.textContent = `Prompt: ${count} chars`; +} + +function renderTasks(tasks) { + state.tasks = tasks; + taskSelect.innerHTML = ""; + + if (!tasks.length) { + const option = document.createElement("option"); + option.textContent = "No tasks configured"; + option.value = ""; + taskSelect.appendChild(option); + taskSelect.disabled = true; + return; + } + + taskSelect.disabled = false; + for (const task of tasks) { + const option = document.createElement("option"); + option.value = task.id; + option.textContent = task.name || "Untitled task"; + taskSelect.appendChild(option); + } +} + +function sendToActiveTab(message) { + return new Promise((resolve, reject) => { + chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { + const tab = tabs[0]; + if (!tab?.id) { + reject(new Error("No active tab found.")); + return; + } + + chrome.tabs.sendMessage(tab.id, message, (response) => { + const error = chrome.runtime.lastError; + if (error) { + reject(new Error(error.message)); + return; + } + resolve(response); + }); + }); + }); +} + +function ensurePort() { + if (state.port) return state.port; + + const port = chrome.runtime.connect({ name: "analysis" }); + port.onMessage.addListener((message) => { + if (message?.type === "DELTA") { + outputEl.textContent += message.text; + outputEl.scrollTop = outputEl.scrollHeight; + return; + } + + if (message?.type === "DONE") { + setAnalyzing(false); + setStatus("Done"); + return; + } + + if (message?.type === "ABORTED") { + setAnalyzing(false); + setStatus("Aborted."); + return; + } + + if (message?.type === "ERROR") { + setAnalyzing(false); + setStatus(message.message || "Error during analysis."); + } + }); + + port.onDisconnect.addListener(() => { + state.port = null; + setAnalyzing(false); + }); + + state.port = port; + return port; +} + +async function loadTasks() { + const { tasks = [] } = await getStorage(["tasks"]); + renderTasks(tasks); +} + +async function handleExtract() { + setStatus("Extracting..."); + try { + const response = await sendToActiveTab({ type: "EXTRACT_POSTING" }); + if (!response?.ok) { + setStatus(response?.error || "No posting detected."); + return; + } + + state.postingText = response.sanitized || ""; + updatePostingCount(); + updatePromptCount(0); + setStatus("Posting extracted."); + } catch (error) { + setStatus(error.message || "Unable to extract posting."); + } +} + +async function handleAnalyze() { + if (!state.postingText) { + setStatus("Extract a job posting first."); + return; + } + + const taskId = taskSelect.value; + const task = state.tasks.find((item) => item.id === taskId); + if (!task) { + setStatus("Select a task prompt."); + return; + } + + const { apiKey, model, systemPrompt, resume } = await getStorage([ + "apiKey", + "model", + "systemPrompt", + "resume" + ]); + + if (!apiKey) { + setStatus("Add your OpenAI API key in Settings."); + return; + } + + if (!model) { + setStatus("Set a model name in Settings."); + return; + } + + const promptText = buildUserMessage(resume || "", task.text || "", state.postingText); + updatePromptCount(promptText.length); + + outputEl.textContent = ""; + setAnalyzing(true); + setStatus("Analyzing..."); + + const port = ensurePort(); + port.postMessage({ + type: "START_ANALYSIS", + payload: { + apiKey, + model, + systemPrompt: systemPrompt || "", + resume: resume || "", + taskText: task.text || "", + postingText: state.postingText + } + }); +} + +function handleAbort() { + if (!state.port) return; + state.port.postMessage({ type: "ABORT_ANALYSIS" }); + setAnalyzing(false); + setStatus("Aborted."); +} + +extractBtn.addEventListener("click", handleExtract); +analyzeBtn.addEventListener("click", handleAnalyze); +abortBtn.addEventListener("click", handleAbort); +settingsBtn.addEventListener("click", () => chrome.runtime.openOptionsPage()); + +updatePostingCount(); +updatePromptCount(0); +loadTasks(); diff --git a/settings.css b/settings.css new file mode 100644 index 0000000..72753a9 --- /dev/null +++ b/settings.css @@ -0,0 +1,143 @@ +:root { + --ink: #221b15; + --muted: #6b5f55; + --accent: #b14d2b; + --panel: #fffaf1; + --border: #eadbc8; + --bg: #f5ead7; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + padding: 24px; + font-family: "Iowan Old Style", "Palatino Linotype", "Book Antiqua", Palatino, + "Times New Roman", serif; + color: var(--ink); + background: linear-gradient(160deg, #f8efe1, #efe0c9); +} + +.title-block { + margin-bottom: 16px; +} + +.title { + font-size: 26px; + font-weight: 700; +} + +.subtitle { + font-size: 13px; + color: var(--muted); +} + +.panel { + background: var(--panel); + border: 1px solid var(--border); + border-radius: 16px; + padding: 16px; + margin-bottom: 16px; + box-shadow: 0 18px 40px rgba(120, 85, 55, 0.12); +} + +.row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 12px; +} + +h2 { + margin: 0; + font-size: 16px; + text-transform: uppercase; + letter-spacing: 1px; + color: var(--muted); +} + +.field { + display: grid; + gap: 6px; + margin-bottom: 12px; +} + +label { + font-size: 12px; + text-transform: uppercase; + letter-spacing: 1px; + color: var(--muted); +} + +input, +textarea { + width: 100%; + padding: 10px 12px; + border-radius: 10px; + border: 1px solid var(--border); + background: #fffdf9; + font-family: inherit; + font-size: 13px; +} + +textarea { + resize: vertical; + min-height: 120px; +} + +.inline { + display: flex; + gap: 8px; +} + +button { + font-family: inherit; + border: none; + border-radius: 10px; + padding: 8px 12px; + cursor: pointer; + transition: transform 0.15s ease, box-shadow 0.15s ease; +} + +button:active { + transform: translateY(1px); +} + +.accent { + background: var(--accent); + color: #fff9f3; + box-shadow: 0 8px 20px rgba(177, 77, 43, 0.2); +} + +.ghost { + background: transparent; + border: 1px solid var(--border); +} + +.status { + font-size: 12px; + color: var(--accent); +} + +.tasks { + display: grid; + gap: 12px; +} + +.task-card { + padding: 12px; + border-radius: 12px; + border: 1px solid var(--border); + background: #fffefb; + display: grid; + gap: 8px; +} + +.task-actions { + display: flex; + gap: 8px; + justify-content: flex-end; +} diff --git a/settings.html b/settings.html new file mode 100644 index 0000000..1e131ae --- /dev/null +++ b/settings.html @@ -0,0 +1,54 @@ + + + + + + WWCompanion Settings + + + +
+
WWCompanion Settings
+
Configure prompts, resume, and API access
+
+ +
+
+

OpenAI

+ +
+
+ +
+ + +
+
+
+ + +
+
+
+ +
+

System Prompt

+ +
+ +
+

Resume

+ +
+ +
+
+

Task Presets

+ +
+
+
+ + + + diff --git a/settings.js b/settings.js new file mode 100644 index 0000000..77d7bc8 --- /dev/null +++ b/settings.js @@ -0,0 +1,148 @@ +const apiKeyInput = document.getElementById("apiKey"); +const modelInput = document.getElementById("model"); +const systemPromptInput = document.getElementById("systemPrompt"); +const resumeInput = document.getElementById("resume"); +const saveBtn = document.getElementById("saveBtn"); +const addTaskBtn = document.getElementById("addTaskBtn"); +const tasksContainer = document.getElementById("tasks"); +const statusEl = document.getElementById("status"); +const toggleKeyBtn = document.getElementById("toggleKey"); + +function getStorage(keys) { + return new Promise((resolve) => chrome.storage.local.get(keys, resolve)); +} + +function setStatus(message) { + statusEl.textContent = message; + if (!message) return; + setTimeout(() => { + if (statusEl.textContent === message) statusEl.textContent = ""; + }, 2000); +} + +function newTaskId() { + if (crypto?.randomUUID) return crypto.randomUUID(); + return `task-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`; +} + +function buildTaskCard(task) { + const card = document.createElement("div"); + card.className = "task-card"; + card.dataset.id = task.id || newTaskId(); + + const nameField = document.createElement("div"); + nameField.className = "field"; + const nameLabel = document.createElement("label"); + nameLabel.textContent = "Name"; + const nameInput = document.createElement("input"); + nameInput.type = "text"; + nameInput.value = task.name || ""; + nameInput.className = "task-name"; + nameField.appendChild(nameLabel); + nameField.appendChild(nameInput); + + const textField = document.createElement("div"); + textField.className = "field"; + const textLabel = document.createElement("label"); + textLabel.textContent = "Task prompt"; + const textArea = document.createElement("textarea"); + textArea.rows = 6; + textArea.value = task.text || ""; + textArea.className = "task-text"; + textField.appendChild(textLabel); + textField.appendChild(textArea); + + const actions = document.createElement("div"); + actions.className = "task-actions"; + const duplicateBtn = document.createElement("button"); + duplicateBtn.type = "button"; + duplicateBtn.className = "ghost"; + duplicateBtn.textContent = "Duplicate"; + const deleteBtn = document.createElement("button"); + deleteBtn.type = "button"; + deleteBtn.className = "ghost"; + deleteBtn.textContent = "Delete"; + + duplicateBtn.addEventListener("click", () => { + const copy = { + id: newTaskId(), + name: `${nameInput.value || "Untitled"} Copy`, + text: textArea.value + }; + const newCard = buildTaskCard(copy); + card.insertAdjacentElement("afterend", newCard); + }); + + deleteBtn.addEventListener("click", () => { + card.remove(); + }); + + actions.appendChild(duplicateBtn); + actions.appendChild(deleteBtn); + + card.appendChild(nameField); + card.appendChild(textField); + card.appendChild(actions); + + return card; +} + +function collectTasks() { + const cards = [...tasksContainer.querySelectorAll(".task-card")]; + return cards.map((card) => { + const nameInput = card.querySelector(".task-name"); + const textArea = card.querySelector(".task-text"); + return { + id: card.dataset.id || newTaskId(), + name: (nameInput?.value || "Untitled Task").trim(), + text: (textArea?.value || "").trim() + }; + }); +} + +async function loadSettings() { + const { apiKey = "", model = "", systemPrompt = "", resume = "", tasks = [] } = + await getStorage(["apiKey", "model", "systemPrompt", "resume", "tasks"]); + + apiKeyInput.value = apiKey; + modelInput.value = model; + systemPromptInput.value = systemPrompt; + resumeInput.value = resume; + + tasksContainer.innerHTML = ""; + if (!tasks.length) { + tasksContainer.appendChild( + buildTaskCard({ id: newTaskId(), name: "", text: "" }) + ); + return; + } + + for (const task of tasks) { + tasksContainer.appendChild(buildTaskCard(task)); + } +} + +async function saveSettings() { + const tasks = collectTasks(); + await chrome.storage.local.set({ + apiKey: apiKeyInput.value.trim(), + model: modelInput.value.trim(), + systemPrompt: systemPromptInput.value, + resume: resumeInput.value, + tasks + }); + setStatus("Saved."); +} + +saveBtn.addEventListener("click", () => void saveSettings()); +addTaskBtn.addEventListener("click", () => { + tasksContainer.appendChild(buildTaskCard({ id: newTaskId(), name: "", text: "" })); +}); + +toggleKeyBtn.addEventListener("click", () => { + const isPassword = apiKeyInput.type === "password"; + apiKeyInput.type = isPassword ? "text" : "password"; + toggleKeyBtn.textContent = isPassword ? "Hide" : "Show"; +}); + +loadSettings();