added sidebar for ergonomics and error display of the configuration
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
const resumeInput = document.getElementById("resume");
|
||||
const saveBtn = document.getElementById("saveBtn");
|
||||
const saveBtnSidebar = document.getElementById("saveBtnSidebar");
|
||||
const addApiConfigBtn = document.getElementById("addApiConfigBtn");
|
||||
const apiConfigsContainer = document.getElementById("apiConfigs");
|
||||
const addApiKeyBtn = document.getElementById("addApiKeyBtn");
|
||||
@@ -8,7 +8,11 @@ const addEnvConfigBtn = document.getElementById("addEnvConfigBtn");
|
||||
const envConfigsContainer = document.getElementById("envConfigs");
|
||||
const addTaskBtn = document.getElementById("addTaskBtn");
|
||||
const tasksContainer = document.getElementById("tasks");
|
||||
const addProfileBtn = document.getElementById("addProfileBtn");
|
||||
const profilesContainer = document.getElementById("profiles");
|
||||
const statusEl = document.getElementById("status");
|
||||
const statusSidebarEl = document.getElementById("statusSidebar");
|
||||
const sidebarErrorsEl = document.getElementById("sidebarErrors");
|
||||
const themeSelect = document.getElementById("themeSelect");
|
||||
|
||||
const OPENAI_DEFAULTS = {
|
||||
@@ -26,12 +30,24 @@ function getStorage(keys) {
|
||||
|
||||
function setStatus(message) {
|
||||
statusEl.textContent = message;
|
||||
if (statusSidebarEl) statusSidebarEl.textContent = message;
|
||||
if (!message) return;
|
||||
setTimeout(() => {
|
||||
if (statusEl.textContent === message) statusEl.textContent = "";
|
||||
if (statusSidebarEl?.textContent === message) statusSidebarEl.textContent = "";
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
let sidebarErrorFrame = null;
|
||||
function scheduleSidebarErrors() {
|
||||
if (!sidebarErrorsEl) return;
|
||||
if (sidebarErrorFrame) return;
|
||||
sidebarErrorFrame = requestAnimationFrame(() => {
|
||||
sidebarErrorFrame = null;
|
||||
updateSidebarErrors();
|
||||
});
|
||||
}
|
||||
|
||||
function applyTheme(theme) {
|
||||
const value = theme || "system";
|
||||
document.documentElement.dataset.theme = value;
|
||||
@@ -57,6 +73,11 @@ function newEnvConfigId() {
|
||||
return `env-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
||||
}
|
||||
|
||||
function newProfileId() {
|
||||
if (crypto?.randomUUID) return crypto.randomUUID();
|
||||
return `profile-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
||||
}
|
||||
|
||||
function buildChatUrlFromBase(baseUrl) {
|
||||
const trimmed = (baseUrl || "").trim().replace(/\/+$/, "");
|
||||
if (!trimmed) return "";
|
||||
@@ -94,6 +115,10 @@ function getTopEnvId() {
|
||||
return collectEnvConfigs()[0]?.id || "";
|
||||
}
|
||||
|
||||
function getTopProfileId() {
|
||||
return collectProfiles()[0]?.id || "";
|
||||
}
|
||||
|
||||
function setApiConfigAdvanced(card, isAdvanced) {
|
||||
card.classList.toggle("is-advanced", isAdvanced);
|
||||
card.dataset.mode = isAdvanced ? "advanced" : "basic";
|
||||
@@ -253,6 +278,10 @@ function buildApiConfigCard(config) {
|
||||
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "api-config-actions";
|
||||
const leftActions = document.createElement("div");
|
||||
leftActions.className = "api-config-actions-left";
|
||||
const rightActions = document.createElement("div");
|
||||
rightActions.className = "api-config-actions-right";
|
||||
const moveTopBtn = document.createElement("button");
|
||||
moveTopBtn.type = "button";
|
||||
moveTopBtn.className = "ghost move-top";
|
||||
@@ -265,6 +294,10 @@ function buildApiConfigCard(config) {
|
||||
moveDownBtn.type = "button";
|
||||
moveDownBtn.className = "ghost move-down";
|
||||
moveDownBtn.textContent = "Down";
|
||||
const addBelowBtn = document.createElement("button");
|
||||
addBelowBtn.type = "button";
|
||||
addBelowBtn.className = "ghost add-below";
|
||||
addBelowBtn.textContent = "Add";
|
||||
|
||||
moveTopBtn.addEventListener("click", () => {
|
||||
const first = apiConfigsContainer.firstElementChild;
|
||||
@@ -290,9 +323,27 @@ function buildApiConfigCard(config) {
|
||||
updateEnvApiOptions();
|
||||
});
|
||||
|
||||
actions.appendChild(moveTopBtn);
|
||||
actions.appendChild(moveUpBtn);
|
||||
actions.appendChild(moveDownBtn);
|
||||
addBelowBtn.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
|
||||
});
|
||||
card.insertAdjacentElement("afterend", newCard);
|
||||
updateApiConfigKeyOptions();
|
||||
updateEnvApiOptions();
|
||||
updateApiConfigControls();
|
||||
});
|
||||
|
||||
const advancedBtn = document.createElement("button");
|
||||
advancedBtn.type = "button";
|
||||
advancedBtn.className = "ghost advanced-toggle";
|
||||
@@ -314,8 +365,6 @@ function buildApiConfigCard(config) {
|
||||
setApiConfigAdvanced(card, true);
|
||||
updateEnvApiOptions();
|
||||
});
|
||||
actions.appendChild(advancedBtn);
|
||||
|
||||
const duplicateBtn = document.createElement("button");
|
||||
duplicateBtn.type = "button";
|
||||
duplicateBtn.className = "ghost duplicate";
|
||||
@@ -330,8 +379,6 @@ function buildApiConfigCard(config) {
|
||||
updateApiConfigKeyOptions();
|
||||
updateEnvApiOptions();
|
||||
});
|
||||
actions.appendChild(duplicateBtn);
|
||||
|
||||
const resetBtn = document.createElement("button");
|
||||
resetBtn.type = "button";
|
||||
resetBtn.className = "ghost reset-openai";
|
||||
@@ -346,8 +393,6 @@ function buildApiConfigCard(config) {
|
||||
setApiConfigAdvanced(card, false);
|
||||
updateEnvApiOptions();
|
||||
});
|
||||
actions.appendChild(resetBtn);
|
||||
|
||||
const deleteBtn = document.createElement("button");
|
||||
deleteBtn.type = "button";
|
||||
deleteBtn.className = "ghost delete";
|
||||
@@ -357,7 +402,6 @@ function buildApiConfigCard(config) {
|
||||
updateEnvApiOptions();
|
||||
updateApiConfigControls();
|
||||
});
|
||||
actions.appendChild(deleteBtn);
|
||||
|
||||
const updateSelect = () => updateEnvApiOptions();
|
||||
nameInput.addEventListener("input", updateSelect);
|
||||
@@ -368,6 +412,19 @@ function buildApiConfigCard(config) {
|
||||
urlInput.addEventListener("input", updateSelect);
|
||||
templateInput.addEventListener("input", updateSelect);
|
||||
|
||||
rightActions.appendChild(moveTopBtn);
|
||||
rightActions.appendChild(moveUpBtn);
|
||||
rightActions.appendChild(moveDownBtn);
|
||||
rightActions.appendChild(addBelowBtn);
|
||||
rightActions.appendChild(duplicateBtn);
|
||||
rightActions.appendChild(deleteBtn);
|
||||
|
||||
leftActions.appendChild(advancedBtn);
|
||||
leftActions.appendChild(resetBtn);
|
||||
|
||||
actions.appendChild(leftActions);
|
||||
actions.appendChild(rightActions);
|
||||
|
||||
card.appendChild(nameField);
|
||||
card.appendChild(keyField);
|
||||
card.appendChild(baseField);
|
||||
@@ -398,6 +455,7 @@ function updateApiConfigControls() {
|
||||
if (moveUpBtn) moveUpBtn.disabled = index === 0;
|
||||
if (moveDownBtn) moveDownBtn.disabled = index === cards.length - 1;
|
||||
});
|
||||
scheduleSidebarErrors();
|
||||
}
|
||||
|
||||
function buildApiKeyCard(entry) {
|
||||
@@ -456,6 +514,10 @@ function buildApiKeyCard(entry) {
|
||||
moveDownBtn.type = "button";
|
||||
moveDownBtn.className = "ghost move-down";
|
||||
moveDownBtn.textContent = "Down";
|
||||
const addBelowBtn = document.createElement("button");
|
||||
addBelowBtn.type = "button";
|
||||
addBelowBtn.className = "ghost add-below";
|
||||
addBelowBtn.textContent = "Add";
|
||||
|
||||
moveTopBtn.addEventListener("click", () => {
|
||||
const first = apiKeysContainer.firstElementChild;
|
||||
@@ -481,9 +543,20 @@ function buildApiKeyCard(entry) {
|
||||
updateApiConfigKeyOptions();
|
||||
});
|
||||
|
||||
addBelowBtn.addEventListener("click", () => {
|
||||
const name = buildUniqueDefaultName(
|
||||
collectNames(apiKeysContainer, ".api-key-name")
|
||||
);
|
||||
const newCard = buildApiKeyCard({ id: newApiKeyId(), name, key: "" });
|
||||
card.insertAdjacentElement("afterend", newCard);
|
||||
updateApiConfigKeyOptions();
|
||||
updateApiKeyControls();
|
||||
});
|
||||
|
||||
actions.appendChild(moveTopBtn);
|
||||
actions.appendChild(moveUpBtn);
|
||||
actions.appendChild(moveDownBtn);
|
||||
actions.appendChild(addBelowBtn);
|
||||
const deleteBtn = document.createElement("button");
|
||||
deleteBtn.type = "button";
|
||||
deleteBtn.className = "ghost delete";
|
||||
@@ -529,6 +602,7 @@ function updateApiKeyControls() {
|
||||
if (moveUpBtn) moveUpBtn.disabled = index === 0;
|
||||
if (moveDownBtn) moveDownBtn.disabled = index === cards.length - 1;
|
||||
});
|
||||
scheduleSidebarErrors();
|
||||
}
|
||||
|
||||
function updateApiConfigKeyOptions() {
|
||||
@@ -615,6 +689,10 @@ function buildEnvConfigCard(config) {
|
||||
moveDownBtn.type = "button";
|
||||
moveDownBtn.className = "ghost move-down";
|
||||
moveDownBtn.textContent = "Down";
|
||||
const addBelowBtn = document.createElement("button");
|
||||
addBelowBtn.type = "button";
|
||||
addBelowBtn.className = "ghost add-below";
|
||||
addBelowBtn.textContent = "Add";
|
||||
|
||||
moveTopBtn.addEventListener("click", () => {
|
||||
const first = envConfigsContainer.firstElementChild;
|
||||
@@ -643,6 +721,24 @@ function buildEnvConfigCard(config) {
|
||||
actions.appendChild(moveTopBtn);
|
||||
actions.appendChild(moveUpBtn);
|
||||
actions.appendChild(moveDownBtn);
|
||||
actions.appendChild(addBelowBtn);
|
||||
|
||||
addBelowBtn.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
|
||||
});
|
||||
card.insertAdjacentElement("afterend", newCard);
|
||||
updateEnvApiOptions();
|
||||
updateEnvControls();
|
||||
updateTaskEnvOptions();
|
||||
});
|
||||
|
||||
const duplicateBtn = document.createElement("button");
|
||||
duplicateBtn.type = "button";
|
||||
@@ -700,6 +796,7 @@ function updateEnvControls() {
|
||||
if (moveUpBtn) moveUpBtn.disabled = index === 0;
|
||||
if (moveDownBtn) moveDownBtn.disabled = index === cards.length - 1;
|
||||
});
|
||||
scheduleSidebarErrors();
|
||||
}
|
||||
|
||||
function updateTaskEnvOptions() {
|
||||
@@ -733,6 +830,7 @@ function updateTaskEnvOptions() {
|
||||
|
||||
select.dataset.preferred = select.value;
|
||||
});
|
||||
scheduleSidebarErrors();
|
||||
}
|
||||
|
||||
function collectEnvConfigs() {
|
||||
@@ -750,6 +848,222 @@ function collectEnvConfigs() {
|
||||
});
|
||||
}
|
||||
|
||||
function buildProfileCard(profile) {
|
||||
const card = document.createElement("div");
|
||||
card.className = "profile-card";
|
||||
card.dataset.id = profile.id || newProfileId();
|
||||
|
||||
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 = profile.name || "";
|
||||
nameInput.className = "profile-name";
|
||||
nameField.appendChild(nameLabel);
|
||||
nameField.appendChild(nameInput);
|
||||
|
||||
const typeField = document.createElement("div");
|
||||
typeField.className = "field";
|
||||
const typeLabel = document.createElement("label");
|
||||
typeLabel.textContent = "Type";
|
||||
const typeSelect = document.createElement("select");
|
||||
typeSelect.className = "profile-type";
|
||||
const resumeOption = document.createElement("option");
|
||||
resumeOption.value = "Resume";
|
||||
resumeOption.textContent = "Resume";
|
||||
const profileOption = document.createElement("option");
|
||||
profileOption.value = "Profile";
|
||||
profileOption.textContent = "Profile";
|
||||
typeSelect.appendChild(resumeOption);
|
||||
typeSelect.appendChild(profileOption);
|
||||
typeSelect.value = profile.type === "Profile" ? "Profile" : "Resume";
|
||||
typeField.appendChild(typeLabel);
|
||||
typeField.appendChild(typeSelect);
|
||||
|
||||
const textField = document.createElement("div");
|
||||
textField.className = "field";
|
||||
const textLabel = document.createElement("label");
|
||||
textLabel.textContent = "Profile text";
|
||||
const textArea = document.createElement("textarea");
|
||||
textArea.rows = 8;
|
||||
textArea.value = profile.text || "";
|
||||
textArea.className = "profile-text";
|
||||
textField.appendChild(textLabel);
|
||||
textField.appendChild(textArea);
|
||||
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "profile-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";
|
||||
const addBelowBtn = document.createElement("button");
|
||||
addBelowBtn.type = "button";
|
||||
addBelowBtn.className = "ghost add-below";
|
||||
addBelowBtn.textContent = "Add";
|
||||
|
||||
moveTopBtn.addEventListener("click", () => {
|
||||
const first = profilesContainer.firstElementChild;
|
||||
if (!first || first === card) return;
|
||||
profilesContainer.insertBefore(card, first);
|
||||
updateProfileControls();
|
||||
updateTaskProfileOptions();
|
||||
});
|
||||
|
||||
moveUpBtn.addEventListener("click", () => {
|
||||
const previous = card.previousElementSibling;
|
||||
if (!previous) return;
|
||||
profilesContainer.insertBefore(card, previous);
|
||||
updateProfileControls();
|
||||
updateTaskProfileOptions();
|
||||
});
|
||||
|
||||
moveDownBtn.addEventListener("click", () => {
|
||||
const next = card.nextElementSibling;
|
||||
if (!next) return;
|
||||
profilesContainer.insertBefore(card, next.nextElementSibling);
|
||||
updateProfileControls();
|
||||
updateTaskProfileOptions();
|
||||
});
|
||||
|
||||
actions.appendChild(moveTopBtn);
|
||||
actions.appendChild(moveUpBtn);
|
||||
actions.appendChild(moveDownBtn);
|
||||
actions.appendChild(addBelowBtn);
|
||||
|
||||
addBelowBtn.addEventListener("click", () => {
|
||||
const name = buildUniqueDefaultName(
|
||||
collectNames(profilesContainer, ".profile-name")
|
||||
);
|
||||
const newCard = buildProfileCard({
|
||||
id: newProfileId(),
|
||||
name,
|
||||
text: "",
|
||||
type: "Resume"
|
||||
});
|
||||
card.insertAdjacentElement("afterend", newCard);
|
||||
updateProfileControls();
|
||||
updateTaskProfileOptions();
|
||||
});
|
||||
|
||||
const duplicateBtn = document.createElement("button");
|
||||
duplicateBtn.type = "button";
|
||||
duplicateBtn.className = "ghost duplicate";
|
||||
duplicateBtn.textContent = "Duplicate";
|
||||
duplicateBtn.addEventListener("click", () => {
|
||||
const names = collectNames(profilesContainer, ".profile-name");
|
||||
const copy = collectProfiles().find((entry) => entry.id === card.dataset.id) || {
|
||||
id: card.dataset.id,
|
||||
name: nameInput.value || "Default",
|
||||
text: textArea.value || "",
|
||||
type: typeSelect.value || "Resume"
|
||||
};
|
||||
const newCard = buildProfileCard({
|
||||
id: newProfileId(),
|
||||
name: ensureUniqueName(`${copy.name || "Default"} Copy`, names),
|
||||
text: copy.text,
|
||||
type: copy.type || "Resume"
|
||||
});
|
||||
card.insertAdjacentElement("afterend", newCard);
|
||||
updateProfileControls();
|
||||
updateTaskProfileOptions();
|
||||
});
|
||||
|
||||
const deleteBtn = document.createElement("button");
|
||||
deleteBtn.type = "button";
|
||||
deleteBtn.className = "ghost delete";
|
||||
deleteBtn.textContent = "Delete";
|
||||
deleteBtn.addEventListener("click", () => {
|
||||
card.remove();
|
||||
updateProfileControls();
|
||||
updateTaskProfileOptions();
|
||||
});
|
||||
|
||||
actions.appendChild(duplicateBtn);
|
||||
actions.appendChild(deleteBtn);
|
||||
|
||||
nameInput.addEventListener("input", () => updateTaskProfileOptions());
|
||||
|
||||
card.appendChild(nameField);
|
||||
card.appendChild(typeField);
|
||||
card.appendChild(textField);
|
||||
card.appendChild(actions);
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
function collectProfiles() {
|
||||
const cards = [...profilesContainer.querySelectorAll(".profile-card")];
|
||||
return cards.map((card) => {
|
||||
const nameInput = card.querySelector(".profile-name");
|
||||
const textArea = card.querySelector(".profile-text");
|
||||
const typeSelect = card.querySelector(".profile-type");
|
||||
return {
|
||||
id: card.dataset.id || newProfileId(),
|
||||
name: (nameInput?.value || "Default").trim(),
|
||||
text: (textArea?.value || "").trim(),
|
||||
type: typeSelect?.value || "Resume"
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function updateProfileControls() {
|
||||
const cards = [...profilesContainer.querySelectorAll(".profile-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;
|
||||
});
|
||||
scheduleSidebarErrors();
|
||||
}
|
||||
|
||||
function updateTaskProfileOptions() {
|
||||
const profiles = collectProfiles();
|
||||
const selects = tasksContainer.querySelectorAll(".task-profile-select");
|
||||
selects.forEach((select) => {
|
||||
const preferred = select.dataset.preferred || select.value;
|
||||
select.innerHTML = "";
|
||||
if (!profiles.length) {
|
||||
const option = document.createElement("option");
|
||||
option.value = "";
|
||||
option.textContent = "No profiles configured";
|
||||
select.appendChild(option);
|
||||
select.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
select.disabled = false;
|
||||
for (const profile of profiles) {
|
||||
const option = document.createElement("option");
|
||||
option.value = profile.id;
|
||||
option.textContent = profile.name || "Default";
|
||||
select.appendChild(option);
|
||||
}
|
||||
|
||||
if (preferred && profiles.some((profile) => profile.id === preferred)) {
|
||||
select.value = preferred;
|
||||
} else {
|
||||
select.value = profiles[0].id;
|
||||
}
|
||||
|
||||
select.dataset.preferred = select.value;
|
||||
});
|
||||
scheduleSidebarErrors();
|
||||
}
|
||||
|
||||
function updateEnvApiOptions() {
|
||||
const apiConfigs = collectApiConfigs();
|
||||
const selects = envConfigsContainer.querySelectorAll(".env-config-api-select");
|
||||
@@ -810,6 +1124,16 @@ function buildTaskCard(task) {
|
||||
envField.appendChild(envLabel);
|
||||
envField.appendChild(envSelect);
|
||||
|
||||
const profileField = document.createElement("div");
|
||||
profileField.className = "field";
|
||||
const profileLabel = document.createElement("label");
|
||||
profileLabel.textContent = "Default profile";
|
||||
const profileSelect = document.createElement("select");
|
||||
profileSelect.className = "task-profile-select";
|
||||
profileSelect.dataset.preferred = task.defaultProfileId || "";
|
||||
profileField.appendChild(profileLabel);
|
||||
profileField.appendChild(profileSelect);
|
||||
|
||||
const textField = document.createElement("div");
|
||||
textField.className = "field";
|
||||
const textLabel = document.createElement("label");
|
||||
@@ -889,11 +1213,13 @@ function buildTaskCard(task) {
|
||||
id: newTaskId(),
|
||||
name,
|
||||
text: "",
|
||||
defaultEnvId: getTopEnvId()
|
||||
defaultEnvId: getTopEnvId(),
|
||||
defaultProfileId: getTopProfileId()
|
||||
});
|
||||
card.insertAdjacentElement("afterend", newCard);
|
||||
updateTaskControls();
|
||||
updateTaskEnvOptions();
|
||||
updateTaskProfileOptions();
|
||||
});
|
||||
|
||||
duplicateBtn.addEventListener("click", () => {
|
||||
@@ -904,12 +1230,14 @@ function buildTaskCard(task) {
|
||||
collectNames(tasksContainer, ".task-name")
|
||||
),
|
||||
text: textArea.value,
|
||||
defaultEnvId: envSelect.value || ""
|
||||
defaultEnvId: envSelect.value || "",
|
||||
defaultProfileId: profileSelect.value || ""
|
||||
};
|
||||
const newCard = buildTaskCard(copy);
|
||||
card.insertAdjacentElement("afterend", newCard);
|
||||
updateTaskControls();
|
||||
updateTaskEnvOptions();
|
||||
updateTaskProfileOptions();
|
||||
});
|
||||
|
||||
deleteBtn.addEventListener("click", () => {
|
||||
@@ -926,6 +1254,7 @@ function buildTaskCard(task) {
|
||||
|
||||
card.appendChild(nameField);
|
||||
card.appendChild(envField);
|
||||
card.appendChild(profileField);
|
||||
card.appendChild(textField);
|
||||
card.appendChild(actions);
|
||||
|
||||
@@ -942,6 +1271,7 @@ function updateTaskControls() {
|
||||
if (moveUpBtn) moveUpBtn.disabled = index === 0;
|
||||
if (moveDownBtn) moveDownBtn.disabled = index === cards.length - 1;
|
||||
});
|
||||
scheduleSidebarErrors();
|
||||
}
|
||||
|
||||
function collectTasks() {
|
||||
@@ -950,15 +1280,127 @@ function collectTasks() {
|
||||
const nameInput = card.querySelector(".task-name");
|
||||
const textArea = card.querySelector(".task-text");
|
||||
const envSelect = card.querySelector(".task-env-select");
|
||||
const profileSelect = card.querySelector(".task-profile-select");
|
||||
return {
|
||||
id: card.dataset.id || newTaskId(),
|
||||
name: (nameInput?.value || "Untitled Task").trim(),
|
||||
text: (textArea?.value || "").trim(),
|
||||
defaultEnvId: envSelect?.value || ""
|
||||
defaultEnvId: envSelect?.value || "",
|
||||
defaultProfileId: profileSelect?.value || ""
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function updateSidebarErrors() {
|
||||
if (!sidebarErrorsEl) return;
|
||||
const errors = [];
|
||||
|
||||
const tasks = collectTasks();
|
||||
const envs = collectEnvConfigs();
|
||||
const profiles = collectProfiles();
|
||||
const apiConfigs = collectApiConfigs();
|
||||
const apiKeys = collectApiKeys();
|
||||
|
||||
const checkNameInputs = (container, selector, label) => {
|
||||
if (!container) return;
|
||||
const inputs = [...container.querySelectorAll(selector)];
|
||||
if (!inputs.length) return;
|
||||
const seen = new Map();
|
||||
let hasEmpty = false;
|
||||
for (const input of inputs) {
|
||||
const name = (input.value || "").trim();
|
||||
if (!name) {
|
||||
hasEmpty = true;
|
||||
continue;
|
||||
}
|
||||
const lower = name.toLowerCase();
|
||||
seen.set(lower, (seen.get(lower) || 0) + 1);
|
||||
}
|
||||
if (hasEmpty) {
|
||||
errors.push(`${label} has empty names.`);
|
||||
}
|
||||
for (const [name, count] of seen.entries()) {
|
||||
if (count > 1) {
|
||||
errors.push(`${label} has duplicate name: ${name}.`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
checkNameInputs(tasksContainer, ".task-name", "Task presets");
|
||||
checkNameInputs(envConfigsContainer, ".env-config-name", "Environments");
|
||||
checkNameInputs(profilesContainer, ".profile-name", "Profiles");
|
||||
checkNameInputs(apiConfigsContainer, ".api-config-name", "API configs");
|
||||
checkNameInputs(apiKeysContainer, ".api-key-name", "API keys");
|
||||
|
||||
if (!tasks.length) errors.push("No task presets configured.");
|
||||
if (!envs.length) errors.push("No environments configured.");
|
||||
if (!profiles.length) errors.push("No profiles configured.");
|
||||
if (!apiConfigs.length) errors.push("No API configs configured.");
|
||||
if (!apiKeys.length) errors.push("No API keys configured.");
|
||||
|
||||
if (tasks.length) {
|
||||
const defaultTask = tasks[0];
|
||||
if (!defaultTask.text) errors.push("Default task prompt is empty.");
|
||||
|
||||
const defaultEnv =
|
||||
envs.find((env) => env.id === defaultTask.defaultEnvId) || envs[0];
|
||||
if (!defaultEnv) {
|
||||
errors.push("Default task environment is missing.");
|
||||
}
|
||||
|
||||
const defaultProfile =
|
||||
profiles.find((profile) => profile.id === defaultTask.defaultProfileId) ||
|
||||
profiles[0];
|
||||
if (!defaultProfile) {
|
||||
errors.push("Default task profile is missing.");
|
||||
} else if (!defaultProfile.text) {
|
||||
errors.push("Default profile text is empty.");
|
||||
}
|
||||
|
||||
const defaultApiConfig = defaultEnv
|
||||
? apiConfigs.find((config) => config.id === defaultEnv.apiConfigId)
|
||||
: null;
|
||||
if (!defaultApiConfig) {
|
||||
errors.push("Default environment is missing an API config.");
|
||||
} else if (defaultApiConfig.advanced) {
|
||||
if (!defaultApiConfig.apiUrl) {
|
||||
errors.push("Default API config is missing an API URL.");
|
||||
}
|
||||
if (!defaultApiConfig.requestTemplate) {
|
||||
errors.push("Default API config is missing a request template.");
|
||||
}
|
||||
} else {
|
||||
if (!defaultApiConfig.apiBaseUrl) {
|
||||
errors.push("Default API config is missing a base URL.");
|
||||
}
|
||||
if (!defaultApiConfig.model) {
|
||||
errors.push("Default API config is missing a model name.");
|
||||
}
|
||||
}
|
||||
|
||||
const needsKey =
|
||||
Boolean(defaultApiConfig?.apiKeyHeader) ||
|
||||
Boolean(
|
||||
defaultApiConfig?.requestTemplate?.includes("API_KEY_GOES_HERE")
|
||||
);
|
||||
if (needsKey) {
|
||||
const key = apiKeys.find((entry) => entry.id === defaultApiConfig?.apiKeyId);
|
||||
if (!key || !key.key) {
|
||||
errors.push("Default API config is missing an API key.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!errors.length) {
|
||||
sidebarErrorsEl.classList.add("hidden");
|
||||
sidebarErrorsEl.textContent = "";
|
||||
return;
|
||||
}
|
||||
|
||||
sidebarErrorsEl.textContent = errors.map((error) => `- ${error}`).join("\n");
|
||||
sidebarErrorsEl.classList.remove("hidden");
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
const {
|
||||
apiKey = "",
|
||||
@@ -968,6 +1410,7 @@ async function loadSettings() {
|
||||
activeApiConfigId = "",
|
||||
envConfigs = [],
|
||||
activeEnvConfigId = "",
|
||||
profiles = [],
|
||||
apiBaseUrl = "",
|
||||
apiKeyHeader = "",
|
||||
apiKeyPrefix = "",
|
||||
@@ -984,6 +1427,7 @@ async function loadSettings() {
|
||||
"activeApiConfigId",
|
||||
"envConfigs",
|
||||
"activeEnvConfigId",
|
||||
"profiles",
|
||||
"apiBaseUrl",
|
||||
"apiKeyHeader",
|
||||
"apiKeyPrefix",
|
||||
@@ -994,7 +1438,6 @@ async function loadSettings() {
|
||||
"theme"
|
||||
]);
|
||||
|
||||
resumeInput.value = resume;
|
||||
themeSelect.value = theme;
|
||||
applyTheme(theme);
|
||||
|
||||
@@ -1130,18 +1573,55 @@ async function loadSettings() {
|
||||
updateEnvApiOptions();
|
||||
updateEnvControls();
|
||||
|
||||
let resolvedProfiles = Array.isArray(profiles) ? profiles : [];
|
||||
if (!resolvedProfiles.length) {
|
||||
const migrated = {
|
||||
id: newProfileId(),
|
||||
name: "Default",
|
||||
text: resume || "",
|
||||
type: "Resume"
|
||||
};
|
||||
resolvedProfiles = [migrated];
|
||||
await chrome.storage.local.set({ profiles: resolvedProfiles });
|
||||
} else {
|
||||
const normalized = resolvedProfiles.map((profile) => ({
|
||||
...profile,
|
||||
text: profile.text ?? "",
|
||||
type: profile.type === "Profile" ? "Profile" : "Resume"
|
||||
}));
|
||||
const needsUpdate = normalized.some(
|
||||
(profile, index) =>
|
||||
(profile.text || "") !== (resolvedProfiles[index]?.text || "") ||
|
||||
(profile.type || "Resume") !== (resolvedProfiles[index]?.type || "Resume")
|
||||
);
|
||||
if (needsUpdate) {
|
||||
resolvedProfiles = normalized;
|
||||
await chrome.storage.local.set({ profiles: resolvedProfiles });
|
||||
}
|
||||
}
|
||||
|
||||
profilesContainer.innerHTML = "";
|
||||
for (const profile of resolvedProfiles) {
|
||||
profilesContainer.appendChild(buildProfileCard(profile));
|
||||
}
|
||||
updateProfileControls();
|
||||
|
||||
tasksContainer.innerHTML = "";
|
||||
const defaultEnvId = resolvedEnvConfigs[0]?.id || "";
|
||||
const defaultProfileId = resolvedProfiles[0]?.id || "";
|
||||
const normalizedTasks = Array.isArray(tasks)
|
||||
? tasks.map((task) => ({
|
||||
...task,
|
||||
defaultEnvId: task.defaultEnvId || defaultEnvId
|
||||
defaultEnvId: task.defaultEnvId || defaultEnvId,
|
||||
defaultProfileId: task.defaultProfileId || defaultProfileId
|
||||
}))
|
||||
: [];
|
||||
if (
|
||||
normalizedTasks.length &&
|
||||
normalizedTasks.some(
|
||||
(task, index) => task.defaultEnvId !== tasks[index]?.defaultEnvId
|
||||
(task, index) =>
|
||||
task.defaultEnvId !== tasks[index]?.defaultEnvId ||
|
||||
task.defaultProfileId !== tasks[index]?.defaultProfileId
|
||||
)
|
||||
) {
|
||||
await chrome.storage.local.set({ tasks: normalizedTasks });
|
||||
@@ -1153,11 +1633,13 @@ async function loadSettings() {
|
||||
id: newTaskId(),
|
||||
name: "",
|
||||
text: "",
|
||||
defaultEnvId
|
||||
defaultEnvId,
|
||||
defaultProfileId
|
||||
})
|
||||
);
|
||||
updateTaskControls();
|
||||
updateTaskEnvOptions();
|
||||
updateTaskProfileOptions();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1166,6 +1648,8 @@ async function loadSettings() {
|
||||
}
|
||||
updateTaskControls();
|
||||
updateTaskEnvOptions();
|
||||
updateTaskProfileOptions();
|
||||
updateSidebarErrors();
|
||||
}
|
||||
|
||||
async function saveSettings() {
|
||||
@@ -1173,6 +1657,7 @@ async function saveSettings() {
|
||||
const apiKeys = collectApiKeys();
|
||||
const apiConfigs = collectApiConfigs();
|
||||
const envConfigs = collectEnvConfigs();
|
||||
const profiles = collectProfiles();
|
||||
const activeEnvConfigId = envConfigs[0]?.id || "";
|
||||
const activeEnv = envConfigs[0];
|
||||
const activeApiConfigId =
|
||||
@@ -1190,7 +1675,8 @@ async function saveSettings() {
|
||||
envConfigs,
|
||||
activeEnvConfigId,
|
||||
systemPrompt: activeEnv?.systemPrompt || "",
|
||||
resume: resumeInput.value,
|
||||
profiles,
|
||||
resume: profiles[0]?.text || "",
|
||||
tasks,
|
||||
theme: themeSelect.value
|
||||
});
|
||||
@@ -1198,6 +1684,9 @@ async function saveSettings() {
|
||||
}
|
||||
|
||||
saveBtn.addEventListener("click", () => void saveSettings());
|
||||
if (saveBtnSidebar) {
|
||||
saveBtnSidebar.addEventListener("click", () => void saveSettings());
|
||||
}
|
||||
addTaskBtn.addEventListener("click", () => {
|
||||
const name = buildUniqueDefaultName(
|
||||
collectNames(tasksContainer, ".task-name")
|
||||
@@ -1206,7 +1695,8 @@ addTaskBtn.addEventListener("click", () => {
|
||||
id: newTaskId(),
|
||||
name,
|
||||
text: "",
|
||||
defaultEnvId: getTopEnvId()
|
||||
defaultEnvId: getTopEnvId(),
|
||||
defaultProfileId: getTopProfileId()
|
||||
});
|
||||
const first = tasksContainer.firstElementChild;
|
||||
if (first) {
|
||||
@@ -1216,6 +1706,7 @@ addTaskBtn.addEventListener("click", () => {
|
||||
}
|
||||
updateTaskControls();
|
||||
updateTaskEnvOptions();
|
||||
updateTaskProfileOptions();
|
||||
});
|
||||
|
||||
addApiKeyBtn.addEventListener("click", () => {
|
||||
@@ -1281,7 +1772,41 @@ addEnvConfigBtn.addEventListener("click", () => {
|
||||
updateTaskEnvOptions();
|
||||
});
|
||||
|
||||
addProfileBtn.addEventListener("click", () => {
|
||||
const name = buildUniqueDefaultName(
|
||||
collectNames(profilesContainer, ".profile-name")
|
||||
);
|
||||
const newCard = buildProfileCard({
|
||||
id: newProfileId(),
|
||||
name,
|
||||
text: "",
|
||||
type: "Resume"
|
||||
});
|
||||
const first = profilesContainer.firstElementChild;
|
||||
if (first) {
|
||||
profilesContainer.insertBefore(newCard, first);
|
||||
} else {
|
||||
profilesContainer.appendChild(newCard);
|
||||
}
|
||||
updateProfileControls();
|
||||
updateTaskProfileOptions();
|
||||
});
|
||||
|
||||
themeSelect.addEventListener("change", () => applyTheme(themeSelect.value));
|
||||
themeSelect.addEventListener("change", () => applyTheme(themeSelect.value));
|
||||
|
||||
loadSettings();
|
||||
|
||||
document.querySelectorAll(".toc a").forEach((link) => {
|
||||
link.addEventListener("click", (event) => {
|
||||
const href = link.getAttribute("href");
|
||||
if (!href || !href.startsWith("#")) return;
|
||||
const target = document.querySelector(href);
|
||||
if (target && target.tagName === "DETAILS") {
|
||||
target.open = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener("input", scheduleSidebarErrors);
|
||||
document.addEventListener("change", scheduleSidebarErrors);
|
||||
|
||||
Reference in New Issue
Block a user