Compare commits
5 Commits
4a44fac8cf
...
dev-multi-
| Author | SHA1 | Date | |
|---|---|---|---|
| 08f7ee8055 | |||
| df80aee8eb | |||
| e3c7cbba95 | |||
| 2638e08453 | |||
| 3bb350f3cf |
@@ -15,9 +15,9 @@ LLM-enabled AI companionship with WaterlooWorks job postings.
|
|||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
- Load the extension into a Chromium-based browser (root directory at [wwcompanion-extension](wwcompanion-extension/))
|
- Load the extension into a Chromium-based browser (root directory at [wwcompanion-extension](wwcompanion-extension/))
|
||||||
- Configure API settings, prompts, your profiles, and tasks in **Settings**.
|
- Configure API settings, prompts, and tasks in **Settings**.
|
||||||
- Open a WaterlooWorks job posting modal.
|
- Open a WaterlooWorks job posting modal.
|
||||||
- Click **Run** in the popup to extract the posting text and run the task.
|
- Click **Extract** or **Extract & Run** in the popup.
|
||||||
- Review the streamed response, then copy or clear it as needed.
|
- Review the streamed response, then copy or clear it as needed.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
@@ -25,9 +25,8 @@ LLM-enabled AI companionship with WaterlooWorks job postings.
|
|||||||
- Open **Settings** from the popup.
|
- Open **Settings** from the popup.
|
||||||
- Add your API key, base URL, and model (defaults target
|
- Add your API key, base URL, and model (defaults target
|
||||||
OpenAI-compatible APIs).
|
OpenAI-compatible APIs).
|
||||||
- Set your system prompt in environments and resume text.
|
- Set your system prompt and resume text.
|
||||||
- Set your profile with text from your resume or a profile description text.
|
- Customize task presets; the top task is used as the default.
|
||||||
- Customize task presets; **the top task is used as the default**.
|
|
||||||
|
|
||||||
## Example prompts
|
## Example prompts
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ 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 ",
|
||||||
@@ -243,44 +242,12 @@ 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)
|
||||||
@@ -289,13 +256,10 @@ 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, index) => task.defaultEnvId !== taskSource[index]?.defaultEnvId
|
||||||
task.defaultEnvId !== taskSource[index]?.defaultEnvId ||
|
|
||||||
task.defaultProfileId !== taskSource[index]?.defaultProfileId
|
|
||||||
);
|
);
|
||||||
if (needsTaskUpdate) {
|
if (needsTaskUpdate) {
|
||||||
updates.tasks = normalizedTasks;
|
updates.tasks = normalizedTasks;
|
||||||
@@ -364,10 +328,9 @@ chrome.runtime.onMessage.addListener((message) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function buildUserMessage(resume, resumeType, task, posting) {
|
function buildUserMessage(resume, task, posting) {
|
||||||
const header = resumeType === "Profile" ? "=== PROFILE ===" : "=== RESUME ===";
|
|
||||||
return [
|
return [
|
||||||
header,
|
"=== RESUME ===",
|
||||||
resume || "",
|
resume || "",
|
||||||
"",
|
"",
|
||||||
"=== TASK ===",
|
"=== TASK ===",
|
||||||
@@ -407,7 +370,6 @@ async function handleAnalysisRequest(port, payload, signal) {
|
|||||||
model,
|
model,
|
||||||
systemPrompt,
|
systemPrompt,
|
||||||
resume,
|
resume,
|
||||||
resumeType,
|
|
||||||
taskText,
|
taskText,
|
||||||
postingText,
|
postingText,
|
||||||
tabId
|
tabId
|
||||||
@@ -454,12 +416,7 @@ async function handleAnalysisRequest(port, payload, signal) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userMessage = buildUserMessage(
|
const userMessage = buildUserMessage(resume, taskText, postingText);
|
||||||
resume,
|
|
||||||
resumeType,
|
|
||||||
taskText,
|
|
||||||
postingText
|
|
||||||
);
|
|
||||||
|
|
||||||
await chrome.storage.local.set({ [OUTPUT_STORAGE_KEY]: "" });
|
await chrome.storage.local.set({ [OUTPUT_STORAGE_KEY]: "" });
|
||||||
openKeepalive(tabId);
|
openKeepalive(tabId);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "WWCompanion",
|
"name": "WWCompanion",
|
||||||
"version": "0.3.2",
|
"version": "0.3.0",
|
||||||
"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/*"],
|
||||||
|
|||||||
@@ -127,13 +127,12 @@ select {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selector-row {
|
.env-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
gap: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.selector-row .selector-field {
|
.env-row .env-field {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,15 +17,11 @@
|
|||||||
<section class="panel">
|
<section class="panel">
|
||||||
<div class="controls-block">
|
<div class="controls-block">
|
||||||
<div class="config-block">
|
<div class="config-block">
|
||||||
<div class="selector-row">
|
<div class="env-row">
|
||||||
<div class="field selector-field">
|
<div class="field inline-field env-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">
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ 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");
|
||||||
@@ -16,30 +15,26 @@ 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, resumeType, task, posting) {
|
function buildUserMessage(resume, task, posting) {
|
||||||
const header = resumeType === "Profile" ? "=== PROFILE ===" : "=== RESUME ===";
|
|
||||||
return [
|
return [
|
||||||
header,
|
"=== RESUME ===",
|
||||||
resume || "",
|
resume || "",
|
||||||
"",
|
"",
|
||||||
"=== TASK ===",
|
"=== TASK ===",
|
||||||
@@ -84,20 +79,6 @@ function applyInline(text) {
|
|||||||
)}" target="_blank" rel="noreferrer">${label}</a>`;
|
)}" target="_blank" rel="noreferrer">${label}</a>`;
|
||||||
});
|
});
|
||||||
|
|
||||||
output = output.replace(/https?:\/\/[^\s<]+/g, (match) => {
|
|
||||||
let url = match;
|
|
||||||
let suffix = "";
|
|
||||||
while (/[),.;!?]$/.test(url)) {
|
|
||||||
suffix = url.slice(-1) + suffix;
|
|
||||||
url = url.slice(0, -1);
|
|
||||||
}
|
|
||||||
const safeUrl = sanitizeUrl(url);
|
|
||||||
if (!safeUrl) return match;
|
|
||||||
return `<a href="${escapeAttribute(
|
|
||||||
safeUrl
|
|
||||||
)}" target="_blank" rel="noreferrer">${safeUrl}</a>${suffix}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
output = output.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
output = output.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
||||||
output = output.replace(/\*([^*]+)\*/g, "<em>$1</em>");
|
output = output.replace(/\*([^*]+)\*/g, "<em>$1</em>");
|
||||||
output = output.replace(/_([^_]+)_/g, "<em>$1</em>");
|
output = output.replace(/_([^_]+)_/g, "<em>$1</em>");
|
||||||
@@ -272,7 +253,6 @@ function setAnalyzing(isAnalyzing) {
|
|||||||
abortBtn.classList.toggle("hidden", !isAnalyzing);
|
abortBtn.classList.toggle("hidden", !isAnalyzing);
|
||||||
updateTaskSelectState();
|
updateTaskSelectState();
|
||||||
updateEnvSelectState();
|
updateEnvSelectState();
|
||||||
updateProfileSelectState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updatePostingCount() {
|
function updatePostingCount() {
|
||||||
@@ -337,41 +317,10 @@ 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)
|
||||||
@@ -383,17 +332,6 @@ 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;
|
||||||
@@ -401,15 +339,13 @@ 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
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -503,28 +439,22 @@ 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 = "";
|
||||||
setEnvironmentSelection(envs[0]?.id || "");
|
state.selectedEnvId = 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;
|
||||||
@@ -537,16 +467,9 @@ 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();
|
||||||
}
|
}
|
||||||
@@ -605,7 +528,6 @@ async function handleAnalyze() {
|
|||||||
apiConfigs = [],
|
apiConfigs = [],
|
||||||
activeApiConfigId = "",
|
activeApiConfigId = "",
|
||||||
envConfigs = [],
|
envConfigs = [],
|
||||||
profiles = [],
|
|
||||||
apiBaseUrl,
|
apiBaseUrl,
|
||||||
apiKeyHeader,
|
apiKeyHeader,
|
||||||
apiKeyPrefix,
|
apiKeyPrefix,
|
||||||
@@ -618,7 +540,6 @@ async function handleAnalyze() {
|
|||||||
"apiConfigs",
|
"apiConfigs",
|
||||||
"activeApiConfigId",
|
"activeApiConfigId",
|
||||||
"envConfigs",
|
"envConfigs",
|
||||||
"profiles",
|
|
||||||
"apiBaseUrl",
|
"apiBaseUrl",
|
||||||
"apiKeyHeader",
|
"apiKeyHeader",
|
||||||
"apiKeyPrefix",
|
"apiKeyPrefix",
|
||||||
@@ -629,7 +550,6 @@ 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) ||
|
||||||
@@ -649,13 +569,6 @@ 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 || "";
|
||||||
@@ -701,12 +614,7 @@ async function handleAnalyze() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const promptText = buildUserMessage(
|
const promptText = buildUserMessage(resume || "", task.text || "", state.postingText);
|
||||||
resumeText,
|
|
||||||
resumeType,
|
|
||||||
task.text || "",
|
|
||||||
state.postingText
|
|
||||||
);
|
|
||||||
updatePromptCount(promptText.length);
|
updatePromptCount(promptText.length);
|
||||||
|
|
||||||
state.outputRaw = "";
|
state.outputRaw = "";
|
||||||
@@ -727,8 +635,7 @@ async function handleAnalyze() {
|
|||||||
apiKeyPrefix: resolvedApiKeyPrefix,
|
apiKeyPrefix: resolvedApiKeyPrefix,
|
||||||
model: resolvedModel,
|
model: resolvedModel,
|
||||||
systemPrompt: resolvedSystemPrompt,
|
systemPrompt: resolvedSystemPrompt,
|
||||||
resume: resumeText,
|
resume: resume || "",
|
||||||
resumeType,
|
|
||||||
taskText: task.text || "",
|
taskText: task.text || "",
|
||||||
postingText: state.postingText,
|
postingText: state.postingText,
|
||||||
tabId: tab.id
|
tabId: tab.id
|
||||||
@@ -797,10 +704,6 @@ 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);
|
||||||
|
|||||||
@@ -42,77 +42,6 @@ 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;
|
||||||
}
|
}
|
||||||
@@ -140,6 +69,7 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,15 +288,7 @@ button:active {
|
|||||||
.api-config-actions {
|
.api-config-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
justify-content: space-between;
|
justify-content: flex-end;
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-config-actions-left,
|
|
||||||
.api-config-actions-right {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.env-configs {
|
.env-configs {
|
||||||
@@ -389,32 +311,6 @@ 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"] {
|
||||||
@@ -437,14 +333,3 @@ 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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -16,24 +16,7 @@
|
|||||||
<button id="saveBtn" class="accent">Save Settings</button>
|
<button id="saveBtn" class="accent">Save Settings</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-layout">
|
<details class="panel">
|
||||||
<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>
|
||||||
@@ -53,7 +36,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details class="panel" id="api-keys-panel">
|
<details class="panel">
|
||||||
<summary class="panel-summary">
|
<summary class="panel-summary">
|
||||||
<span class="panel-caret" aria-hidden="true">
|
<span class="panel-caret" aria-hidden="true">
|
||||||
<span class="caret-closed">▸</span>
|
<span class="caret-closed">▸</span>
|
||||||
@@ -70,7 +53,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details class="panel" id="api-panel">
|
<details class="panel">
|
||||||
<summary class="panel-summary">
|
<summary class="panel-summary">
|
||||||
<span class="panel-caret" aria-hidden="true">
|
<span class="panel-caret" aria-hidden="true">
|
||||||
<span class="caret-closed">▸</span>
|
<span class="caret-closed">▸</span>
|
||||||
@@ -89,7 +72,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details class="panel" id="environment-panel">
|
<details class="panel">
|
||||||
<summary class="panel-summary">
|
<summary class="panel-summary">
|
||||||
<span class="panel-caret" aria-hidden="true">
|
<span class="panel-caret" aria-hidden="true">
|
||||||
<span class="caret-closed">▸</span>
|
<span class="caret-closed">▸</span>
|
||||||
@@ -109,27 +92,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details class="panel" id="profiles-panel">
|
<details class="panel">
|
||||||
<summary class="panel-summary">
|
<summary class="panel-summary">
|
||||||
<span class="panel-caret" aria-hidden="true">
|
<span class="panel-caret" aria-hidden="true">
|
||||||
<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>My Profiles</h2>
|
<h2>Resume</h2>
|
||||||
<span class="hint hint-accent">Text to your resumes or generic profiles goes here</span>
|
<span class="hint hint-accent">Text to your profile goes here</span>
|
||||||
</div>
|
</div>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="row">
|
<textarea id="resume" rows="10" placeholder="Paste your resume text..."></textarea>
|
||||||
<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" id="tasks-panel">
|
<details class="panel">
|
||||||
<summary class="panel-summary">
|
<summary class="panel-summary">
|
||||||
<span class="panel-caret" aria-hidden="true">
|
<span class="panel-caret" aria-hidden="true">
|
||||||
<span class="caret-closed">▸</span>
|
<span class="caret-closed">▸</span>
|
||||||
@@ -148,8 +127,6 @@
|
|||||||
<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>
|
||||||
|
|||||||
@@ -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,11 +8,7 @@ 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 = {
|
||||||
@@ -30,24 +26,12 @@ 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;
|
||||||
@@ -73,11 +57,6 @@ 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 "";
|
||||||
@@ -115,10 +94,6 @@ 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";
|
||||||
@@ -278,10 +253,6 @@ 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";
|
||||||
@@ -294,10 +265,6 @@ 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;
|
||||||
@@ -323,27 +290,9 @@ function buildApiConfigCard(config) {
|
|||||||
updateEnvApiOptions();
|
updateEnvApiOptions();
|
||||||
});
|
});
|
||||||
|
|
||||||
addBelowBtn.addEventListener("click", () => {
|
actions.appendChild(moveTopBtn);
|
||||||
const name = buildUniqueDefaultName(
|
actions.appendChild(moveUpBtn);
|
||||||
collectNames(apiConfigsContainer, ".api-config-name")
|
actions.appendChild(moveDownBtn);
|
||||||
);
|
|
||||||
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";
|
||||||
@@ -365,6 +314,8 @@ 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";
|
||||||
@@ -379,6 +330,8 @@ 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";
|
||||||
@@ -393,6 +346,8 @@ 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";
|
||||||
@@ -402,6 +357,7 @@ function buildApiConfigCard(config) {
|
|||||||
updateEnvApiOptions();
|
updateEnvApiOptions();
|
||||||
updateApiConfigControls();
|
updateApiConfigControls();
|
||||||
});
|
});
|
||||||
|
actions.appendChild(deleteBtn);
|
||||||
|
|
||||||
const updateSelect = () => updateEnvApiOptions();
|
const updateSelect = () => updateEnvApiOptions();
|
||||||
nameInput.addEventListener("input", updateSelect);
|
nameInput.addEventListener("input", updateSelect);
|
||||||
@@ -412,19 +368,6 @@ 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);
|
||||||
@@ -455,7 +398,6 @@ 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) {
|
||||||
@@ -514,10 +456,6 @@ 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;
|
||||||
@@ -543,20 +481,9 @@ 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";
|
||||||
@@ -602,7 +529,6 @@ 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() {
|
||||||
@@ -689,10 +615,6 @@ 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;
|
||||||
@@ -721,24 +643,6 @@ 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";
|
||||||
@@ -796,7 +700,6 @@ 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() {
|
||||||
@@ -830,7 +733,6 @@ function updateTaskEnvOptions() {
|
|||||||
|
|
||||||
select.dataset.preferred = select.value;
|
select.dataset.preferred = select.value;
|
||||||
});
|
});
|
||||||
scheduleSidebarErrors();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectEnvConfigs() {
|
function collectEnvConfigs() {
|
||||||
@@ -848,222 +750,6 @@ 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");
|
||||||
@@ -1124,16 +810,6 @@ 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");
|
||||||
@@ -1213,13 +889,11 @@ 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", () => {
|
||||||
@@ -1230,14 +904,12 @@ 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", () => {
|
||||||
@@ -1254,7 +926,6 @@ 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);
|
||||||
|
|
||||||
@@ -1271,7 +942,6 @@ 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() {
|
||||||
@@ -1280,127 +950,15 @@ 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 = "",
|
||||||
@@ -1410,7 +968,6 @@ async function loadSettings() {
|
|||||||
activeApiConfigId = "",
|
activeApiConfigId = "",
|
||||||
envConfigs = [],
|
envConfigs = [],
|
||||||
activeEnvConfigId = "",
|
activeEnvConfigId = "",
|
||||||
profiles = [],
|
|
||||||
apiBaseUrl = "",
|
apiBaseUrl = "",
|
||||||
apiKeyHeader = "",
|
apiKeyHeader = "",
|
||||||
apiKeyPrefix = "",
|
apiKeyPrefix = "",
|
||||||
@@ -1427,7 +984,6 @@ async function loadSettings() {
|
|||||||
"activeApiConfigId",
|
"activeApiConfigId",
|
||||||
"envConfigs",
|
"envConfigs",
|
||||||
"activeEnvConfigId",
|
"activeEnvConfigId",
|
||||||
"profiles",
|
|
||||||
"apiBaseUrl",
|
"apiBaseUrl",
|
||||||
"apiKeyHeader",
|
"apiKeyHeader",
|
||||||
"apiKeyPrefix",
|
"apiKeyPrefix",
|
||||||
@@ -1438,6 +994,7 @@ async function loadSettings() {
|
|||||||
"theme"
|
"theme"
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
resumeInput.value = resume;
|
||||||
themeSelect.value = theme;
|
themeSelect.value = theme;
|
||||||
applyTheme(theme);
|
applyTheme(theme);
|
||||||
|
|
||||||
@@ -1573,55 +1130,18 @@ 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, index) => task.defaultEnvId !== tasks[index]?.defaultEnvId
|
||||||
task.defaultEnvId !== tasks[index]?.defaultEnvId ||
|
|
||||||
task.defaultProfileId !== tasks[index]?.defaultProfileId
|
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
await chrome.storage.local.set({ tasks: normalizedTasks });
|
await chrome.storage.local.set({ tasks: normalizedTasks });
|
||||||
@@ -1633,13 +1153,11 @@ async function loadSettings() {
|
|||||||
id: newTaskId(),
|
id: newTaskId(),
|
||||||
name: "",
|
name: "",
|
||||||
text: "",
|
text: "",
|
||||||
defaultEnvId,
|
defaultEnvId
|
||||||
defaultProfileId
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
updateTaskControls();
|
updateTaskControls();
|
||||||
updateTaskEnvOptions();
|
updateTaskEnvOptions();
|
||||||
updateTaskProfileOptions();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1648,8 +1166,6 @@ async function loadSettings() {
|
|||||||
}
|
}
|
||||||
updateTaskControls();
|
updateTaskControls();
|
||||||
updateTaskEnvOptions();
|
updateTaskEnvOptions();
|
||||||
updateTaskProfileOptions();
|
|
||||||
updateSidebarErrors();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveSettings() {
|
async function saveSettings() {
|
||||||
@@ -1657,7 +1173,6 @@ 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 =
|
||||||
@@ -1675,8 +1190,7 @@ async function saveSettings() {
|
|||||||
envConfigs,
|
envConfigs,
|
||||||
activeEnvConfigId,
|
activeEnvConfigId,
|
||||||
systemPrompt: activeEnv?.systemPrompt || "",
|
systemPrompt: activeEnv?.systemPrompt || "",
|
||||||
profiles,
|
resume: resumeInput.value,
|
||||||
resume: profiles[0]?.text || "",
|
|
||||||
tasks,
|
tasks,
|
||||||
theme: themeSelect.value
|
theme: themeSelect.value
|
||||||
});
|
});
|
||||||
@@ -1684,9 +1198,6 @@ 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")
|
||||||
@@ -1695,8 +1206,7 @@ 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) {
|
||||||
@@ -1706,7 +1216,6 @@ addTaskBtn.addEventListener("click", () => {
|
|||||||
}
|
}
|
||||||
updateTaskControls();
|
updateTaskControls();
|
||||||
updateTaskEnvOptions();
|
updateTaskEnvOptions();
|
||||||
updateTaskProfileOptions();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
addApiKeyBtn.addEventListener("click", () => {
|
addApiKeyBtn.addEventListener("click", () => {
|
||||||
@@ -1772,41 +1281,7 @@ 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);
|
|
||||||
|
|||||||
Reference in New Issue
Block a user