UI/UX changes, added feature to switch to other LLM APIs

This commit is contained in:
2026-01-16 22:56:11 -05:00
parent 5829aa0269
commit 322a3f4488
7 changed files with 295 additions and 32 deletions

View File

@@ -9,6 +9,9 @@ const DEFAULT_TASKS = [
const DEFAULT_SETTINGS = { const DEFAULT_SETTINGS = {
apiKey: "", apiKey: "",
apiBaseUrl: "https://api.openai.com/v1",
apiKeyHeader: "Authorization",
apiKeyPrefix: "Bearer ",
model: "gpt-4o-mini", model: "gpt-4o-mini",
systemPrompt: systemPrompt:
"You are a precise, honest assistant. Be concise, highlight uncertainties, and avoid inventing details.", "You are a precise, honest assistant. Be concise, highlight uncertainties, and avoid inventing details.",
@@ -89,10 +92,25 @@ function buildUserMessage(resume, task, posting) {
} }
async function handleAnalysisRequest(port, payload, signal) { async function handleAnalysisRequest(port, payload, signal) {
const { apiKey, model, systemPrompt, resume, taskText, postingText } = payload || {}; const {
apiKey,
apiBaseUrl,
apiKeyHeader,
apiKeyPrefix,
model,
systemPrompt,
resume,
taskText,
postingText
} = payload || {};
if (!apiKey) { if (!apiBaseUrl) {
port.postMessage({ type: "ERROR", message: "Missing OpenAI API key." }); port.postMessage({ type: "ERROR", message: "Missing API base URL." });
return;
}
if (apiKeyHeader && !apiKey) {
port.postMessage({ type: "ERROR", message: "Missing API key." });
return; return;
} }
@@ -115,6 +133,9 @@ async function handleAnalysisRequest(port, payload, signal) {
await streamChatCompletion({ await streamChatCompletion({
apiKey, apiKey,
apiBaseUrl,
apiKeyHeader,
apiKeyPrefix,
model, model,
systemPrompt: systemPrompt || "", systemPrompt: systemPrompt || "",
userMessage, userMessage,
@@ -125,20 +146,49 @@ async function handleAnalysisRequest(port, payload, signal) {
port.postMessage({ type: "DONE" }); port.postMessage({ type: "DONE" });
} }
function buildChatUrl(apiBaseUrl) {
const trimmed = (apiBaseUrl || "").trim().replace(/\/+$/, "");
if (!trimmed) return "";
if (trimmed.endsWith("/chat/completions")) return trimmed;
return `${trimmed}/chat/completions`;
}
function buildAuthHeader(apiKeyHeader, apiKeyPrefix, apiKey) {
if (!apiKeyHeader) return null;
return {
name: apiKeyHeader,
value: `${apiKeyPrefix || ""}${apiKey || ""}`
};
}
async function streamChatCompletion({ async function streamChatCompletion({
apiKey, apiKey,
apiBaseUrl,
apiKeyHeader,
apiKeyPrefix,
model, model,
systemPrompt, systemPrompt,
userMessage, userMessage,
signal, signal,
onDelta onDelta
}) { }) {
const response = await fetch("https://api.openai.com/v1/chat/completions", { const chatUrl = buildChatUrl(apiBaseUrl);
if (!chatUrl) {
throw new Error("Invalid API base URL.");
}
const headers = {
"Content-Type": "application/json"
};
const authHeader = buildAuthHeader(apiKeyHeader, apiKeyPrefix, apiKey);
if (authHeader) {
headers[authHeader.name] = authHeader.value;
}
const response = await fetch(chatUrl, {
method: "POST", method: "POST",
headers: { headers,
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`
},
body: JSON.stringify({ body: JSON.stringify({
model, model,
stream: true, stream: true,

View File

@@ -128,16 +128,31 @@ select {
} }
.button-row { .button-row {
display: flex; display: grid;
grid-template-columns: minmax(64px, 0.8fr) minmax(0, 1.4fr) minmax(64px, 0.8fr);
gap: 8px; gap: 8px;
} }
.button-row button { .button-row button {
flex: 1; white-space: nowrap;
} }
.button-row .ghost { .button-row .primary {
flex: 0 0 64px; padding: 6px 8px;
}
.stop-row {
display: flex;
justify-content: stretch;
margin-top: 4px;
}
.stop-row button {
width: 100%;
}
.hidden {
display: none;
} }
button { button {
@@ -177,6 +192,12 @@ button:active {
border: 1px solid var(--border); border: 1px solid var(--border);
} }
.stop-row .ghost {
background: #c0392b;
border-color: #c0392b;
color: #fff6f2;
}
.meta { .meta {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;

View File

@@ -22,8 +22,11 @@
<select id="taskSelect"></select> <select id="taskSelect"></select>
</div> </div>
<div class="button-row"> <div class="button-row">
<button id="extractBtn" class="primary">Extract Posting</button> <button id="extractBtn" class="primary">Extract</button>
<button id="analyzeBtn" class="accent">Analyze</button> <button id="extractRunBtn" class="accent">Extract &amp; Run</button>
<button id="analyzeBtn" class="ghost">Run Task</button>
</div>
<div id="stopRow" class="stop-row hidden">
<button id="abortBtn" class="ghost" disabled>Stop</button> <button id="abortBtn" class="ghost" disabled>Stop</button>
</div> </div>
</div> </div>

View File

@@ -1,6 +1,9 @@
const extractBtn = document.getElementById("extractBtn"); const extractBtn = document.getElementById("extractBtn");
const analyzeBtn = document.getElementById("analyzeBtn"); const analyzeBtn = document.getElementById("analyzeBtn");
const abortBtn = document.getElementById("abortBtn"); const abortBtn = document.getElementById("abortBtn");
const extractRunBtn = document.getElementById("extractRunBtn");
const stopRow = document.getElementById("stopRow");
const buttonRow = document.querySelector(".button-row");
const taskSelect = document.getElementById("taskSelect"); const taskSelect = document.getElementById("taskSelect");
const outputEl = document.getElementById("output"); const outputEl = document.getElementById("output");
const statusEl = document.getElementById("status"); const statusEl = document.getElementById("status");
@@ -226,6 +229,11 @@ function setAnalyzing(isAnalyzing) {
analyzeBtn.disabled = isAnalyzing; analyzeBtn.disabled = isAnalyzing;
abortBtn.disabled = !isAnalyzing; abortBtn.disabled = !isAnalyzing;
extractBtn.disabled = isAnalyzing; extractBtn.disabled = isAnalyzing;
extractRunBtn.disabled = isAnalyzing;
if (buttonRow && stopRow) {
buttonRow.classList.toggle("hidden", isAnalyzing);
stopRow.classList.toggle("hidden", !isAnalyzing);
}
} }
function updatePostingCount() { function updatePostingCount() {
@@ -333,15 +341,17 @@ async function handleExtract() {
const response = await sendToActiveTab({ type: "EXTRACT_POSTING" }); const response = await sendToActiveTab({ type: "EXTRACT_POSTING" });
if (!response?.ok) { if (!response?.ok) {
setStatus(response?.error || "No posting detected."); setStatus(response?.error || "No posting detected.");
return; return false;
} }
state.postingText = response.sanitized || ""; state.postingText = response.sanitized || "";
updatePostingCount(); updatePostingCount();
updatePromptCount(0); updatePromptCount(0);
setStatus("Posting extracted."); setStatus("Posting extracted.");
return true;
} catch (error) { } catch (error) {
setStatus(error.message || "Unable to extract posting."); setStatus(error.message || "Unable to extract posting.");
return false;
} }
} }
@@ -358,15 +368,24 @@ async function handleAnalyze() {
return; return;
} }
const { apiKey, model, systemPrompt, resume } = await getStorage([ const { apiKey, apiBaseUrl, apiKeyHeader, apiKeyPrefix, model, systemPrompt, resume } =
await getStorage([
"apiKey", "apiKey",
"apiBaseUrl",
"apiKeyHeader",
"apiKeyPrefix",
"model", "model",
"systemPrompt", "systemPrompt",
"resume" "resume"
]); ]);
if (!apiKey) { if (!apiBaseUrl) {
setStatus("Add your OpenAI API key in Settings."); setStatus("Set an API base URL in Settings.");
return;
}
if (apiKeyHeader && !apiKey) {
setStatus("Add your API key in Settings.");
return; return;
} }
@@ -388,6 +407,9 @@ async function handleAnalyze() {
type: "START_ANALYSIS", type: "START_ANALYSIS",
payload: { payload: {
apiKey, apiKey,
apiBaseUrl,
apiKeyHeader,
apiKeyPrefix,
model, model,
systemPrompt: systemPrompt || "", systemPrompt: systemPrompt || "",
resume: resume || "", resume: resume || "",
@@ -397,6 +419,12 @@ async function handleAnalyze() {
}); });
} }
async function handleExtractAndAnalyze() {
const extracted = await handleExtract();
if (!extracted) return;
await handleAnalyze();
}
function handleAbort() { function handleAbort() {
if (!state.port) return; if (!state.port) return;
state.port.postMessage({ type: "ABORT_ANALYSIS" }); state.port.postMessage({ type: "ABORT_ANALYSIS" });
@@ -406,12 +434,14 @@ function handleAbort() {
extractBtn.addEventListener("click", handleExtract); extractBtn.addEventListener("click", handleExtract);
analyzeBtn.addEventListener("click", handleAnalyze); analyzeBtn.addEventListener("click", handleAnalyze);
extractRunBtn.addEventListener("click", handleExtractAndAnalyze);
abortBtn.addEventListener("click", handleAbort); abortBtn.addEventListener("click", handleAbort);
settingsBtn.addEventListener("click", () => chrome.runtime.openOptionsPage()); settingsBtn.addEventListener("click", () => chrome.runtime.openOptionsPage());
updatePostingCount(); updatePostingCount();
updatePromptCount(0); updatePromptCount(0);
renderOutput(); renderOutput();
setAnalyzing(false);
loadTasks(); loadTasks();
loadTheme(); loadTheme();

View File

@@ -46,6 +46,14 @@ body {
margin-bottom: 16px; margin-bottom: 16px;
} }
.page-bar {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
.title { .title {
font-size: 26px; font-size: 26px;
font-weight: 700; font-weight: 700;
@@ -73,6 +81,13 @@ body {
margin-bottom: 12px; margin-bottom: 12px;
} }
.row-title {
display: flex;
align-items: baseline;
gap: 8px;
flex-wrap: wrap;
}
h2 { h2 {
margin: 0; margin: 0;
font-size: 16px; font-size: 16px;
@@ -81,6 +96,17 @@ h2 {
color: var(--muted); color: var(--muted);
} }
.hint {
font-size: 12px;
text-transform: none;
letter-spacing: 0;
color: var(--muted);
}
.hint-accent {
color: var(--accent);
}
.field { .field {
display: grid; display: grid;
gap: 6px; gap: 6px;
@@ -179,6 +205,17 @@ button:active {
.task-actions { .task-actions {
display: flex; display: flex;
gap: 8px; gap: 6px;
justify-content: flex-end; justify-content: flex-end;
} }
.icon-btn {
width: 34px;
padding: 6px 0;
font-weight: 700;
line-height: 1;
}
.icon-btn.delete {
color: #c0392b;
}

View File

@@ -11,11 +11,15 @@
<div class="title">WWCompanion Settings</div> <div class="title">WWCompanion Settings</div>
<div class="subtitle">Configure prompts, resume, and API access</div> <div class="subtitle">Configure prompts, resume, and API access</div>
</header> </header>
<div class="page-bar">
<div id="status" class="status"></div>
<button id="saveBtn" class="accent">Save Settings</button>
</div>
<section class="panel"> <section class="panel">
<div class="row"> <div class="row">
<h2>OpenAI</h2> <h2>API</h2>
<button id="saveBtn" class="accent">Save Settings</button> <button id="resetApiBtn" class="ghost" type="button">Reset to OpenAI</button>
</div> </div>
<div class="field"> <div class="field">
<label for="apiKey">API Key</label> <label for="apiKey">API Key</label>
@@ -24,11 +28,26 @@
<button id="toggleKey" class="ghost" type="button">Show</button> <button id="toggleKey" class="ghost" type="button">Show</button>
</div> </div>
</div> </div>
<div class="field">
<label for="apiBaseUrl">API Base URL</label>
<input
id="apiBaseUrl"
type="text"
placeholder="https://api.openai.com/v1"
/>
</div>
<div class="field">
<label for="apiKeyHeader">API Key Header</label>
<input id="apiKeyHeader" type="text" placeholder="Authorization" />
</div>
<div class="field">
<label for="apiKeyPrefix">API Key Prefix</label>
<input id="apiKeyPrefix" type="text" placeholder="Bearer " />
</div>
<div class="field"> <div class="field">
<label for="model">Model name</label> <label for="model">Model name</label>
<input id="model" type="text" placeholder="gpt-4o-mini" /> <input id="model" type="text" placeholder="gpt-4o-mini" />
</div> </div>
<div id="status" class="status"></div>
</section> </section>
<section class="panel"> <section class="panel">
@@ -55,7 +74,10 @@
<section class="panel"> <section class="panel">
<div class="row"> <div class="row">
<div class="row-title">
<h2>Task Presets</h2> <h2>Task Presets</h2>
<span class="hint hint-accent">Top task is the default</span>
</div>
<button id="addTaskBtn" class="ghost" type="button">Add Task</button> <button id="addTaskBtn" class="ghost" type="button">Add Task</button>
</div> </div>
<div id="tasks" class="tasks"></div> <div id="tasks" class="tasks"></div>

View File

@@ -1,4 +1,7 @@
const apiKeyInput = document.getElementById("apiKey"); const apiKeyInput = document.getElementById("apiKey");
const apiBaseUrlInput = document.getElementById("apiBaseUrl");
const apiKeyHeaderInput = document.getElementById("apiKeyHeader");
const apiKeyPrefixInput = document.getElementById("apiKeyPrefix");
const modelInput = document.getElementById("model"); const modelInput = document.getElementById("model");
const systemPromptInput = document.getElementById("systemPrompt"); const systemPromptInput = document.getElementById("systemPrompt");
const resumeInput = document.getElementById("resume"); const resumeInput = document.getElementById("resume");
@@ -8,6 +11,13 @@ const tasksContainer = document.getElementById("tasks");
const statusEl = document.getElementById("status"); const statusEl = document.getElementById("status");
const toggleKeyBtn = document.getElementById("toggleKey"); const toggleKeyBtn = document.getElementById("toggleKey");
const themeSelect = document.getElementById("themeSelect"); const themeSelect = document.getElementById("themeSelect");
const resetApiBtn = document.getElementById("resetApiBtn");
const OPENAI_DEFAULTS = {
apiBaseUrl: "https://api.openai.com/v1",
apiKeyHeader: "Authorization",
apiKeyPrefix: "Bearer "
};
function getStorage(keys) { function getStorage(keys) {
return new Promise((resolve) => chrome.storage.local.get(keys, resolve)); return new Promise((resolve) => chrome.storage.local.get(keys, resolve));
@@ -60,14 +70,56 @@ function buildTaskCard(task) {
const actions = document.createElement("div"); const actions = document.createElement("div");
actions.className = "task-actions"; actions.className = "task-actions";
const moveUpBtn = document.createElement("button");
moveUpBtn.type = "button";
moveUpBtn.className = "ghost icon-btn move-up";
moveUpBtn.textContent = "↑";
moveUpBtn.setAttribute("aria-label", "Move task up");
moveUpBtn.setAttribute("title", "Move up");
const moveDownBtn = document.createElement("button");
moveDownBtn.type = "button";
moveDownBtn.className = "ghost icon-btn move-down";
moveDownBtn.textContent = "↓";
moveDownBtn.setAttribute("aria-label", "Move task down");
moveDownBtn.setAttribute("title", "Move down");
const addBelowBtn = document.createElement("button");
addBelowBtn.type = "button";
addBelowBtn.className = "ghost icon-btn add-below";
addBelowBtn.textContent = "";
addBelowBtn.setAttribute("aria-label", "Add task below");
addBelowBtn.setAttribute("title", "Add below");
const duplicateBtn = document.createElement("button"); const duplicateBtn = document.createElement("button");
duplicateBtn.type = "button"; duplicateBtn.type = "button";
duplicateBtn.className = "ghost"; duplicateBtn.className = "ghost duplicate";
duplicateBtn.textContent = "Duplicate"; duplicateBtn.textContent = "Duplicate";
duplicateBtn.setAttribute("aria-label", "Duplicate task");
duplicateBtn.setAttribute("title", "Duplicate");
const deleteBtn = document.createElement("button"); const deleteBtn = document.createElement("button");
deleteBtn.type = "button"; deleteBtn.type = "button";
deleteBtn.className = "ghost"; deleteBtn.className = "ghost icon-btn delete";
deleteBtn.textContent = "Delete"; deleteBtn.textContent = "🗑";
deleteBtn.setAttribute("aria-label", "Delete task");
deleteBtn.setAttribute("title", "Delete");
moveUpBtn.addEventListener("click", () => {
const previous = card.previousElementSibling;
if (!previous) return;
tasksContainer.insertBefore(card, previous);
updateTaskControls();
});
moveDownBtn.addEventListener("click", () => {
const next = card.nextElementSibling;
if (!next) return;
tasksContainer.insertBefore(card, next.nextElementSibling);
updateTaskControls();
});
addBelowBtn.addEventListener("click", () => {
const newCard = buildTaskCard({ id: newTaskId(), name: "", text: "" });
card.insertAdjacentElement("afterend", newCard);
updateTaskControls();
});
duplicateBtn.addEventListener("click", () => { duplicateBtn.addEventListener("click", () => {
const copy = { const copy = {
@@ -77,12 +129,17 @@ function buildTaskCard(task) {
}; };
const newCard = buildTaskCard(copy); const newCard = buildTaskCard(copy);
card.insertAdjacentElement("afterend", newCard); card.insertAdjacentElement("afterend", newCard);
updateTaskControls();
}); });
deleteBtn.addEventListener("click", () => { deleteBtn.addEventListener("click", () => {
card.remove(); card.remove();
updateTaskControls();
}); });
actions.appendChild(moveUpBtn);
actions.appendChild(moveDownBtn);
actions.appendChild(addBelowBtn);
actions.appendChild(duplicateBtn); actions.appendChild(duplicateBtn);
actions.appendChild(deleteBtn); actions.appendChild(deleteBtn);
@@ -93,6 +150,16 @@ function buildTaskCard(task) {
return card; return card;
} }
function updateTaskControls() {
const cards = [...tasksContainer.querySelectorAll(".task-card")];
cards.forEach((card, index) => {
const moveUpBtn = card.querySelector(".move-up");
const moveDownBtn = card.querySelector(".move-down");
if (moveUpBtn) moveUpBtn.disabled = index === 0;
if (moveDownBtn) moveDownBtn.disabled = index === cards.length - 1;
});
}
function collectTasks() { function collectTasks() {
const cards = [...tasksContainer.querySelectorAll(".task-card")]; const cards = [...tasksContainer.querySelectorAll(".task-card")];
return cards.map((card) => { return cards.map((card) => {
@@ -109,14 +176,30 @@ function collectTasks() {
async function loadSettings() { async function loadSettings() {
const { const {
apiKey = "", apiKey = "",
apiBaseUrl = "",
apiKeyHeader = "",
apiKeyPrefix = "",
model = "", model = "",
systemPrompt = "", systemPrompt = "",
resume = "", resume = "",
tasks = [], tasks = [],
theme = "system" theme = "system"
} = await getStorage(["apiKey", "model", "systemPrompt", "resume", "tasks", "theme"]); } = await getStorage([
"apiKey",
"apiBaseUrl",
"apiKeyHeader",
"apiKeyPrefix",
"model",
"systemPrompt",
"resume",
"tasks",
"theme"
]);
apiKeyInput.value = apiKey; apiKeyInput.value = apiKey;
apiBaseUrlInput.value = apiBaseUrl;
apiKeyHeaderInput.value = apiKeyHeader;
apiKeyPrefixInput.value = apiKeyPrefix;
modelInput.value = model; modelInput.value = model;
systemPromptInput.value = systemPrompt; systemPromptInput.value = systemPrompt;
resumeInput.value = resume; resumeInput.value = resume;
@@ -128,18 +211,23 @@ async function loadSettings() {
tasksContainer.appendChild( tasksContainer.appendChild(
buildTaskCard({ id: newTaskId(), name: "", text: "" }) buildTaskCard({ id: newTaskId(), name: "", text: "" })
); );
updateTaskControls();
return; return;
} }
for (const task of tasks) { for (const task of tasks) {
tasksContainer.appendChild(buildTaskCard(task)); tasksContainer.appendChild(buildTaskCard(task));
} }
updateTaskControls();
} }
async function saveSettings() { async function saveSettings() {
const tasks = collectTasks(); const tasks = collectTasks();
await chrome.storage.local.set({ await chrome.storage.local.set({
apiKey: apiKeyInput.value.trim(), apiKey: apiKeyInput.value.trim(),
apiBaseUrl: apiBaseUrlInput.value.trim(),
apiKeyHeader: apiKeyHeaderInput.value.trim(),
apiKeyPrefix: apiKeyPrefixInput.value,
model: modelInput.value.trim(), model: modelInput.value.trim(),
systemPrompt: systemPromptInput.value, systemPrompt: systemPromptInput.value,
resume: resumeInput.value, resume: resumeInput.value,
@@ -152,6 +240,7 @@ async function saveSettings() {
saveBtn.addEventListener("click", () => void saveSettings()); saveBtn.addEventListener("click", () => void saveSettings());
addTaskBtn.addEventListener("click", () => { addTaskBtn.addEventListener("click", () => {
tasksContainer.appendChild(buildTaskCard({ id: newTaskId(), name: "", text: "" })); tasksContainer.appendChild(buildTaskCard({ id: newTaskId(), name: "", text: "" }));
updateTaskControls();
}); });
toggleKeyBtn.addEventListener("click", () => { toggleKeyBtn.addEventListener("click", () => {
@@ -161,5 +250,16 @@ toggleKeyBtn.addEventListener("click", () => {
}); });
themeSelect.addEventListener("change", () => applyTheme(themeSelect.value)); themeSelect.addEventListener("change", () => applyTheme(themeSelect.value));
resetApiBtn.addEventListener("click", async () => {
apiBaseUrlInput.value = OPENAI_DEFAULTS.apiBaseUrl;
apiKeyHeaderInput.value = OPENAI_DEFAULTS.apiKeyHeader;
apiKeyPrefixInput.value = OPENAI_DEFAULTS.apiKeyPrefix;
await chrome.storage.local.set({
apiBaseUrl: OPENAI_DEFAULTS.apiBaseUrl,
apiKeyHeader: OPENAI_DEFAULTS.apiKeyHeader,
apiKeyPrefix: OPENAI_DEFAULTS.apiKeyPrefix
});
setStatus("OpenAI defaults restored.");
});
loadSettings(); loadSettings();