added sidebar for ergonomics and error display of the configuration

This commit is contained in:
2026-01-17 18:53:29 -05:00
parent b5be587a63
commit d49c33268a
8 changed files with 844 additions and 50 deletions

View File

@@ -21,6 +21,7 @@ const DEFAULT_SETTINGS = {
activeApiConfigId: "", activeApiConfigId: "",
envConfigs: [], envConfigs: [],
activeEnvConfigId: "", activeEnvConfigId: "",
profiles: [],
apiBaseUrl: "https://api.openai.com/v1", apiBaseUrl: "https://api.openai.com/v1",
apiKeyHeader: "Authorization", apiKeyHeader: "Authorization",
apiKeyPrefix: "Bearer ", apiKeyPrefix: "Bearer ",
@@ -242,12 +243,44 @@ chrome.runtime.onInstalled.addListener(async () => {
} }
} }
const hasProfiles =
Array.isArray(stored.profiles) && stored.profiles.length > 0;
if (!hasProfiles) {
const id = crypto?.randomUUID
? crypto.randomUUID()
: `profile-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
updates.profiles = [
{
id,
name: "Default",
text: stored.resume || "",
type: "Resume"
}
];
} else {
const normalizedProfiles = stored.profiles.map((profile) => ({
...profile,
text: profile.text ?? "",
type: profile.type === "Profile" ? "Profile" : "Resume"
}));
const needsProfileUpdate = normalizedProfiles.some(
(profile, index) =>
(profile.text || "") !== (stored.profiles[index]?.text || "") ||
(profile.type || "Resume") !== (stored.profiles[index]?.type || "Resume")
);
if (needsProfileUpdate) {
updates.profiles = normalizedProfiles;
}
}
const resolvedEnvConfigs = updates.envConfigs || stored.envConfigs || []; const resolvedEnvConfigs = updates.envConfigs || stored.envConfigs || [];
const defaultEnvId = const defaultEnvId =
resolvedEnvConfigs[0]?.id || resolvedEnvConfigs[0]?.id ||
updates.activeEnvConfigId || updates.activeEnvConfigId ||
stored.activeEnvConfigId || stored.activeEnvConfigId ||
""; "";
const resolvedProfiles = updates.profiles || stored.profiles || [];
const defaultProfileId = resolvedProfiles[0]?.id || "";
const taskSource = Array.isArray(updates.tasks) const taskSource = Array.isArray(updates.tasks)
? updates.tasks ? updates.tasks
: Array.isArray(stored.tasks) : Array.isArray(stored.tasks)
@@ -256,10 +289,13 @@ chrome.runtime.onInstalled.addListener(async () => {
if (taskSource.length) { if (taskSource.length) {
const normalizedTasks = taskSource.map((task) => ({ const normalizedTasks = taskSource.map((task) => ({
...task, ...task,
defaultEnvId: task.defaultEnvId || defaultEnvId defaultEnvId: task.defaultEnvId || defaultEnvId,
defaultProfileId: task.defaultProfileId || defaultProfileId
})); }));
const needsTaskUpdate = normalizedTasks.some( const needsTaskUpdate = normalizedTasks.some(
(task, index) => task.defaultEnvId !== taskSource[index]?.defaultEnvId (task, index) =>
task.defaultEnvId !== taskSource[index]?.defaultEnvId ||
task.defaultProfileId !== taskSource[index]?.defaultProfileId
); );
if (needsTaskUpdate) { if (needsTaskUpdate) {
updates.tasks = normalizedTasks; updates.tasks = normalizedTasks;
@@ -328,9 +364,10 @@ chrome.runtime.onMessage.addListener((message) => {
} }
}); });
function buildUserMessage(resume, task, posting) { function buildUserMessage(resume, resumeType, task, posting) {
const header = resumeType === "Profile" ? "=== PROFILE ===" : "=== RESUME ===";
return [ return [
"=== RESUME ===", header,
resume || "", resume || "",
"", "",
"=== TASK ===", "=== TASK ===",
@@ -370,6 +407,7 @@ async function handleAnalysisRequest(port, payload, signal) {
model, model,
systemPrompt, systemPrompt,
resume, resume,
resumeType,
taskText, taskText,
postingText, postingText,
tabId tabId
@@ -416,7 +454,12 @@ async function handleAnalysisRequest(port, payload, signal) {
return; return;
} }
const userMessage = buildUserMessage(resume, taskText, postingText); const userMessage = buildUserMessage(
resume,
resumeType,
taskText,
postingText
);
await chrome.storage.local.set({ [OUTPUT_STORAGE_KEY]: "" }); await chrome.storage.local.set({ [OUTPUT_STORAGE_KEY]: "" });
openKeepalive(tabId); openKeepalive(tabId);

View File

@@ -1,7 +1,7 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "WWCompanion", "name": "WWCompanion",
"version": "0.3.0", "version": "0.3.1",
"description": "AI companion for WaterlooWorks job postings.", "description": "AI companion for WaterlooWorks job postings.",
"permissions": ["storage", "activeTab"], "permissions": ["storage", "activeTab"],
"host_permissions": ["https://waterlooworks.uwaterloo.ca/*"], "host_permissions": ["https://waterlooworks.uwaterloo.ca/*"],

View File

@@ -127,12 +127,13 @@ select {
font-size: 12px; font-size: 12px;
} }
.env-row { .selector-row {
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;
gap: 8px;
} }
.env-row .env-field { .selector-row .selector-field {
flex: 1; flex: 1;
margin: 0; margin: 0;
} }

View File

@@ -17,11 +17,15 @@
<section class="panel"> <section class="panel">
<div class="controls-block"> <div class="controls-block">
<div class="config-block"> <div class="config-block">
<div class="env-row"> <div class="selector-row">
<div class="field inline-field env-field"> <div class="field selector-field">
<label for="envSelect">Environment</label> <label for="envSelect">Environment</label>
<select id="envSelect"></select> <select id="envSelect"></select>
</div> </div>
<div class="field selector-field">
<label for="profileSelect">Profile</label>
<select id="profileSelect"></select>
</div>
</div> </div>
<div class="task-row"> <div class="task-row">
<div class="field inline-field task-field"> <div class="field inline-field task-field">

View File

@@ -2,6 +2,7 @@ const runBtn = document.getElementById("runBtn");
const abortBtn = document.getElementById("abortBtn"); const abortBtn = document.getElementById("abortBtn");
const taskSelect = document.getElementById("taskSelect"); const taskSelect = document.getElementById("taskSelect");
const envSelect = document.getElementById("envSelect"); const envSelect = document.getElementById("envSelect");
const profileSelect = document.getElementById("profileSelect");
const outputEl = document.getElementById("output"); const outputEl = document.getElementById("output");
const statusEl = document.getElementById("status"); const statusEl = document.getElementById("status");
const postingCountEl = document.getElementById("postingCount"); const postingCountEl = document.getElementById("postingCount");
@@ -15,26 +16,30 @@ const OUTPUT_STORAGE_KEY = "lastOutput";
const AUTO_RUN_KEY = "autoRunDefaultTask"; const AUTO_RUN_KEY = "autoRunDefaultTask";
const LAST_TASK_KEY = "lastSelectedTaskId"; const LAST_TASK_KEY = "lastSelectedTaskId";
const LAST_ENV_KEY = "lastSelectedEnvId"; const LAST_ENV_KEY = "lastSelectedEnvId";
const LAST_PROFILE_KEY = "lastSelectedProfileId";
const state = { const state = {
postingText: "", postingText: "",
tasks: [], tasks: [],
envs: [], envs: [],
profiles: [],
port: null, port: null,
isAnalyzing: false, isAnalyzing: false,
outputRaw: "", outputRaw: "",
autoRunPending: false, autoRunPending: false,
selectedTaskId: "", selectedTaskId: "",
selectedEnvId: "" selectedEnvId: "",
selectedProfileId: ""
}; };
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));
} }
function buildUserMessage(resume, task, posting) { function buildUserMessage(resume, resumeType, task, posting) {
const header = resumeType === "Profile" ? "=== PROFILE ===" : "=== RESUME ===";
return [ return [
"=== RESUME ===", header,
resume || "", resume || "",
"", "",
"=== TASK ===", "=== TASK ===",
@@ -253,6 +258,7 @@ function setAnalyzing(isAnalyzing) {
abortBtn.classList.toggle("hidden", !isAnalyzing); abortBtn.classList.toggle("hidden", !isAnalyzing);
updateTaskSelectState(); updateTaskSelectState();
updateEnvSelectState(); updateEnvSelectState();
updateProfileSelectState();
} }
function updatePostingCount() { function updatePostingCount() {
@@ -317,10 +323,41 @@ function updateEnvSelectState() {
envSelect.disabled = state.isAnalyzing || !hasEnvs; envSelect.disabled = state.isAnalyzing || !hasEnvs;
} }
function renderProfiles(profiles) {
state.profiles = profiles;
profileSelect.innerHTML = "";
if (!profiles.length) {
const option = document.createElement("option");
option.textContent = "No profiles configured";
option.value = "";
profileSelect.appendChild(option);
updateProfileSelectState();
return;
}
for (const profile of profiles) {
const option = document.createElement("option");
option.value = profile.id;
option.textContent = profile.name || "Default";
profileSelect.appendChild(option);
}
updateProfileSelectState();
}
function updateProfileSelectState() {
const hasProfiles = state.profiles.length > 0;
profileSelect.disabled = state.isAnalyzing || !hasProfiles;
}
function getTaskDefaultEnvId(task) { function getTaskDefaultEnvId(task) {
return task?.defaultEnvId || state.envs[0]?.id || ""; return task?.defaultEnvId || state.envs[0]?.id || "";
} }
function getTaskDefaultProfileId(task) {
return task?.defaultProfileId || state.profiles[0]?.id || "";
}
function setEnvironmentSelection(envId) { function setEnvironmentSelection(envId) {
const target = const target =
envId && state.envs.some((env) => env.id === envId) envId && state.envs.some((env) => env.id === envId)
@@ -332,6 +369,17 @@ function setEnvironmentSelection(envId) {
state.selectedEnvId = target; state.selectedEnvId = target;
} }
function setProfileSelection(profileId) {
const target =
profileId && state.profiles.some((profile) => profile.id === profileId)
? profileId
: state.profiles[0]?.id || "";
if (target) {
profileSelect.value = target;
}
state.selectedProfileId = target;
}
function selectTask(taskId, { resetEnv } = { resetEnv: false }) { function selectTask(taskId, { resetEnv } = { resetEnv: false }) {
if (!taskId) return; if (!taskId) return;
taskSelect.value = taskId; taskSelect.value = taskId;
@@ -339,13 +387,15 @@ function selectTask(taskId, { resetEnv } = { resetEnv: false }) {
const task = state.tasks.find((item) => item.id === taskId); const task = state.tasks.find((item) => item.id === taskId);
if (resetEnv) { if (resetEnv) {
setEnvironmentSelection(getTaskDefaultEnvId(task)); setEnvironmentSelection(getTaskDefaultEnvId(task));
setProfileSelection(getTaskDefaultProfileId(task));
} }
} }
async function persistSelections() { async function persistSelections() {
await chrome.storage.local.set({ await chrome.storage.local.set({
[LAST_TASK_KEY]: state.selectedTaskId, [LAST_TASK_KEY]: state.selectedTaskId,
[LAST_ENV_KEY]: state.selectedEnvId [LAST_ENV_KEY]: state.selectedEnvId,
[LAST_PROFILE_KEY]: state.selectedProfileId
}); });
} }
@@ -439,22 +489,28 @@ async function loadConfig() {
const stored = await getStorage([ const stored = await getStorage([
"tasks", "tasks",
"envConfigs", "envConfigs",
"profiles",
LAST_TASK_KEY, LAST_TASK_KEY,
LAST_ENV_KEY LAST_ENV_KEY,
LAST_PROFILE_KEY
]); ]);
const tasks = Array.isArray(stored.tasks) ? stored.tasks : []; const tasks = Array.isArray(stored.tasks) ? stored.tasks : [];
const envs = Array.isArray(stored.envConfigs) ? stored.envConfigs : []; const envs = Array.isArray(stored.envConfigs) ? stored.envConfigs : [];
const profiles = Array.isArray(stored.profiles) ? stored.profiles : [];
renderTasks(tasks); renderTasks(tasks);
renderEnvironments(envs); renderEnvironments(envs);
renderProfiles(profiles);
if (!tasks.length) { if (!tasks.length) {
state.selectedTaskId = ""; state.selectedTaskId = "";
state.selectedEnvId = envs[0]?.id || ""; setEnvironmentSelection(envs[0]?.id || "");
setProfileSelection(profiles[0]?.id || "");
return; return;
} }
const storedTaskId = stored[LAST_TASK_KEY]; const storedTaskId = stored[LAST_TASK_KEY];
const storedEnvId = stored[LAST_ENV_KEY]; const storedEnvId = stored[LAST_ENV_KEY];
const storedProfileId = stored[LAST_PROFILE_KEY];
const initialTaskId = tasks.some((task) => task.id === storedTaskId) const initialTaskId = tasks.some((task) => task.id === storedTaskId)
? storedTaskId ? storedTaskId
: tasks[0].id; : tasks[0].id;
@@ -467,9 +523,16 @@ async function loadConfig() {
setEnvironmentSelection(getTaskDefaultEnvId(task)); setEnvironmentSelection(getTaskDefaultEnvId(task));
} }
if (storedProfileId && profiles.some((profile) => profile.id === storedProfileId)) {
setProfileSelection(storedProfileId);
} else {
setProfileSelection(getTaskDefaultProfileId(task));
}
if ( if (
storedTaskId !== state.selectedTaskId || storedTaskId !== state.selectedTaskId ||
storedEnvId !== state.selectedEnvId storedEnvId !== state.selectedEnvId ||
storedProfileId !== state.selectedProfileId
) { ) {
await persistSelections(); await persistSelections();
} }
@@ -528,6 +591,7 @@ async function handleAnalyze() {
apiConfigs = [], apiConfigs = [],
activeApiConfigId = "", activeApiConfigId = "",
envConfigs = [], envConfigs = [],
profiles = [],
apiBaseUrl, apiBaseUrl,
apiKeyHeader, apiKeyHeader,
apiKeyPrefix, apiKeyPrefix,
@@ -540,6 +604,7 @@ async function handleAnalyze() {
"apiConfigs", "apiConfigs",
"activeApiConfigId", "activeApiConfigId",
"envConfigs", "envConfigs",
"profiles",
"apiBaseUrl", "apiBaseUrl",
"apiKeyHeader", "apiKeyHeader",
"apiKeyPrefix", "apiKeyPrefix",
@@ -550,6 +615,7 @@ async function handleAnalyze() {
const resolvedConfigs = Array.isArray(apiConfigs) ? apiConfigs : []; const resolvedConfigs = Array.isArray(apiConfigs) ? apiConfigs : [];
const resolvedEnvs = Array.isArray(envConfigs) ? envConfigs : []; const resolvedEnvs = Array.isArray(envConfigs) ? envConfigs : [];
const resolvedProfiles = Array.isArray(profiles) ? profiles : [];
const selectedEnvId = envSelect.value; const selectedEnvId = envSelect.value;
const activeEnv = const activeEnv =
resolvedEnvs.find((entry) => entry.id === selectedEnvId) || resolvedEnvs.find((entry) => entry.id === selectedEnvId) ||
@@ -569,6 +635,13 @@ async function handleAnalyze() {
setStatus("Add an API configuration in Settings."); setStatus("Add an API configuration in Settings.");
return; return;
} }
const selectedProfileId = profileSelect.value;
const activeProfile =
resolvedProfiles.find((entry) => entry.id === selectedProfileId) ||
resolvedProfiles[0];
const resumeText = activeProfile?.text || resume || "";
const resumeType = activeProfile?.type || "Resume";
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 || "";
@@ -614,7 +687,12 @@ async function handleAnalyze() {
} }
} }
const promptText = buildUserMessage(resume || "", task.text || "", state.postingText); const promptText = buildUserMessage(
resumeText,
resumeType,
task.text || "",
state.postingText
);
updatePromptCount(promptText.length); updatePromptCount(promptText.length);
state.outputRaw = ""; state.outputRaw = "";
@@ -635,7 +713,8 @@ async function handleAnalyze() {
apiKeyPrefix: resolvedApiKeyPrefix, apiKeyPrefix: resolvedApiKeyPrefix,
model: resolvedModel, model: resolvedModel,
systemPrompt: resolvedSystemPrompt, systemPrompt: resolvedSystemPrompt,
resume: resume || "", resume: resumeText,
resumeType,
taskText: task.text || "", taskText: task.text || "",
postingText: state.postingText, postingText: state.postingText,
tabId: tab.id tabId: tab.id
@@ -704,6 +783,10 @@ envSelect.addEventListener("change", () => {
setEnvironmentSelection(envSelect.value); setEnvironmentSelection(envSelect.value);
void persistSelections(); void persistSelections();
}); });
profileSelect.addEventListener("change", () => {
setProfileSelection(profileSelect.value);
void persistSelections();
});
updatePostingCount(); updatePostingCount();
updatePromptCount(0); updatePromptCount(0);

View File

@@ -42,6 +42,77 @@ body {
background: var(--bg); background: var(--bg);
} }
.settings-layout {
display: flex;
gap: 16px;
}
.toc {
flex: 0 0 160px;
display: flex;
flex-direction: column;
gap: 8px;
align-self: flex-start;
position: sticky;
top: 16px;
padding: 12px;
border-radius: 12px;
border: 1px solid var(--border);
background: var(--panel);
box-shadow: var(--panel-shadow);
}
.toc-title {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.8px;
color: var(--muted);
}
.toc-heading {
font-size: 16px;
font-weight: 700;
color: var(--ink);
}
.toc-links {
display: grid;
gap: 8px;
}
.toc a {
color: var(--ink);
text-decoration: none;
font-size: 12px;
padding: 4px 6px;
border-radius: 8px;
}
.toc a:hover {
background: var(--card-bg);
}
.sidebar-errors {
margin-top: auto;
border-radius: 10px;
border: 1px solid #c0392b;
background: rgba(192, 57, 43, 0.08);
color: #c0392b;
font-size: 11px;
padding: 8px;
white-space: pre-line;
}
.hidden {
display: none;
}
.settings-main {
flex: 1;
display: grid;
gap: 14px;
}
.title-block { .title-block {
margin-bottom: 16px; margin-bottom: 16px;
} }
@@ -69,7 +140,6 @@ body {
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 16px; border-radius: 16px;
padding: 16px; padding: 16px;
margin-bottom: 16px;
box-shadow: var(--panel-shadow); box-shadow: var(--panel-shadow);
} }
@@ -288,7 +358,15 @@ button:active {
.api-config-actions { .api-config-actions {
display: flex; display: flex;
gap: 8px; gap: 8px;
justify-content: flex-end; justify-content: space-between;
align-items: center;
}
.api-config-actions-left,
.api-config-actions-right {
display: flex;
gap: 8px;
flex-wrap: wrap;
} }
.env-configs { .env-configs {
@@ -311,6 +389,32 @@ button:active {
justify-content: flex-end; justify-content: flex-end;
} }
.profiles {
display: grid;
gap: 12px;
}
.profile-card {
padding: 12px;
border-radius: 12px;
border: 1px solid var(--border);
background: var(--card-bg);
display: grid;
gap: 8px;
}
.profile-actions {
display: flex;
gap: 8px;
justify-content: flex-end;
}
.profile-actions .delete {
background: #c0392b;
border-color: #c0392b;
color: #fff6f2;
}
@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"] {
@@ -333,3 +437,14 @@ button:active {
gap: 6px; gap: 6px;
justify-content: flex-end; justify-content: flex-end;
} }
@media (max-width: 720px) {
.settings-layout {
flex-direction: column;
}
.toc {
width: 100%;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
}
}

View File

@@ -16,7 +16,24 @@
<button id="saveBtn" class="accent">Save Settings</button> <button id="saveBtn" class="accent">Save Settings</button>
</div> </div>
<details class="panel"> <div class="settings-layout">
<nav class="toc" aria-label="Settings table of contents">
<div class="toc-heading">WWCompanion Settings</div>
<button id="saveBtnSidebar" class="accent" type="button">Save Settings</button>
<div id="statusSidebar" class="status"> </div>
<div class="toc-title">Sections</div>
<div class="toc-links">
<a href="#appearance-panel">Appearance</a>
<a href="#api-keys-panel">API Keys</a>
<a href="#api-panel">API</a>
<a href="#environment-panel">Environment</a>
<a href="#profiles-panel">My Profiles</a>
<a href="#tasks-panel">Task Presets</a>
</div>
<div id="sidebarErrors" class="sidebar-errors hidden"></div>
</nav>
<main class="settings-main">
<details class="panel" id="appearance-panel">
<summary class="panel-summary"> <summary class="panel-summary">
<span class="panel-caret" aria-hidden="true"> <span class="panel-caret" aria-hidden="true">
<span class="caret-closed"></span> <span class="caret-closed"></span>
@@ -36,7 +53,7 @@
</div> </div>
</details> </details>
<details class="panel"> <details class="panel" id="api-keys-panel">
<summary class="panel-summary"> <summary class="panel-summary">
<span class="panel-caret" aria-hidden="true"> <span class="panel-caret" aria-hidden="true">
<span class="caret-closed"></span> <span class="caret-closed"></span>
@@ -53,7 +70,7 @@
</div> </div>
</details> </details>
<details class="panel"> <details class="panel" id="api-panel">
<summary class="panel-summary"> <summary class="panel-summary">
<span class="panel-caret" aria-hidden="true"> <span class="panel-caret" aria-hidden="true">
<span class="caret-closed"></span> <span class="caret-closed"></span>
@@ -72,7 +89,7 @@
</div> </div>
</details> </details>
<details class="panel"> <details class="panel" id="environment-panel">
<summary class="panel-summary"> <summary class="panel-summary">
<span class="panel-caret" aria-hidden="true"> <span class="panel-caret" aria-hidden="true">
<span class="caret-closed"></span> <span class="caret-closed"></span>
@@ -92,23 +109,27 @@
</div> </div>
</details> </details>
<details class="panel"> <details class="panel" id="profiles-panel">
<summary class="panel-summary"> <summary class="panel-summary">
<span class="panel-caret" aria-hidden="true"> <span class="panel-caret" aria-hidden="true">
<span class="caret-closed"></span> <span class="caret-closed"></span>
<span class="caret-open"></span> <span class="caret-open"></span>
</span> </span>
<div class="row-title"> <div class="row-title">
<h2>Resume</h2> <h2>My Profiles</h2>
<span class="hint hint-accent">Text to your profile goes here</span> <span class="hint hint-accent">Text to your resumes or generic profiles goes here</span>
</div> </div>
</summary> </summary>
<div class="panel-body"> <div class="panel-body">
<textarea id="resume" rows="10" placeholder="Paste your resume text..."></textarea> <div class="row">
<div></div>
<button id="addProfileBtn" class="ghost" type="button">Add Profile</button>
</div>
<div id="profiles" class="profiles"></div>
</div> </div>
</details> </details>
<details class="panel"> <details class="panel" id="tasks-panel">
<summary class="panel-summary"> <summary class="panel-summary">
<span class="panel-caret" aria-hidden="true"> <span class="panel-caret" aria-hidden="true">
<span class="caret-closed"></span> <span class="caret-closed"></span>
@@ -127,6 +148,8 @@
<div id="tasks" class="tasks"></div> <div id="tasks" class="tasks"></div>
</div> </div>
</details> </details>
</main>
</div>
<script src="settings.js"></script> <script src="settings.js"></script>
</body> </body>

View File

@@ -1,5 +1,5 @@
const resumeInput = document.getElementById("resume");
const saveBtn = document.getElementById("saveBtn"); const saveBtn = document.getElementById("saveBtn");
const saveBtnSidebar = document.getElementById("saveBtnSidebar");
const addApiConfigBtn = document.getElementById("addApiConfigBtn"); const addApiConfigBtn = document.getElementById("addApiConfigBtn");
const apiConfigsContainer = document.getElementById("apiConfigs"); const apiConfigsContainer = document.getElementById("apiConfigs");
const addApiKeyBtn = document.getElementById("addApiKeyBtn"); const addApiKeyBtn = document.getElementById("addApiKeyBtn");
@@ -8,7 +8,11 @@ const addEnvConfigBtn = document.getElementById("addEnvConfigBtn");
const envConfigsContainer = document.getElementById("envConfigs"); const envConfigsContainer = document.getElementById("envConfigs");
const addTaskBtn = document.getElementById("addTaskBtn"); const addTaskBtn = document.getElementById("addTaskBtn");
const tasksContainer = document.getElementById("tasks"); const tasksContainer = document.getElementById("tasks");
const addProfileBtn = document.getElementById("addProfileBtn");
const profilesContainer = document.getElementById("profiles");
const statusEl = document.getElementById("status"); const statusEl = document.getElementById("status");
const statusSidebarEl = document.getElementById("statusSidebar");
const sidebarErrorsEl = document.getElementById("sidebarErrors");
const themeSelect = document.getElementById("themeSelect"); const themeSelect = document.getElementById("themeSelect");
const OPENAI_DEFAULTS = { const OPENAI_DEFAULTS = {
@@ -26,12 +30,24 @@ function getStorage(keys) {
function setStatus(message) { function setStatus(message) {
statusEl.textContent = message; statusEl.textContent = message;
if (statusSidebarEl) statusSidebarEl.textContent = message;
if (!message) return; if (!message) return;
setTimeout(() => { setTimeout(() => {
if (statusEl.textContent === message) statusEl.textContent = ""; if (statusEl.textContent === message) statusEl.textContent = "";
if (statusSidebarEl?.textContent === message) statusSidebarEl.textContent = "";
}, 2000); }, 2000);
} }
let sidebarErrorFrame = null;
function scheduleSidebarErrors() {
if (!sidebarErrorsEl) return;
if (sidebarErrorFrame) return;
sidebarErrorFrame = requestAnimationFrame(() => {
sidebarErrorFrame = null;
updateSidebarErrors();
});
}
function applyTheme(theme) { function applyTheme(theme) {
const value = theme || "system"; const value = theme || "system";
document.documentElement.dataset.theme = value; document.documentElement.dataset.theme = value;
@@ -57,6 +73,11 @@ function newEnvConfigId() {
return `env-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`; 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) { function buildChatUrlFromBase(baseUrl) {
const trimmed = (baseUrl || "").trim().replace(/\/+$/, ""); const trimmed = (baseUrl || "").trim().replace(/\/+$/, "");
if (!trimmed) return ""; if (!trimmed) return "";
@@ -94,6 +115,10 @@ function getTopEnvId() {
return collectEnvConfigs()[0]?.id || ""; return collectEnvConfigs()[0]?.id || "";
} }
function getTopProfileId() {
return collectProfiles()[0]?.id || "";
}
function setApiConfigAdvanced(card, isAdvanced) { function setApiConfigAdvanced(card, isAdvanced) {
card.classList.toggle("is-advanced", isAdvanced); card.classList.toggle("is-advanced", isAdvanced);
card.dataset.mode = isAdvanced ? "advanced" : "basic"; card.dataset.mode = isAdvanced ? "advanced" : "basic";
@@ -253,6 +278,10 @@ function buildApiConfigCard(config) {
const actions = document.createElement("div"); const actions = document.createElement("div");
actions.className = "api-config-actions"; 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"); const moveTopBtn = document.createElement("button");
moveTopBtn.type = "button"; moveTopBtn.type = "button";
moveTopBtn.className = "ghost move-top"; moveTopBtn.className = "ghost move-top";
@@ -265,6 +294,10 @@ function buildApiConfigCard(config) {
moveDownBtn.type = "button"; moveDownBtn.type = "button";
moveDownBtn.className = "ghost move-down"; moveDownBtn.className = "ghost move-down";
moveDownBtn.textContent = "Down"; moveDownBtn.textContent = "Down";
const addBelowBtn = document.createElement("button");
addBelowBtn.type = "button";
addBelowBtn.className = "ghost add-below";
addBelowBtn.textContent = "Add";
moveTopBtn.addEventListener("click", () => { moveTopBtn.addEventListener("click", () => {
const first = apiConfigsContainer.firstElementChild; const first = apiConfigsContainer.firstElementChild;
@@ -290,9 +323,27 @@ function buildApiConfigCard(config) {
updateEnvApiOptions(); updateEnvApiOptions();
}); });
actions.appendChild(moveTopBtn); addBelowBtn.addEventListener("click", () => {
actions.appendChild(moveUpBtn); const name = buildUniqueDefaultName(
actions.appendChild(moveDownBtn); 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"); const advancedBtn = document.createElement("button");
advancedBtn.type = "button"; advancedBtn.type = "button";
advancedBtn.className = "ghost advanced-toggle"; advancedBtn.className = "ghost advanced-toggle";
@@ -314,8 +365,6 @@ function buildApiConfigCard(config) {
setApiConfigAdvanced(card, true); setApiConfigAdvanced(card, true);
updateEnvApiOptions(); updateEnvApiOptions();
}); });
actions.appendChild(advancedBtn);
const duplicateBtn = document.createElement("button"); const duplicateBtn = document.createElement("button");
duplicateBtn.type = "button"; duplicateBtn.type = "button";
duplicateBtn.className = "ghost duplicate"; duplicateBtn.className = "ghost duplicate";
@@ -330,8 +379,6 @@ function buildApiConfigCard(config) {
updateApiConfigKeyOptions(); updateApiConfigKeyOptions();
updateEnvApiOptions(); updateEnvApiOptions();
}); });
actions.appendChild(duplicateBtn);
const resetBtn = document.createElement("button"); const resetBtn = document.createElement("button");
resetBtn.type = "button"; resetBtn.type = "button";
resetBtn.className = "ghost reset-openai"; resetBtn.className = "ghost reset-openai";
@@ -346,8 +393,6 @@ function buildApiConfigCard(config) {
setApiConfigAdvanced(card, false); setApiConfigAdvanced(card, false);
updateEnvApiOptions(); updateEnvApiOptions();
}); });
actions.appendChild(resetBtn);
const deleteBtn = document.createElement("button"); const deleteBtn = document.createElement("button");
deleteBtn.type = "button"; deleteBtn.type = "button";
deleteBtn.className = "ghost delete"; deleteBtn.className = "ghost delete";
@@ -357,7 +402,6 @@ function buildApiConfigCard(config) {
updateEnvApiOptions(); updateEnvApiOptions();
updateApiConfigControls(); updateApiConfigControls();
}); });
actions.appendChild(deleteBtn);
const updateSelect = () => updateEnvApiOptions(); const updateSelect = () => updateEnvApiOptions();
nameInput.addEventListener("input", updateSelect); nameInput.addEventListener("input", updateSelect);
@@ -368,6 +412,19 @@ function buildApiConfigCard(config) {
urlInput.addEventListener("input", updateSelect); urlInput.addEventListener("input", updateSelect);
templateInput.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(nameField);
card.appendChild(keyField); card.appendChild(keyField);
card.appendChild(baseField); card.appendChild(baseField);
@@ -398,6 +455,7 @@ function updateApiConfigControls() {
if (moveUpBtn) moveUpBtn.disabled = index === 0; if (moveUpBtn) moveUpBtn.disabled = index === 0;
if (moveDownBtn) moveDownBtn.disabled = index === cards.length - 1; if (moveDownBtn) moveDownBtn.disabled = index === cards.length - 1;
}); });
scheduleSidebarErrors();
} }
function buildApiKeyCard(entry) { function buildApiKeyCard(entry) {
@@ -456,6 +514,10 @@ function buildApiKeyCard(entry) {
moveDownBtn.type = "button"; moveDownBtn.type = "button";
moveDownBtn.className = "ghost move-down"; moveDownBtn.className = "ghost move-down";
moveDownBtn.textContent = "Down"; moveDownBtn.textContent = "Down";
const addBelowBtn = document.createElement("button");
addBelowBtn.type = "button";
addBelowBtn.className = "ghost add-below";
addBelowBtn.textContent = "Add";
moveTopBtn.addEventListener("click", () => { moveTopBtn.addEventListener("click", () => {
const first = apiKeysContainer.firstElementChild; const first = apiKeysContainer.firstElementChild;
@@ -481,9 +543,20 @@ function buildApiKeyCard(entry) {
updateApiConfigKeyOptions(); 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(moveTopBtn);
actions.appendChild(moveUpBtn); actions.appendChild(moveUpBtn);
actions.appendChild(moveDownBtn); actions.appendChild(moveDownBtn);
actions.appendChild(addBelowBtn);
const deleteBtn = document.createElement("button"); const deleteBtn = document.createElement("button");
deleteBtn.type = "button"; deleteBtn.type = "button";
deleteBtn.className = "ghost delete"; deleteBtn.className = "ghost delete";
@@ -529,6 +602,7 @@ function updateApiKeyControls() {
if (moveUpBtn) moveUpBtn.disabled = index === 0; if (moveUpBtn) moveUpBtn.disabled = index === 0;
if (moveDownBtn) moveDownBtn.disabled = index === cards.length - 1; if (moveDownBtn) moveDownBtn.disabled = index === cards.length - 1;
}); });
scheduleSidebarErrors();
} }
function updateApiConfigKeyOptions() { function updateApiConfigKeyOptions() {
@@ -615,6 +689,10 @@ function buildEnvConfigCard(config) {
moveDownBtn.type = "button"; moveDownBtn.type = "button";
moveDownBtn.className = "ghost move-down"; moveDownBtn.className = "ghost move-down";
moveDownBtn.textContent = "Down"; moveDownBtn.textContent = "Down";
const addBelowBtn = document.createElement("button");
addBelowBtn.type = "button";
addBelowBtn.className = "ghost add-below";
addBelowBtn.textContent = "Add";
moveTopBtn.addEventListener("click", () => { moveTopBtn.addEventListener("click", () => {
const first = envConfigsContainer.firstElementChild; const first = envConfigsContainer.firstElementChild;
@@ -643,6 +721,24 @@ function buildEnvConfigCard(config) {
actions.appendChild(moveTopBtn); actions.appendChild(moveTopBtn);
actions.appendChild(moveUpBtn); actions.appendChild(moveUpBtn);
actions.appendChild(moveDownBtn); 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"); const duplicateBtn = document.createElement("button");
duplicateBtn.type = "button"; duplicateBtn.type = "button";
@@ -700,6 +796,7 @@ function updateEnvControls() {
if (moveUpBtn) moveUpBtn.disabled = index === 0; if (moveUpBtn) moveUpBtn.disabled = index === 0;
if (moveDownBtn) moveDownBtn.disabled = index === cards.length - 1; if (moveDownBtn) moveDownBtn.disabled = index === cards.length - 1;
}); });
scheduleSidebarErrors();
} }
function updateTaskEnvOptions() { function updateTaskEnvOptions() {
@@ -733,6 +830,7 @@ function updateTaskEnvOptions() {
select.dataset.preferred = select.value; select.dataset.preferred = select.value;
}); });
scheduleSidebarErrors();
} }
function collectEnvConfigs() { 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() { function updateEnvApiOptions() {
const apiConfigs = collectApiConfigs(); const apiConfigs = collectApiConfigs();
const selects = envConfigsContainer.querySelectorAll(".env-config-api-select"); const selects = envConfigsContainer.querySelectorAll(".env-config-api-select");
@@ -810,6 +1124,16 @@ function buildTaskCard(task) {
envField.appendChild(envLabel); envField.appendChild(envLabel);
envField.appendChild(envSelect); 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"); const textField = document.createElement("div");
textField.className = "field"; textField.className = "field";
const textLabel = document.createElement("label"); const textLabel = document.createElement("label");
@@ -889,11 +1213,13 @@ function buildTaskCard(task) {
id: newTaskId(), id: newTaskId(),
name, name,
text: "", text: "",
defaultEnvId: getTopEnvId() defaultEnvId: getTopEnvId(),
defaultProfileId: getTopProfileId()
}); });
card.insertAdjacentElement("afterend", newCard); card.insertAdjacentElement("afterend", newCard);
updateTaskControls(); updateTaskControls();
updateTaskEnvOptions(); updateTaskEnvOptions();
updateTaskProfileOptions();
}); });
duplicateBtn.addEventListener("click", () => { duplicateBtn.addEventListener("click", () => {
@@ -904,12 +1230,14 @@ function buildTaskCard(task) {
collectNames(tasksContainer, ".task-name") collectNames(tasksContainer, ".task-name")
), ),
text: textArea.value, text: textArea.value,
defaultEnvId: envSelect.value || "" defaultEnvId: envSelect.value || "",
defaultProfileId: profileSelect.value || ""
}; };
const newCard = buildTaskCard(copy); const newCard = buildTaskCard(copy);
card.insertAdjacentElement("afterend", newCard); card.insertAdjacentElement("afterend", newCard);
updateTaskControls(); updateTaskControls();
updateTaskEnvOptions(); updateTaskEnvOptions();
updateTaskProfileOptions();
}); });
deleteBtn.addEventListener("click", () => { deleteBtn.addEventListener("click", () => {
@@ -926,6 +1254,7 @@ function buildTaskCard(task) {
card.appendChild(nameField); card.appendChild(nameField);
card.appendChild(envField); card.appendChild(envField);
card.appendChild(profileField);
card.appendChild(textField); card.appendChild(textField);
card.appendChild(actions); card.appendChild(actions);
@@ -942,6 +1271,7 @@ function updateTaskControls() {
if (moveUpBtn) moveUpBtn.disabled = index === 0; if (moveUpBtn) moveUpBtn.disabled = index === 0;
if (moveDownBtn) moveDownBtn.disabled = index === cards.length - 1; if (moveDownBtn) moveDownBtn.disabled = index === cards.length - 1;
}); });
scheduleSidebarErrors();
} }
function collectTasks() { function collectTasks() {
@@ -950,15 +1280,127 @@ function collectTasks() {
const nameInput = card.querySelector(".task-name"); const nameInput = card.querySelector(".task-name");
const textArea = card.querySelector(".task-text"); const textArea = card.querySelector(".task-text");
const envSelect = card.querySelector(".task-env-select"); const envSelect = card.querySelector(".task-env-select");
const profileSelect = card.querySelector(".task-profile-select");
return { return {
id: card.dataset.id || newTaskId(), id: card.dataset.id || newTaskId(),
name: (nameInput?.value || "Untitled Task").trim(), name: (nameInput?.value || "Untitled Task").trim(),
text: (textArea?.value || "").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() { async function loadSettings() {
const { const {
apiKey = "", apiKey = "",
@@ -968,6 +1410,7 @@ async function loadSettings() {
activeApiConfigId = "", activeApiConfigId = "",
envConfigs = [], envConfigs = [],
activeEnvConfigId = "", activeEnvConfigId = "",
profiles = [],
apiBaseUrl = "", apiBaseUrl = "",
apiKeyHeader = "", apiKeyHeader = "",
apiKeyPrefix = "", apiKeyPrefix = "",
@@ -984,6 +1427,7 @@ async function loadSettings() {
"activeApiConfigId", "activeApiConfigId",
"envConfigs", "envConfigs",
"activeEnvConfigId", "activeEnvConfigId",
"profiles",
"apiBaseUrl", "apiBaseUrl",
"apiKeyHeader", "apiKeyHeader",
"apiKeyPrefix", "apiKeyPrefix",
@@ -994,7 +1438,6 @@ async function loadSettings() {
"theme" "theme"
]); ]);
resumeInput.value = resume;
themeSelect.value = theme; themeSelect.value = theme;
applyTheme(theme); applyTheme(theme);
@@ -1130,18 +1573,55 @@ async function loadSettings() {
updateEnvApiOptions(); updateEnvApiOptions();
updateEnvControls(); 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 = ""; tasksContainer.innerHTML = "";
const defaultEnvId = resolvedEnvConfigs[0]?.id || ""; const defaultEnvId = resolvedEnvConfigs[0]?.id || "";
const defaultProfileId = resolvedProfiles[0]?.id || "";
const normalizedTasks = Array.isArray(tasks) const normalizedTasks = Array.isArray(tasks)
? tasks.map((task) => ({ ? tasks.map((task) => ({
...task, ...task,
defaultEnvId: task.defaultEnvId || defaultEnvId defaultEnvId: task.defaultEnvId || defaultEnvId,
defaultProfileId: task.defaultProfileId || defaultProfileId
})) }))
: []; : [];
if ( if (
normalizedTasks.length && normalizedTasks.length &&
normalizedTasks.some( 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 }); await chrome.storage.local.set({ tasks: normalizedTasks });
@@ -1153,11 +1633,13 @@ async function loadSettings() {
id: newTaskId(), id: newTaskId(),
name: "", name: "",
text: "", text: "",
defaultEnvId defaultEnvId,
defaultProfileId
}) })
); );
updateTaskControls(); updateTaskControls();
updateTaskEnvOptions(); updateTaskEnvOptions();
updateTaskProfileOptions();
return; return;
} }
@@ -1166,6 +1648,8 @@ async function loadSettings() {
} }
updateTaskControls(); updateTaskControls();
updateTaskEnvOptions(); updateTaskEnvOptions();
updateTaskProfileOptions();
updateSidebarErrors();
} }
async function saveSettings() { async function saveSettings() {
@@ -1173,6 +1657,7 @@ async function saveSettings() {
const apiKeys = collectApiKeys(); const apiKeys = collectApiKeys();
const apiConfigs = collectApiConfigs(); const apiConfigs = collectApiConfigs();
const envConfigs = collectEnvConfigs(); const envConfigs = collectEnvConfigs();
const profiles = collectProfiles();
const activeEnvConfigId = envConfigs[0]?.id || ""; const activeEnvConfigId = envConfigs[0]?.id || "";
const activeEnv = envConfigs[0]; const activeEnv = envConfigs[0];
const activeApiConfigId = const activeApiConfigId =
@@ -1190,7 +1675,8 @@ async function saveSettings() {
envConfigs, envConfigs,
activeEnvConfigId, activeEnvConfigId,
systemPrompt: activeEnv?.systemPrompt || "", systemPrompt: activeEnv?.systemPrompt || "",
resume: resumeInput.value, profiles,
resume: profiles[0]?.text || "",
tasks, tasks,
theme: themeSelect.value theme: themeSelect.value
}); });
@@ -1198,6 +1684,9 @@ async function saveSettings() {
} }
saveBtn.addEventListener("click", () => void saveSettings()); saveBtn.addEventListener("click", () => void saveSettings());
if (saveBtnSidebar) {
saveBtnSidebar.addEventListener("click", () => void saveSettings());
}
addTaskBtn.addEventListener("click", () => { addTaskBtn.addEventListener("click", () => {
const name = buildUniqueDefaultName( const name = buildUniqueDefaultName(
collectNames(tasksContainer, ".task-name") collectNames(tasksContainer, ".task-name")
@@ -1206,7 +1695,8 @@ addTaskBtn.addEventListener("click", () => {
id: newTaskId(), id: newTaskId(),
name, name,
text: "", text: "",
defaultEnvId: getTopEnvId() defaultEnvId: getTopEnvId(),
defaultProfileId: getTopProfileId()
}); });
const first = tasksContainer.firstElementChild; const first = tasksContainer.firstElementChild;
if (first) { if (first) {
@@ -1216,6 +1706,7 @@ addTaskBtn.addEventListener("click", () => {
} }
updateTaskControls(); updateTaskControls();
updateTaskEnvOptions(); updateTaskEnvOptions();
updateTaskProfileOptions();
}); });
addApiKeyBtn.addEventListener("click", () => { addApiKeyBtn.addEventListener("click", () => {
@@ -1281,7 +1772,41 @@ addEnvConfigBtn.addEventListener("click", () => {
updateTaskEnvOptions(); 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));
themeSelect.addEventListener("change", () => applyTheme(themeSelect.value)); themeSelect.addEventListener("change", () => applyTheme(themeSelect.value));
loadSettings(); 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);