gemini handoff to codex
This commit is contained in:
@@ -28,9 +28,9 @@ const DEFAULT_SETTINGS = {
|
|||||||
model: "gpt-4o-mini",
|
model: "gpt-4o-mini",
|
||||||
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.",
|
||||||
resume: "",
|
|
||||||
tasks: DEFAULT_TASKS,
|
tasks: DEFAULT_TASKS,
|
||||||
theme: "system"
|
theme: "system",
|
||||||
|
workspaces: []
|
||||||
};
|
};
|
||||||
|
|
||||||
const OUTPUT_STORAGE_KEY = "lastOutput";
|
const OUTPUT_STORAGE_KEY = "lastOutput";
|
||||||
@@ -54,7 +54,7 @@ function resetAbort() {
|
|||||||
function openKeepalive(tabId) {
|
function openKeepalive(tabId) {
|
||||||
if (!tabId || keepalivePort) return;
|
if (!tabId || keepalivePort) return;
|
||||||
try {
|
try {
|
||||||
keepalivePort = chrome.tabs.connect(tabId, { name: "wwcompanion-keepalive" });
|
keepalivePort = chrome.tabs.connect(tabId, { name: "sitecompanion-keepalive" });
|
||||||
keepalivePort.onDisconnect.addListener(() => {
|
keepalivePort.onDisconnect.addListener(() => {
|
||||||
keepalivePort = null;
|
keepalivePort = null;
|
||||||
});
|
});
|
||||||
@@ -253,20 +253,17 @@ chrome.runtime.onInstalled.addListener(async () => {
|
|||||||
{
|
{
|
||||||
id,
|
id,
|
||||||
name: "Default",
|
name: "Default",
|
||||||
text: stored.resume || "",
|
text: stored.resume || ""
|
||||||
type: "Resume"
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
const normalizedProfiles = stored.profiles.map((profile) => ({
|
const normalizedProfiles = stored.profiles.map((profile) => ({
|
||||||
...profile,
|
...profile,
|
||||||
text: profile.text ?? "",
|
text: profile.text ?? ""
|
||||||
type: profile.type === "Profile" ? "Profile" : "Resume"
|
|
||||||
}));
|
}));
|
||||||
const needsProfileUpdate = normalizedProfiles.some(
|
const needsProfileUpdate = normalizedProfiles.some(
|
||||||
(profile, index) =>
|
(profile, index) =>
|
||||||
(profile.text || "") !== (stored.profiles[index]?.text || "") ||
|
(profile.text || "") !== (stored.profiles[index]?.text || "")
|
||||||
(profile.type || "Resume") !== (stored.profiles[index]?.type || "Resume")
|
|
||||||
);
|
);
|
||||||
if (needsProfileUpdate) {
|
if (needsProfileUpdate) {
|
||||||
updates.profiles = normalizedProfiles;
|
updates.profiles = normalizedProfiles;
|
||||||
@@ -364,17 +361,16 @@ chrome.runtime.onMessage.addListener((message) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function buildUserMessage(resume, resumeType, task, posting) {
|
function buildUserMessage(profileText, taskText, siteText) {
|
||||||
const header = resumeType === "Profile" ? "=== PROFILE ===" : "=== RESUME ===";
|
|
||||||
return [
|
return [
|
||||||
header,
|
"=== Profile ===",
|
||||||
resume || "",
|
profileText || "",
|
||||||
"",
|
"",
|
||||||
"=== TASK ===",
|
"=== Task ===",
|
||||||
task || "",
|
taskText || "",
|
||||||
"",
|
"",
|
||||||
"=== JOB POSTING ===",
|
"=== Site Text ===",
|
||||||
posting || ""
|
siteText || ""
|
||||||
].join("\n");
|
].join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,10 +402,9 @@ async function handleAnalysisRequest(port, payload, signal) {
|
|||||||
apiKeyPrefix,
|
apiKeyPrefix,
|
||||||
model,
|
model,
|
||||||
systemPrompt,
|
systemPrompt,
|
||||||
resume,
|
profileText,
|
||||||
resumeType,
|
|
||||||
taskText,
|
taskText,
|
||||||
postingText,
|
siteText,
|
||||||
tabId
|
tabId
|
||||||
} = payload || {};
|
} = payload || {};
|
||||||
|
|
||||||
@@ -444,21 +439,20 @@ async function handleAnalysisRequest(port, payload, signal) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!postingText) {
|
if (!siteText) {
|
||||||
safePost(port, { type: "ERROR", message: "No job posting text provided." });
|
safePost(port, { type: "ERROR", message: "No site text provided." });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!taskText) {
|
if (!taskText) {
|
||||||
safePost(port, { type: "ERROR", message: "No task prompt selected." });
|
safePost(port, { type: "ERROR", message: "No task selected." });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userMessage = buildUserMessage(
|
const userMessage = buildUserMessage(
|
||||||
resume,
|
profileText,
|
||||||
resumeType,
|
|
||||||
taskText,
|
taskText,
|
||||||
postingText
|
siteText
|
||||||
);
|
);
|
||||||
|
|
||||||
await chrome.storage.local.set({ [OUTPUT_STORAGE_KEY]: "" });
|
await chrome.storage.local.set({ [OUTPUT_STORAGE_KEY]: "" });
|
||||||
|
|||||||
@@ -1,140 +1,143 @@
|
|||||||
const HEADER_LINES = new Set([
|
function findMinimumScope(text) {
|
||||||
"OVERVIEW",
|
if (!text) return null;
|
||||||
"PRE-SCREENING",
|
const normalized = text.trim();
|
||||||
"WORK TERM RATINGS",
|
if (!normalized) return null;
|
||||||
"JOB POSTING INFORMATION",
|
|
||||||
"APPLICATION INFORMATION",
|
|
||||||
"COMPANY INFORMATION",
|
|
||||||
"SERVICE TEAM"
|
|
||||||
]);
|
|
||||||
|
|
||||||
const ACTION_BAR_SELECTOR = "nav.floating--action-bar";
|
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT, {
|
||||||
const INJECTED_ATTR = "data-wwcompanion-default-task";
|
acceptNode: (node) => {
|
||||||
const DEFAULT_TASK_LABEL = "Default WWCompanion Task";
|
if (node.innerText.includes(normalized)) {
|
||||||
|
return NodeFilter.FILTER_ACCEPT;
|
||||||
function isJobPostingOpen() {
|
}
|
||||||
return document.getElementsByClassName("modal__content").length > 0;
|
return NodeFilter.FILTER_REJECT;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sanitizePostingText(text) {
|
|
||||||
let cleaned = text.replaceAll("fiber_manual_record", "");
|
|
||||||
const lines = cleaned.split(/\r?\n/);
|
|
||||||
const filtered = lines.filter((line) => {
|
|
||||||
const trimmed = line.trim();
|
|
||||||
if (!trimmed) return true;
|
|
||||||
return !HEADER_LINES.has(trimmed.toUpperCase());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
cleaned = filtered.join("\n");
|
let deepest = null;
|
||||||
cleaned = cleaned.replace(/[ \t]+/g, " ");
|
let node = walker.nextNode();
|
||||||
cleaned = cleaned.replace(/\n{3,}/g, "\n\n");
|
while (node) {
|
||||||
return cleaned.trim();
|
deepest = node;
|
||||||
}
|
node = walker.nextNode();
|
||||||
|
|
||||||
function buildDefaultTaskButton(templateButton) {
|
|
||||||
const button = document.createElement("button");
|
|
||||||
button.type = "button";
|
|
||||||
button.textContent = DEFAULT_TASK_LABEL;
|
|
||||||
button.className = templateButton.className;
|
|
||||||
button.setAttribute(INJECTED_ATTR, "true");
|
|
||||||
button.setAttribute("aria-label", DEFAULT_TASK_LABEL);
|
|
||||||
button.addEventListener("click", () => {
|
|
||||||
document.dispatchEvent(new CustomEvent("WWCOMPANION_RUN_DEFAULT_TASK"));
|
|
||||||
chrome.runtime.sendMessage({ type: "RUN_DEFAULT_TASK" });
|
|
||||||
});
|
|
||||||
return button;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getActionBars() {
|
|
||||||
return [...document.querySelectorAll(ACTION_BAR_SELECTOR)];
|
|
||||||
}
|
|
||||||
|
|
||||||
function getActionBarButtonCount(bar) {
|
|
||||||
return bar.querySelectorAll(`button:not([${INJECTED_ATTR}])`).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectTargetActionBar(bars) {
|
|
||||||
if (!bars.length) return null;
|
|
||||||
let best = bars[0];
|
|
||||||
let bestCount = getActionBarButtonCount(best);
|
|
||||||
for (const bar of bars.slice(1)) {
|
|
||||||
const count = getActionBarButtonCount(bar);
|
|
||||||
if (count > bestCount) {
|
|
||||||
best = bar;
|
|
||||||
bestCount = count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return best;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureDefaultTaskButton() {
|
|
||||||
const bars = getActionBars();
|
|
||||||
if (!bars.length) return;
|
|
||||||
|
|
||||||
if (!isJobPostingOpen()) {
|
|
||||||
for (const bar of bars) {
|
|
||||||
const injected = bar.querySelector(`[${INJECTED_ATTR}]`);
|
|
||||||
if (injected) injected.remove();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const toolbar = selectTargetActionBar(bars);
|
return deepest;
|
||||||
if (!toolbar) return;
|
}
|
||||||
|
|
||||||
for (const bar of bars) {
|
function createToolbar(presets, position = "bottom-right") {
|
||||||
if (bar === toolbar) continue;
|
let toolbar = document.getElementById("sitecompanion-toolbar");
|
||||||
const injected = bar.querySelector(`[${INJECTED_ATTR}]`);
|
if (toolbar) toolbar.remove();
|
||||||
if (injected) injected.remove();
|
|
||||||
|
toolbar = document.createElement("div");
|
||||||
|
toolbar.id = "sitecompanion-toolbar";
|
||||||
|
|
||||||
|
let posStyle = "";
|
||||||
|
switch (position) {
|
||||||
|
case "top-left":
|
||||||
|
posStyle = "top: 20px; left: 20px;";
|
||||||
|
break;
|
||||||
|
case "top-right":
|
||||||
|
posStyle = "top: 20px; right: 20px;";
|
||||||
|
break;
|
||||||
|
case "bottom-left":
|
||||||
|
posStyle = "bottom: 20px; left: 20px;";
|
||||||
|
break;
|
||||||
|
case "bottom-center":
|
||||||
|
posStyle = "bottom: 20px; left: 50%; transform: translateX(-50%);";
|
||||||
|
break;
|
||||||
|
case "bottom-right":
|
||||||
|
default:
|
||||||
|
posStyle = "bottom: 20px; right: 20px;";
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const existing = toolbar.querySelector(`[${INJECTED_ATTR}]`);
|
toolbar.style.cssText = `
|
||||||
if (existing) return;
|
position: fixed;
|
||||||
|
${posStyle}
|
||||||
|
background: #fff7ec;
|
||||||
|
border: 1px solid #e4d6c5;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 8px;
|
||||||
|
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
|
||||||
|
z-index: 999999;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
font-family: system-ui, sans-serif;
|
||||||
|
`;
|
||||||
|
|
||||||
const templateButton = toolbar.querySelector("button");
|
if (!presets || !presets.length) {
|
||||||
if (!templateButton) return;
|
const label = document.createElement("span");
|
||||||
|
label.textContent = "SiteCompanion";
|
||||||
const button = buildDefaultTaskButton(templateButton);
|
label.style.fontSize = "12px";
|
||||||
const firstChild = toolbar.firstElementChild;
|
label.style.color = "#6b5f55";
|
||||||
if (firstChild) {
|
toolbar.appendChild(label);
|
||||||
toolbar.insertBefore(button, firstChild);
|
|
||||||
} else {
|
} else {
|
||||||
toolbar.appendChild(button);
|
for (const preset of presets) {
|
||||||
|
const btn = document.createElement("button");
|
||||||
|
btn.textContent = preset.name;
|
||||||
|
btn.style.cssText = `
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: #b14d2b;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
`;
|
||||||
|
btn.addEventListener("click", () => {
|
||||||
|
chrome.runtime.sendMessage({ type: "RUN_PRESET", presetId: preset.id });
|
||||||
|
});
|
||||||
|
toolbar.appendChild(btn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.appendChild(toolbar);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function matchUrl(url, pattern) {
|
||||||
|
|
||||||
|
if (!pattern) return false;
|
||||||
|
|
||||||
|
const regex = new RegExp("^" + pattern.split("*").join(".*") + "$");
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
const urlObj = new URL(url);
|
||||||
|
|
||||||
|
const target = urlObj.hostname + urlObj.pathname;
|
||||||
|
|
||||||
|
return regex.test(target);
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
|
||||||
|
return false;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async function refreshToolbar() {
|
||||||
|
const { sites = [], presets = [], toolbarPosition = "bottom-right" } = await chrome.storage.local.get(["sites", "presets", "toolbarPosition"]);
|
||||||
|
const currentUrl = window.location.href;
|
||||||
|
const site = sites.find(s => matchUrl(currentUrl, s.urlPattern));
|
||||||
|
|
||||||
|
if (site) {
|
||||||
|
createToolbar(presets, toolbarPosition);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractPostingText() {
|
|
||||||
const contents = [...document.getElementsByClassName("modal__content")];
|
|
||||||
if (!contents.length) {
|
|
||||||
return { ok: false, error: "No modal content found on this page." };
|
|
||||||
}
|
|
||||||
|
|
||||||
// WaterlooWorks renders multiple modal containers; choose the longest visible text block.
|
|
||||||
const el = contents.reduce((best, cur) =>
|
|
||||||
cur.innerText.length > best.innerText.length ? cur : best
|
|
||||||
);
|
|
||||||
|
|
||||||
const rawText = el.innerText;
|
|
||||||
const sanitized = sanitizePostingText(rawText);
|
|
||||||
|
|
||||||
return { ok: true, rawText, sanitized };
|
|
||||||
}
|
|
||||||
|
|
||||||
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
|
||||||
if (message?.type !== "EXTRACT_POSTING") return;
|
|
||||||
|
|
||||||
const result = extractPostingText();
|
|
||||||
sendResponse(result);
|
|
||||||
});
|
|
||||||
|
|
||||||
chrome.runtime.onConnect.addListener((port) => {
|
|
||||||
if (port.name !== "wwcompanion-keepalive") return;
|
|
||||||
port.onDisconnect.addListener(() => {});
|
|
||||||
});
|
|
||||||
|
|
||||||
const observer = new MutationObserver(() => {
|
const observer = new MutationObserver(() => {
|
||||||
ensureDefaultTaskButton();
|
|
||||||
|
// refreshToolbar(); // Debounce this?
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
observer.observe(document.documentElement, { childList: true, subtree: true });
|
|
||||||
ensureDefaultTaskButton();
|
|
||||||
|
// observer.observe(document.documentElement, { childList: true, subtree: true });
|
||||||
|
|
||||||
|
refreshToolbar();
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "WWCompanion",
|
"name": "SiteCompanion",
|
||||||
"version": "0.3.1",
|
"version": "0.4.0",
|
||||||
"description": "AI companion for WaterlooWorks job postings.",
|
"description": "AI companion for site-bound text extraction and tasks.",
|
||||||
"permissions": ["storage", "activeTab"],
|
"permissions": ["storage", "activeTab"],
|
||||||
"host_permissions": ["https://waterlooworks.uwaterloo.ca/*"],
|
"host_permissions": ["<all_urls>"],
|
||||||
"action": {
|
"action": {
|
||||||
"default_title": "WWCompanion",
|
"default_title": "SiteCompanion",
|
||||||
"default_popup": "popup.html"
|
"default_popup": "popup.html"
|
||||||
},
|
},
|
||||||
"background": {
|
"background": {
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
},
|
},
|
||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
{
|
{
|
||||||
"matches": ["https://waterlooworks.uwaterloo.ca/*"],
|
"matches": ["<all_urls>"],
|
||||||
"js": ["content.js"]
|
"js": ["content.js"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -158,7 +158,63 @@ select {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hidden {
|
.hidden {
|
||||||
display: none;
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-body {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-body p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea#partialTextPaste {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--input-bg);
|
||||||
|
color: var(--input-fg);
|
||||||
|
font-size: 12px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-box {
|
||||||
|
max-height: 120px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--code-bg);
|
||||||
|
font-size: 11px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-info {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--muted);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--input-bg);
|
||||||
|
color: var(--input-fg);
|
||||||
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
|
|||||||
@@ -3,18 +3,46 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>WWCompanion</title>
|
<title>SiteCompanion</title>
|
||||||
<link rel="stylesheet" href="popup.css" />
|
<link rel="stylesheet" href="popup.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="title-block">
|
<header class="title-block">
|
||||||
<div class="title-line">
|
<div class="title-line">
|
||||||
<span class="title">WWCompanion</span>
|
<span class="title">SiteCompanion</span>
|
||||||
<span class="subtitle">AI companion for WaterlooWorks.</span>
|
<span class="subtitle">AI companion for site-bound tasks.</span>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="panel">
|
<section id="unknownSiteState" class="panel hidden">
|
||||||
|
<div class="state-body">
|
||||||
|
<p>This site is not recognized. Paste partial text from the page you want to extract:</p>
|
||||||
|
<textarea id="partialTextPaste" rows="4" placeholder="Paste some text here..."></textarea>
|
||||||
|
<div class="row">
|
||||||
|
<button id="extractFullBtn" class="accent">Try Extracting Full Text</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="extractionReviewState" class="panel hidden">
|
||||||
|
<div class="state-body">
|
||||||
|
<p>Review extracted text:</p>
|
||||||
|
<div id="extractedPreview" class="preview-box"></div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="urlPatternInput">URL Match Pattern</label>
|
||||||
|
<input type="text" id="urlPatternInput" placeholder="example.com/*" />
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<button id="retryExtractBtn" class="ghost">Retry</button>
|
||||||
|
<button id="confirmSiteBtn" class="accent">Confirm</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="normalExecutionState" class="panel">
|
||||||
|
<div class="workspace-info">
|
||||||
|
Workspace: <span id="currentWorkspaceName">Global</span>
|
||||||
|
</div>
|
||||||
<div class="controls-block">
|
<div class="controls-block">
|
||||||
<div class="config-block">
|
<div class="config-block">
|
||||||
<div class="selector-row">
|
<div class="selector-row">
|
||||||
@@ -38,8 +66,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
<span id="postingCount">Posting: 0 chars</span>
|
<span id="postingCount">Site Text: 0 chars</span>
|
||||||
<span id="promptCount">Prompt: 0 chars</span>
|
<span id="promptCount">Task: 0 chars</span>
|
||||||
<span id="status" class="status">Idle</span>
|
<span id="status" class="status">Idle</span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -18,11 +18,26 @@ 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";
|
||||||
|
|
||||||
|
const unknownSiteState = document.getElementById("unknownSiteState");
|
||||||
|
const extractionReviewState = document.getElementById("extractionReviewState");
|
||||||
|
const normalExecutionState = document.getElementById("normalExecutionState");
|
||||||
|
const partialTextPaste = document.getElementById("partialTextPaste");
|
||||||
|
const extractFullBtn = document.getElementById("extractFullBtn");
|
||||||
|
const extractedPreview = document.getElementById("extractedPreview");
|
||||||
|
const urlPatternInput = document.getElementById("urlPatternInput");
|
||||||
|
const retryExtractBtn = document.getElementById("retryExtractBtn");
|
||||||
|
const confirmSiteBtn = document.getElementById("confirmSiteBtn");
|
||||||
|
const currentWorkspaceName = document.getElementById("currentWorkspaceName");
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
postingText: "",
|
siteText: "",
|
||||||
tasks: [],
|
tasks: [],
|
||||||
envs: [],
|
envs: [],
|
||||||
profiles: [],
|
profiles: [],
|
||||||
|
sites: [],
|
||||||
|
workspaces: [],
|
||||||
|
currentSite: null,
|
||||||
|
currentWorkspace: null,
|
||||||
port: null,
|
port: null,
|
||||||
isAnalyzing: false,
|
isAnalyzing: false,
|
||||||
outputRaw: "",
|
outputRaw: "",
|
||||||
@@ -32,21 +47,65 @@ const state = {
|
|||||||
selectedProfileId: ""
|
selectedProfileId: ""
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function switchState(stateName) {
|
||||||
|
unknownSiteState.classList.add("hidden");
|
||||||
|
extractionReviewState.classList.add("hidden");
|
||||||
|
normalExecutionState.classList.add("hidden");
|
||||||
|
|
||||||
|
if (stateName === "unknown") {
|
||||||
|
unknownSiteState.classList.remove("hidden");
|
||||||
|
} else if (stateName === "review") {
|
||||||
|
extractionReviewState.classList.remove("hidden");
|
||||||
|
} else if (stateName === "normal") {
|
||||||
|
normalExecutionState.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
await chrome.storage.local.set({ lastPopupState: stateName });
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchUrl(url, pattern) {
|
||||||
|
if (!pattern) return false;
|
||||||
|
const regex = new RegExp("^" + pattern.split("*").join(".*") + "$");
|
||||||
|
try {
|
||||||
|
const urlObj = new URL(url);
|
||||||
|
const target = urlObj.hostname + urlObj.pathname;
|
||||||
|
return regex.test(target);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function detectSite(url) {
|
||||||
|
const { sites = [], workspaces = [] } = await getStorage(["sites", "workspaces"]);
|
||||||
|
state.sites = sites;
|
||||||
|
state.workspaces = workspaces;
|
||||||
|
|
||||||
|
const site = sites.find(s => matchUrl(url, s.urlPattern));
|
||||||
|
if (site) {
|
||||||
|
state.currentSite = site;
|
||||||
|
state.currentWorkspace = workspaces.find(w => w.id === site.workspaceId) || { name: "Global", id: "global" };
|
||||||
|
currentWorkspaceName.textContent = state.currentWorkspace.name;
|
||||||
|
switchState("normal");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
switchState("unknown");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
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(profileText, taskText, siteText) {
|
||||||
const header = resumeType === "Profile" ? "=== PROFILE ===" : "=== RESUME ===";
|
|
||||||
return [
|
return [
|
||||||
header,
|
"=== Profile ===",
|
||||||
resume || "",
|
profileText || "",
|
||||||
"",
|
"",
|
||||||
"=== TASK ===",
|
"=== Task ===",
|
||||||
task || "",
|
taskText || "",
|
||||||
"",
|
"",
|
||||||
"=== JOB POSTING ===",
|
"=== Site Text ===",
|
||||||
posting || ""
|
siteText || ""
|
||||||
].join("\n");
|
].join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,12 +320,12 @@ function setAnalyzing(isAnalyzing) {
|
|||||||
updateProfileSelectState();
|
updateProfileSelectState();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updatePostingCount() {
|
function updateSiteTextCount() {
|
||||||
postingCountEl.textContent = `Posting: ${state.postingText.length} chars`;
|
postingCountEl.textContent = `Site Text: ${state.siteText.length} chars`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updatePromptCount(count) {
|
function updatePromptCount(count) {
|
||||||
promptCountEl.textContent = `Prompt: ${count} chars`;
|
promptCountEl.textContent = `Task: ${count} chars`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTasks(tasks) {
|
function renderTasks(tasks) {
|
||||||
@@ -399,14 +458,6 @@ async function persistSelections() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function isWaterlooWorksUrl(url) {
|
|
||||||
try {
|
|
||||||
return new URL(url).hostname === "waterlooworks.uwaterloo.ca";
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendToActiveTab(message) {
|
function sendToActiveTab(message) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
||||||
@@ -416,17 +467,12 @@ function sendToActiveTab(message) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isWaterlooWorksUrl(tab.url || "")) {
|
|
||||||
reject(new Error("Open waterlooworks.uwaterloo.ca to use this."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
chrome.tabs.sendMessage(tab.id, message, (response) => {
|
chrome.tabs.sendMessage(tab.id, message, (response) => {
|
||||||
const error = chrome.runtime.lastError;
|
const error = chrome.runtime.lastError;
|
||||||
if (error) {
|
if (error) {
|
||||||
const msg =
|
const msg =
|
||||||
error.message && error.message.includes("Receiving end does not exist")
|
error.message && error.message.includes("Receiving end does not exist")
|
||||||
? "Couldn't reach the page. Try refreshing WaterlooWorks and retry."
|
? "Couldn't reach the page. Try refreshing and retry."
|
||||||
: error.message;
|
: error.message;
|
||||||
reject(new Error(msg));
|
reject(new Error(msg));
|
||||||
return;
|
return;
|
||||||
@@ -486,6 +532,22 @@ function ensurePort() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadConfig() {
|
async function loadConfig() {
|
||||||
|
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||||
|
const currentUrl = tabs[0]?.url || "";
|
||||||
|
|
||||||
|
const { lastPopupState } = await getStorage(["lastPopupState"]);
|
||||||
|
await detectSite(currentUrl);
|
||||||
|
if (lastPopupState && lastPopupState !== "unknown") {
|
||||||
|
// If we had a state like 'review', we might want to stay there,
|
||||||
|
// but detectSite might have switched to 'normal' if it matched.
|
||||||
|
// AGENTS.md says popup state must be persisted.
|
||||||
|
if (state.currentSite && lastPopupState === "normal") {
|
||||||
|
await switchState("normal");
|
||||||
|
} else if (!state.currentSite && (lastPopupState === "unknown" || lastPopupState === "review")) {
|
||||||
|
await switchState(lastPopupState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const stored = await getStorage([
|
const stored = await getStorage([
|
||||||
"tasks",
|
"tasks",
|
||||||
"envConfigs",
|
"envConfigs",
|
||||||
@@ -548,40 +610,40 @@ async function loadTheme() {
|
|||||||
async function handleExtract() {
|
async function handleExtract() {
|
||||||
setStatus("Extracting...");
|
setStatus("Extracting...");
|
||||||
try {
|
try {
|
||||||
const response = await sendToActiveTab({ type: "EXTRACT_POSTING" });
|
const response = await sendToActiveTab({ type: "EXTRACT_FULL" });
|
||||||
if (!response?.ok) {
|
if (!response?.ok) {
|
||||||
setStatus(response?.error || "No posting detected.");
|
setStatus(response?.error || "No text detected.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
state.postingText = response.sanitized || "";
|
state.siteText = response.extracted || "";
|
||||||
updatePostingCount();
|
updateSiteTextCount();
|
||||||
updatePromptCount(0);
|
updatePromptCount(0);
|
||||||
setStatus("Posting extracted.");
|
setStatus("Text extracted.");
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setStatus(error.message || "Unable to extract posting.");
|
setStatus(error.message || "Unable to extract text.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleAnalyze() {
|
async function handleAnalyze() {
|
||||||
if (!state.postingText) {
|
if (!state.siteText) {
|
||||||
setStatus("Extract a job posting first.");
|
setStatus("Extract site text first.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
|
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||||
const tab = tabs[0];
|
const tab = tabs[0];
|
||||||
if (!tab?.url || !isWaterlooWorksUrl(tab.url)) {
|
if (!tab?.url) {
|
||||||
setStatus("Open waterlooworks.uwaterloo.ca to run tasks.");
|
setStatus("Open a page to run tasks.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const taskId = taskSelect.value;
|
const taskId = taskSelect.value;
|
||||||
const task = state.tasks.find((item) => item.id === taskId);
|
const task = state.tasks.find((item) => item.id === taskId);
|
||||||
if (!task) {
|
if (!task) {
|
||||||
setStatus("Select a task prompt.");
|
setStatus("Select a task.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -640,8 +702,7 @@ async function handleAnalyze() {
|
|||||||
const activeProfile =
|
const activeProfile =
|
||||||
resolvedProfiles.find((entry) => entry.id === selectedProfileId) ||
|
resolvedProfiles.find((entry) => entry.id === selectedProfileId) ||
|
||||||
resolvedProfiles[0];
|
resolvedProfiles[0];
|
||||||
const resumeText = activeProfile?.text || resume || "";
|
const profileText = 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 || "";
|
||||||
@@ -688,10 +749,9 @@ async function handleAnalyze() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const promptText = buildUserMessage(
|
const promptText = buildUserMessage(
|
||||||
resumeText,
|
profileText,
|
||||||
resumeType,
|
|
||||||
task.text || "",
|
task.text || "",
|
||||||
state.postingText
|
state.siteText
|
||||||
);
|
);
|
||||||
updatePromptCount(promptText.length);
|
updatePromptCount(promptText.length);
|
||||||
|
|
||||||
@@ -713,10 +773,9 @@ async function handleAnalyze() {
|
|||||||
apiKeyPrefix: resolvedApiKeyPrefix,
|
apiKeyPrefix: resolvedApiKeyPrefix,
|
||||||
model: resolvedModel,
|
model: resolvedModel,
|
||||||
systemPrompt: resolvedSystemPrompt,
|
systemPrompt: resolvedSystemPrompt,
|
||||||
resume: resumeText,
|
profileText,
|
||||||
resumeType,
|
|
||||||
taskText: task.text || "",
|
taskText: task.text || "",
|
||||||
postingText: state.postingText,
|
siteText: state.siteText,
|
||||||
tabId: tab.id
|
tabId: tab.id
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -769,6 +828,81 @@ function handleCopyRaw() {
|
|||||||
void copyTextToClipboard(text, "Markdown");
|
void copyTextToClipboard(text, "Markdown");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
partialTextPaste.addEventListener("input", async () => {
|
||||||
|
const text = partialTextPaste.value.trim();
|
||||||
|
if (text.length < 5) return;
|
||||||
|
|
||||||
|
setStatus("Finding scope...");
|
||||||
|
try {
|
||||||
|
const response = await sendToActiveTab({ type: "FIND_SCOPE", text });
|
||||||
|
if (response?.ok) {
|
||||||
|
state.siteText = response.extracted;
|
||||||
|
extractedPreview.textContent = state.siteText;
|
||||||
|
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||||
|
const url = new URL(tabs[0].url);
|
||||||
|
urlPatternInput.value = url.hostname + url.pathname + "*";
|
||||||
|
switchState("review");
|
||||||
|
setStatus("Review extraction.");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setStatus("Error finding scope.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
extractFullBtn.addEventListener("click", async () => {
|
||||||
|
setStatus("Extracting full text...");
|
||||||
|
try {
|
||||||
|
const response = await sendToActiveTab({ type: "EXTRACT_FULL" });
|
||||||
|
if (response?.ok) {
|
||||||
|
state.siteText = response.extracted;
|
||||||
|
extractedPreview.textContent = state.siteText;
|
||||||
|
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||||
|
const url = new URL(tabs[0].url);
|
||||||
|
urlPatternInput.value = url.hostname + url.pathname + "*";
|
||||||
|
switchState("review");
|
||||||
|
setStatus("Review extraction.");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setStatus("Error extracting text.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
retryExtractBtn.addEventListener("click", () => {
|
||||||
|
switchState("unknown");
|
||||||
|
partialTextPaste.value = "";
|
||||||
|
setStatus("Ready.");
|
||||||
|
});
|
||||||
|
|
||||||
|
confirmSiteBtn.addEventListener("click", async () => {
|
||||||
|
const pattern = urlPatternInput.value.trim();
|
||||||
|
if (!pattern) {
|
||||||
|
setStatus("Enter a URL pattern.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// AGENTS.md: No URL pattern may be a substring of another.
|
||||||
|
const conflict = state.sites.find(s => s.urlPattern.includes(pattern) || pattern.includes(s.urlPattern));
|
||||||
|
if (conflict) {
|
||||||
|
setStatus("URL pattern conflict.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newSite = {
|
||||||
|
id: `site-${Date.now()}`,
|
||||||
|
urlPattern: pattern,
|
||||||
|
workspaceId: "global" // Default to global for now
|
||||||
|
};
|
||||||
|
|
||||||
|
state.sites.push(newSite);
|
||||||
|
await chrome.storage.local.set({ sites: state.sites });
|
||||||
|
state.currentSite = newSite;
|
||||||
|
state.currentWorkspace = { name: "Global", id: "global" };
|
||||||
|
currentWorkspaceName.textContent = "Global";
|
||||||
|
switchState("normal");
|
||||||
|
updateSiteTextCount();
|
||||||
|
setStatus("Site saved.");
|
||||||
|
});
|
||||||
|
|
||||||
runBtn.addEventListener("click", handleExtractAndAnalyze);
|
runBtn.addEventListener("click", handleExtractAndAnalyze);
|
||||||
abortBtn.addEventListener("click", handleAbort);
|
abortBtn.addEventListener("click", handleAbort);
|
||||||
settingsBtn.addEventListener("click", () => chrome.runtime.openOptionsPage());
|
settingsBtn.addEventListener("click", () => chrome.runtime.openOptionsPage());
|
||||||
@@ -788,7 +922,7 @@ profileSelect.addEventListener("change", () => {
|
|||||||
void persistSelections();
|
void persistSelections();
|
||||||
});
|
});
|
||||||
|
|
||||||
updatePostingCount();
|
updateSiteTextCount();
|
||||||
updatePromptCount(0);
|
updatePromptCount(0);
|
||||||
renderOutput();
|
renderOutput();
|
||||||
setAnalyzing(false);
|
setAnalyzing(false);
|
||||||
|
|||||||
@@ -76,22 +76,77 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.toc-links {
|
.toc-links {
|
||||||
display: grid;
|
display: block;
|
||||||
gap: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.toc a {
|
.toc ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
display: grid;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-sub {
|
||||||
|
padding-left: 12px !important;
|
||||||
|
margin-top: 2px !important;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-sub.expanded {
|
||||||
|
display: grid !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px 6px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--ink);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-item:hover {
|
||||||
|
background: var(--card-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-item .toc-caret {
|
||||||
|
display: inline-block;
|
||||||
|
width: 10px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 10px;
|
||||||
|
transition: transform 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-item.expanded .toc-caret {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc li a {
|
||||||
|
display: block;
|
||||||
|
padding: 4px 6px;
|
||||||
|
border-radius: 6px;
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
padding: 4px 6px;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.toc a:hover {
|
.toc li a:hover {
|
||||||
background: var(--card-bg);
|
background: var(--card-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toc-sub li a {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-sub li a:hover {
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-errors {
|
.sidebar-errors {
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
@@ -139,8 +194,9 @@ body {
|
|||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
padding: 16px;
|
padding: 0;
|
||||||
box-shadow: var(--panel-shadow);
|
box-shadow: var(--panel-shadow);
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-summary {
|
.panel-summary {
|
||||||
@@ -150,40 +206,58 @@ body {
|
|||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-summary::-webkit-details-marker {
|
/* Restore native marker but keep list-style: none from above? No, remove list-style: none to show marker. */
|
||||||
display: none;
|
/* 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 {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin: 0;
|
||||||
|
/* display: list-item; default */
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-caret {
|
/* Need to align H2. */
|
||||||
display: inline-flex;
|
.panel-summary h2 {
|
||||||
align-items: center;
|
|
||||||
width: 16px;
|
|
||||||
justify-content: center;
|
|
||||||
color: var(--muted);
|
|
||||||
font-weight: 700;
|
|
||||||
font-family: "Segoe UI Symbol", "Apple Symbols", system-ui, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-caret .caret-open {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel[open] .panel-caret .caret-open {
|
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel[open] .panel-caret .caret-closed {
|
.sub-panel .panel-summary {
|
||||||
display: none;
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-panel .panel-body {
|
||||||
|
padding: 0 12px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-body {
|
.panel-body {
|
||||||
margin-top: 12px;
|
margin-top: 0;
|
||||||
|
padding: 0 16px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel[open] .panel-summary {
|
.panel[open] .panel-summary {
|
||||||
margin-bottom: 6px;
|
margin-bottom: 0;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-panel {
|
||||||
|
box-shadow: none;
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-panel:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>WWCompanion Settings</title>
|
<title>SiteCompanion Settings</title>
|
||||||
<link rel="stylesheet" href="settings.css" />
|
<link rel="stylesheet" href="settings.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="title-block">
|
<header class="title-block">
|
||||||
<div class="title">WWCompanion Settings</div>
|
<div class="title">SiteCompanion Settings</div>
|
||||||
<div class="subtitle">Configure prompts, resume, and API access</div>
|
<div class="subtitle">Configure workspaces, tasks, and API access</div>
|
||||||
</header>
|
</header>
|
||||||
<div class="page-bar">
|
<div class="page-bar">
|
||||||
<div id="status" class="status"></div>
|
<div id="status" class="status"></div>
|
||||||
@@ -18,27 +18,51 @@
|
|||||||
|
|
||||||
<div class="settings-layout">
|
<div class="settings-layout">
|
||||||
<nav class="toc" aria-label="Settings table of contents">
|
<nav class="toc" aria-label="Settings table of contents">
|
||||||
<div class="toc-heading">WWCompanion Settings</div>
|
<div class="toc-heading">SiteCompanion Settings</div>
|
||||||
<button id="saveBtnSidebar" class="accent" type="button">Save Settings</button>
|
<button id="saveBtnSidebar" class="accent" type="button">Save Settings</button>
|
||||||
<div id="statusSidebar" class="status"> </div>
|
<div id="statusSidebar" class="status"> </div>
|
||||||
<div class="toc-title">Sections</div>
|
<div class="toc-title">Sections</div>
|
||||||
<div class="toc-links">
|
<div class="toc-links">
|
||||||
<a href="#appearance-panel">Appearance</a>
|
<ul>
|
||||||
<a href="#api-keys-panel">API Keys</a>
|
<li>
|
||||||
<a href="#api-panel">API</a>
|
<div class="toc-item">
|
||||||
<a href="#environment-panel">Environment</a>
|
<span class="toc-caret">▸</span> <a href="#global-config-panel">Global Configuration</a>
|
||||||
<a href="#profiles-panel">My Profiles</a>
|
</div>
|
||||||
<a href="#tasks-panel">Task Presets</a>
|
<ul class="toc-sub hidden">
|
||||||
|
<li><a href="#appearance-panel">Appearance</a></li>
|
||||||
|
<li><a href="#api-keys-panel">API Keys</a></li>
|
||||||
|
<li><a href="#api-panel">API</a></li>
|
||||||
|
<li><a href="#environment-panel">Environments</a></li>
|
||||||
|
<li><a href="#profiles-panel">Profiles</a></li>
|
||||||
|
<li><a href="#tasks-panel">Tasks</a></li>
|
||||||
|
<li><a href="#presets-panel">Presets</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div class="toc-item">
|
||||||
|
<span class="toc-caret">▸</span> <a href="#workspaces-panel">Workspaces</a>
|
||||||
|
</div>
|
||||||
|
<ul class="toc-sub hidden" id="toc-workspaces-list"></ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div class="toc-item">
|
||||||
|
<span class="toc-caret">▸</span> <a href="#sites-panel">Sites</a>
|
||||||
|
</div>
|
||||||
|
<ul class="toc-sub hidden" id="toc-sites-list"></ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div id="sidebarErrors" class="sidebar-errors hidden"></div>
|
<div id="sidebarErrors" class="sidebar-errors hidden"></div>
|
||||||
</nav>
|
</nav>
|
||||||
<main class="settings-main">
|
<main class="settings-main">
|
||||||
<details class="panel" id="appearance-panel">
|
<details class="panel" id="global-config-panel" open>
|
||||||
|
<summary class="panel-summary">
|
||||||
|
<h2>Global Configuration</h2>
|
||||||
|
</summary>
|
||||||
|
<div class="panel-body">
|
||||||
|
<!-- Appearance -->
|
||||||
|
<details class="panel sub-panel" id="appearance-panel">
|
||||||
<summary class="panel-summary">
|
<summary class="panel-summary">
|
||||||
<span class="panel-caret" aria-hidden="true">
|
|
||||||
<span class="caret-closed">▸</span>
|
|
||||||
<span class="caret-open">▾</span>
|
|
||||||
</span>
|
|
||||||
<h2>Appearance</h2>
|
<h2>Appearance</h2>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
@@ -50,15 +74,22 @@
|
|||||||
<option value="dark">Dark</option>
|
<option value="dark">Dark</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="toolbarPositionSelect">Toolbar Position</label>
|
||||||
|
<select id="toolbarPositionSelect">
|
||||||
|
<option value="bottom-right">Bottom Right</option>
|
||||||
|
<option value="bottom-left">Bottom Left</option>
|
||||||
|
<option value="top-right">Top Right</option>
|
||||||
|
<option value="top-left">Top Left</option>
|
||||||
|
<option value="bottom-center">Bottom Center</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details class="panel" id="api-keys-panel">
|
<!-- API Keys -->
|
||||||
|
<details class="panel sub-panel" id="api-keys-panel">
|
||||||
<summary class="panel-summary">
|
<summary class="panel-summary">
|
||||||
<span class="panel-caret" aria-hidden="true">
|
|
||||||
<span class="caret-closed">▸</span>
|
|
||||||
<span class="caret-open">▾</span>
|
|
||||||
</span>
|
|
||||||
<h2>API KEYS</h2>
|
<h2>API KEYS</h2>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
@@ -70,12 +101,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details class="panel" id="api-panel">
|
<!-- API -->
|
||||||
|
<details class="panel sub-panel" id="api-panel">
|
||||||
<summary class="panel-summary">
|
<summary class="panel-summary">
|
||||||
<span class="panel-caret" aria-hidden="true">
|
|
||||||
<span class="caret-closed">▸</span>
|
|
||||||
<span class="caret-open">▾</span>
|
|
||||||
</span>
|
|
||||||
<h2>API</h2>
|
<h2>API</h2>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
@@ -89,35 +117,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details class="panel" id="environment-panel">
|
<!-- Envs -->
|
||||||
|
<details class="panel sub-panel" id="environment-panel">
|
||||||
<summary class="panel-summary">
|
<summary class="panel-summary">
|
||||||
<span class="panel-caret" aria-hidden="true">
|
|
||||||
<span class="caret-closed">▸</span>
|
|
||||||
<span class="caret-open">▾</span>
|
|
||||||
</span>
|
|
||||||
<div class="row-title">
|
<div class="row-title">
|
||||||
<h2>Environment</h2>
|
<h2>ENVIRONMENTS</h2>
|
||||||
<span class="hint hint-accent">API configuration and system prompt go here</span>
|
<span class="hint hint-accent">Baseline environments</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="addEnvConfigBtn" class="ghost" type="button">Add Config</button>
|
<button id="addEnvConfigBtn" class="ghost" type="button">Add Env</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="envConfigs" class="env-configs"></div>
|
<div id="envConfigs" class="env-configs"></div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details class="panel" id="profiles-panel">
|
<!-- Profiles -->
|
||||||
|
<details class="panel sub-panel" id="profiles-panel">
|
||||||
<summary class="panel-summary">
|
<summary class="panel-summary">
|
||||||
<span class="panel-caret" aria-hidden="true">
|
|
||||||
<span class="caret-closed">▸</span>
|
|
||||||
<span class="caret-open">▾</span>
|
|
||||||
</span>
|
|
||||||
<div class="row-title">
|
<div class="row-title">
|
||||||
<h2>My Profiles</h2>
|
<h2>PROFILES</h2>
|
||||||
<span class="hint hint-accent">Text to your resumes or generic profiles goes here</span>
|
<span class="hint hint-accent">Baseline user contexts</span>
|
||||||
</div>
|
</div>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
@@ -129,15 +151,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details class="panel" id="tasks-panel">
|
<!-- Tasks -->
|
||||||
|
<details class="panel sub-panel" id="tasks-panel">
|
||||||
<summary class="panel-summary">
|
<summary class="panel-summary">
|
||||||
<span class="panel-caret" aria-hidden="true">
|
|
||||||
<span class="caret-closed">▸</span>
|
|
||||||
<span class="caret-open">▾</span>
|
|
||||||
</span>
|
|
||||||
<div class="row-title">
|
<div class="row-title">
|
||||||
<h2>Task Presets</h2>
|
<h2>TASKS</h2>
|
||||||
<span class="hint hint-accent">Top task is the default</span>
|
<span class="hint hint-accent">Baseline execution units</span>
|
||||||
</div>
|
</div>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
@@ -148,6 +167,57 @@
|
|||||||
<div id="tasks" class="tasks"></div>
|
<div id="tasks" class="tasks"></div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
<!-- Presets -->
|
||||||
|
<details class="panel sub-panel" id="presets-panel">
|
||||||
|
<summary class="panel-summary">
|
||||||
|
<div class="row-title">
|
||||||
|
<h2>PRESETS</h2>
|
||||||
|
<span class="hint hint-accent">Toolbar shortcuts</span>
|
||||||
|
</div>
|
||||||
|
</summary>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="row">
|
||||||
|
<div></div>
|
||||||
|
<button id="addPresetBtn" class="ghost" type="button">Add Preset</button>
|
||||||
|
</div>
|
||||||
|
<div id="presets" class="presets"></div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details class="panel" id="workspaces-panel">
|
||||||
|
<summary class="panel-summary">
|
||||||
|
<div class="row-title">
|
||||||
|
<h2>Workspaces</h2>
|
||||||
|
<span class="hint hint-accent">Namespace for sites and resources</span>
|
||||||
|
</div>
|
||||||
|
</summary>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="row">
|
||||||
|
<div></div>
|
||||||
|
<button id="addWorkspaceBtn" class="ghost" type="button">Add Workspace</button>
|
||||||
|
</div>
|
||||||
|
<div id="workspaces" class="workspaces"></div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details class="panel" id="sites-panel">
|
||||||
|
<summary class="panel-summary">
|
||||||
|
<div class="row-title">
|
||||||
|
<h2>Sites</h2>
|
||||||
|
<span class="hint hint-accent">Configure known sites</span>
|
||||||
|
</div>
|
||||||
|
</summary>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="row">
|
||||||
|
<div></div>
|
||||||
|
<button id="addSiteBtn" class="ghost" type="button">Add Site</button>
|
||||||
|
</div>
|
||||||
|
<div id="sites" class="sites"></div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user