From e3c7cbba95db1dbcbbc876ce5f6dc7a7aa324a53 Mon Sep 17 00:00:00 2001 From: Peisong Xiao Date: Sat, 17 Jan 2026 17:09:47 -0500 Subject: [PATCH] Added multi-env support --- wwcompanion-extension/background.js | 82 ++++++- wwcompanion-extension/popup.js | 39 ++- wwcompanion-extension/settings.css | 21 ++ wwcompanion-extension/settings.html | 92 +++---- wwcompanion-extension/settings.js | 367 +++++++++++++++++++++++----- 5 files changed, 475 insertions(+), 126 deletions(-) diff --git a/wwcompanion-extension/background.js b/wwcompanion-extension/background.js index 6b92c07..82a9250 100644 --- a/wwcompanion-extension/background.js +++ b/wwcompanion-extension/background.js @@ -19,6 +19,8 @@ const DEFAULT_SETTINGS = { activeApiKeyId: "", apiConfigs: [], activeApiConfigId: "", + envConfigs: [], + activeEnvConfigId: "", apiBaseUrl: "https://api.openai.com/v1", apiKeyHeader: "Authorization", apiKeyPrefix: "Bearer ", @@ -188,6 +190,58 @@ chrome.runtime.onInstalled.addListener(async () => { } } + const resolvedApiConfigs = updates.apiConfigs || stored.apiConfigs || []; + const resolvedActiveApiConfigId = + updates.activeApiConfigId || + stored.activeApiConfigId || + resolvedApiConfigs[0]?.id || + ""; + const hasEnvConfigs = + Array.isArray(stored.envConfigs) && stored.envConfigs.length > 0; + + if (!hasEnvConfigs) { + const id = crypto?.randomUUID + ? crypto.randomUUID() + : `env-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`; + updates.envConfigs = [ + { + id, + name: "Default", + apiConfigId: resolvedActiveApiConfigId, + systemPrompt: stored.systemPrompt || DEFAULT_SETTINGS.systemPrompt + } + ]; + updates.activeEnvConfigId = id; + } else { + const normalizedEnvs = stored.envConfigs.map((config) => ({ + ...config, + apiConfigId: config.apiConfigId || resolvedActiveApiConfigId, + systemPrompt: config.systemPrompt ?? "" + })); + const envNeedsUpdate = normalizedEnvs.some((config, index) => { + const original = stored.envConfigs[index]; + return ( + config.apiConfigId !== original.apiConfigId || + (config.systemPrompt || "") !== (original.systemPrompt || "") + ); + }); + if (envNeedsUpdate) { + updates.envConfigs = normalizedEnvs; + } + + const envActiveId = updates.activeEnvConfigId || stored.activeEnvConfigId; + if (envActiveId) { + const exists = stored.envConfigs.some( + (config) => config.id === envActiveId + ); + if (!exists) { + updates.activeEnvConfigId = stored.envConfigs[0].id; + } + } else { + updates.activeEnvConfigId = stored.envConfigs[0].id; + } + } + if (Object.keys(updates).length) { await chrome.storage.local.set(updates); } @@ -307,6 +361,10 @@ async function handleAnalysisRequest(port, payload, signal) { safePost(port, { type: "ERROR", message: "Missing request template." }); return; } + if (apiKeyHeader && !apiKey) { + safePost(port, { type: "ERROR", message: "Missing API key." }); + return; + } } else { if (!apiBaseUrl) { safePost(port, { type: "ERROR", message: "Missing API base URL." }); @@ -345,6 +403,10 @@ async function handleAnalysisRequest(port, payload, signal) { apiKey, apiUrl, requestTemplate, + apiKeyHeader, + apiKeyPrefix, + apiBaseUrl, + model, systemPrompt: systemPrompt || "", userMessage, signal, @@ -511,6 +573,10 @@ async function streamCustomCompletion({ apiKey, apiUrl, requestTemplate, + apiKeyHeader, + apiKeyPrefix, + apiBaseUrl, + model, systemPrompt, userMessage, signal, @@ -519,16 +585,24 @@ async function streamCustomCompletion({ const replacements = { PROMPT_GOES_HERE: userMessage, SYSTEM_PROMPT_GOES_HERE: systemPrompt, - API_KEY_GOES_HERE: apiKey + API_KEY_GOES_HERE: apiKey, + MODEL_GOES_HERE: model || "", + API_BASE_URL_GOES_HERE: apiBaseUrl || "" }; const resolvedUrl = replaceUrlTokens(apiUrl, replacements); const body = buildTemplateBody(requestTemplate, replacements); + const headers = { + "Content-Type": "application/json" + }; + const authHeader = buildAuthHeader(apiKeyHeader, apiKeyPrefix, apiKey); + if (authHeader) { + headers[authHeader.name] = authHeader.value; + } + const response = await fetch(resolvedUrl, { method: "POST", - headers: { - "Content-Type": "application/json" - }, + headers, body: JSON.stringify(body), signal }); diff --git a/wwcompanion-extension/popup.js b/wwcompanion-extension/popup.js index 3be90d0..d9af489 100644 --- a/wwcompanion-extension/popup.js +++ b/wwcompanion-extension/popup.js @@ -433,6 +433,8 @@ async function handleAnalyze() { activeApiKeyId = "", apiConfigs = [], activeApiConfigId = "", + envConfigs = [], + activeEnvConfigId = "", apiBaseUrl, apiKeyHeader, apiKeyPrefix, @@ -444,6 +446,8 @@ async function handleAnalyze() { "activeApiKeyId", "apiConfigs", "activeApiConfigId", + "envConfigs", + "activeEnvConfigId", "apiBaseUrl", "apiKeyHeader", "apiKeyPrefix", @@ -453,22 +457,28 @@ async function handleAnalyze() { ]); const resolvedConfigs = Array.isArray(apiConfigs) ? apiConfigs : []; + const resolvedEnvs = Array.isArray(envConfigs) ? envConfigs : []; + const activeEnv = + resolvedEnvs.find((entry) => entry.id === activeEnvConfigId) || + resolvedEnvs[0]; + const resolvedSystemPrompt = + activeEnv?.systemPrompt ?? systemPrompt ?? ""; + const resolvedApiConfigId = + activeEnv?.apiConfigId || activeApiConfigId || resolvedConfigs[0]?.id || ""; const activeConfig = - resolvedConfigs.find((entry) => entry.id === activeApiConfigId) || + resolvedConfigs.find((entry) => entry.id === resolvedApiConfigId) || resolvedConfigs[0]; + if (!activeConfig) { + setStatus("Add an API configuration in Settings."); + return; + } const isAdvanced = Boolean(activeConfig?.advanced); const resolvedApiUrl = activeConfig?.apiUrl || ""; const resolvedTemplate = activeConfig?.requestTemplate || ""; - const resolvedApiBaseUrl = isAdvanced - ? "" - : activeConfig?.apiBaseUrl || apiBaseUrl || ""; - const resolvedApiKeyHeader = isAdvanced - ? "" - : activeConfig?.apiKeyHeader ?? apiKeyHeader ?? ""; - const resolvedApiKeyPrefix = isAdvanced - ? "" - : activeConfig?.apiKeyPrefix ?? apiKeyPrefix ?? ""; - const resolvedModel = isAdvanced ? "" : activeConfig?.model || model || ""; + const resolvedApiBaseUrl = activeConfig?.apiBaseUrl || apiBaseUrl || ""; + const resolvedApiKeyHeader = activeConfig?.apiKeyHeader ?? apiKeyHeader ?? ""; + const resolvedApiKeyPrefix = activeConfig?.apiKeyPrefix ?? apiKeyPrefix ?? ""; + const resolvedModel = activeConfig?.model || model || ""; const resolvedKeys = Array.isArray(apiKeys) ? apiKeys : []; const resolvedKeyId = @@ -485,7 +495,10 @@ async function handleAnalyze() { setStatus("Set a request template in Settings."); return; } - if (resolvedTemplate.includes("API_KEY_GOES_HERE") && !apiKey) { + const needsKey = + Boolean(resolvedApiKeyHeader) || + resolvedTemplate.includes("API_KEY_GOES_HERE"); + if (needsKey && !apiKey) { setStatus("Add an API key in Settings."); return; } @@ -524,7 +537,7 @@ async function handleAnalyze() { apiKeyHeader: resolvedApiKeyHeader, apiKeyPrefix: resolvedApiKeyPrefix, model: resolvedModel, - systemPrompt: systemPrompt || "", + systemPrompt: resolvedSystemPrompt, resume: resume || "", taskText: task.text || "", postingText: state.postingText, diff --git a/wwcompanion-extension/settings.css b/wwcompanion-extension/settings.css index 2326294..21f63bf 100644 --- a/wwcompanion-extension/settings.css +++ b/wwcompanion-extension/settings.css @@ -256,6 +256,7 @@ button:active { .api-key-actions .delete, .api-config-actions .delete, +.env-config-actions .delete, .task-actions .delete { background: #c0392b; border-color: #c0392b; @@ -290,6 +291,26 @@ button:active { justify-content: flex-end; } +.env-configs { + display: grid; + gap: 12px; +} + +.env-config-card { + padding: 12px; + border-radius: 12px; + border: 1px solid var(--border); + background: var(--card-bg); + display: grid; + gap: 8px; +} + +.env-config-actions { + display: flex; + gap: 8px; + justify-content: flex-end; +} + @media (prefers-color-scheme: dark) { :root:not([data-theme]), :root[data-theme="system"] { diff --git a/wwcompanion-extension/settings.html b/wwcompanion-extension/settings.html index 9c3d605..3ae9916 100644 --- a/wwcompanion-extension/settings.html +++ b/wwcompanion-extension/settings.html @@ -16,46 +16,6 @@ -
- - -

API

-
-
-
-
-
- -
-
-
- - -
-
-
-
- -
- - -

API KEYS

-
-
-
-
- -
-
-
-
-
-

System Prompt

+

API KEYS

- +
+
+ +
+
+
+
+ +
+ + +

API

+
+
+
+
+
+ +
+
+
+
+
+ +
+ + +

Environment

+
+
+
+
+ +
+
+ + +
+
diff --git a/wwcompanion-extension/settings.js b/wwcompanion-extension/settings.js index 0983954..510720e 100644 --- a/wwcompanion-extension/settings.js +++ b/wwcompanion-extension/settings.js @@ -1,11 +1,12 @@ -const systemPromptInput = document.getElementById("systemPrompt"); const resumeInput = document.getElementById("resume"); const saveBtn = document.getElementById("saveBtn"); const addApiConfigBtn = document.getElementById("addApiConfigBtn"); const apiConfigsContainer = document.getElementById("apiConfigs"); -const activeApiConfigSelect = document.getElementById("activeApiConfigSelect"); const addApiKeyBtn = document.getElementById("addApiKeyBtn"); const apiKeysContainer = document.getElementById("apiKeys"); +const addEnvConfigBtn = document.getElementById("addEnvConfigBtn"); +const envConfigsContainer = document.getElementById("envConfigs"); +const activeEnvConfigSelect = document.getElementById("activeEnvConfigSelect"); const addTaskBtn = document.getElementById("addTaskBtn"); const tasksContainer = document.getElementById("tasks"); const statusEl = document.getElementById("status"); @@ -17,6 +18,8 @@ const OPENAI_DEFAULTS = { 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)); @@ -50,6 +53,18 @@ function newApiConfigId() { 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)] @@ -94,10 +109,10 @@ function setApiConfigAdvanced(card, isAdvanced) { }); const resetBtn = card.querySelector(".reset-openai"); - if (resetBtn) resetBtn.disabled = isAdvanced; + if (resetBtn) resetBtn.disabled = false; const advancedBtn = card.querySelector(".advanced-toggle"); - if (advancedBtn && isAdvanced) advancedBtn.remove(); + if (advancedBtn) advancedBtn.disabled = isAdvanced; } function readApiConfigFromCard(card) { @@ -165,7 +180,7 @@ function buildApiConfigCard(config) { baseField.appendChild(baseInput); const headerField = document.createElement("div"); - headerField.className = "field basic-only"; + headerField.className = "field advanced-only"; const headerLabel = document.createElement("label"); headerLabel.textContent = "API Key Header"; const headerInput = document.createElement("input"); @@ -177,7 +192,7 @@ function buildApiConfigCard(config) { headerField.appendChild(headerInput); const prefixField = document.createElement("div"); - prefixField.className = "field basic-only"; + prefixField.className = "field advanced-only"; const prefixLabel = document.createElement("label"); prefixLabel.textContent = "API Key Prefix"; const prefixInput = document.createElement("input"); @@ -235,17 +250,28 @@ function buildApiConfigCard(config) { const actions = document.createElement("div"); actions.className = "api-config-actions"; - if (!isAdvanced) { - const advancedBtn = document.createElement("button"); - advancedBtn.type = "button"; - advancedBtn.className = "ghost advanced-toggle"; - advancedBtn.textContent = "Advanced Mode"; - advancedBtn.addEventListener("click", () => { - setApiConfigAdvanced(card, true); - updateApiConfigSelect(activeApiConfigSelect.value); - }); - actions.appendChild(advancedBtn); - } + 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"; @@ -259,7 +285,7 @@ function buildApiConfigCard(config) { const newCard = buildApiConfigCard(copy); card.insertAdjacentElement("afterend", newCard); updateApiConfigKeyOptions(); - updateApiConfigSelect(newCard.dataset.id); + updateEnvApiOptions(); }); actions.appendChild(duplicateBtn); @@ -268,14 +294,14 @@ function buildApiConfigCard(config) { resetBtn.className = "ghost reset-openai"; resetBtn.textContent = "Reset to OpenAI"; resetBtn.addEventListener("click", () => { - if (card.classList.contains("is-advanced")) { - setStatus("Advanced mode cannot be reset to OpenAI."); - return; - } baseInput.value = OPENAI_DEFAULTS.apiBaseUrl; headerInput.value = OPENAI_DEFAULTS.apiKeyHeader; prefixInput.value = OPENAI_DEFAULTS.apiKeyPrefix; - updateApiConfigSelect(activeApiConfigSelect.value); + modelInput.value = DEFAULT_MODEL; + urlInput.value = ""; + templateInput.value = ""; + setApiConfigAdvanced(card, false); + updateEnvApiOptions(); }); actions.appendChild(resetBtn); @@ -285,11 +311,11 @@ function buildApiConfigCard(config) { deleteBtn.textContent = "Delete"; deleteBtn.addEventListener("click", () => { card.remove(); - updateApiConfigSelect(activeApiConfigSelect.value); + updateEnvApiOptions(); }); actions.appendChild(deleteBtn); - const updateSelect = () => updateApiConfigSelect(activeApiConfigSelect.value); + const updateSelect = () => updateEnvApiOptions(); nameInput.addEventListener("input", updateSelect); baseInput.addEventListener("input", updateSelect); headerInput.addEventListener("input", updateSelect); @@ -318,35 +344,6 @@ function collectApiConfigs() { return cards.map((card) => readApiConfigFromCard(card)); } -function updateApiConfigSelect(preferredId) { - const configs = collectApiConfigs(); - activeApiConfigSelect.innerHTML = ""; - - if (!configs.length) { - const option = document.createElement("option"); - option.value = ""; - option.textContent = "No configs configured"; - activeApiConfigSelect.appendChild(option); - activeApiConfigSelect.disabled = true; - return; - } - - activeApiConfigSelect.disabled = false; - const selectedId = - preferredId && configs.some((config) => config.id === preferredId) - ? preferredId - : configs[0].id; - - for (const config of configs) { - const option = document.createElement("option"); - option.value = config.id; - option.textContent = config.name || "Default"; - activeApiConfigSelect.appendChild(option); - } - - activeApiConfigSelect.value = selectedId; -} - function buildApiKeyCard(entry) { const card = document.createElement("div"); card.className = "api-key-card"; @@ -458,6 +455,170 @@ function updateApiConfigKeyOptions() { }); } +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 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(); + updateEnvConfigSelect(newCard.dataset.id); + }); + + const deleteBtn = document.createElement("button"); + deleteBtn.type = "button"; + deleteBtn.className = "ghost delete"; + deleteBtn.textContent = "Delete"; + deleteBtn.addEventListener("click", () => { + card.remove(); + updateEnvConfigSelect(activeEnvConfigSelect.value); + }); + + actions.appendChild(duplicateBtn); + actions.appendChild(deleteBtn); + + nameInput.addEventListener("input", () => + updateEnvConfigSelect(activeEnvConfigSelect.value) + ); + + card.appendChild(nameField); + card.appendChild(apiField); + card.appendChild(promptField); + card.appendChild(actions); + + return card; +} + +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 updateEnvConfigSelect(preferredId) { + const configs = collectEnvConfigs(); + activeEnvConfigSelect.innerHTML = ""; + + if (!configs.length) { + const option = document.createElement("option"); + option.value = ""; + option.textContent = "No environments configured"; + activeEnvConfigSelect.appendChild(option); + activeEnvConfigSelect.disabled = true; + return; + } + + activeEnvConfigSelect.disabled = false; + const selectedId = + preferredId && configs.some((config) => config.id === preferredId) + ? preferredId + : configs[0].id; + + for (const config of configs) { + const option = document.createElement("option"); + option.value = config.id; + option.textContent = config.name || "Default"; + activeEnvConfigSelect.appendChild(option); + } + + activeEnvConfigSelect.value = selectedId; +} + +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; + }); +} + function buildTaskCard(task) { const card = document.createElement("div"); card.className = "task-card"; @@ -619,6 +780,8 @@ async function loadSettings() { activeApiKeyId = "", apiConfigs = [], activeApiConfigId = "", + envConfigs = [], + activeEnvConfigId = "", apiBaseUrl = "", apiKeyHeader = "", apiKeyPrefix = "", @@ -633,6 +796,8 @@ async function loadSettings() { "activeApiKeyId", "apiConfigs", "activeApiConfigId", + "envConfigs", + "activeEnvConfigId", "apiBaseUrl", "apiKeyHeader", "apiKeyPrefix", @@ -643,7 +808,6 @@ async function loadSettings() { "theme" ]); - systemPromptInput.value = systemPrompt; resumeInput.value = resume; themeSelect.value = theme; applyTheme(theme); @@ -727,7 +891,57 @@ async function loadSettings() { apiConfigsContainer.appendChild(buildApiConfigCard(config)); } updateApiConfigKeyOptions(); - updateApiConfigSelect(resolvedActiveConfigId); + + let resolvedEnvConfigs = Array.isArray(envConfigs) ? envConfigs : []; + let resolvedActiveEnvId = activeEnvConfigId; + const fallbackApiConfigId = + resolvedActiveConfigId || resolvedConfigs[0]?.id || ""; + + if (!resolvedEnvConfigs.length) { + const migrated = { + id: newEnvConfigId(), + name: "Default", + apiConfigId: fallbackApiConfigId, + systemPrompt: systemPrompt || DEFAULT_SYSTEM_PROMPT + }; + resolvedEnvConfigs = [migrated]; + resolvedActiveEnvId = migrated.id; + await chrome.storage.local.set({ + envConfigs: resolvedEnvConfigs, + activeEnvConfigId: resolvedActiveEnvId + }); + } 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 === resolvedActiveEnvId + ); + if (!hasActive) { + resolvedActiveEnvId = resolvedEnvConfigs[0].id; + await chrome.storage.local.set({ activeEnvConfigId: resolvedActiveEnvId }); + } + } + + envConfigsContainer.innerHTML = ""; + for (const config of resolvedEnvConfigs) { + envConfigsContainer.appendChild(buildEnvConfigCard(config)); + } + updateEnvApiOptions(); + updateEnvConfigSelect(resolvedActiveEnvId); tasksContainer.innerHTML = ""; if (!tasks.length) { @@ -748,10 +962,14 @@ async function saveSettings() { const tasks = collectTasks(); const apiKeys = collectApiKeys(); const apiConfigs = collectApiConfigs(); - const activeApiConfigId = - apiConfigs.find((entry) => entry.id === activeApiConfigSelect.value)?.id || - apiConfigs[0]?.id || + const envConfigs = collectEnvConfigs(); + const activeEnvConfigId = + envConfigs.find((entry) => entry.id === activeEnvConfigSelect.value)?.id || + envConfigs[0]?.id || ""; + const activeEnv = envConfigs.find((entry) => entry.id === activeEnvConfigId); + const activeApiConfigId = + activeEnv?.apiConfigId || apiConfigs[0]?.id || ""; const activeConfig = apiConfigs.find((entry) => entry.id === activeApiConfigId); const activeApiKeyId = activeConfig?.apiKeyId || @@ -762,7 +980,9 @@ async function saveSettings() { activeApiKeyId, apiConfigs, activeApiConfigId, - systemPrompt: systemPromptInput.value, + envConfigs, + activeEnvConfigId, + systemPrompt: activeEnv?.systemPrompt || "", resume: resumeInput.value, tasks, theme: themeSelect.value @@ -821,11 +1041,32 @@ addApiConfigBtn.addEventListener("click", () => { apiConfigsContainer.appendChild(newCard); } updateApiConfigKeyOptions(); - updateApiConfigSelect(activeApiConfigSelect.value); + updateEnvApiOptions(); }); -activeApiConfigSelect.addEventListener("change", () => { - updateApiConfigSelect(activeApiConfigSelect.value); +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(); + updateEnvConfigSelect(newCard.dataset.id); +}); + +activeEnvConfigSelect.addEventListener("change", () => { + updateEnvConfigSelect(activeEnvConfigSelect.value); }); themeSelect.addEventListener("change", () => applyTheme(themeSelect.value));