Added multi-env support

This commit is contained in:
2026-01-17 17:09:47 -05:00
parent 2638e08453
commit e3c7cbba95
5 changed files with 475 additions and 126 deletions

View File

@@ -19,6 +19,8 @@ const DEFAULT_SETTINGS = {
activeApiKeyId: "", activeApiKeyId: "",
apiConfigs: [], apiConfigs: [],
activeApiConfigId: "", activeApiConfigId: "",
envConfigs: [],
activeEnvConfigId: "",
apiBaseUrl: "https://api.openai.com/v1", apiBaseUrl: "https://api.openai.com/v1",
apiKeyHeader: "Authorization", apiKeyHeader: "Authorization",
apiKeyPrefix: "Bearer ", 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) { if (Object.keys(updates).length) {
await chrome.storage.local.set(updates); await chrome.storage.local.set(updates);
} }
@@ -307,6 +361,10 @@ async function handleAnalysisRequest(port, payload, signal) {
safePost(port, { type: "ERROR", message: "Missing request template." }); safePost(port, { type: "ERROR", message: "Missing request template." });
return; return;
} }
if (apiKeyHeader && !apiKey) {
safePost(port, { type: "ERROR", message: "Missing API key." });
return;
}
} else { } else {
if (!apiBaseUrl) { if (!apiBaseUrl) {
safePost(port, { type: "ERROR", message: "Missing API base URL." }); safePost(port, { type: "ERROR", message: "Missing API base URL." });
@@ -345,6 +403,10 @@ async function handleAnalysisRequest(port, payload, signal) {
apiKey, apiKey,
apiUrl, apiUrl,
requestTemplate, requestTemplate,
apiKeyHeader,
apiKeyPrefix,
apiBaseUrl,
model,
systemPrompt: systemPrompt || "", systemPrompt: systemPrompt || "",
userMessage, userMessage,
signal, signal,
@@ -511,6 +573,10 @@ async function streamCustomCompletion({
apiKey, apiKey,
apiUrl, apiUrl,
requestTemplate, requestTemplate,
apiKeyHeader,
apiKeyPrefix,
apiBaseUrl,
model,
systemPrompt, systemPrompt,
userMessage, userMessage,
signal, signal,
@@ -519,16 +585,24 @@ async function streamCustomCompletion({
const replacements = { const replacements = {
PROMPT_GOES_HERE: userMessage, PROMPT_GOES_HERE: userMessage,
SYSTEM_PROMPT_GOES_HERE: systemPrompt, 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 resolvedUrl = replaceUrlTokens(apiUrl, replacements);
const body = buildTemplateBody(requestTemplate, 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, { const response = await fetch(resolvedUrl, {
method: "POST", method: "POST",
headers: { headers,
"Content-Type": "application/json"
},
body: JSON.stringify(body), body: JSON.stringify(body),
signal signal
}); });

View File

@@ -433,6 +433,8 @@ async function handleAnalyze() {
activeApiKeyId = "", activeApiKeyId = "",
apiConfigs = [], apiConfigs = [],
activeApiConfigId = "", activeApiConfigId = "",
envConfigs = [],
activeEnvConfigId = "",
apiBaseUrl, apiBaseUrl,
apiKeyHeader, apiKeyHeader,
apiKeyPrefix, apiKeyPrefix,
@@ -444,6 +446,8 @@ async function handleAnalyze() {
"activeApiKeyId", "activeApiKeyId",
"apiConfigs", "apiConfigs",
"activeApiConfigId", "activeApiConfigId",
"envConfigs",
"activeEnvConfigId",
"apiBaseUrl", "apiBaseUrl",
"apiKeyHeader", "apiKeyHeader",
"apiKeyPrefix", "apiKeyPrefix",
@@ -453,22 +457,28 @@ async function handleAnalyze() {
]); ]);
const resolvedConfigs = Array.isArray(apiConfigs) ? apiConfigs : []; 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 = const activeConfig =
resolvedConfigs.find((entry) => entry.id === activeApiConfigId) || resolvedConfigs.find((entry) => entry.id === resolvedApiConfigId) ||
resolvedConfigs[0]; resolvedConfigs[0];
if (!activeConfig) {
setStatus("Add an API configuration in Settings.");
return;
}
const isAdvanced = Boolean(activeConfig?.advanced); const isAdvanced = Boolean(activeConfig?.advanced);
const resolvedApiUrl = activeConfig?.apiUrl || ""; const resolvedApiUrl = activeConfig?.apiUrl || "";
const resolvedTemplate = activeConfig?.requestTemplate || ""; const resolvedTemplate = activeConfig?.requestTemplate || "";
const resolvedApiBaseUrl = isAdvanced const resolvedApiBaseUrl = activeConfig?.apiBaseUrl || apiBaseUrl || "";
? "" const resolvedApiKeyHeader = activeConfig?.apiKeyHeader ?? apiKeyHeader ?? "";
: activeConfig?.apiBaseUrl || apiBaseUrl || ""; const resolvedApiKeyPrefix = activeConfig?.apiKeyPrefix ?? apiKeyPrefix ?? "";
const resolvedApiKeyHeader = isAdvanced const resolvedModel = activeConfig?.model || model || "";
? ""
: activeConfig?.apiKeyHeader ?? apiKeyHeader ?? "";
const resolvedApiKeyPrefix = isAdvanced
? ""
: activeConfig?.apiKeyPrefix ?? apiKeyPrefix ?? "";
const resolvedModel = isAdvanced ? "" : activeConfig?.model || model || "";
const resolvedKeys = Array.isArray(apiKeys) ? apiKeys : []; const resolvedKeys = Array.isArray(apiKeys) ? apiKeys : [];
const resolvedKeyId = const resolvedKeyId =
@@ -485,7 +495,10 @@ async function handleAnalyze() {
setStatus("Set a request template in Settings."); setStatus("Set a request template in Settings.");
return; 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."); setStatus("Add an API key in Settings.");
return; return;
} }
@@ -524,7 +537,7 @@ async function handleAnalyze() {
apiKeyHeader: resolvedApiKeyHeader, apiKeyHeader: resolvedApiKeyHeader,
apiKeyPrefix: resolvedApiKeyPrefix, apiKeyPrefix: resolvedApiKeyPrefix,
model: resolvedModel, model: resolvedModel,
systemPrompt: systemPrompt || "", systemPrompt: resolvedSystemPrompt,
resume: resume || "", resume: resume || "",
taskText: task.text || "", taskText: task.text || "",
postingText: state.postingText, postingText: state.postingText,

View File

@@ -256,6 +256,7 @@ button:active {
.api-key-actions .delete, .api-key-actions .delete,
.api-config-actions .delete, .api-config-actions .delete,
.env-config-actions .delete,
.task-actions .delete { .task-actions .delete {
background: #c0392b; background: #c0392b;
border-color: #c0392b; border-color: #c0392b;
@@ -290,6 +291,26 @@ button:active {
justify-content: flex-end; 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) { @media (prefers-color-scheme: dark) {
:root:not([data-theme]), :root:not([data-theme]),
:root[data-theme="system"] { :root[data-theme="system"] {

View File

@@ -16,46 +16,6 @@
<button id="saveBtn" class="accent">Save Settings</button> <button id="saveBtn" class="accent">Save Settings</button>
</div> </div>
<details class="panel">
<summary class="panel-summary">
<span class="panel-caret" aria-hidden="true">
<span class="caret-closed"></span>
<span class="caret-open"></span>
</span>
<h2>API</h2>
</summary>
<div class="panel-body">
<div class="row">
<div></div>
<div class="row-actions">
<button id="addApiConfigBtn" class="ghost" type="button">Add Config</button>
</div>
</div>
<div class="field">
<label for="activeApiConfigSelect">Active config</label>
<select id="activeApiConfigSelect"></select>
</div>
<div id="apiConfigs" class="api-configs"></div>
</div>
</details>
<details class="panel">
<summary class="panel-summary">
<span class="panel-caret" aria-hidden="true">
<span class="caret-closed"></span>
<span class="caret-open"></span>
</span>
<h2>API KEYS</h2>
</summary>
<div class="panel-body">
<div class="row">
<div></div>
<button id="addApiKeyBtn" class="ghost" type="button">Add Key</button>
</div>
<div id="apiKeys" class="api-keys"></div>
</div>
</details>
<details class="panel"> <details class="panel">
<summary class="panel-summary"> <summary class="panel-summary">
<span class="panel-caret" aria-hidden="true"> <span class="panel-caret" aria-hidden="true">
@@ -82,14 +42,54 @@
<span class="caret-closed"></span> <span class="caret-closed"></span>
<span class="caret-open"></span> <span class="caret-open"></span>
</span> </span>
<h2>System Prompt</h2> <h2>API KEYS</h2>
</summary> </summary>
<div class="panel-body"> <div class="panel-body">
<textarea <div class="row">
id="systemPrompt" <div></div>
rows="8" <button id="addApiKeyBtn" class="ghost" type="button">Add Key</button>
placeholder="Define tone and standards..." </div>
></textarea> <div id="apiKeys" class="api-keys"></div>
</div>
</details>
<details class="panel">
<summary class="panel-summary">
<span class="panel-caret" aria-hidden="true">
<span class="caret-closed"></span>
<span class="caret-open"></span>
</span>
<h2>API</h2>
</summary>
<div class="panel-body">
<div class="row">
<div></div>
<div class="row-actions">
<button id="addApiConfigBtn" class="ghost" type="button">Add Config</button>
</div>
</div>
<div id="apiConfigs" class="api-configs"></div>
</div>
</details>
<details class="panel">
<summary class="panel-summary">
<span class="panel-caret" aria-hidden="true">
<span class="caret-closed"></span>
<span class="caret-open"></span>
</span>
<h2>Environment</h2>
</summary>
<div class="panel-body">
<div class="row">
<div></div>
<button id="addEnvConfigBtn" class="ghost" type="button">Add Config</button>
</div>
<div class="field">
<label for="activeEnvConfigSelect">Active environment</label>
<select id="activeEnvConfigSelect"></select>
</div>
<div id="envConfigs" class="env-configs"></div>
</div> </div>
</details> </details>

View File

@@ -1,11 +1,12 @@
const systemPromptInput = document.getElementById("systemPrompt");
const resumeInput = document.getElementById("resume"); const resumeInput = document.getElementById("resume");
const saveBtn = document.getElementById("saveBtn"); const saveBtn = document.getElementById("saveBtn");
const addApiConfigBtn = document.getElementById("addApiConfigBtn"); const addApiConfigBtn = document.getElementById("addApiConfigBtn");
const apiConfigsContainer = document.getElementById("apiConfigs"); const apiConfigsContainer = document.getElementById("apiConfigs");
const activeApiConfigSelect = document.getElementById("activeApiConfigSelect");
const addApiKeyBtn = document.getElementById("addApiKeyBtn"); const addApiKeyBtn = document.getElementById("addApiKeyBtn");
const apiKeysContainer = document.getElementById("apiKeys"); 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 addTaskBtn = document.getElementById("addTaskBtn");
const tasksContainer = document.getElementById("tasks"); const tasksContainer = document.getElementById("tasks");
const statusEl = document.getElementById("status"); const statusEl = document.getElementById("status");
@@ -17,6 +18,8 @@ const OPENAI_DEFAULTS = {
apiKeyPrefix: "Bearer " apiKeyPrefix: "Bearer "
}; };
const DEFAULT_MODEL = "gpt-4o-mini"; 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) { function getStorage(keys) {
return new Promise((resolve) => chrome.storage.local.get(keys, resolve)); 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)}`; 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) { function collectNames(container, selector) {
if (!container) return []; if (!container) return [];
return [...container.querySelectorAll(selector)] return [...container.querySelectorAll(selector)]
@@ -94,10 +109,10 @@ function setApiConfigAdvanced(card, isAdvanced) {
}); });
const resetBtn = card.querySelector(".reset-openai"); const resetBtn = card.querySelector(".reset-openai");
if (resetBtn) resetBtn.disabled = isAdvanced; if (resetBtn) resetBtn.disabled = false;
const advancedBtn = card.querySelector(".advanced-toggle"); const advancedBtn = card.querySelector(".advanced-toggle");
if (advancedBtn && isAdvanced) advancedBtn.remove(); if (advancedBtn) advancedBtn.disabled = isAdvanced;
} }
function readApiConfigFromCard(card) { function readApiConfigFromCard(card) {
@@ -165,7 +180,7 @@ function buildApiConfigCard(config) {
baseField.appendChild(baseInput); baseField.appendChild(baseInput);
const headerField = document.createElement("div"); const headerField = document.createElement("div");
headerField.className = "field basic-only"; headerField.className = "field advanced-only";
const headerLabel = document.createElement("label"); const headerLabel = document.createElement("label");
headerLabel.textContent = "API Key Header"; headerLabel.textContent = "API Key Header";
const headerInput = document.createElement("input"); const headerInput = document.createElement("input");
@@ -177,7 +192,7 @@ function buildApiConfigCard(config) {
headerField.appendChild(headerInput); headerField.appendChild(headerInput);
const prefixField = document.createElement("div"); const prefixField = document.createElement("div");
prefixField.className = "field basic-only"; prefixField.className = "field advanced-only";
const prefixLabel = document.createElement("label"); const prefixLabel = document.createElement("label");
prefixLabel.textContent = "API Key Prefix"; prefixLabel.textContent = "API Key Prefix";
const prefixInput = document.createElement("input"); const prefixInput = document.createElement("input");
@@ -235,17 +250,28 @@ function buildApiConfigCard(config) {
const actions = document.createElement("div"); const actions = document.createElement("div");
actions.className = "api-config-actions"; actions.className = "api-config-actions";
if (!isAdvanced) { const advancedBtn = document.createElement("button");
const advancedBtn = document.createElement("button"); advancedBtn.type = "button";
advancedBtn.type = "button"; advancedBtn.className = "ghost advanced-toggle";
advancedBtn.className = "ghost advanced-toggle"; advancedBtn.textContent = "Advanced Mode";
advancedBtn.textContent = "Advanced Mode"; advancedBtn.addEventListener("click", () => {
advancedBtn.addEventListener("click", () => { if (card.classList.contains("is-advanced")) return;
setApiConfigAdvanced(card, true); urlInput.value = buildChatUrlFromBase(baseInput.value);
updateApiConfigSelect(activeApiConfigSelect.value); templateInput.value = [
}); "{",
actions.appendChild(advancedBtn); ` \"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"); const duplicateBtn = document.createElement("button");
duplicateBtn.type = "button"; duplicateBtn.type = "button";
@@ -259,7 +285,7 @@ function buildApiConfigCard(config) {
const newCard = buildApiConfigCard(copy); const newCard = buildApiConfigCard(copy);
card.insertAdjacentElement("afterend", newCard); card.insertAdjacentElement("afterend", newCard);
updateApiConfigKeyOptions(); updateApiConfigKeyOptions();
updateApiConfigSelect(newCard.dataset.id); updateEnvApiOptions();
}); });
actions.appendChild(duplicateBtn); actions.appendChild(duplicateBtn);
@@ -268,14 +294,14 @@ function buildApiConfigCard(config) {
resetBtn.className = "ghost reset-openai"; resetBtn.className = "ghost reset-openai";
resetBtn.textContent = "Reset to OpenAI"; resetBtn.textContent = "Reset to OpenAI";
resetBtn.addEventListener("click", () => { resetBtn.addEventListener("click", () => {
if (card.classList.contains("is-advanced")) {
setStatus("Advanced mode cannot be reset to OpenAI.");
return;
}
baseInput.value = OPENAI_DEFAULTS.apiBaseUrl; baseInput.value = OPENAI_DEFAULTS.apiBaseUrl;
headerInput.value = OPENAI_DEFAULTS.apiKeyHeader; headerInput.value = OPENAI_DEFAULTS.apiKeyHeader;
prefixInput.value = OPENAI_DEFAULTS.apiKeyPrefix; prefixInput.value = OPENAI_DEFAULTS.apiKeyPrefix;
updateApiConfigSelect(activeApiConfigSelect.value); modelInput.value = DEFAULT_MODEL;
urlInput.value = "";
templateInput.value = "";
setApiConfigAdvanced(card, false);
updateEnvApiOptions();
}); });
actions.appendChild(resetBtn); actions.appendChild(resetBtn);
@@ -285,11 +311,11 @@ function buildApiConfigCard(config) {
deleteBtn.textContent = "Delete"; deleteBtn.textContent = "Delete";
deleteBtn.addEventListener("click", () => { deleteBtn.addEventListener("click", () => {
card.remove(); card.remove();
updateApiConfigSelect(activeApiConfigSelect.value); updateEnvApiOptions();
}); });
actions.appendChild(deleteBtn); actions.appendChild(deleteBtn);
const updateSelect = () => updateApiConfigSelect(activeApiConfigSelect.value); const updateSelect = () => updateEnvApiOptions();
nameInput.addEventListener("input", updateSelect); nameInput.addEventListener("input", updateSelect);
baseInput.addEventListener("input", updateSelect); baseInput.addEventListener("input", updateSelect);
headerInput.addEventListener("input", updateSelect); headerInput.addEventListener("input", updateSelect);
@@ -318,35 +344,6 @@ function collectApiConfigs() {
return cards.map((card) => readApiConfigFromCard(card)); 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) { function buildApiKeyCard(entry) {
const card = document.createElement("div"); const card = document.createElement("div");
card.className = "api-key-card"; 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) { function buildTaskCard(task) {
const card = document.createElement("div"); const card = document.createElement("div");
card.className = "task-card"; card.className = "task-card";
@@ -619,6 +780,8 @@ async function loadSettings() {
activeApiKeyId = "", activeApiKeyId = "",
apiConfigs = [], apiConfigs = [],
activeApiConfigId = "", activeApiConfigId = "",
envConfigs = [],
activeEnvConfigId = "",
apiBaseUrl = "", apiBaseUrl = "",
apiKeyHeader = "", apiKeyHeader = "",
apiKeyPrefix = "", apiKeyPrefix = "",
@@ -633,6 +796,8 @@ async function loadSettings() {
"activeApiKeyId", "activeApiKeyId",
"apiConfigs", "apiConfigs",
"activeApiConfigId", "activeApiConfigId",
"envConfigs",
"activeEnvConfigId",
"apiBaseUrl", "apiBaseUrl",
"apiKeyHeader", "apiKeyHeader",
"apiKeyPrefix", "apiKeyPrefix",
@@ -643,7 +808,6 @@ async function loadSettings() {
"theme" "theme"
]); ]);
systemPromptInput.value = systemPrompt;
resumeInput.value = resume; resumeInput.value = resume;
themeSelect.value = theme; themeSelect.value = theme;
applyTheme(theme); applyTheme(theme);
@@ -727,7 +891,57 @@ async function loadSettings() {
apiConfigsContainer.appendChild(buildApiConfigCard(config)); apiConfigsContainer.appendChild(buildApiConfigCard(config));
} }
updateApiConfigKeyOptions(); 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 = ""; tasksContainer.innerHTML = "";
if (!tasks.length) { if (!tasks.length) {
@@ -748,10 +962,14 @@ async function saveSettings() {
const tasks = collectTasks(); const tasks = collectTasks();
const apiKeys = collectApiKeys(); const apiKeys = collectApiKeys();
const apiConfigs = collectApiConfigs(); const apiConfigs = collectApiConfigs();
const activeApiConfigId = const envConfigs = collectEnvConfigs();
apiConfigs.find((entry) => entry.id === activeApiConfigSelect.value)?.id || const activeEnvConfigId =
apiConfigs[0]?.id || 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 activeConfig = apiConfigs.find((entry) => entry.id === activeApiConfigId);
const activeApiKeyId = const activeApiKeyId =
activeConfig?.apiKeyId || activeConfig?.apiKeyId ||
@@ -762,7 +980,9 @@ async function saveSettings() {
activeApiKeyId, activeApiKeyId,
apiConfigs, apiConfigs,
activeApiConfigId, activeApiConfigId,
systemPrompt: systemPromptInput.value, envConfigs,
activeEnvConfigId,
systemPrompt: activeEnv?.systemPrompt || "",
resume: resumeInput.value, resume: resumeInput.value,
tasks, tasks,
theme: themeSelect.value theme: themeSelect.value
@@ -821,11 +1041,32 @@ addApiConfigBtn.addEventListener("click", () => {
apiConfigsContainer.appendChild(newCard); apiConfigsContainer.appendChild(newCard);
} }
updateApiConfigKeyOptions(); updateApiConfigKeyOptions();
updateApiConfigSelect(activeApiConfigSelect.value); updateEnvApiOptions();
}); });
activeApiConfigSelect.addEventListener("change", () => { addEnvConfigBtn.addEventListener("click", () => {
updateApiConfigSelect(activeApiConfigSelect.value); 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)); themeSelect.addEventListener("change", () => applyTheme(themeSelect.value));