dev-multi-env: modularized environments (#1)

Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
2026-01-17 22:38:36 +00:00
parent 3eb9863c3e
commit b08495815f
8 changed files with 1785 additions and 225 deletions

View File

@@ -1,10 +1,7 @@
const extractBtn = document.getElementById("extractBtn");
const analyzeBtn = document.getElementById("analyzeBtn");
const runBtn = document.getElementById("runBtn");
const abortBtn = document.getElementById("abortBtn");
const extractRunBtn = document.getElementById("extractRunBtn");
const stopRow = document.getElementById("stopRow");
const buttonRow = document.querySelector(".button-row");
const taskSelect = document.getElementById("taskSelect");
const envSelect = document.getElementById("envSelect");
const outputEl = document.getElementById("output");
const statusEl = document.getElementById("status");
const postingCountEl = document.getElementById("postingCount");
@@ -16,14 +13,19 @@ const clearOutputBtn = document.getElementById("clearOutputBtn");
const OUTPUT_STORAGE_KEY = "lastOutput";
const AUTO_RUN_KEY = "autoRunDefaultTask";
const LAST_TASK_KEY = "lastSelectedTaskId";
const LAST_ENV_KEY = "lastSelectedEnvId";
const state = {
postingText: "",
tasks: [],
envs: [],
port: null,
isAnalyzing: false,
outputRaw: "",
autoRunPending: false
autoRunPending: false,
selectedTaskId: "",
selectedEnvId: ""
};
function getStorage(keys) {
@@ -245,15 +247,12 @@ function applyTheme(theme) {
function setAnalyzing(isAnalyzing) {
state.isAnalyzing = isAnalyzing;
analyzeBtn.disabled = isAnalyzing;
runBtn.disabled = isAnalyzing;
abortBtn.disabled = !isAnalyzing;
extractBtn.disabled = isAnalyzing;
extractRunBtn.disabled = isAnalyzing;
if (buttonRow && stopRow) {
buttonRow.classList.toggle("hidden", isAnalyzing);
stopRow.classList.toggle("hidden", !isAnalyzing);
}
runBtn.classList.toggle("hidden", isAnalyzing);
abortBtn.classList.toggle("hidden", !isAnalyzing);
updateTaskSelectState();
updateEnvSelectState();
}
function updatePostingCount() {
@@ -286,11 +285,70 @@ function renderTasks(tasks) {
updateTaskSelectState();
}
function renderEnvironments(envs) {
state.envs = envs;
envSelect.innerHTML = "";
if (!envs.length) {
const option = document.createElement("option");
option.textContent = "No environments configured";
option.value = "";
envSelect.appendChild(option);
updateEnvSelectState();
return;
}
for (const env of envs) {
const option = document.createElement("option");
option.value = env.id;
option.textContent = env.name || "Default";
envSelect.appendChild(option);
}
updateEnvSelectState();
}
function updateTaskSelectState() {
const hasTasks = state.tasks.length > 0;
taskSelect.disabled = state.isAnalyzing || !hasTasks;
}
function updateEnvSelectState() {
const hasEnvs = state.envs.length > 0;
envSelect.disabled = state.isAnalyzing || !hasEnvs;
}
function getTaskDefaultEnvId(task) {
return task?.defaultEnvId || state.envs[0]?.id || "";
}
function setEnvironmentSelection(envId) {
const target =
envId && state.envs.some((env) => env.id === envId)
? envId
: state.envs[0]?.id || "";
if (target) {
envSelect.value = target;
}
state.selectedEnvId = target;
}
function selectTask(taskId, { resetEnv } = { resetEnv: false }) {
if (!taskId) return;
taskSelect.value = taskId;
state.selectedTaskId = taskId;
const task = state.tasks.find((item) => item.id === taskId);
if (resetEnv) {
setEnvironmentSelection(getTaskDefaultEnvId(task));
}
}
async function persistSelections() {
await chrome.storage.local.set({
[LAST_TASK_KEY]: state.selectedTaskId,
[LAST_ENV_KEY]: state.selectedEnvId
});
}
function isWaterlooWorksUrl(url) {
try {
return new URL(url).hostname === "waterlooworks.uwaterloo.ca";
@@ -377,9 +435,45 @@ function ensurePort() {
return port;
}
async function loadTasks() {
const { tasks = [] } = await getStorage(["tasks"]);
async function loadConfig() {
const stored = await getStorage([
"tasks",
"envConfigs",
LAST_TASK_KEY,
LAST_ENV_KEY
]);
const tasks = Array.isArray(stored.tasks) ? stored.tasks : [];
const envs = Array.isArray(stored.envConfigs) ? stored.envConfigs : [];
renderTasks(tasks);
renderEnvironments(envs);
if (!tasks.length) {
state.selectedTaskId = "";
state.selectedEnvId = envs[0]?.id || "";
return;
}
const storedTaskId = stored[LAST_TASK_KEY];
const storedEnvId = stored[LAST_ENV_KEY];
const initialTaskId = tasks.some((task) => task.id === storedTaskId)
? storedTaskId
: tasks[0].id;
selectTask(initialTaskId, { resetEnv: false });
const task = tasks.find((item) => item.id === initialTaskId);
if (storedEnvId && envs.some((env) => env.id === storedEnvId)) {
setEnvironmentSelection(storedEnvId);
} else {
setEnvironmentSelection(getTaskDefaultEnvId(task));
}
if (
storedTaskId !== state.selectedTaskId ||
storedEnvId !== state.selectedEnvId
) {
await persistSelections();
}
maybeRunDefaultTask();
}
@@ -428,30 +522,96 @@ async function handleAnalyze() {
return;
}
const { apiKey, apiBaseUrl, apiKeyHeader, apiKeyPrefix, model, systemPrompt, resume } =
await getStorage([
"apiKey",
"apiBaseUrl",
"apiKeyHeader",
"apiKeyPrefix",
"model",
"systemPrompt",
"resume"
]);
const {
apiKeys = [],
activeApiKeyId = "",
apiConfigs = [],
activeApiConfigId = "",
envConfigs = [],
apiBaseUrl,
apiKeyHeader,
apiKeyPrefix,
model,
systemPrompt,
resume
} = await getStorage([
"apiKeys",
"activeApiKeyId",
"apiConfigs",
"activeApiConfigId",
"envConfigs",
"apiBaseUrl",
"apiKeyHeader",
"apiKeyPrefix",
"model",
"systemPrompt",
"resume"
]);
if (!apiBaseUrl) {
setStatus("Set an API base URL in Settings.");
const resolvedConfigs = Array.isArray(apiConfigs) ? apiConfigs : [];
const resolvedEnvs = Array.isArray(envConfigs) ? envConfigs : [];
const selectedEnvId = envSelect.value;
const activeEnv =
resolvedEnvs.find((entry) => entry.id === selectedEnvId) ||
resolvedEnvs[0];
if (!activeEnv) {
setStatus("Add an environment in Settings.");
return;
}
if (apiKeyHeader && !apiKey) {
setStatus("Add your API key in Settings.");
const resolvedSystemPrompt =
activeEnv.systemPrompt ?? systemPrompt ?? "";
const resolvedApiConfigId =
activeEnv.apiConfigId || activeApiConfigId || resolvedConfigs[0]?.id || "";
const activeConfig =
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 = activeConfig?.apiBaseUrl || apiBaseUrl || "";
const resolvedApiKeyHeader = activeConfig?.apiKeyHeader ?? apiKeyHeader ?? "";
const resolvedApiKeyPrefix = activeConfig?.apiKeyPrefix ?? apiKeyPrefix ?? "";
const resolvedModel = activeConfig?.model || model || "";
if (!model) {
setStatus("Set a model name in Settings.");
return;
const resolvedKeys = Array.isArray(apiKeys) ? apiKeys : [];
const resolvedKeyId =
activeConfig?.apiKeyId || activeApiKeyId || resolvedKeys[0]?.id || "";
const activeKey = resolvedKeys.find((entry) => entry.id === resolvedKeyId);
const apiKey = activeKey?.key || "";
if (isAdvanced) {
if (!resolvedApiUrl) {
setStatus("Set an API URL in Settings.");
return;
}
if (!resolvedTemplate) {
setStatus("Set a request template in Settings.");
return;
}
const needsKey =
Boolean(resolvedApiKeyHeader) ||
resolvedTemplate.includes("API_KEY_GOES_HERE");
if (needsKey && !apiKey) {
setStatus("Add an API key in Settings.");
return;
}
} else {
if (!resolvedApiBaseUrl) {
setStatus("Set an API base URL in Settings.");
return;
}
if (resolvedApiKeyHeader && !apiKey) {
setStatus("Add an API key in Settings.");
return;
}
if (!resolvedModel) {
setStatus("Set a model name in Settings.");
return;
}
}
const promptText = buildUserMessage(resume || "", task.text || "", state.postingText);
@@ -467,11 +627,14 @@ async function handleAnalyze() {
type: "START_ANALYSIS",
payload: {
apiKey,
apiBaseUrl,
apiKeyHeader,
apiKeyPrefix,
model,
systemPrompt: systemPrompt || "",
apiMode: isAdvanced ? "advanced" : "basic",
apiUrl: resolvedApiUrl,
requestTemplate: resolvedTemplate,
apiBaseUrl: resolvedApiBaseUrl,
apiKeyHeader: resolvedApiKeyHeader,
apiKeyPrefix: resolvedApiKeyPrefix,
model: resolvedModel,
systemPrompt: resolvedSystemPrompt,
resume: resume || "",
taskText: task.text || "",
postingText: state.postingText,
@@ -527,20 +690,26 @@ function handleCopyRaw() {
void copyTextToClipboard(text, "Markdown");
}
extractBtn.addEventListener("click", handleExtract);
analyzeBtn.addEventListener("click", handleAnalyze);
extractRunBtn.addEventListener("click", handleExtractAndAnalyze);
runBtn.addEventListener("click", handleExtractAndAnalyze);
abortBtn.addEventListener("click", handleAbort);
settingsBtn.addEventListener("click", () => chrome.runtime.openOptionsPage());
copyRenderedBtn.addEventListener("click", handleCopyRendered);
copyRawBtn.addEventListener("click", handleCopyRaw);
clearOutputBtn.addEventListener("click", () => void handleClearOutput());
taskSelect.addEventListener("change", () => {
selectTask(taskSelect.value, { resetEnv: true });
void persistSelections();
});
envSelect.addEventListener("change", () => {
setEnvironmentSelection(envSelect.value);
void persistSelections();
});
updatePostingCount();
updatePromptCount(0);
renderOutput();
setAnalyzing(false);
loadTasks();
loadConfig();
loadTheme();
async function loadSavedOutput() {
@@ -562,7 +731,8 @@ function maybeRunDefaultTask() {
if (!state.autoRunPending) return;
if (state.isAnalyzing) return;
if (!state.tasks.length) return;
taskSelect.value = state.tasks[0].id;
selectTask(state.tasks[0].id, { resetEnv: true });
void persistSelections();
state.autoRunPending = false;
void handleExtractAndAnalyze();
}