Files
wwcompanion/wwcompanion-extension/settings.js
2026-01-17 22:38:36 +00:00

1288 lines
42 KiB
JavaScript

const resumeInput = document.getElementById("resume");
const saveBtn = document.getElementById("saveBtn");
const addApiConfigBtn = document.getElementById("addApiConfigBtn");
const apiConfigsContainer = document.getElementById("apiConfigs");
const addApiKeyBtn = document.getElementById("addApiKeyBtn");
const apiKeysContainer = document.getElementById("apiKeys");
const addEnvConfigBtn = document.getElementById("addEnvConfigBtn");
const envConfigsContainer = document.getElementById("envConfigs");
const addTaskBtn = document.getElementById("addTaskBtn");
const tasksContainer = document.getElementById("tasks");
const statusEl = document.getElementById("status");
const themeSelect = document.getElementById("themeSelect");
const OPENAI_DEFAULTS = {
apiBaseUrl: "https://api.openai.com/v1",
apiKeyHeader: "Authorization",
apiKeyPrefix: "Bearer "
};
const DEFAULT_MODEL = "gpt-4o-mini";
const DEFAULT_SYSTEM_PROMPT =
"You are a precise, honest assistant. Be concise and avoid inventing details, be critical about evaluations. You should put in a small summary of all the sections at the end. You should answer in no longer than 3 sections including the summary. And remember to bold or italicize key points.";
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 newApiConfigId() {
if (crypto?.randomUUID) return crypto.randomUUID();
return `config-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
}
function newEnvConfigId() {
if (crypto?.randomUUID) return crypto.randomUUID();
return `env-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
}
function buildChatUrlFromBase(baseUrl) {
const trimmed = (baseUrl || "").trim().replace(/\/+$/, "");
if (!trimmed) return "";
if (trimmed.endsWith("/chat/completions")) return trimmed;
return `${trimmed}/chat/completions`;
}
function collectNames(container, selector) {
if (!container) return [];
return [...container.querySelectorAll(selector)]
.map((input) => (input.value || "").trim())
.filter(Boolean);
}
function buildUniqueDefaultName(names) {
const lower = new Set(names.map((name) => name.toLowerCase()));
if (!lower.has("default")) return "Default";
let index = 2;
while (lower.has(`default-${index}`)) {
index += 1;
}
return `Default-${index}`;
}
function ensureUniqueName(desired, existingNames) {
const trimmed = (desired || "").trim();
const lowerNames = existingNames.map((name) => name.toLowerCase());
if (trimmed && !lowerNames.includes(trimmed.toLowerCase())) {
return trimmed;
}
return buildUniqueDefaultName(existingNames);
}
function getTopEnvId() {
return collectEnvConfigs()[0]?.id || "";
}
function setApiConfigAdvanced(card, isAdvanced) {
card.classList.toggle("is-advanced", isAdvanced);
card.dataset.mode = isAdvanced ? "advanced" : "basic";
const basicFields = card.querySelectorAll(
".basic-only input, .basic-only textarea"
);
const advancedFields = card.querySelectorAll(
".advanced-only input, .advanced-only textarea"
);
basicFields.forEach((field) => {
field.disabled = isAdvanced;
});
advancedFields.forEach((field) => {
field.disabled = !isAdvanced;
});
const resetBtn = card.querySelector(".reset-openai");
if (resetBtn) resetBtn.disabled = false;
const advancedBtn = card.querySelector(".advanced-toggle");
if (advancedBtn) advancedBtn.disabled = isAdvanced;
}
function readApiConfigFromCard(card) {
const nameInput = card.querySelector(".api-config-name");
const keySelect = card.querySelector(".api-config-key-select");
const baseInput = card.querySelector(".api-config-base");
const headerInput = card.querySelector(".api-config-header");
const prefixInput = card.querySelector(".api-config-prefix");
const modelInput = card.querySelector(".api-config-model");
const urlInput = card.querySelector(".api-config-url");
const templateInput = card.querySelector(".api-config-template");
const isAdvanced = card.classList.contains("is-advanced");
return {
id: card.dataset.id || newApiConfigId(),
name: (nameInput?.value || "Default").trim(),
apiKeyId: keySelect?.value || "",
apiBaseUrl: (baseInput?.value || "").trim(),
apiKeyHeader: (headerInput?.value || "").trim(),
apiKeyPrefix: prefixInput?.value || "",
model: (modelInput?.value || "").trim(),
apiUrl: (urlInput?.value || "").trim(),
requestTemplate: (templateInput?.value || "").trim(),
advanced: isAdvanced
};
}
function buildApiConfigCard(config) {
const card = document.createElement("div");
card.className = "api-config-card";
card.dataset.id = config.id || newApiConfigId();
const isAdvanced = Boolean(config.advanced);
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 = config.name || "";
nameInput.className = "api-config-name";
nameField.appendChild(nameLabel);
nameField.appendChild(nameInput);
const keyField = document.createElement("div");
keyField.className = "field";
const keyLabel = document.createElement("label");
keyLabel.textContent = "API Key";
const keySelect = document.createElement("select");
keySelect.className = "api-config-key-select";
keySelect.dataset.preferred = config.apiKeyId || "";
keyField.appendChild(keyLabel);
keyField.appendChild(keySelect);
const baseField = document.createElement("div");
baseField.className = "field basic-only";
const baseLabel = document.createElement("label");
baseLabel.textContent = "API Base URL";
const baseInput = document.createElement("input");
baseInput.type = "text";
baseInput.placeholder = OPENAI_DEFAULTS.apiBaseUrl;
baseInput.value = config.apiBaseUrl || "";
baseInput.className = "api-config-base";
baseField.appendChild(baseLabel);
baseField.appendChild(baseInput);
const headerField = document.createElement("div");
headerField.className = "field advanced-only";
const headerLabel = document.createElement("label");
headerLabel.textContent = "API Key Header";
const headerInput = document.createElement("input");
headerInput.type = "text";
headerInput.placeholder = OPENAI_DEFAULTS.apiKeyHeader;
headerInput.value = config.apiKeyHeader || "";
headerInput.className = "api-config-header";
headerField.appendChild(headerLabel);
headerField.appendChild(headerInput);
const prefixField = document.createElement("div");
prefixField.className = "field advanced-only";
const prefixLabel = document.createElement("label");
prefixLabel.textContent = "API Key Prefix";
const prefixInput = document.createElement("input");
prefixInput.type = "text";
prefixInput.placeholder = OPENAI_DEFAULTS.apiKeyPrefix;
prefixInput.value = config.apiKeyPrefix || "";
prefixInput.className = "api-config-prefix";
prefixField.appendChild(prefixLabel);
prefixField.appendChild(prefixInput);
const modelField = document.createElement("div");
modelField.className = "field basic-only";
const modelLabel = document.createElement("label");
modelLabel.textContent = "Model name";
const modelInput = document.createElement("input");
modelInput.type = "text";
modelInput.placeholder = DEFAULT_MODEL;
modelInput.value = config.model || "";
modelInput.className = "api-config-model";
modelField.appendChild(modelLabel);
modelField.appendChild(modelInput);
const urlField = document.createElement("div");
urlField.className = "field advanced-only";
const urlLabel = document.createElement("label");
urlLabel.textContent = "API URL";
const urlInput = document.createElement("input");
urlInput.type = "text";
urlInput.placeholder = "https://api.example.com/v1/chat/completions";
urlInput.value = config.apiUrl || "";
urlInput.className = "api-config-url";
urlField.appendChild(urlLabel);
urlField.appendChild(urlInput);
const templateField = document.createElement("div");
templateField.className = "field advanced-only";
const templateLabel = document.createElement("label");
templateLabel.textContent = "Request JSON template";
const templateInput = document.createElement("textarea");
templateInput.rows = 8;
templateInput.placeholder = [
"{",
" \"stream\": true,",
" \"messages\": [",
" { \"role\": \"system\", \"content\": \"SYSTEM_PROMPT_GOES_HERE\" },",
" { \"role\": \"user\", \"content\": \"PROMPT_GOES_HERE\" }",
" ],",
" \"api_key\": \"API_KEY_GOES_HERE\"",
"}"
].join("\n");
templateInput.value = config.requestTemplate || "";
templateInput.className = "api-config-template";
templateField.appendChild(templateLabel);
templateField.appendChild(templateInput);
const actions = document.createElement("div");
actions.className = "api-config-actions";
const moveTopBtn = document.createElement("button");
moveTopBtn.type = "button";
moveTopBtn.className = "ghost move-top";
moveTopBtn.textContent = "Top";
const moveUpBtn = document.createElement("button");
moveUpBtn.type = "button";
moveUpBtn.className = "ghost move-up";
moveUpBtn.textContent = "Up";
const moveDownBtn = document.createElement("button");
moveDownBtn.type = "button";
moveDownBtn.className = "ghost move-down";
moveDownBtn.textContent = "Down";
moveTopBtn.addEventListener("click", () => {
const first = apiConfigsContainer.firstElementChild;
if (!first || first === card) return;
apiConfigsContainer.insertBefore(card, first);
updateApiConfigControls();
updateEnvApiOptions();
});
moveUpBtn.addEventListener("click", () => {
const previous = card.previousElementSibling;
if (!previous) return;
apiConfigsContainer.insertBefore(card, previous);
updateApiConfigControls();
updateEnvApiOptions();
});
moveDownBtn.addEventListener("click", () => {
const next = card.nextElementSibling;
if (!next) return;
apiConfigsContainer.insertBefore(card, next.nextElementSibling);
updateApiConfigControls();
updateEnvApiOptions();
});
actions.appendChild(moveTopBtn);
actions.appendChild(moveUpBtn);
actions.appendChild(moveDownBtn);
const advancedBtn = document.createElement("button");
advancedBtn.type = "button";
advancedBtn.className = "ghost advanced-toggle";
advancedBtn.textContent = "Advanced Mode";
advancedBtn.addEventListener("click", () => {
if (card.classList.contains("is-advanced")) return;
urlInput.value = buildChatUrlFromBase(baseInput.value);
templateInput.value = [
"{",
` \"model\": \"${modelInput.value || DEFAULT_MODEL}\",`,
" \"stream\": true,",
" \"messages\": [",
" { \"role\": \"system\", \"content\": \"SYSTEM_PROMPT_GOES_HERE\" },",
" { \"role\": \"user\", \"content\": \"PROMPT_GOES_HERE\" }",
" ],",
" \"api_key\": \"API_KEY_GOES_HERE\"",
"}"
].join("\n");
setApiConfigAdvanced(card, true);
updateEnvApiOptions();
});
actions.appendChild(advancedBtn);
const duplicateBtn = document.createElement("button");
duplicateBtn.type = "button";
duplicateBtn.className = "ghost duplicate";
duplicateBtn.textContent = "Duplicate";
duplicateBtn.addEventListener("click", () => {
const names = collectNames(apiConfigsContainer, ".api-config-name");
const copy = readApiConfigFromCard(card);
copy.id = newApiConfigId();
copy.name = ensureUniqueName(`${copy.name || "Default"} Copy`, names);
const newCard = buildApiConfigCard(copy);
card.insertAdjacentElement("afterend", newCard);
updateApiConfigKeyOptions();
updateEnvApiOptions();
});
actions.appendChild(duplicateBtn);
const resetBtn = document.createElement("button");
resetBtn.type = "button";
resetBtn.className = "ghost reset-openai";
resetBtn.textContent = "Reset to OpenAI";
resetBtn.addEventListener("click", () => {
baseInput.value = OPENAI_DEFAULTS.apiBaseUrl;
headerInput.value = OPENAI_DEFAULTS.apiKeyHeader;
prefixInput.value = OPENAI_DEFAULTS.apiKeyPrefix;
modelInput.value = DEFAULT_MODEL;
urlInput.value = "";
templateInput.value = "";
setApiConfigAdvanced(card, false);
updateEnvApiOptions();
});
actions.appendChild(resetBtn);
const deleteBtn = document.createElement("button");
deleteBtn.type = "button";
deleteBtn.className = "ghost delete";
deleteBtn.textContent = "Delete";
deleteBtn.addEventListener("click", () => {
card.remove();
updateEnvApiOptions();
updateApiConfigControls();
});
actions.appendChild(deleteBtn);
const updateSelect = () => updateEnvApiOptions();
nameInput.addEventListener("input", updateSelect);
baseInput.addEventListener("input", updateSelect);
headerInput.addEventListener("input", updateSelect);
prefixInput.addEventListener("input", updateSelect);
modelInput.addEventListener("input", updateSelect);
urlInput.addEventListener("input", updateSelect);
templateInput.addEventListener("input", updateSelect);
card.appendChild(nameField);
card.appendChild(keyField);
card.appendChild(baseField);
card.appendChild(headerField);
card.appendChild(prefixField);
card.appendChild(modelField);
card.appendChild(urlField);
card.appendChild(templateField);
card.appendChild(actions);
setApiConfigAdvanced(card, isAdvanced);
return card;
}
function collectApiConfigs() {
const cards = [...apiConfigsContainer.querySelectorAll(".api-config-card")];
return cards.map((card) => readApiConfigFromCard(card));
}
function updateApiConfigControls() {
const cards = [...apiConfigsContainer.querySelectorAll(".api-config-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 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 moveTopBtn = document.createElement("button");
moveTopBtn.type = "button";
moveTopBtn.className = "ghost move-top";
moveTopBtn.textContent = "Top";
const moveUpBtn = document.createElement("button");
moveUpBtn.type = "button";
moveUpBtn.className = "ghost move-up";
moveUpBtn.textContent = "Up";
const moveDownBtn = document.createElement("button");
moveDownBtn.type = "button";
moveDownBtn.className = "ghost move-down";
moveDownBtn.textContent = "Down";
moveTopBtn.addEventListener("click", () => {
const first = apiKeysContainer.firstElementChild;
if (!first || first === card) return;
apiKeysContainer.insertBefore(card, first);
updateApiKeyControls();
updateApiConfigKeyOptions();
});
moveUpBtn.addEventListener("click", () => {
const previous = card.previousElementSibling;
if (!previous) return;
apiKeysContainer.insertBefore(card, previous);
updateApiKeyControls();
updateApiConfigKeyOptions();
});
moveDownBtn.addEventListener("click", () => {
const next = card.nextElementSibling;
if (!next) return;
apiKeysContainer.insertBefore(card, next.nextElementSibling);
updateApiKeyControls();
updateApiConfigKeyOptions();
});
actions.appendChild(moveTopBtn);
actions.appendChild(moveUpBtn);
actions.appendChild(moveDownBtn);
const deleteBtn = document.createElement("button");
deleteBtn.type = "button";
deleteBtn.className = "ghost delete";
deleteBtn.textContent = "Delete";
deleteBtn.addEventListener("click", () => {
card.remove();
updateApiConfigKeyOptions();
updateApiKeyControls();
});
actions.appendChild(deleteBtn);
const updateSelect = () => updateApiConfigKeyOptions();
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 updateApiKeyControls() {
const cards = [...apiKeysContainer.querySelectorAll(".api-key-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 updateApiConfigKeyOptions() {
const keys = collectApiKeys();
const selects = apiConfigsContainer.querySelectorAll(".api-config-key-select");
selects.forEach((select) => {
const preferred = select.dataset.preferred || select.value;
select.innerHTML = "";
if (!keys.length) {
const option = document.createElement("option");
option.value = "";
option.textContent = "No keys configured";
select.appendChild(option);
select.disabled = true;
return;
}
select.disabled = false;
for (const key of keys) {
const option = document.createElement("option");
option.value = key.id;
option.textContent = key.name || "Default";
select.appendChild(option);
}
if (preferred && keys.some((key) => key.id === preferred)) {
select.value = preferred;
} else {
select.value = keys[0].id;
}
select.dataset.preferred = select.value;
});
}
function buildEnvConfigCard(config) {
const card = document.createElement("div");
card.className = "env-config-card";
card.dataset.id = config.id || newEnvConfigId();
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 = config.name || "";
nameInput.className = "env-config-name";
nameField.appendChild(nameLabel);
nameField.appendChild(nameInput);
const apiField = document.createElement("div");
apiField.className = "field";
const apiLabel = document.createElement("label");
apiLabel.textContent = "API config";
const apiSelect = document.createElement("select");
apiSelect.className = "env-config-api-select";
apiSelect.dataset.preferred = config.apiConfigId || "";
apiField.appendChild(apiLabel);
apiField.appendChild(apiSelect);
const promptField = document.createElement("div");
promptField.className = "field";
const promptLabel = document.createElement("label");
promptLabel.textContent = "System prompt";
const promptInput = document.createElement("textarea");
promptInput.rows = 8;
promptInput.value = config.systemPrompt || "";
promptInput.className = "env-config-prompt";
promptField.appendChild(promptLabel);
promptField.appendChild(promptInput);
const actions = document.createElement("div");
actions.className = "env-config-actions";
const moveTopBtn = document.createElement("button");
moveTopBtn.type = "button";
moveTopBtn.className = "ghost move-top";
moveTopBtn.textContent = "Top";
const moveUpBtn = document.createElement("button");
moveUpBtn.type = "button";
moveUpBtn.className = "ghost move-up";
moveUpBtn.textContent = "Up";
const moveDownBtn = document.createElement("button");
moveDownBtn.type = "button";
moveDownBtn.className = "ghost move-down";
moveDownBtn.textContent = "Down";
moveTopBtn.addEventListener("click", () => {
const first = envConfigsContainer.firstElementChild;
if (!first || first === card) return;
envConfigsContainer.insertBefore(card, first);
updateEnvControls();
updateTaskEnvOptions();
});
moveUpBtn.addEventListener("click", () => {
const previous = card.previousElementSibling;
if (!previous) return;
envConfigsContainer.insertBefore(card, previous);
updateEnvControls();
updateTaskEnvOptions();
});
moveDownBtn.addEventListener("click", () => {
const next = card.nextElementSibling;
if (!next) return;
envConfigsContainer.insertBefore(card, next.nextElementSibling);
updateEnvControls();
updateTaskEnvOptions();
});
actions.appendChild(moveTopBtn);
actions.appendChild(moveUpBtn);
actions.appendChild(moveDownBtn);
const duplicateBtn = document.createElement("button");
duplicateBtn.type = "button";
duplicateBtn.className = "ghost duplicate";
duplicateBtn.textContent = "Duplicate";
duplicateBtn.addEventListener("click", () => {
const names = collectNames(envConfigsContainer, ".env-config-name");
const copy = collectEnvConfigs().find((entry) => entry.id === card.dataset.id) || {
id: card.dataset.id,
name: nameInput.value || "Default",
apiConfigId: apiSelect.value || "",
systemPrompt: promptInput.value || ""
};
const newCard = buildEnvConfigCard({
id: newEnvConfigId(),
name: ensureUniqueName(`${copy.name || "Default"} Copy`, names),
apiConfigId: copy.apiConfigId,
systemPrompt: copy.systemPrompt
});
card.insertAdjacentElement("afterend", newCard);
updateEnvApiOptions();
updateEnvControls();
updateTaskEnvOptions();
});
const deleteBtn = document.createElement("button");
deleteBtn.type = "button";
deleteBtn.className = "ghost delete";
deleteBtn.textContent = "Delete";
deleteBtn.addEventListener("click", () => {
card.remove();
updateEnvControls();
updateTaskEnvOptions();
});
actions.appendChild(duplicateBtn);
actions.appendChild(deleteBtn);
nameInput.addEventListener("input", () => updateEnvApiOptions());
card.appendChild(nameField);
card.appendChild(apiField);
card.appendChild(promptField);
card.appendChild(actions);
return card;
}
function updateEnvControls() {
const cards = [...envConfigsContainer.querySelectorAll(".env-config-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 updateTaskEnvOptions() {
const envs = collectEnvConfigs();
const selects = tasksContainer.querySelectorAll(".task-env-select");
selects.forEach((select) => {
const preferred = select.dataset.preferred || select.value;
select.innerHTML = "";
if (!envs.length) {
const option = document.createElement("option");
option.value = "";
option.textContent = "No environments configured";
select.appendChild(option);
select.disabled = true;
return;
}
select.disabled = false;
for (const env of envs) {
const option = document.createElement("option");
option.value = env.id;
option.textContent = env.name || "Default";
select.appendChild(option);
}
if (preferred && envs.some((env) => env.id === preferred)) {
select.value = preferred;
} else {
select.value = envs[0].id;
}
select.dataset.preferred = select.value;
});
}
function collectEnvConfigs() {
const cards = [...envConfigsContainer.querySelectorAll(".env-config-card")];
return cards.map((card) => {
const nameInput = card.querySelector(".env-config-name");
const apiSelect = card.querySelector(".env-config-api-select");
const promptInput = card.querySelector(".env-config-prompt");
return {
id: card.dataset.id || newEnvConfigId(),
name: (nameInput?.value || "Default").trim(),
apiConfigId: apiSelect?.value || "",
systemPrompt: (promptInput?.value || "").trim()
};
});
}
function updateEnvApiOptions() {
const apiConfigs = collectApiConfigs();
const selects = envConfigsContainer.querySelectorAll(".env-config-api-select");
selects.forEach((select) => {
const preferred = select.dataset.preferred || select.value;
select.innerHTML = "";
if (!apiConfigs.length) {
const option = document.createElement("option");
option.value = "";
option.textContent = "No API configs configured";
select.appendChild(option);
select.disabled = true;
return;
}
select.disabled = false;
for (const config of apiConfigs) {
const option = document.createElement("option");
option.value = config.id;
option.textContent = config.name || "Default";
select.appendChild(option);
}
if (preferred && apiConfigs.some((config) => config.id === preferred)) {
select.value = preferred;
} else {
select.value = apiConfigs[0].id;
}
select.dataset.preferred = select.value;
});
updateTaskEnvOptions();
}
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 envField = document.createElement("div");
envField.className = "field";
const envLabel = document.createElement("label");
envLabel.textContent = "Default environment";
const envSelect = document.createElement("select");
envSelect.className = "task-env-select";
envSelect.dataset.preferred = task.defaultEnvId || "";
envField.appendChild(envLabel);
envField.appendChild(envSelect);
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 name = buildUniqueDefaultName(
collectNames(tasksContainer, ".task-name")
);
const newCard = buildTaskCard({
id: newTaskId(),
name,
text: "",
defaultEnvId: getTopEnvId()
});
card.insertAdjacentElement("afterend", newCard);
updateTaskControls();
updateTaskEnvOptions();
});
duplicateBtn.addEventListener("click", () => {
const copy = {
id: newTaskId(),
name: ensureUniqueName(
`${nameInput.value || "Untitled"} Copy`,
collectNames(tasksContainer, ".task-name")
),
text: textArea.value,
defaultEnvId: envSelect.value || ""
};
const newCard = buildTaskCard(copy);
card.insertAdjacentElement("afterend", newCard);
updateTaskControls();
updateTaskEnvOptions();
});
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(envField);
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");
const envSelect = card.querySelector(".task-env-select");
return {
id: card.dataset.id || newTaskId(),
name: (nameInput?.value || "Untitled Task").trim(),
text: (textArea?.value || "").trim(),
defaultEnvId: envSelect?.value || ""
};
});
}
async function loadSettings() {
const {
apiKey = "",
apiKeys = [],
activeApiKeyId = "",
apiConfigs = [],
activeApiConfigId = "",
envConfigs = [],
activeEnvConfigId = "",
apiBaseUrl = "",
apiKeyHeader = "",
apiKeyPrefix = "",
model = "",
systemPrompt = "",
resume = "",
tasks = [],
theme = "system"
} = await getStorage([
"apiKey",
"apiKeys",
"activeApiKeyId",
"apiConfigs",
"activeApiConfigId",
"envConfigs",
"activeEnvConfigId",
"apiBaseUrl",
"apiKeyHeader",
"apiKeyPrefix",
"model",
"systemPrompt",
"resume",
"tasks",
"theme"
]);
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
});
} else if (resolvedKeys.length) {
const hasActive = resolvedKeys.some((entry) => entry.id === resolvedActiveId);
if (!hasActive) {
resolvedActiveId = resolvedKeys[0].id;
await chrome.storage.local.set({ activeApiKeyId: resolvedActiveId });
}
}
apiKeysContainer.innerHTML = "";
if (!resolvedKeys.length) {
apiKeysContainer.appendChild(
buildApiKeyCard({ id: newApiKeyId(), name: "", key: "" })
);
} else {
for (const entry of resolvedKeys) {
apiKeysContainer.appendChild(buildApiKeyCard(entry));
}
}
updateApiKeyControls();
let resolvedConfigs = Array.isArray(apiConfigs) ? apiConfigs : [];
let resolvedActiveConfigId = activeApiConfigId;
if (!resolvedConfigs.length) {
const migrated = {
id: newApiConfigId(),
name: "Default",
apiBaseUrl: apiBaseUrl || OPENAI_DEFAULTS.apiBaseUrl,
apiKeyHeader: apiKeyHeader || OPENAI_DEFAULTS.apiKeyHeader,
apiKeyPrefix: apiKeyPrefix || OPENAI_DEFAULTS.apiKeyPrefix,
model: model || DEFAULT_MODEL,
apiKeyId: resolvedActiveId || resolvedKeys[0]?.id || "",
apiUrl: "",
requestTemplate: "",
advanced: false
};
resolvedConfigs = [migrated];
resolvedActiveConfigId = migrated.id;
await chrome.storage.local.set({
apiConfigs: resolvedConfigs,
activeApiConfigId: resolvedActiveConfigId
});
} else {
const fallbackKeyId = resolvedActiveId || resolvedKeys[0]?.id || "";
const withKeys = resolvedConfigs.map((config) => ({
...config,
apiKeyId: config.apiKeyId || fallbackKeyId,
apiUrl: config.apiUrl || "",
requestTemplate: config.requestTemplate || "",
advanced: Boolean(config.advanced)
}));
if (withKeys.some((config, index) => config.apiKeyId !== resolvedConfigs[index].apiKeyId)) {
resolvedConfigs = withKeys;
await chrome.storage.local.set({ apiConfigs: resolvedConfigs });
}
const hasActive = resolvedConfigs.some(
(config) => config.id === resolvedActiveConfigId
);
if (!hasActive) {
resolvedActiveConfigId = resolvedConfigs[0].id;
await chrome.storage.local.set({ activeApiConfigId: resolvedActiveConfigId });
}
}
apiConfigsContainer.innerHTML = "";
for (const config of resolvedConfigs) {
apiConfigsContainer.appendChild(buildApiConfigCard(config));
}
updateApiConfigKeyOptions();
updateApiConfigControls();
let resolvedEnvConfigs = Array.isArray(envConfigs) ? envConfigs : [];
const fallbackApiConfigId =
resolvedActiveConfigId || resolvedConfigs[0]?.id || "";
if (!resolvedEnvConfigs.length) {
const migrated = {
id: newEnvConfigId(),
name: "Default",
apiConfigId: fallbackApiConfigId,
systemPrompt: systemPrompt || DEFAULT_SYSTEM_PROMPT
};
resolvedEnvConfigs = [migrated];
await chrome.storage.local.set({
envConfigs: resolvedEnvConfigs,
activeEnvConfigId: migrated.id
});
} else {
const withDefaults = resolvedEnvConfigs.map((config) => ({
...config,
apiConfigId: config.apiConfigId || fallbackApiConfigId,
systemPrompt: config.systemPrompt ?? ""
}));
const needsUpdate = withDefaults.some((config, index) => {
const original = resolvedEnvConfigs[index];
return (
config.apiConfigId !== original.apiConfigId ||
(config.systemPrompt || "") !== (original.systemPrompt || "")
);
});
if (needsUpdate) {
resolvedEnvConfigs = withDefaults;
await chrome.storage.local.set({ envConfigs: resolvedEnvConfigs });
}
const hasActive = resolvedEnvConfigs.some(
(config) => config.id === activeEnvConfigId
);
if (!hasActive && resolvedEnvConfigs.length) {
await chrome.storage.local.set({
activeEnvConfigId: resolvedEnvConfigs[0].id
});
}
}
envConfigsContainer.innerHTML = "";
for (const config of resolvedEnvConfigs) {
envConfigsContainer.appendChild(buildEnvConfigCard(config));
}
updateEnvApiOptions();
updateEnvControls();
tasksContainer.innerHTML = "";
const defaultEnvId = resolvedEnvConfigs[0]?.id || "";
const normalizedTasks = Array.isArray(tasks)
? tasks.map((task) => ({
...task,
defaultEnvId: task.defaultEnvId || defaultEnvId
}))
: [];
if (
normalizedTasks.length &&
normalizedTasks.some(
(task, index) => task.defaultEnvId !== tasks[index]?.defaultEnvId
)
) {
await chrome.storage.local.set({ tasks: normalizedTasks });
}
if (!normalizedTasks.length) {
tasksContainer.appendChild(
buildTaskCard({
id: newTaskId(),
name: "",
text: "",
defaultEnvId
})
);
updateTaskControls();
updateTaskEnvOptions();
return;
}
for (const task of normalizedTasks) {
tasksContainer.appendChild(buildTaskCard(task));
}
updateTaskControls();
updateTaskEnvOptions();
}
async function saveSettings() {
const tasks = collectTasks();
const apiKeys = collectApiKeys();
const apiConfigs = collectApiConfigs();
const envConfigs = collectEnvConfigs();
const activeEnvConfigId = envConfigs[0]?.id || "";
const activeEnv = envConfigs[0];
const activeApiConfigId =
activeEnv?.apiConfigId || apiConfigs[0]?.id || "";
const activeConfig = apiConfigs.find((entry) => entry.id === activeApiConfigId);
const activeApiKeyId =
activeConfig?.apiKeyId ||
apiKeys[0]?.id ||
"";
await chrome.storage.local.set({
apiKeys,
activeApiKeyId,
apiConfigs,
activeApiConfigId,
envConfigs,
activeEnvConfigId,
systemPrompt: activeEnv?.systemPrompt || "",
resume: resumeInput.value,
tasks,
theme: themeSelect.value
});
setStatus("Saved.");
}
saveBtn.addEventListener("click", () => void saveSettings());
addTaskBtn.addEventListener("click", () => {
const name = buildUniqueDefaultName(
collectNames(tasksContainer, ".task-name")
);
const newCard = buildTaskCard({
id: newTaskId(),
name,
text: "",
defaultEnvId: getTopEnvId()
});
const first = tasksContainer.firstElementChild;
if (first) {
tasksContainer.insertBefore(newCard, first);
} else {
tasksContainer.appendChild(newCard);
}
updateTaskControls();
updateTaskEnvOptions();
});
addApiKeyBtn.addEventListener("click", () => {
const name = buildUniqueDefaultName(
collectNames(apiKeysContainer, ".api-key-name")
);
const newCard = buildApiKeyCard({ id: newApiKeyId(), name, key: "" });
const first = apiKeysContainer.firstElementChild;
if (first) {
apiKeysContainer.insertBefore(newCard, first);
} else {
apiKeysContainer.appendChild(newCard);
}
updateApiConfigKeyOptions();
updateApiKeyControls();
});
addApiConfigBtn.addEventListener("click", () => {
const name = buildUniqueDefaultName(
collectNames(apiConfigsContainer, ".api-config-name")
);
const newCard = buildApiConfigCard({
id: newApiConfigId(),
name,
apiBaseUrl: OPENAI_DEFAULTS.apiBaseUrl,
apiKeyHeader: OPENAI_DEFAULTS.apiKeyHeader,
apiKeyPrefix: OPENAI_DEFAULTS.apiKeyPrefix,
model: DEFAULT_MODEL,
apiUrl: "",
requestTemplate: "",
advanced: false
});
const first = apiConfigsContainer.firstElementChild;
if (first) {
apiConfigsContainer.insertBefore(newCard, first);
} else {
apiConfigsContainer.appendChild(newCard);
}
updateApiConfigKeyOptions();
updateEnvApiOptions();
updateApiConfigControls();
});
addEnvConfigBtn.addEventListener("click", () => {
const name = buildUniqueDefaultName(
collectNames(envConfigsContainer, ".env-config-name")
);
const fallbackApiConfigId = collectApiConfigs()[0]?.id || "";
const newCard = buildEnvConfigCard({
id: newEnvConfigId(),
name,
apiConfigId: fallbackApiConfigId,
systemPrompt: DEFAULT_SYSTEM_PROMPT
});
const first = envConfigsContainer.firstElementChild;
if (first) {
envConfigsContainer.insertBefore(newCard, first);
} else {
envConfigsContainer.appendChild(newCard);
}
updateEnvApiOptions();
updateEnvControls();
updateTaskEnvOptions();
});
themeSelect.addEventListener("change", () => applyTheme(themeSelect.value));
themeSelect.addEventListener("change", () => applyTheme(themeSelect.value));
loadSettings();