Files
wwcompanion/wwcompanion-extension/settings.js
2026-01-17 16:12:41 -05:00

442 lines
14 KiB
JavaScript

const apiBaseUrlInput = document.getElementById("apiBaseUrl");
const apiKeyHeaderInput = document.getElementById("apiKeyHeader");
const apiKeyPrefixInput = document.getElementById("apiKeyPrefix");
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 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) {
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 applyTheme(theme) {
const value = theme || "system";
document.documentElement.dataset.theme = value;
}
function newTaskId() {
if (crypto?.randomUUID) return crypto.randomUUID();
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";
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 moveTopBtn = document.createElement("button");
moveTopBtn.type = "button";
moveTopBtn.className = "ghost move-top";
moveTopBtn.textContent = "Top";
moveTopBtn.setAttribute("aria-label", "Move task to top");
moveTopBtn.setAttribute("title", "Move to top");
const moveUpBtn = document.createElement("button");
moveUpBtn.type = "button";
moveUpBtn.className = "ghost move-up";
moveUpBtn.textContent = "Up";
moveUpBtn.setAttribute("aria-label", "Move task up");
moveUpBtn.setAttribute("title", "Move up");
const moveDownBtn = document.createElement("button");
moveDownBtn.type = "button";
moveDownBtn.className = "ghost move-down";
moveDownBtn.textContent = "Down";
moveDownBtn.setAttribute("aria-label", "Move task down");
moveDownBtn.setAttribute("title", "Move down");
const addBelowBtn = document.createElement("button");
addBelowBtn.type = "button";
addBelowBtn.className = "ghost add-below";
addBelowBtn.textContent = "Add";
addBelowBtn.setAttribute("aria-label", "Add task below");
addBelowBtn.setAttribute("title", "Add below");
const duplicateBtn = document.createElement("button");
duplicateBtn.type = "button";
duplicateBtn.className = "ghost duplicate";
duplicateBtn.textContent = "Duplicate";
duplicateBtn.setAttribute("aria-label", "Duplicate task");
duplicateBtn.setAttribute("title", "Duplicate");
const deleteBtn = document.createElement("button");
deleteBtn.type = "button";
deleteBtn.className = "ghost delete";
deleteBtn.textContent = "Delete";
deleteBtn.setAttribute("aria-label", "Delete task");
deleteBtn.setAttribute("title", "Delete");
moveTopBtn.addEventListener("click", () => {
const first = tasksContainer.firstElementChild;
if (!first || first === card) return;
tasksContainer.insertBefore(card, first);
updateTaskControls();
});
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", () => {
const copy = {
id: newTaskId(),
name: `${nameInput.value || "Untitled"} Copy`,
text: textArea.value
};
const newCard = buildTaskCard(copy);
card.insertAdjacentElement("afterend", newCard);
updateTaskControls();
});
deleteBtn.addEventListener("click", () => {
card.remove();
updateTaskControls();
});
actions.appendChild(moveTopBtn);
actions.appendChild(moveUpBtn);
actions.appendChild(moveDownBtn);
actions.appendChild(addBelowBtn);
actions.appendChild(duplicateBtn);
actions.appendChild(deleteBtn);
card.appendChild(nameField);
card.appendChild(textField);
card.appendChild(actions);
return card;
}
function updateTaskControls() {
const cards = [...tasksContainer.querySelectorAll(".task-card")];
cards.forEach((card, index) => {
const moveTopBtn = card.querySelector(".move-top");
const moveUpBtn = card.querySelector(".move-up");
const moveDownBtn = card.querySelector(".move-down");
if (moveTopBtn) moveTopBtn.disabled = index === 0;
if (moveUpBtn) moveUpBtn.disabled = index === 0;
if (moveDownBtn) moveDownBtn.disabled = index === cards.length - 1;
});
}
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 = "",
apiKeys = [],
activeApiKeyId = "",
apiBaseUrl = "",
apiKeyHeader = "",
apiKeyPrefix = "",
model = "",
systemPrompt = "",
resume = "",
tasks = [],
theme = "system"
} = await getStorage([
"apiKey",
"apiKeys",
"activeApiKeyId",
"apiBaseUrl",
"apiKeyHeader",
"apiKeyPrefix",
"model",
"systemPrompt",
"resume",
"tasks",
"theme"
]);
apiBaseUrlInput.value = apiBaseUrl;
apiKeyHeaderInput.value = apiKeyHeader;
apiKeyPrefixInput.value = apiKeyPrefix;
modelInput.value = model;
systemPromptInput.value = systemPrompt;
resumeInput.value = resume;
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(
buildTaskCard({ id: newTaskId(), name: "", text: "" })
);
updateTaskControls();
return;
}
for (const task of tasks) {
tasksContainer.appendChild(buildTaskCard(task));
}
updateTaskControls();
}
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({
apiBaseUrl: apiBaseUrlInput.value.trim(),
apiKeyHeader: apiKeyHeaderInput.value.trim(),
apiKeyPrefix: apiKeyPrefixInput.value,
apiKeys,
activeApiKeyId,
model: modelInput.value.trim(),
systemPrompt: systemPromptInput.value,
resume: resumeInput.value,
tasks,
theme: themeSelect.value
});
setStatus("Saved.");
}
saveBtn.addEventListener("click", () => void saveSettings());
addTaskBtn.addEventListener("click", () => {
const newCard = buildTaskCard({ id: newTaskId(), name: "", text: "" });
const first = tasksContainer.firstElementChild;
if (first) {
tasksContainer.insertBefore(newCard, first);
} else {
tasksContainer.appendChild(newCard);
}
updateTaskControls();
});
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));
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();