gemini handoff to codex
This commit is contained in:
@@ -28,9 +28,9 @@ const DEFAULT_SETTINGS = {
|
||||
model: "gpt-4o-mini",
|
||||
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.",
|
||||
resume: "",
|
||||
tasks: DEFAULT_TASKS,
|
||||
theme: "system"
|
||||
theme: "system",
|
||||
workspaces: []
|
||||
};
|
||||
|
||||
const OUTPUT_STORAGE_KEY = "lastOutput";
|
||||
@@ -54,7 +54,7 @@ function resetAbort() {
|
||||
function openKeepalive(tabId) {
|
||||
if (!tabId || keepalivePort) return;
|
||||
try {
|
||||
keepalivePort = chrome.tabs.connect(tabId, { name: "wwcompanion-keepalive" });
|
||||
keepalivePort = chrome.tabs.connect(tabId, { name: "sitecompanion-keepalive" });
|
||||
keepalivePort.onDisconnect.addListener(() => {
|
||||
keepalivePort = null;
|
||||
});
|
||||
@@ -253,20 +253,17 @@ chrome.runtime.onInstalled.addListener(async () => {
|
||||
{
|
||||
id,
|
||||
name: "Default",
|
||||
text: stored.resume || "",
|
||||
type: "Resume"
|
||||
text: stored.resume || ""
|
||||
}
|
||||
];
|
||||
} else {
|
||||
const normalizedProfiles = stored.profiles.map((profile) => ({
|
||||
...profile,
|
||||
text: profile.text ?? "",
|
||||
type: profile.type === "Profile" ? "Profile" : "Resume"
|
||||
text: profile.text ?? ""
|
||||
}));
|
||||
const needsProfileUpdate = normalizedProfiles.some(
|
||||
(profile, index) =>
|
||||
(profile.text || "") !== (stored.profiles[index]?.text || "") ||
|
||||
(profile.type || "Resume") !== (stored.profiles[index]?.type || "Resume")
|
||||
(profile.text || "") !== (stored.profiles[index]?.text || "")
|
||||
);
|
||||
if (needsProfileUpdate) {
|
||||
updates.profiles = normalizedProfiles;
|
||||
@@ -364,17 +361,16 @@ chrome.runtime.onMessage.addListener((message) => {
|
||||
}
|
||||
});
|
||||
|
||||
function buildUserMessage(resume, resumeType, task, posting) {
|
||||
const header = resumeType === "Profile" ? "=== PROFILE ===" : "=== RESUME ===";
|
||||
function buildUserMessage(profileText, taskText, siteText) {
|
||||
return [
|
||||
header,
|
||||
resume || "",
|
||||
"=== Profile ===",
|
||||
profileText || "",
|
||||
"",
|
||||
"=== TASK ===",
|
||||
task || "",
|
||||
"=== Task ===",
|
||||
taskText || "",
|
||||
"",
|
||||
"=== JOB POSTING ===",
|
||||
posting || ""
|
||||
"=== Site Text ===",
|
||||
siteText || ""
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
@@ -406,10 +402,9 @@ async function handleAnalysisRequest(port, payload, signal) {
|
||||
apiKeyPrefix,
|
||||
model,
|
||||
systemPrompt,
|
||||
resume,
|
||||
resumeType,
|
||||
profileText,
|
||||
taskText,
|
||||
postingText,
|
||||
siteText,
|
||||
tabId
|
||||
} = payload || {};
|
||||
|
||||
@@ -444,21 +439,20 @@ async function handleAnalysisRequest(port, payload, signal) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!postingText) {
|
||||
safePost(port, { type: "ERROR", message: "No job posting text provided." });
|
||||
if (!siteText) {
|
||||
safePost(port, { type: "ERROR", message: "No site text provided." });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!taskText) {
|
||||
safePost(port, { type: "ERROR", message: "No task prompt selected." });
|
||||
safePost(port, { type: "ERROR", message: "No task selected." });
|
||||
return;
|
||||
}
|
||||
|
||||
const userMessage = buildUserMessage(
|
||||
resume,
|
||||
resumeType,
|
||||
profileText,
|
||||
taskText,
|
||||
postingText
|
||||
siteText
|
||||
);
|
||||
|
||||
await chrome.storage.local.set({ [OUTPUT_STORAGE_KEY]: "" });
|
||||
|
||||
@@ -1,140 +1,143 @@
|
||||
const HEADER_LINES = new Set([
|
||||
"OVERVIEW",
|
||||
"PRE-SCREENING",
|
||||
"WORK TERM RATINGS",
|
||||
"JOB POSTING INFORMATION",
|
||||
"APPLICATION INFORMATION",
|
||||
"COMPANY INFORMATION",
|
||||
"SERVICE TEAM"
|
||||
]);
|
||||
function findMinimumScope(text) {
|
||||
if (!text) return null;
|
||||
const normalized = text.trim();
|
||||
if (!normalized) return null;
|
||||
|
||||
const ACTION_BAR_SELECTOR = "nav.floating--action-bar";
|
||||
const INJECTED_ATTR = "data-wwcompanion-default-task";
|
||||
const DEFAULT_TASK_LABEL = "Default WWCompanion Task";
|
||||
|
||||
function isJobPostingOpen() {
|
||||
return document.getElementsByClassName("modal__content").length > 0;
|
||||
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT, {
|
||||
acceptNode: (node) => {
|
||||
if (node.innerText.includes(normalized)) {
|
||||
return NodeFilter.FILTER_ACCEPT;
|
||||
}
|
||||
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");
|
||||
cleaned = cleaned.replace(/[ \t]+/g, " ");
|
||||
cleaned = cleaned.replace(/\n{3,}/g, "\n\n");
|
||||
return cleaned.trim();
|
||||
let deepest = null;
|
||||
let node = walker.nextNode();
|
||||
while (node) {
|
||||
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;
|
||||
return deepest;
|
||||
}
|
||||
|
||||
function getActionBars() {
|
||||
return [...document.querySelectorAll(ACTION_BAR_SELECTOR)];
|
||||
function createToolbar(presets, position = "bottom-right") {
|
||||
let toolbar = document.getElementById("sitecompanion-toolbar");
|
||||
if (toolbar) toolbar.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;
|
||||
}
|
||||
|
||||
function getActionBarButtonCount(bar) {
|
||||
return bar.querySelectorAll(`button:not([${INJECTED_ATTR}])`).length;
|
||||
}
|
||||
toolbar.style.cssText = `
|
||||
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;
|
||||
`;
|
||||
|
||||
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);
|
||||
if (!toolbar) return;
|
||||
|
||||
for (const bar of bars) {
|
||||
if (bar === toolbar) continue;
|
||||
const injected = bar.querySelector(`[${INJECTED_ATTR}]`);
|
||||
if (injected) injected.remove();
|
||||
}
|
||||
|
||||
const existing = toolbar.querySelector(`[${INJECTED_ATTR}]`);
|
||||
if (existing) return;
|
||||
|
||||
const templateButton = toolbar.querySelector("button");
|
||||
if (!templateButton) return;
|
||||
|
||||
const button = buildDefaultTaskButton(templateButton);
|
||||
const firstChild = toolbar.firstElementChild;
|
||||
if (firstChild) {
|
||||
toolbar.insertBefore(button, firstChild);
|
||||
if (!presets || !presets.length) {
|
||||
const label = document.createElement("span");
|
||||
label.textContent = "SiteCompanion";
|
||||
label.style.fontSize = "12px";
|
||||
label.style.color = "#6b5f55";
|
||||
toolbar.appendChild(label);
|
||||
} else {
|
||||
toolbar.appendChild(button);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
chrome.runtime.onConnect.addListener((port) => {
|
||||
if (port.name !== "wwcompanion-keepalive") return;
|
||||
port.onDisconnect.addListener(() => {});
|
||||
});
|
||||
|
||||
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,
|
||||
"name": "WWCompanion",
|
||||
"version": "0.3.1",
|
||||
"description": "AI companion for WaterlooWorks job postings.",
|
||||
"name": "SiteCompanion",
|
||||
"version": "0.4.0",
|
||||
"description": "AI companion for site-bound text extraction and tasks.",
|
||||
"permissions": ["storage", "activeTab"],
|
||||
"host_permissions": ["https://waterlooworks.uwaterloo.ca/*"],
|
||||
"host_permissions": ["<all_urls>"],
|
||||
"action": {
|
||||
"default_title": "WWCompanion",
|
||||
"default_title": "SiteCompanion",
|
||||
"default_popup": "popup.html"
|
||||
},
|
||||
"background": {
|
||||
@@ -15,7 +15,7 @@
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["https://waterlooworks.uwaterloo.ca/*"],
|
||||
"matches": ["<all_urls>"],
|
||||
"js": ["content.js"]
|
||||
}
|
||||
],
|
||||
|
||||
@@ -158,7 +158,63 @@ select {
|
||||
}
|
||||
|
||||
.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 {
|
||||
|
||||
@@ -3,18 +3,46 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>WWCompanion</title>
|
||||
<title>SiteCompanion</title>
|
||||
<link rel="stylesheet" href="popup.css" />
|
||||
</head>
|
||||
<body>
|
||||
<header class="title-block">
|
||||
<div class="title-line">
|
||||
<span class="title">WWCompanion</span>
|
||||
<span class="subtitle">AI companion for WaterlooWorks.</span>
|
||||
<span class="title">SiteCompanion</span>
|
||||
<span class="subtitle">AI companion for site-bound tasks.</span>
|
||||
</div>
|
||||
</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="config-block">
|
||||
<div class="selector-row">
|
||||
@@ -38,8 +66,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<span id="postingCount">Posting: 0 chars</span>
|
||||
<span id="promptCount">Prompt: 0 chars</span>
|
||||
<span id="postingCount">Site Text: 0 chars</span>
|
||||
<span id="promptCount">Task: 0 chars</span>
|
||||
<span id="status" class="status">Idle</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -18,11 +18,26 @@ const LAST_TASK_KEY = "lastSelectedTaskId";
|
||||
const LAST_ENV_KEY = "lastSelectedEnvId";
|
||||
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 = {
|
||||
postingText: "",
|
||||
siteText: "",
|
||||
tasks: [],
|
||||
envs: [],
|
||||
profiles: [],
|
||||
sites: [],
|
||||
workspaces: [],
|
||||
currentSite: null,
|
||||
currentWorkspace: null,
|
||||
port: null,
|
||||
isAnalyzing: false,
|
||||
outputRaw: "",
|
||||
@@ -32,21 +47,65 @@ const state = {
|
||||
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) {
|
||||
return new Promise((resolve) => chrome.storage.local.get(keys, resolve));
|
||||
}
|
||||
|
||||
function buildUserMessage(resume, resumeType, task, posting) {
|
||||
const header = resumeType === "Profile" ? "=== PROFILE ===" : "=== RESUME ===";
|
||||
function buildUserMessage(profileText, taskText, siteText) {
|
||||
return [
|
||||
header,
|
||||
resume || "",
|
||||
"=== Profile ===",
|
||||
profileText || "",
|
||||
"",
|
||||
"=== TASK ===",
|
||||
task || "",
|
||||
"=== Task ===",
|
||||
taskText || "",
|
||||
"",
|
||||
"=== JOB POSTING ===",
|
||||
posting || ""
|
||||
"=== Site Text ===",
|
||||
siteText || ""
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
@@ -261,12 +320,12 @@ function setAnalyzing(isAnalyzing) {
|
||||
updateProfileSelectState();
|
||||
}
|
||||
|
||||
function updatePostingCount() {
|
||||
postingCountEl.textContent = `Posting: ${state.postingText.length} chars`;
|
||||
function updateSiteTextCount() {
|
||||
postingCountEl.textContent = `Site Text: ${state.siteText.length} chars`;
|
||||
}
|
||||
|
||||
function updatePromptCount(count) {
|
||||
promptCountEl.textContent = `Prompt: ${count} chars`;
|
||||
promptCountEl.textContent = `Task: ${count} chars`;
|
||||
}
|
||||
|
||||
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) {
|
||||
return new Promise((resolve, reject) => {
|
||||
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
||||
@@ -416,17 +467,12 @@ function sendToActiveTab(message) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isWaterlooWorksUrl(tab.url || "")) {
|
||||
reject(new Error("Open waterlooworks.uwaterloo.ca to use this."));
|
||||
return;
|
||||
}
|
||||
|
||||
chrome.tabs.sendMessage(tab.id, message, (response) => {
|
||||
const error = chrome.runtime.lastError;
|
||||
if (error) {
|
||||
const msg =
|
||||
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;
|
||||
reject(new Error(msg));
|
||||
return;
|
||||
@@ -486,6 +532,22 @@ function ensurePort() {
|
||||
}
|
||||
|
||||
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([
|
||||
"tasks",
|
||||
"envConfigs",
|
||||
@@ -548,40 +610,40 @@ async function loadTheme() {
|
||||
async function handleExtract() {
|
||||
setStatus("Extracting...");
|
||||
try {
|
||||
const response = await sendToActiveTab({ type: "EXTRACT_POSTING" });
|
||||
const response = await sendToActiveTab({ type: "EXTRACT_FULL" });
|
||||
if (!response?.ok) {
|
||||
setStatus(response?.error || "No posting detected.");
|
||||
setStatus(response?.error || "No text detected.");
|
||||
return false;
|
||||
}
|
||||
|
||||
state.postingText = response.sanitized || "";
|
||||
updatePostingCount();
|
||||
state.siteText = response.extracted || "";
|
||||
updateSiteTextCount();
|
||||
updatePromptCount(0);
|
||||
setStatus("Posting extracted.");
|
||||
setStatus("Text extracted.");
|
||||
return true;
|
||||
} catch (error) {
|
||||
setStatus(error.message || "Unable to extract posting.");
|
||||
setStatus(error.message || "Unable to extract text.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAnalyze() {
|
||||
if (!state.postingText) {
|
||||
setStatus("Extract a job posting first.");
|
||||
if (!state.siteText) {
|
||||
setStatus("Extract site text first.");
|
||||
return;
|
||||
}
|
||||
|
||||
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||
const tab = tabs[0];
|
||||
if (!tab?.url || !isWaterlooWorksUrl(tab.url)) {
|
||||
setStatus("Open waterlooworks.uwaterloo.ca to run tasks.");
|
||||
if (!tab?.url) {
|
||||
setStatus("Open a page to run tasks.");
|
||||
return;
|
||||
}
|
||||
|
||||
const taskId = taskSelect.value;
|
||||
const task = state.tasks.find((item) => item.id === taskId);
|
||||
if (!task) {
|
||||
setStatus("Select a task prompt.");
|
||||
setStatus("Select a task.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -640,8 +702,7 @@ async function handleAnalyze() {
|
||||
const activeProfile =
|
||||
resolvedProfiles.find((entry) => entry.id === selectedProfileId) ||
|
||||
resolvedProfiles[0];
|
||||
const resumeText = activeProfile?.text || resume || "";
|
||||
const resumeType = activeProfile?.type || "Resume";
|
||||
const profileText = activeProfile?.text || resume || "";
|
||||
const isAdvanced = Boolean(activeConfig?.advanced);
|
||||
const resolvedApiUrl = activeConfig?.apiUrl || "";
|
||||
const resolvedTemplate = activeConfig?.requestTemplate || "";
|
||||
@@ -688,10 +749,9 @@ async function handleAnalyze() {
|
||||
}
|
||||
|
||||
const promptText = buildUserMessage(
|
||||
resumeText,
|
||||
resumeType,
|
||||
profileText,
|
||||
task.text || "",
|
||||
state.postingText
|
||||
state.siteText
|
||||
);
|
||||
updatePromptCount(promptText.length);
|
||||
|
||||
@@ -713,10 +773,9 @@ async function handleAnalyze() {
|
||||
apiKeyPrefix: resolvedApiKeyPrefix,
|
||||
model: resolvedModel,
|
||||
systemPrompt: resolvedSystemPrompt,
|
||||
resume: resumeText,
|
||||
resumeType,
|
||||
profileText,
|
||||
taskText: task.text || "",
|
||||
postingText: state.postingText,
|
||||
siteText: state.siteText,
|
||||
tabId: tab.id
|
||||
}
|
||||
});
|
||||
@@ -769,6 +828,81 @@ function handleCopyRaw() {
|
||||
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);
|
||||
abortBtn.addEventListener("click", handleAbort);
|
||||
settingsBtn.addEventListener("click", () => chrome.runtime.openOptionsPage());
|
||||
@@ -788,7 +922,7 @@ profileSelect.addEventListener("change", () => {
|
||||
void persistSelections();
|
||||
});
|
||||
|
||||
updatePostingCount();
|
||||
updateSiteTextCount();
|
||||
updatePromptCount(0);
|
||||
renderOutput();
|
||||
setAnalyzing(false);
|
||||
|
||||
@@ -76,22 +76,77 @@ body {
|
||||
}
|
||||
|
||||
.toc-links {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.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);
|
||||
text-decoration: none;
|
||||
font-size: 12px;
|
||||
padding: 4px 6px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.toc a:hover {
|
||||
.toc li a:hover {
|
||||
background: var(--card-bg);
|
||||
}
|
||||
|
||||
.toc-sub li a {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.toc-sub li a:hover {
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.sidebar-errors {
|
||||
margin-top: auto;
|
||||
border-radius: 10px;
|
||||
@@ -139,8 +194,9 @@ body {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
padding: 16px;
|
||||
padding: 0;
|
||||
box-shadow: var(--panel-shadow);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-summary {
|
||||
@@ -150,40 +206,58 @@ body {
|
||||
align-items: baseline;
|
||||
justify-content: flex-start;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.panel-summary::-webkit-details-marker {
|
||||
display: none;
|
||||
/* 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 {
|
||||
cursor: pointer;
|
||||
padding: 12px 16px;
|
||||
margin: 0;
|
||||
/* display: list-item; default */
|
||||
}
|
||||
|
||||
.panel-caret {
|
||||
display: inline-flex;
|
||||
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 {
|
||||
/* Need to align H2. */
|
||||
.panel-summary h2 {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.panel[open] .panel-caret .caret-closed {
|
||||
display: none;
|
||||
.sub-panel .panel-summary {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.sub-panel .panel-body {
|
||||
padding: 0 12px 12px;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
margin-top: 12px;
|
||||
margin-top: 0;
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
|
||||
.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 {
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>WWCompanion Settings</title>
|
||||
<title>SiteCompanion Settings</title>
|
||||
<link rel="stylesheet" href="settings.css" />
|
||||
</head>
|
||||
<body>
|
||||
<header class="title-block">
|
||||
<div class="title">WWCompanion Settings</div>
|
||||
<div class="subtitle">Configure prompts, resume, and API access</div>
|
||||
<div class="title">SiteCompanion Settings</div>
|
||||
<div class="subtitle">Configure workspaces, tasks, and API access</div>
|
||||
</header>
|
||||
<div class="page-bar">
|
||||
<div id="status" class="status"></div>
|
||||
@@ -18,27 +18,51 @@
|
||||
|
||||
<div class="settings-layout">
|
||||
<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>
|
||||
<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>
|
||||
<ul>
|
||||
<li>
|
||||
<div class="toc-item">
|
||||
<span class="toc-caret">▸</span> <a href="#global-config-panel">Global Configuration</a>
|
||||
</div>
|
||||
<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 id="sidebarErrors" class="sidebar-errors hidden"></div>
|
||||
</nav>
|
||||
<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">
|
||||
<span class="panel-caret" aria-hidden="true">
|
||||
<span class="caret-closed">▸</span>
|
||||
<span class="caret-open">▾</span>
|
||||
</span>
|
||||
<h2>Appearance</h2>
|
||||
</summary>
|
||||
<div class="panel-body">
|
||||
@@ -50,15 +74,22 @@
|
||||
<option value="dark">Dark</option>
|
||||
</select>
|
||||
</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>
|
||||
</details>
|
||||
|
||||
<details class="panel" id="api-keys-panel">
|
||||
<!-- API Keys -->
|
||||
<details class="panel sub-panel" id="api-keys-panel">
|
||||
<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>
|
||||
</summary>
|
||||
<div class="panel-body">
|
||||
@@ -70,12 +101,9 @@
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="panel" id="api-panel">
|
||||
<!-- API -->
|
||||
<details class="panel sub-panel" id="api-panel">
|
||||
<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>
|
||||
</summary>
|
||||
<div class="panel-body">
|
||||
@@ -89,35 +117,29 @@
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="panel" id="environment-panel">
|
||||
<!-- Envs -->
|
||||
<details class="panel sub-panel" id="environment-panel">
|
||||
<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">
|
||||
<h2>Environment</h2>
|
||||
<span class="hint hint-accent">API configuration and system prompt go here</span>
|
||||
<h2>ENVIRONMENTS</h2>
|
||||
<span class="hint hint-accent">Baseline environments</span>
|
||||
</div>
|
||||
</summary>
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
<div></div>
|
||||
<button id="addEnvConfigBtn" class="ghost" type="button">Add Config</button>
|
||||
<button id="addEnvConfigBtn" class="ghost" type="button">Add Env</button>
|
||||
</div>
|
||||
<div id="envConfigs" class="env-configs"></div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="panel" id="profiles-panel">
|
||||
<!-- Profiles -->
|
||||
<details class="panel sub-panel" id="profiles-panel">
|
||||
<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">
|
||||
<h2>My Profiles</h2>
|
||||
<span class="hint hint-accent">Text to your resumes or generic profiles goes here</span>
|
||||
<h2>PROFILES</h2>
|
||||
<span class="hint hint-accent">Baseline user contexts</span>
|
||||
</div>
|
||||
</summary>
|
||||
<div class="panel-body">
|
||||
@@ -129,15 +151,12 @@
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="panel" id="tasks-panel">
|
||||
<!-- Tasks -->
|
||||
<details class="panel sub-panel" id="tasks-panel">
|
||||
<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">
|
||||
<h2>Task Presets</h2>
|
||||
<span class="hint hint-accent">Top task is the default</span>
|
||||
<h2>TASKS</h2>
|
||||
<span class="hint hint-accent">Baseline execution units</span>
|
||||
</div>
|
||||
</summary>
|
||||
<div class="panel-body">
|
||||
@@ -148,6 +167,57 @@
|
||||
<div id="tasks" class="tasks"></div>
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user