[Prototype] first working version of SiteCompanion
This commit is contained in:
@@ -29,12 +29,15 @@ const DEFAULT_SETTINGS = {
|
|||||||
systemPrompt:
|
systemPrompt:
|
||||||
"You are a precise, honest assistant. Be concise and avoid inventing details, be critical about evaluations. You should put in a small summary of all the sections at the end. You should answer in no longer than 3 sections including the summary. And remember to bold or italicize key points.",
|
"You are a precise, honest assistant. Be concise and avoid inventing details, be critical about evaluations. You should put in a small summary of all the sections at the end. You should answer in no longer than 3 sections including the summary. And remember to bold or italicize key points.",
|
||||||
tasks: DEFAULT_TASKS,
|
tasks: DEFAULT_TASKS,
|
||||||
|
shortcuts: [],
|
||||||
theme: "system",
|
theme: "system",
|
||||||
|
toolbarAutoHide: true,
|
||||||
workspaces: []
|
workspaces: []
|
||||||
};
|
};
|
||||||
|
|
||||||
const OUTPUT_STORAGE_KEY = "lastOutput";
|
const OUTPUT_STORAGE_KEY = "lastOutput";
|
||||||
const AUTO_RUN_KEY = "autoRunDefaultTask";
|
const AUTO_RUN_KEY = "autoRunDefaultTask";
|
||||||
|
const SHORTCUT_RUN_KEY = "runShortcutId";
|
||||||
let activeAbortController = null;
|
let activeAbortController = null;
|
||||||
let keepalivePort = null;
|
let keepalivePort = null;
|
||||||
const streamState = {
|
const streamState = {
|
||||||
@@ -354,6 +357,16 @@ chrome.runtime.onConnect.addListener((port) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
chrome.runtime.onMessage.addListener((message) => {
|
chrome.runtime.onMessage.addListener((message) => {
|
||||||
|
if (message?.type === "RUN_SHORTCUT") {
|
||||||
|
const shortcutId = message.shortcutId || "";
|
||||||
|
if (shortcutId) {
|
||||||
|
void chrome.storage.local.set({ [SHORTCUT_RUN_KEY]: shortcutId });
|
||||||
|
if (chrome.action?.openPopup) {
|
||||||
|
void chrome.action.openPopup().catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (message?.type !== "RUN_DEFAULT_TASK") return;
|
if (message?.type !== "RUN_DEFAULT_TASK") return;
|
||||||
void chrome.storage.local.set({ [AUTO_RUN_KEY]: Date.now() });
|
void chrome.storage.local.set({ [AUTO_RUN_KEY]: Date.now() });
|
||||||
if (chrome.action?.openPopup) {
|
if (chrome.action?.openPopup) {
|
||||||
|
|||||||
@@ -22,7 +22,32 @@ function findMinimumScope(text) {
|
|||||||
return deepest;
|
return deepest;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createToolbar(presets, position = "bottom-right") {
|
function normalizeName(value) {
|
||||||
|
return (value || "").trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveScopedItems(parentItems, localItems, disabledNames) {
|
||||||
|
const parent = Array.isArray(parentItems) ? parentItems : [];
|
||||||
|
const local = Array.isArray(localItems) ? localItems : [];
|
||||||
|
const disabledSet = new Set(
|
||||||
|
(disabledNames || []).map((name) => normalizeName(name)).filter(Boolean)
|
||||||
|
);
|
||||||
|
const localNameSet = new Set(
|
||||||
|
local.map((item) => normalizeName(item.name)).filter(Boolean)
|
||||||
|
);
|
||||||
|
const inherited = parent.filter((item) => {
|
||||||
|
if (item?.enabled === false) return false;
|
||||||
|
const key = normalizeName(item?.name);
|
||||||
|
if (!key) return false;
|
||||||
|
if (localNameSet.has(key)) return false;
|
||||||
|
if (disabledSet.has(key)) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
const localEnabled = local.filter((item) => item?.enabled !== false);
|
||||||
|
return [...inherited, ...localEnabled];
|
||||||
|
}
|
||||||
|
|
||||||
|
function createToolbar(shortcuts, position = "bottom-right") {
|
||||||
let toolbar = document.getElementById("sitecompanion-toolbar");
|
let toolbar = document.getElementById("sitecompanion-toolbar");
|
||||||
if (toolbar) toolbar.remove();
|
if (toolbar) toolbar.remove();
|
||||||
|
|
||||||
@@ -63,16 +88,16 @@ function createToolbar(presets, position = "bottom-right") {
|
|||||||
font-family: system-ui, sans-serif;
|
font-family: system-ui, sans-serif;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
if (!presets || !presets.length) {
|
if (!shortcuts || !shortcuts.length) {
|
||||||
const label = document.createElement("span");
|
const label = document.createElement("span");
|
||||||
label.textContent = "SiteCompanion";
|
label.textContent = "SiteCompanion";
|
||||||
label.style.fontSize = "12px";
|
label.style.fontSize = "12px";
|
||||||
label.style.color = "#6b5f55";
|
label.style.color = "#6b5f55";
|
||||||
toolbar.appendChild(label);
|
toolbar.appendChild(label);
|
||||||
} else {
|
} else {
|
||||||
for (const preset of presets) {
|
for (const shortcut of shortcuts) {
|
||||||
const btn = document.createElement("button");
|
const btn = document.createElement("button");
|
||||||
btn.textContent = preset.name;
|
btn.textContent = shortcut.name;
|
||||||
btn.style.cssText = `
|
btn.style.cssText = `
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
background: #b14d2b;
|
background: #b14d2b;
|
||||||
@@ -83,7 +108,7 @@ function createToolbar(presets, position = "bottom-right") {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
`;
|
`;
|
||||||
btn.addEventListener("click", () => {
|
btn.addEventListener("click", () => {
|
||||||
chrome.runtime.sendMessage({ type: "RUN_PRESET", presetId: preset.id });
|
chrome.runtime.sendMessage({ type: "RUN_SHORTCUT", shortcutId: shortcut.id });
|
||||||
});
|
});
|
||||||
toolbar.appendChild(btn);
|
toolbar.appendChild(btn);
|
||||||
}
|
}
|
||||||
@@ -95,36 +120,68 @@ function createToolbar(presets, position = "bottom-right") {
|
|||||||
|
|
||||||
|
|
||||||
function matchUrl(url, pattern) {
|
function matchUrl(url, pattern) {
|
||||||
|
|
||||||
if (!pattern) return false;
|
if (!pattern) return false;
|
||||||
|
let regex = null;
|
||||||
const regex = new RegExp("^" + pattern.split("*").join(".*") + "$");
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
regex = new RegExp("^" + pattern.split("*").join(".*") + "$");
|
||||||
const urlObj = new URL(url);
|
} catch {
|
||||||
|
return false;
|
||||||
const target = urlObj.hostname + urlObj.pathname;
|
}
|
||||||
|
try {
|
||||||
return regex.test(target);
|
const urlObj = new URL(url);
|
||||||
|
const target = urlObj.hostname + urlObj.pathname;
|
||||||
|
return regex.test(target);
|
||||||
} catch {
|
} catch {
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async function refreshToolbar() {
|
async function refreshToolbar() {
|
||||||
const { sites = [], presets = [], toolbarPosition = "bottom-right" } = await chrome.storage.local.get(["sites", "presets", "toolbarPosition"]);
|
let {
|
||||||
|
sites = [],
|
||||||
|
workspaces = [],
|
||||||
|
shortcuts = [],
|
||||||
|
presets = [],
|
||||||
|
toolbarPosition = "bottom-right"
|
||||||
|
} = await chrome.storage.local.get([
|
||||||
|
"sites",
|
||||||
|
"workspaces",
|
||||||
|
"shortcuts",
|
||||||
|
"presets",
|
||||||
|
"toolbarPosition"
|
||||||
|
]);
|
||||||
const currentUrl = window.location.href;
|
const currentUrl = window.location.href;
|
||||||
const site = sites.find(s => matchUrl(currentUrl, s.urlPattern));
|
const site = sites.find(s => matchUrl(currentUrl, s.urlPattern));
|
||||||
|
|
||||||
if (site) {
|
if (site) {
|
||||||
createToolbar(presets, toolbarPosition);
|
if (!shortcuts.length && Array.isArray(presets) && presets.length) {
|
||||||
|
shortcuts = presets;
|
||||||
|
await chrome.storage.local.set({ shortcuts });
|
||||||
|
await chrome.storage.local.remove("presets");
|
||||||
|
}
|
||||||
|
const workspace =
|
||||||
|
workspaces.find((ws) => ws.id === site.workspaceId) || null;
|
||||||
|
const workspaceDisabled = workspace?.disabledInherited?.shortcuts || [];
|
||||||
|
const siteDisabled = site?.disabledInherited?.shortcuts || [];
|
||||||
|
const workspaceShortcuts = resolveScopedItems(
|
||||||
|
shortcuts,
|
||||||
|
workspace?.shortcuts || [],
|
||||||
|
workspaceDisabled
|
||||||
|
);
|
||||||
|
const siteShortcuts = resolveScopedItems(
|
||||||
|
workspaceShortcuts,
|
||||||
|
site.shortcuts || [],
|
||||||
|
siteDisabled
|
||||||
|
);
|
||||||
|
const resolvedPosition =
|
||||||
|
site.toolbarPosition && site.toolbarPosition !== "inherit"
|
||||||
|
? site.toolbarPosition
|
||||||
|
: workspace?.toolbarPosition && workspace.toolbarPosition !== "inherit"
|
||||||
|
? workspace.toolbarPosition
|
||||||
|
: toolbarPosition;
|
||||||
|
createToolbar(siteShortcuts, resolvedPosition);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,4 +197,25 @@ const observer = new MutationObserver(() => {
|
|||||||
|
|
||||||
// observer.observe(document.documentElement, { childList: true, subtree: true });
|
// observer.observe(document.documentElement, { childList: true, subtree: true });
|
||||||
|
|
||||||
refreshToolbar();
|
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||||
|
if (!message || typeof message !== "object") return;
|
||||||
|
if (message.type === "FIND_SCOPE") {
|
||||||
|
const node = findMinimumScope(message.text || "");
|
||||||
|
if (!node) {
|
||||||
|
sendResponse({ ok: false, error: "Scope not found." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sendResponse({ ok: true, extracted: node.innerText || "" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (message.type === "EXTRACT_FULL") {
|
||||||
|
const extracted = document.body?.innerText || "";
|
||||||
|
sendResponse({ ok: true, extracted });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
refreshToolbar();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("SiteCompanion toolbar failed:", error);
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const clearOutputBtn = document.getElementById("clearOutputBtn");
|
|||||||
|
|
||||||
const OUTPUT_STORAGE_KEY = "lastOutput";
|
const OUTPUT_STORAGE_KEY = "lastOutput";
|
||||||
const AUTO_RUN_KEY = "autoRunDefaultTask";
|
const AUTO_RUN_KEY = "autoRunDefaultTask";
|
||||||
|
const SHORTCUT_RUN_KEY = "runShortcutId";
|
||||||
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 LAST_PROFILE_KEY = "lastSelectedProfileId";
|
||||||
@@ -42,6 +43,7 @@ const state = {
|
|||||||
isAnalyzing: false,
|
isAnalyzing: false,
|
||||||
outputRaw: "",
|
outputRaw: "",
|
||||||
autoRunPending: false,
|
autoRunPending: false,
|
||||||
|
shortcutRunPending: false,
|
||||||
selectedTaskId: "",
|
selectedTaskId: "",
|
||||||
selectedEnvId: "",
|
selectedEnvId: "",
|
||||||
selectedProfileId: ""
|
selectedProfileId: ""
|
||||||
@@ -64,7 +66,12 @@ async function switchState(stateName) {
|
|||||||
|
|
||||||
function matchUrl(url, pattern) {
|
function matchUrl(url, pattern) {
|
||||||
if (!pattern) return false;
|
if (!pattern) return false;
|
||||||
const regex = new RegExp("^" + pattern.split("*").join(".*") + "$");
|
let regex = null;
|
||||||
|
try {
|
||||||
|
regex = new RegExp("^" + pattern.split("*").join(".*") + "$");
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const urlObj = new URL(url);
|
const urlObj = new URL(url);
|
||||||
const target = urlObj.hostname + urlObj.pathname;
|
const target = urlObj.hostname + urlObj.pathname;
|
||||||
@@ -74,6 +81,62 @@ function matchUrl(url, pattern) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeName(value) {
|
||||||
|
return (value || "").trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeConfigList(list) {
|
||||||
|
return Array.isArray(list)
|
||||||
|
? list.map((item) => ({ ...item, enabled: item.enabled !== false }))
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveScopedItems(parentItems, localItems, disabledNames) {
|
||||||
|
const parent = Array.isArray(parentItems) ? parentItems : [];
|
||||||
|
const local = Array.isArray(localItems) ? localItems : [];
|
||||||
|
const disabledSet = new Set(
|
||||||
|
(disabledNames || []).map((name) => normalizeName(name)).filter(Boolean)
|
||||||
|
);
|
||||||
|
const localNameSet = new Set(
|
||||||
|
local.map((item) => normalizeName(item.name)).filter(Boolean)
|
||||||
|
);
|
||||||
|
const inherited = parent.filter((item) => {
|
||||||
|
if (item?.enabled === false) return false;
|
||||||
|
const key = normalizeName(item?.name);
|
||||||
|
if (!key) return false;
|
||||||
|
if (localNameSet.has(key)) return false;
|
||||||
|
if (disabledSet.has(key)) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
const localEnabled = local.filter((item) => item?.enabled !== false);
|
||||||
|
return [...inherited, ...localEnabled];
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveEffectiveList(globalItems, workspace, site, listKey, disabledKey) {
|
||||||
|
const workspaceItems = workspace?.[listKey] || [];
|
||||||
|
const workspaceDisabled = workspace?.disabledInherited?.[disabledKey] || [];
|
||||||
|
const workspaceEffective = resolveScopedItems(
|
||||||
|
globalItems,
|
||||||
|
workspaceItems,
|
||||||
|
workspaceDisabled
|
||||||
|
);
|
||||||
|
const siteItems = site?.[listKey] || [];
|
||||||
|
const siteDisabled = site?.disabledInherited?.[disabledKey] || [];
|
||||||
|
return resolveScopedItems(workspaceEffective, siteItems, siteDisabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterApiConfigsForScope(apiConfigs, workspace, site) {
|
||||||
|
const workspaceDisabled = workspace?.disabledInherited?.apiConfigs || [];
|
||||||
|
const siteDisabled = site?.disabledInherited?.apiConfigs || [];
|
||||||
|
const workspaceFiltered = apiConfigs.filter(
|
||||||
|
(config) =>
|
||||||
|
config?.enabled !== false && !workspaceDisabled.includes(config.id)
|
||||||
|
);
|
||||||
|
return workspaceFiltered.filter(
|
||||||
|
(config) => !siteDisabled.includes(config.id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function detectSite(url) {
|
async function detectSite(url) {
|
||||||
const { sites = [], workspaces = [] } = await getStorage(["sites", "workspaces"]);
|
const { sites = [], workspaces = [] } = await getStorage(["sites", "workspaces"]);
|
||||||
state.sites = sites;
|
state.sites = sites;
|
||||||
@@ -82,7 +145,13 @@ async function detectSite(url) {
|
|||||||
const site = sites.find(s => matchUrl(url, s.urlPattern));
|
const site = sites.find(s => matchUrl(url, s.urlPattern));
|
||||||
if (site) {
|
if (site) {
|
||||||
state.currentSite = site;
|
state.currentSite = site;
|
||||||
state.currentWorkspace = workspaces.find(w => w.id === site.workspaceId) || { name: "Global", id: "global" };
|
const workspace =
|
||||||
|
workspaces.find((entry) => entry.id === site.workspaceId) || null;
|
||||||
|
state.currentWorkspace = workspace || {
|
||||||
|
name: "Global",
|
||||||
|
id: "global",
|
||||||
|
disabledInherited: {}
|
||||||
|
};
|
||||||
currentWorkspaceName.textContent = state.currentWorkspace.name;
|
currentWorkspaceName.textContent = state.currentWorkspace.name;
|
||||||
switchState("normal");
|
switchState("normal");
|
||||||
return true;
|
return true;
|
||||||
@@ -552,40 +621,88 @@ async function loadConfig() {
|
|||||||
"tasks",
|
"tasks",
|
||||||
"envConfigs",
|
"envConfigs",
|
||||||
"profiles",
|
"profiles",
|
||||||
|
"shortcuts",
|
||||||
|
"workspaces",
|
||||||
|
"sites",
|
||||||
LAST_TASK_KEY,
|
LAST_TASK_KEY,
|
||||||
LAST_ENV_KEY,
|
LAST_ENV_KEY,
|
||||||
LAST_PROFILE_KEY
|
LAST_PROFILE_KEY
|
||||||
]);
|
]);
|
||||||
const tasks = Array.isArray(stored.tasks) ? stored.tasks : [];
|
const tasks = normalizeConfigList(stored.tasks);
|
||||||
const envs = Array.isArray(stored.envConfigs) ? stored.envConfigs : [];
|
const envs = normalizeConfigList(stored.envConfigs);
|
||||||
const profiles = Array.isArray(stored.profiles) ? stored.profiles : [];
|
const profiles = normalizeConfigList(stored.profiles);
|
||||||
renderTasks(tasks);
|
const shortcuts = normalizeConfigList(stored.shortcuts);
|
||||||
renderEnvironments(envs);
|
const sites = Array.isArray(stored.sites) ? stored.sites : state.sites;
|
||||||
renderProfiles(profiles);
|
const workspaces = Array.isArray(stored.workspaces)
|
||||||
|
? stored.workspaces
|
||||||
|
: state.workspaces;
|
||||||
|
state.sites = sites;
|
||||||
|
state.workspaces = workspaces;
|
||||||
|
|
||||||
if (!tasks.length) {
|
const activeSite = state.currentSite
|
||||||
|
? sites.find((entry) => entry.id === state.currentSite.id)
|
||||||
|
: null;
|
||||||
|
const activeWorkspace =
|
||||||
|
activeSite && activeSite.workspaceId
|
||||||
|
? workspaces.find((entry) => entry.id === activeSite.workspaceId)
|
||||||
|
: null;
|
||||||
|
if (activeWorkspace) {
|
||||||
|
state.currentWorkspace = activeWorkspace;
|
||||||
|
currentWorkspaceName.textContent = activeWorkspace.name || "Global";
|
||||||
|
}
|
||||||
|
|
||||||
|
const effectiveEnvs = resolveEffectiveList(
|
||||||
|
envs,
|
||||||
|
activeWorkspace,
|
||||||
|
activeSite,
|
||||||
|
"envConfigs",
|
||||||
|
"envs"
|
||||||
|
);
|
||||||
|
const effectiveProfiles = resolveEffectiveList(
|
||||||
|
profiles,
|
||||||
|
activeWorkspace,
|
||||||
|
activeSite,
|
||||||
|
"profiles",
|
||||||
|
"profiles"
|
||||||
|
);
|
||||||
|
const effectiveTasks = resolveEffectiveList(
|
||||||
|
tasks,
|
||||||
|
activeWorkspace,
|
||||||
|
activeSite,
|
||||||
|
"tasks",
|
||||||
|
"tasks"
|
||||||
|
);
|
||||||
|
|
||||||
|
renderTasks(effectiveTasks);
|
||||||
|
renderEnvironments(effectiveEnvs);
|
||||||
|
renderProfiles(effectiveProfiles);
|
||||||
|
|
||||||
|
if (!effectiveTasks.length) {
|
||||||
state.selectedTaskId = "";
|
state.selectedTaskId = "";
|
||||||
setEnvironmentSelection(envs[0]?.id || "");
|
setEnvironmentSelection(effectiveEnvs[0]?.id || "");
|
||||||
setProfileSelection(profiles[0]?.id || "");
|
setProfileSelection(effectiveProfiles[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 storedProfileId = stored[LAST_PROFILE_KEY];
|
||||||
const initialTaskId = tasks.some((task) => task.id === storedTaskId)
|
const initialTaskId = effectiveTasks.some((task) => task.id === storedTaskId)
|
||||||
? storedTaskId
|
? storedTaskId
|
||||||
: tasks[0].id;
|
: effectiveTasks[0].id;
|
||||||
selectTask(initialTaskId, { resetEnv: false });
|
selectTask(initialTaskId, { resetEnv: false });
|
||||||
|
|
||||||
const task = tasks.find((item) => item.id === initialTaskId);
|
const task = effectiveTasks.find((item) => item.id === initialTaskId);
|
||||||
if (storedEnvId && envs.some((env) => env.id === storedEnvId)) {
|
if (storedEnvId && effectiveEnvs.some((env) => env.id === storedEnvId)) {
|
||||||
setEnvironmentSelection(storedEnvId);
|
setEnvironmentSelection(storedEnvId);
|
||||||
} else {
|
} else {
|
||||||
setEnvironmentSelection(getTaskDefaultEnvId(task));
|
setEnvironmentSelection(getTaskDefaultEnvId(task));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (storedProfileId && profiles.some((profile) => profile.id === storedProfileId)) {
|
if (
|
||||||
|
storedProfileId &&
|
||||||
|
effectiveProfiles.some((profile) => profile.id === storedProfileId)
|
||||||
|
) {
|
||||||
setProfileSelection(storedProfileId);
|
setProfileSelection(storedProfileId);
|
||||||
} else {
|
} else {
|
||||||
setProfileSelection(getTaskDefaultProfileId(task));
|
setProfileSelection(getTaskDefaultProfileId(task));
|
||||||
@@ -652,8 +769,6 @@ async function handleAnalyze() {
|
|||||||
activeApiKeyId = "",
|
activeApiKeyId = "",
|
||||||
apiConfigs = [],
|
apiConfigs = [],
|
||||||
activeApiConfigId = "",
|
activeApiConfigId = "",
|
||||||
envConfigs = [],
|
|
||||||
profiles = [],
|
|
||||||
apiBaseUrl,
|
apiBaseUrl,
|
||||||
apiKeyHeader,
|
apiKeyHeader,
|
||||||
apiKeyPrefix,
|
apiKeyPrefix,
|
||||||
@@ -665,8 +780,6 @@ async function handleAnalyze() {
|
|||||||
"activeApiKeyId",
|
"activeApiKeyId",
|
||||||
"apiConfigs",
|
"apiConfigs",
|
||||||
"activeApiConfigId",
|
"activeApiConfigId",
|
||||||
"envConfigs",
|
|
||||||
"profiles",
|
|
||||||
"apiBaseUrl",
|
"apiBaseUrl",
|
||||||
"apiKeyHeader",
|
"apiKeyHeader",
|
||||||
"apiKeyPrefix",
|
"apiKeyPrefix",
|
||||||
@@ -675,9 +788,13 @@ async function handleAnalyze() {
|
|||||||
"resume"
|
"resume"
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const resolvedConfigs = Array.isArray(apiConfigs) ? apiConfigs : [];
|
const resolvedConfigs = filterApiConfigsForScope(
|
||||||
const resolvedEnvs = Array.isArray(envConfigs) ? envConfigs : [];
|
normalizeConfigList(apiConfigs),
|
||||||
const resolvedProfiles = Array.isArray(profiles) ? profiles : [];
|
state.currentWorkspace,
|
||||||
|
state.currentSite
|
||||||
|
);
|
||||||
|
const resolvedEnvs = Array.isArray(state.envs) ? state.envs : [];
|
||||||
|
const resolvedProfiles = Array.isArray(state.profiles) ? state.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) ||
|
||||||
@@ -711,7 +828,9 @@ async function handleAnalyze() {
|
|||||||
const resolvedApiKeyPrefix = activeConfig?.apiKeyPrefix ?? apiKeyPrefix ?? "";
|
const resolvedApiKeyPrefix = activeConfig?.apiKeyPrefix ?? apiKeyPrefix ?? "";
|
||||||
const resolvedModel = activeConfig?.model || model || "";
|
const resolvedModel = activeConfig?.model || model || "";
|
||||||
|
|
||||||
const resolvedKeys = Array.isArray(apiKeys) ? apiKeys : [];
|
const resolvedKeys = normalizeConfigList(apiKeys).filter(
|
||||||
|
(key) => key.enabled !== false
|
||||||
|
);
|
||||||
const resolvedKeyId =
|
const resolvedKeyId =
|
||||||
activeConfig?.apiKeyId || activeApiKeyId || resolvedKeys[0]?.id || "";
|
activeConfig?.apiKeyId || activeApiKeyId || resolvedKeys[0]?.id || "";
|
||||||
const activeKey = resolvedKeys.find((entry) => entry.id === resolvedKeyId);
|
const activeKey = resolvedKeys.find((entry) => entry.id === resolvedKeyId);
|
||||||
@@ -926,8 +1045,7 @@ updateSiteTextCount();
|
|||||||
updatePromptCount(0);
|
updatePromptCount(0);
|
||||||
renderOutput();
|
renderOutput();
|
||||||
setAnalyzing(false);
|
setAnalyzing(false);
|
||||||
loadConfig();
|
void loadTheme();
|
||||||
loadTheme();
|
|
||||||
|
|
||||||
async function loadSavedOutput() {
|
async function loadSavedOutput() {
|
||||||
const stored = await getStorage([OUTPUT_STORAGE_KEY]);
|
const stored = await getStorage([OUTPUT_STORAGE_KEY]);
|
||||||
@@ -935,6 +1053,60 @@ async function loadSavedOutput() {
|
|||||||
renderOutput();
|
renderOutput();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadShortcutRunRequest() {
|
||||||
|
const stored = await getStorage([
|
||||||
|
SHORTCUT_RUN_KEY,
|
||||||
|
"shortcuts",
|
||||||
|
"workspaces",
|
||||||
|
"sites"
|
||||||
|
]);
|
||||||
|
const shortcutId = stored[SHORTCUT_RUN_KEY];
|
||||||
|
if (!shortcutId) return;
|
||||||
|
|
||||||
|
state.shortcutRunPending = true;
|
||||||
|
await chrome.storage.local.remove(SHORTCUT_RUN_KEY);
|
||||||
|
|
||||||
|
const globalShortcuts = normalizeConfigList(stored.shortcuts);
|
||||||
|
const sites = Array.isArray(stored.sites) ? stored.sites : state.sites;
|
||||||
|
const workspaces = Array.isArray(stored.workspaces)
|
||||||
|
? stored.workspaces
|
||||||
|
: state.workspaces;
|
||||||
|
const activeSite = state.currentSite
|
||||||
|
? sites.find((entry) => entry.id === state.currentSite.id)
|
||||||
|
: null;
|
||||||
|
const activeWorkspace =
|
||||||
|
activeSite && activeSite.workspaceId
|
||||||
|
? workspaces.find((entry) => entry.id === activeSite.workspaceId)
|
||||||
|
: null;
|
||||||
|
const effectiveShortcuts = resolveEffectiveList(
|
||||||
|
globalShortcuts,
|
||||||
|
activeWorkspace,
|
||||||
|
activeSite,
|
||||||
|
"shortcuts",
|
||||||
|
"shortcuts"
|
||||||
|
);
|
||||||
|
const shortcut = effectiveShortcuts.find((item) => item.id === shortcutId);
|
||||||
|
if (!shortcut) {
|
||||||
|
setStatus("Shortcut not found.");
|
||||||
|
state.shortcutRunPending = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shortcut.taskId) {
|
||||||
|
selectTask(shortcut.taskId, { resetEnv: true });
|
||||||
|
}
|
||||||
|
if (shortcut.envId) {
|
||||||
|
setEnvironmentSelection(shortcut.envId);
|
||||||
|
}
|
||||||
|
if (shortcut.profileId) {
|
||||||
|
setProfileSelection(shortcut.profileId);
|
||||||
|
}
|
||||||
|
await persistSelections();
|
||||||
|
state.autoRunPending = false;
|
||||||
|
state.shortcutRunPending = false;
|
||||||
|
void handleExtractAndAnalyze();
|
||||||
|
}
|
||||||
|
|
||||||
async function loadAutoRunRequest() {
|
async function loadAutoRunRequest() {
|
||||||
const stored = await getStorage([AUTO_RUN_KEY]);
|
const stored = await getStorage([AUTO_RUN_KEY]);
|
||||||
if (stored[AUTO_RUN_KEY]) {
|
if (stored[AUTO_RUN_KEY]) {
|
||||||
@@ -945,6 +1117,7 @@ async function loadAutoRunRequest() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function maybeRunDefaultTask() {
|
function maybeRunDefaultTask() {
|
||||||
|
if (state.shortcutRunPending) return;
|
||||||
if (!state.autoRunPending) return;
|
if (!state.autoRunPending) return;
|
||||||
if (state.isAnalyzing) return;
|
if (state.isAnalyzing) return;
|
||||||
if (!state.tasks.length) return;
|
if (!state.tasks.length) return;
|
||||||
@@ -954,8 +1127,14 @@ function maybeRunDefaultTask() {
|
|||||||
void handleExtractAndAnalyze();
|
void handleExtractAndAnalyze();
|
||||||
}
|
}
|
||||||
|
|
||||||
loadSavedOutput();
|
async function init() {
|
||||||
loadAutoRunRequest();
|
await loadConfig();
|
||||||
|
await loadShortcutRunRequest();
|
||||||
|
await loadAutoRunRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
void init();
|
||||||
|
void loadSavedOutput();
|
||||||
ensurePort();
|
ensurePort();
|
||||||
|
|
||||||
chrome.storage.onChanged.addListener((changes) => {
|
chrome.storage.onChanged.addListener((changes) => {
|
||||||
@@ -965,6 +1144,10 @@ chrome.storage.onChanged.addListener((changes) => {
|
|||||||
maybeRunDefaultTask();
|
maybeRunDefaultTask();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (changes[SHORTCUT_RUN_KEY]?.newValue) {
|
||||||
|
void loadShortcutRunRequest();
|
||||||
|
}
|
||||||
|
|
||||||
if (changes[OUTPUT_STORAGE_KEY]?.newValue !== undefined) {
|
if (changes[OUTPUT_STORAGE_KEY]?.newValue !== undefined) {
|
||||||
if (!state.isAnalyzing || !state.port) {
|
if (!state.isAnalyzing || !state.port) {
|
||||||
state.outputRaw = changes[OUTPUT_STORAGE_KEY].newValue || "";
|
state.outputRaw = changes[OUTPUT_STORAGE_KEY].newValue || "";
|
||||||
|
|||||||
@@ -199,38 +199,31 @@ body {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-summary {
|
|
||||||
list-style: none;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
justify-content: flex-start;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Restore native marker but keep list-style: none from above? No, remove list-style: none to show marker. */
|
|
||||||
/* Wait, display: flex might hide marker in some browsers. */
|
|
||||||
/* Usually marker is ::marker pseudo-element on summary. */
|
|
||||||
/* To show native marker, summary should be display: list-item or similar? */
|
|
||||||
/* Actually, standard is display: block (or list-item). Flex might kill it. */
|
|
||||||
/* If the user wants native glyphs, I should use list-item and maybe position the h2? */
|
|
||||||
|
|
||||||
/* Let's try reverting panel-summary to default display and styling h2 inline. */
|
|
||||||
|
|
||||||
.panel-summary {
|
.panel-summary {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
/* display: list-item; default */
|
display: list-item;
|
||||||
|
list-style: revert;
|
||||||
|
list-style-position: inside;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Need to align H2. */
|
.panel-summary::marker {
|
||||||
.panel-summary h2 {
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-summary h2,
|
||||||
|
.panel-summary h3 {
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.panel-summary .row-title {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
.sub-panel .panel-summary {
|
.sub-panel .panel-summary {
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
}
|
}
|
||||||
@@ -312,6 +305,20 @@ label {
|
|||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toggle-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
text-transform: none;
|
||||||
|
letter-spacing: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-label input[type="checkbox"] {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
input,
|
input,
|
||||||
textarea,
|
textarea,
|
||||||
select {
|
select {
|
||||||
@@ -378,6 +385,109 @@ button:active {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.shortcuts {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcut-card {
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--card-bg);
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scope-group {
|
||||||
|
margin-top: 12px;
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scope-title {
|
||||||
|
font-size: 11px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.8px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inherited-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inherited-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inherited-button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--panel);
|
||||||
|
font-size: 11px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.8px;
|
||||||
|
color: var(--muted);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inherited-button input {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inherited-item.is-enabled .inherited-button {
|
||||||
|
border: 1px solid var(--accent);
|
||||||
|
background: var(--accent);
|
||||||
|
color: #fff9f3;
|
||||||
|
box-shadow: 0 8px 20px rgba(177, 77, 43, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inherited-item.is-disabled .inherited-button {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inherited-item.is-overridden .inherited-button {
|
||||||
|
cursor: default;
|
||||||
|
opacity: 0.65;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dup-controls {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dup-select {
|
||||||
|
min-width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sites-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sites-list a {
|
||||||
|
color: var(--muted);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sites-list a:hover {
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
.api-keys {
|
.api-keys {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
</header>
|
</header>
|
||||||
<div class="page-bar">
|
<div class="page-bar">
|
||||||
<div id="status" class="status"></div>
|
<div id="status" class="status"></div>
|
||||||
<button id="saveBtn" class="accent">Save Settings</button>
|
<button id="saveBtn" class="accent" type="button">Save Settings</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-layout">
|
<div class="settings-layout">
|
||||||
@@ -28,27 +28,28 @@
|
|||||||
<div class="toc-item">
|
<div class="toc-item">
|
||||||
<span class="toc-caret">▸</span> <a href="#global-config-panel">Global Configuration</a>
|
<span class="toc-caret">▸</span> <a href="#global-config-panel">Global Configuration</a>
|
||||||
</div>
|
</div>
|
||||||
<ul class="toc-sub hidden">
|
<ul class="toc-sub">
|
||||||
<li><a href="#appearance-panel">Appearance</a></li>
|
<li><a href="#appearance-panel">Appearance</a></li>
|
||||||
<li><a href="#api-keys-panel">API Keys</a></li>
|
<li><a href="#api-keys-panel">API Keys</a></li>
|
||||||
<li><a href="#api-panel">API</a></li>
|
<li><a href="#api-panel">API</a></li>
|
||||||
<li><a href="#environment-panel">Environments</a></li>
|
<li><a href="#environment-panel">Environments</a></li>
|
||||||
<li><a href="#profiles-panel">Profiles</a></li>
|
<li><a href="#profiles-panel">Profiles</a></li>
|
||||||
<li><a href="#tasks-panel">Tasks</a></li>
|
<li><a href="#tasks-panel">Tasks</a></li>
|
||||||
<li><a href="#presets-panel">Presets</a></li>
|
<li><a href="#shortcuts-panel">Toolbar Shortcuts</a></li>
|
||||||
|
<li><a href="#global-sites-panel">Sites</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<div class="toc-item">
|
<div class="toc-item">
|
||||||
<span class="toc-caret">▸</span> <a href="#workspaces-panel">Workspaces</a>
|
<span class="toc-caret">▸</span> <a href="#workspaces-panel">Workspaces</a>
|
||||||
</div>
|
</div>
|
||||||
<ul class="toc-sub hidden" id="toc-workspaces-list"></ul>
|
<ul class="toc-sub" id="toc-workspaces-list"></ul>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<div class="toc-item">
|
<div class="toc-item">
|
||||||
<span class="toc-caret">▸</span> <a href="#sites-panel">Sites</a>
|
<span class="toc-caret">▸</span> <a href="#sites-panel">Sites</a>
|
||||||
</div>
|
</div>
|
||||||
<ul class="toc-sub hidden" id="toc-sites-list"></ul>
|
<ul class="toc-sub" id="toc-sites-list"></ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -84,6 +85,12 @@
|
|||||||
<option value="bottom-center">Bottom Center</option>
|
<option value="bottom-center">Bottom Center</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="toggle-label">
|
||||||
|
<input id="toolbarAutoHide" type="checkbox" />
|
||||||
|
Auto-hide toolbar on unknown sites
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
@@ -168,20 +175,33 @@
|
|||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<!-- Presets -->
|
<!-- Toolbar Shortcuts -->
|
||||||
<details class="panel sub-panel" id="presets-panel">
|
<details class="panel sub-panel" id="shortcuts-panel">
|
||||||
<summary class="panel-summary">
|
<summary class="panel-summary">
|
||||||
<div class="row-title">
|
<div class="row-title">
|
||||||
<h2>PRESETS</h2>
|
<h2>TOOLBAR SHORTCUTS</h2>
|
||||||
<span class="hint hint-accent">Toolbar shortcuts</span>
|
<span class="hint hint-accent">One-click toolbar runs</span>
|
||||||
</div>
|
</div>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div></div>
|
<div></div>
|
||||||
<button id="addPresetBtn" class="ghost" type="button">Add Preset</button>
|
<button id="addShortcutBtn" class="ghost" type="button">Add Shortcut</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="presets" class="presets"></div>
|
<div id="shortcuts" class="shortcuts"></div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<!-- Sites -->
|
||||||
|
<details class="panel sub-panel" id="global-sites-panel">
|
||||||
|
<summary class="panel-summary">
|
||||||
|
<div class="row-title">
|
||||||
|
<h2>SITES</h2>
|
||||||
|
<span class="hint hint-accent">Inherit directly from global</span>
|
||||||
|
</div>
|
||||||
|
</summary>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div id="globalSites" class="sites-list"></div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user