UI/UX changes, added feature to switch to other LLM APIs
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
29
popup.css
29
popup.css
@@ -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;
|
||||||
|
|||||||
@@ -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 & 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>
|
||||||
|
|||||||
38
popup.js
38
popup.js
@@ -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();
|
||||||
|
|
||||||
|
|||||||
39
settings.css
39
settings.css
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
108
settings.js
108
settings.js
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user