diff --git a/wwcompanion-extension/background.js b/wwcompanion-extension/background.js
index fb8b610..a76ab6e 100644
--- a/wwcompanion-extension/background.js
+++ b/wwcompanion-extension/background.js
@@ -15,6 +15,8 @@ const DEFAULT_TASKS = [
const DEFAULT_SETTINGS = {
apiKey: "",
+ apiKeys: [],
+ activeApiKeyId: "",
apiBaseUrl: "https://api.openai.com/v1",
apiKeyHeader: "Authorization",
apiKeyPrefix: "Bearer ",
@@ -80,6 +82,17 @@ chrome.runtime.onInstalled.addListener(async () => {
if (missing) updates[key] = value;
}
+ const hasApiKeys =
+ Array.isArray(stored.apiKeys) && stored.apiKeys.length > 0;
+
+ if (!hasApiKeys && stored.apiKey) {
+ const id = crypto?.randomUUID
+ ? crypto.randomUUID()
+ : `key-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
+ updates.apiKeys = [{ id, name: "Default", key: stored.apiKey }];
+ updates.activeApiKeyId = id;
+ }
+
if (Object.keys(updates).length) {
await chrome.storage.local.set(updates);
}
diff --git a/wwcompanion-extension/manifest.json b/wwcompanion-extension/manifest.json
index c0fd612..ad3f641 100644
--- a/wwcompanion-extension/manifest.json
+++ b/wwcompanion-extension/manifest.json
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "WWCompanion",
- "version": "0.2.3",
+ "version": "0.2.4",
"description": "AI companion for WaterlooWorks job postings.",
"permissions": ["storage", "activeTab"],
"host_permissions": ["https://waterlooworks.uwaterloo.ca/*"],
diff --git a/wwcompanion-extension/popup.js b/wwcompanion-extension/popup.js
index 293a044..e251a6b 100644
--- a/wwcompanion-extension/popup.js
+++ b/wwcompanion-extension/popup.js
@@ -428,24 +428,38 @@ async function handleAnalyze() {
return;
}
- const { apiKey, apiBaseUrl, apiKeyHeader, apiKeyPrefix, model, systemPrompt, resume } =
- await getStorage([
- "apiKey",
- "apiBaseUrl",
- "apiKeyHeader",
- "apiKeyPrefix",
- "model",
- "systemPrompt",
- "resume"
- ]);
+ const {
+ apiKeys = [],
+ activeApiKeyId = "",
+ apiBaseUrl,
+ apiKeyHeader,
+ apiKeyPrefix,
+ model,
+ systemPrompt,
+ resume
+ } = await getStorage([
+ "apiKeys",
+ "activeApiKeyId",
+ "apiBaseUrl",
+ "apiKeyHeader",
+ "apiKeyPrefix",
+ "model",
+ "systemPrompt",
+ "resume"
+ ]);
if (!apiBaseUrl) {
setStatus("Set an API base URL in Settings.");
return;
}
+ const resolvedKeys = Array.isArray(apiKeys) ? apiKeys : [];
+ const activeKey =
+ resolvedKeys.find((entry) => entry.id === activeApiKeyId) || resolvedKeys[0];
+ const apiKey = activeKey?.key || "";
+
if (apiKeyHeader && !apiKey) {
- setStatus("Add your API key in Settings.");
+ setStatus("Add an API key in Settings.");
return;
}
diff --git a/wwcompanion-extension/settings.css b/wwcompanion-extension/settings.css
index 3d0c33d..27bcc07 100644
--- a/wwcompanion-extension/settings.css
+++ b/wwcompanion-extension/settings.css
@@ -229,6 +229,30 @@ button:active {
gap: 8px;
}
+.api-keys {
+ display: grid;
+ gap: 12px;
+}
+
+.api-key-card {
+ padding: 12px;
+ border-radius: 12px;
+ border: 1px solid var(--border);
+ background: var(--card-bg);
+ display: grid;
+ gap: 8px;
+}
+
+.api-key-actions {
+ display: flex;
+ gap: 8px;
+ justify-content: flex-end;
+}
+
+.api-key-actions .delete {
+ color: #c0392b;
+}
+
@media (prefers-color-scheme: dark) {
:root:not([data-theme]),
:root[data-theme="system"] {
diff --git a/wwcompanion-extension/settings.html b/wwcompanion-extension/settings.html
index 4a144af..86babd3 100644
--- a/wwcompanion-extension/settings.html
+++ b/wwcompanion-extension/settings.html
@@ -30,11 +30,8 @@
-
-
-
-
-
+
+
@@ -59,6 +56,23 @@
+
+
+
+ ▸
+ ▾
+
+ API Keys
+
+
+
+
diff --git a/wwcompanion-extension/settings.js b/wwcompanion-extension/settings.js
index 1c1adf4..16d5da2 100644
--- a/wwcompanion-extension/settings.js
+++ b/wwcompanion-extension/settings.js
@@ -1,4 +1,3 @@
-const apiKeyInput = document.getElementById("apiKey");
const apiBaseUrlInput = document.getElementById("apiBaseUrl");
const apiKeyHeaderInput = document.getElementById("apiKeyHeader");
const apiKeyPrefixInput = document.getElementById("apiKeyPrefix");
@@ -6,10 +5,12 @@ const modelInput = document.getElementById("model");
const systemPromptInput = document.getElementById("systemPrompt");
const resumeInput = document.getElementById("resume");
const saveBtn = document.getElementById("saveBtn");
+const addApiKeyBtn = document.getElementById("addApiKeyBtn");
+const apiKeysContainer = document.getElementById("apiKeys");
+const activeApiKeySelect = document.getElementById("activeApiKeySelect");
const addTaskBtn = document.getElementById("addTaskBtn");
const tasksContainer = document.getElementById("tasks");
const statusEl = document.getElementById("status");
-const toggleKeyBtn = document.getElementById("toggleKey");
const themeSelect = document.getElementById("themeSelect");
const resetApiBtn = document.getElementById("resetApiBtn");
@@ -41,6 +42,118 @@ function newTaskId() {
return `task-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
}
+function newApiKeyId() {
+ if (crypto?.randomUUID) return crypto.randomUUID();
+ return `key-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
+}
+
+function buildApiKeyCard(entry) {
+ const card = document.createElement("div");
+ card.className = "api-key-card";
+ card.dataset.id = entry.id || newApiKeyId();
+
+ 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 = entry.name || "";
+ nameInput.className = "api-key-name";
+ nameField.appendChild(nameLabel);
+ nameField.appendChild(nameInput);
+
+ const keyField = document.createElement("div");
+ keyField.className = "field";
+ const keyLabel = document.createElement("label");
+ keyLabel.textContent = "Key";
+ const keyInline = document.createElement("div");
+ keyInline.className = "inline";
+ const keyInput = document.createElement("input");
+ keyInput.type = "password";
+ keyInput.autocomplete = "off";
+ keyInput.placeholder = "sk-...";
+ keyInput.value = entry.key || "";
+ keyInput.className = "api-key-value";
+ const showBtn = document.createElement("button");
+ showBtn.type = "button";
+ showBtn.className = "ghost";
+ showBtn.textContent = "Show";
+ showBtn.addEventListener("click", () => {
+ const isPassword = keyInput.type === "password";
+ keyInput.type = isPassword ? "text" : "password";
+ showBtn.textContent = isPassword ? "Hide" : "Show";
+ });
+ keyInline.appendChild(keyInput);
+ keyInline.appendChild(showBtn);
+ keyField.appendChild(keyLabel);
+ keyField.appendChild(keyInline);
+
+ const actions = document.createElement("div");
+ actions.className = "api-key-actions";
+ const deleteBtn = document.createElement("button");
+ deleteBtn.type = "button";
+ deleteBtn.className = "ghost delete";
+ deleteBtn.textContent = "Delete";
+ deleteBtn.addEventListener("click", () => {
+ card.remove();
+ updateApiKeySelect();
+ });
+ actions.appendChild(deleteBtn);
+
+ const updateSelect = () => updateApiKeySelect(activeApiKeySelect.value);
+ nameInput.addEventListener("input", updateSelect);
+ keyInput.addEventListener("input", updateSelect);
+
+ card.appendChild(nameField);
+ card.appendChild(keyField);
+ card.appendChild(actions);
+
+ return card;
+}
+
+function collectApiKeys() {
+ const cards = [...apiKeysContainer.querySelectorAll(".api-key-card")];
+ return cards.map((card) => {
+ const nameInput = card.querySelector(".api-key-name");
+ const keyInput = card.querySelector(".api-key-value");
+ return {
+ id: card.dataset.id || newApiKeyId(),
+ name: (nameInput?.value || "Default").trim(),
+ key: (keyInput?.value || "").trim()
+ };
+ });
+}
+
+function updateApiKeySelect(preferredId) {
+ const keys = collectApiKeys();
+ activeApiKeySelect.innerHTML = "";
+
+ if (!keys.length) {
+ const option = document.createElement("option");
+ option.value = "";
+ option.textContent = "No keys configured";
+ activeApiKeySelect.appendChild(option);
+ activeApiKeySelect.disabled = true;
+ return;
+ }
+
+ activeApiKeySelect.disabled = false;
+ const selectedId =
+ preferredId && keys.some((key) => key.id === preferredId)
+ ? preferredId
+ : keys[0].id;
+
+ for (const key of keys) {
+ const option = document.createElement("option");
+ option.value = key.id;
+ option.textContent = key.name || "Default";
+ activeApiKeySelect.appendChild(option);
+ }
+
+ activeApiKeySelect.value = selectedId;
+}
+
function buildTaskCard(task) {
const card = document.createElement("div");
card.className = "task-card";
@@ -192,6 +305,8 @@ function collectTasks() {
async function loadSettings() {
const {
apiKey = "",
+ apiKeys = [],
+ activeApiKeyId = "",
apiBaseUrl = "",
apiKeyHeader = "",
apiKeyPrefix = "",
@@ -202,6 +317,8 @@ async function loadSettings() {
theme = "system"
} = await getStorage([
"apiKey",
+ "apiKeys",
+ "activeApiKeyId",
"apiBaseUrl",
"apiKeyHeader",
"apiKeyPrefix",
@@ -212,7 +329,6 @@ async function loadSettings() {
"theme"
]);
- apiKeyInput.value = apiKey;
apiBaseUrlInput.value = apiBaseUrl;
apiKeyHeaderInput.value = apiKeyHeader;
apiKeyPrefixInput.value = apiKeyPrefix;
@@ -222,6 +338,29 @@ async function loadSettings() {
themeSelect.value = theme;
applyTheme(theme);
+ let resolvedKeys = Array.isArray(apiKeys) ? apiKeys : [];
+ let resolvedActiveId = activeApiKeyId;
+
+ if (!resolvedKeys.length && apiKey) {
+ const migrated = { id: newApiKeyId(), name: "Default", key: apiKey };
+ resolvedKeys = [migrated];
+ resolvedActiveId = migrated.id;
+ await chrome.storage.local.set({
+ apiKeys: resolvedKeys,
+ activeApiKeyId: resolvedActiveId
+ });
+ }
+
+ apiKeysContainer.innerHTML = "";
+ if (!resolvedKeys.length) {
+ apiKeysContainer.appendChild(buildApiKeyCard({ id: newApiKeyId(), name: "", key: "" }));
+ } else {
+ for (const entry of resolvedKeys) {
+ apiKeysContainer.appendChild(buildApiKeyCard(entry));
+ }
+ }
+ updateApiKeySelect(resolvedActiveId);
+
tasksContainer.innerHTML = "";
if (!tasks.length) {
tasksContainer.appendChild(
@@ -239,11 +378,17 @@ async function loadSettings() {
async function saveSettings() {
const tasks = collectTasks();
+ const apiKeys = collectApiKeys();
+ const activeApiKeyId =
+ apiKeys.find((entry) => entry.id === activeApiKeySelect.value)?.id ||
+ apiKeys[0]?.id ||
+ "";
await chrome.storage.local.set({
- apiKey: apiKeyInput.value.trim(),
apiBaseUrl: apiBaseUrlInput.value.trim(),
apiKeyHeader: apiKeyHeaderInput.value.trim(),
apiKeyPrefix: apiKeyPrefixInput.value,
+ apiKeys,
+ activeApiKeyId,
model: modelInput.value.trim(),
systemPrompt: systemPromptInput.value,
resume: resumeInput.value,
@@ -265,10 +410,19 @@ addTaskBtn.addEventListener("click", () => {
updateTaskControls();
});
-toggleKeyBtn.addEventListener("click", () => {
- const isPassword = apiKeyInput.type === "password";
- apiKeyInput.type = isPassword ? "text" : "password";
- toggleKeyBtn.textContent = isPassword ? "Hide" : "Show";
+addApiKeyBtn.addEventListener("click", () => {
+ const newCard = buildApiKeyCard({ id: newApiKeyId(), name: "", key: "" });
+ const first = apiKeysContainer.firstElementChild;
+ if (first) {
+ apiKeysContainer.insertBefore(newCard, first);
+ } else {
+ apiKeysContainer.appendChild(newCard);
+ }
+ updateApiKeySelect(activeApiKeySelect.value);
+});
+
+activeApiKeySelect.addEventListener("change", () => {
+ updateApiKeySelect(activeApiKeySelect.value);
});
themeSelect.addEventListener("change", () => applyTheme(themeSelect.value));