const extractBtn = document.getElementById("extractBtn"); const analyzeBtn = document.getElementById("analyzeBtn"); const abortBtn = document.getElementById("abortBtn"); const extractRunBtn = document.getElementById("extractRunBtn"); const stopRow = document.getElementById("stopRow"); const buttonRow = document.querySelector(".button-row"); const taskSelect = document.getElementById("taskSelect"); const outputEl = document.getElementById("output"); const statusEl = document.getElementById("status"); const postingCountEl = document.getElementById("postingCount"); const promptCountEl = document.getElementById("promptCount"); const settingsBtn = document.getElementById("settingsBtn"); const copyRenderedBtn = document.getElementById("copyRenderedBtn"); const copyRawBtn = document.getElementById("copyRawBtn"); const clearOutputBtn = document.getElementById("clearOutputBtn"); const OUTPUT_STORAGE_KEY = "lastOutput"; const AUTO_RUN_KEY = "autoRunDefaultTask"; const state = { postingText: "", tasks: [], port: null, isAnalyzing: false, outputRaw: "", autoRunPending: false }; function getStorage(keys) { return new Promise((resolve) => chrome.storage.local.get(keys, resolve)); } function buildUserMessage(resume, task, posting) { return [ "=== RESUME ===", resume || "", "", "=== TASK ===", task || "", "", "=== JOB POSTING ===", posting || "" ].join("\n"); } function escapeHtml(text) { return text .replace(/&/g, "&") .replace(//g, ">"); } function escapeAttribute(text) { return text.replace(/&/g, "&").replace(/"/g, """); } function sanitizeUrl(url) { const trimmed = url.trim().replace(/&/g, "&"); if (/^https?:\/\//i.test(trimmed)) return trimmed; return ""; } function applyInline(text) { if (!text) return ""; const codeSpans = []; let output = text.replace(/`([^`]+)`/g, (_match, code) => { const id = codeSpans.length; codeSpans.push(code); return `@@CODESPAN${id}@@`; }); output = output.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, label, url) => { const safeUrl = sanitizeUrl(url); if (!safeUrl) return label; return `${label}`; }); output = output.replace(/\*\*([^*]+)\*\*/g, "$1"); output = output.replace(/\*([^*]+)\*/g, "$1"); output = output.replace(/_([^_]+)_/g, "$1"); output = output.replace(/@@CODESPAN(\d+)@@/g, (_match, id) => { const code = codeSpans[Number(id)] || ""; return `${code}`; }); return output; } function renderMarkdown(rawText) { const { text, blocks } = (() => { const escaped = escapeHtml(rawText || ""); const codeBlocks = []; const replaced = escaped.replace(/```([\s\S]*?)```/g, (_match, code) => { let content = code; if (content.startsWith("\n")) content = content.slice(1); const firstLine = content.split("\n")[0] || ""; if (/^[a-z0-9+.#-]+$/i.test(firstLine.trim()) && content.includes("\n")) { content = content.split("\n").slice(1).join("\n"); } const id = codeBlocks.length; codeBlocks.push(content); return `@@CODEBLOCK${id}@@`; }); return { text: replaced, blocks: codeBlocks }; })(); const lines = text.split(/\r?\n/); const result = []; let paragraph = []; let listType = null; let inBlockquote = false; let quoteLines = []; const flushParagraph = () => { if (!paragraph.length) return; result.push(`

${applyInline(paragraph.join("
"))}

`); paragraph = []; }; const closeList = () => { if (!listType) return; result.push(``); listType = null; }; const openList = (type) => { if (listType === type) return; if (listType) result.push(``); listType = type; result.push(`<${type}>`); }; const closeBlockquote = () => { if (!inBlockquote) return; result.push(`
${applyInline(quoteLines.join("
"))}
`); inBlockquote = false; quoteLines = []; }; for (const line of lines) { const trimmed = line.trim(); const isQuoteLine = /^\s*>\s?/.test(line); if (trimmed === "") { flushParagraph(); closeList(); closeBlockquote(); continue; } if (inBlockquote && !isQuoteLine) { closeBlockquote(); } if (/^@@CODEBLOCK\d+@@$/.test(trimmed)) { flushParagraph(); closeList(); closeBlockquote(); result.push(trimmed); continue; } const headingMatch = line.match(/^(#{1,6})\s+(.*)$/); if (headingMatch) { flushParagraph(); closeList(); closeBlockquote(); const level = headingMatch[1].length; result.push(`${applyInline(headingMatch[2])}`); continue; } if (/^(\s*[-*_])\1{2,}\s*$/.test(line)) { flushParagraph(); closeList(); closeBlockquote(); result.push("
"); continue; } if (isQuoteLine) { if (!inBlockquote) { flushParagraph(); closeList(); inBlockquote = true; quoteLines = []; } quoteLines.push(line.replace(/^\s*>\s?/, "")); continue; } const unorderedMatch = line.match(/^[-*+]\s+(.+)$/); if (unorderedMatch) { flushParagraph(); closeBlockquote(); openList("ul"); result.push(`
  • ${applyInline(unorderedMatch[1])}
  • `); continue; } const orderedMatch = line.match(/^\d+\.\s+(.+)$/); if (orderedMatch) { flushParagraph(); closeBlockquote(); openList("ol"); result.push(`
  • ${applyInline(orderedMatch[1])}
  • `); continue; } paragraph.push(line); } flushParagraph(); closeList(); closeBlockquote(); return result .join("\n") .replace(/@@CODEBLOCK(\d+)@@/g, (_match, id) => { const code = blocks[Number(id)] || ""; return `
    ${code}
    `; }); } function renderOutput() { outputEl.innerHTML = renderMarkdown(state.outputRaw); outputEl.scrollTop = outputEl.scrollHeight; } function persistOutputNow() { return chrome.storage.local.set({ [OUTPUT_STORAGE_KEY]: state.outputRaw }); } function setStatus(message) { statusEl.textContent = message; } function applyTheme(theme) { const value = theme || "system"; document.documentElement.dataset.theme = value; } function setAnalyzing(isAnalyzing) { state.isAnalyzing = isAnalyzing; analyzeBtn.disabled = isAnalyzing; abortBtn.disabled = !isAnalyzing; extractBtn.disabled = isAnalyzing; extractRunBtn.disabled = isAnalyzing; if (buttonRow && stopRow) { buttonRow.classList.toggle("hidden", isAnalyzing); stopRow.classList.toggle("hidden", !isAnalyzing); } updateTaskSelectState(); } function updatePostingCount() { postingCountEl.textContent = `Posting: ${state.postingText.length} chars`; } function updatePromptCount(count) { promptCountEl.textContent = `Prompt: ${count} chars`; } function renderTasks(tasks) { state.tasks = tasks; taskSelect.innerHTML = ""; if (!tasks.length) { const option = document.createElement("option"); option.textContent = "No tasks configured"; option.value = ""; taskSelect.appendChild(option); updateTaskSelectState(); return; } for (const task of tasks) { const option = document.createElement("option"); option.value = task.id; option.textContent = task.name || "Untitled task"; taskSelect.appendChild(option); } updateTaskSelectState(); } function updateTaskSelectState() { const hasTasks = state.tasks.length > 0; taskSelect.disabled = state.isAnalyzing || !hasTasks; } 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) => { const tab = tabs[0]; if (!tab?.id) { reject(new Error("No active tab found.")); 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." : error.message; reject(new Error(msg)); return; } resolve(response); }); }); }); } function ensurePort() { if (state.port) return state.port; const port = chrome.runtime.connect({ name: "analysis" }); port.onMessage.addListener((message) => { if (message?.type === "DELTA") { state.outputRaw += message.text; renderOutput(); return; } if (message?.type === "SYNC") { state.outputRaw = message.text || ""; renderOutput(); if (message.streaming) { setAnalyzing(true); setStatus("Analyzing..."); } return; } if (message?.type === "DONE") { setAnalyzing(false); setStatus("Done"); return; } if (message?.type === "ABORTED") { setAnalyzing(false); setStatus("Aborted."); return; } if (message?.type === "ERROR") { setAnalyzing(false); setStatus(message.message || "Error during analysis."); } }); port.onDisconnect.addListener(() => { state.port = null; setAnalyzing(false); }); state.port = port; return port; } async function loadTasks() { const { tasks = [] } = await getStorage(["tasks"]); renderTasks(tasks); maybeRunDefaultTask(); } async function loadTheme() { const { theme = "system" } = await getStorage(["theme"]); applyTheme(theme); } async function handleExtract() { setStatus("Extracting..."); try { const response = await sendToActiveTab({ type: "EXTRACT_POSTING" }); if (!response?.ok) { setStatus(response?.error || "No posting detected."); return false; } state.postingText = response.sanitized || ""; updatePostingCount(); updatePromptCount(0); setStatus("Posting extracted."); return true; } catch (error) { setStatus(error.message || "Unable to extract posting."); return false; } } async function handleAnalyze() { if (!state.postingText) { setStatus("Extract a job posting 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."); return; } const taskId = taskSelect.value; const task = state.tasks.find((item) => item.id === taskId); if (!task) { setStatus("Select a task prompt."); return; } const { apiKey, apiBaseUrl, apiKeyHeader, apiKeyPrefix, model, systemPrompt, resume } = await getStorage([ "apiKey", "apiBaseUrl", "apiKeyHeader", "apiKeyPrefix", "model", "systemPrompt", "resume" ]); if (!apiBaseUrl) { setStatus("Set an API base URL in Settings."); return; } if (apiKeyHeader && !apiKey) { setStatus("Add your API key in Settings."); return; } if (!model) { setStatus("Set a model name in Settings."); return; } const promptText = buildUserMessage(resume || "", task.text || "", state.postingText); updatePromptCount(promptText.length); state.outputRaw = ""; renderOutput(); setAnalyzing(true); setStatus("Analyzing..."); const port = ensurePort(); port.postMessage({ type: "START_ANALYSIS", payload: { apiKey, apiBaseUrl, apiKeyHeader, apiKeyPrefix, model, systemPrompt: systemPrompt || "", resume: resume || "", taskText: task.text || "", postingText: state.postingText, tabId: tab.id } }); } async function handleExtractAndAnalyze() { const extracted = await handleExtract(); if (!extracted) return; await handleAnalyze(); } function handleAbort() { if (!state.port) return; state.port.postMessage({ type: "ABORT_ANALYSIS" }); setAnalyzing(false); setStatus("Aborted."); } async function handleClearOutput() { state.outputRaw = ""; renderOutput(); await persistOutputNow(); setStatus("Output cleared."); } async function copyTextToClipboard(text, label) { try { await navigator.clipboard.writeText(text); setStatus(`${label} copied.`); } catch (error) { setStatus(`Unable to copy ${label.toLowerCase()}.`); } } function handleCopyRendered() { const text = outputEl.innerText || ""; if (!text.trim()) { setStatus("Nothing to copy."); return; } void copyTextToClipboard(text, "Output"); } function handleCopyRaw() { const text = state.outputRaw || ""; if (!text.trim()) { setStatus("Nothing to copy."); return; } void copyTextToClipboard(text, "Markdown"); } extractBtn.addEventListener("click", handleExtract); analyzeBtn.addEventListener("click", handleAnalyze); extractRunBtn.addEventListener("click", handleExtractAndAnalyze); abortBtn.addEventListener("click", handleAbort); settingsBtn.addEventListener("click", () => chrome.runtime.openOptionsPage()); copyRenderedBtn.addEventListener("click", handleCopyRendered); copyRawBtn.addEventListener("click", handleCopyRaw); clearOutputBtn.addEventListener("click", () => void handleClearOutput()); updatePostingCount(); updatePromptCount(0); renderOutput(); setAnalyzing(false); loadTasks(); loadTheme(); async function loadSavedOutput() { const stored = await getStorage([OUTPUT_STORAGE_KEY]); state.outputRaw = stored[OUTPUT_STORAGE_KEY] || ""; renderOutput(); } async function loadAutoRunRequest() { const stored = await getStorage([AUTO_RUN_KEY]); if (stored[AUTO_RUN_KEY]) { state.autoRunPending = true; await chrome.storage.local.remove(AUTO_RUN_KEY); } maybeRunDefaultTask(); } function maybeRunDefaultTask() { if (!state.autoRunPending) return; if (state.isAnalyzing) return; if (!state.tasks.length) return; taskSelect.value = state.tasks[0].id; state.autoRunPending = false; void handleExtractAndAnalyze(); } loadSavedOutput(); loadAutoRunRequest(); ensurePort(); chrome.storage.onChanged.addListener((changes) => { if (changes[AUTO_RUN_KEY]?.newValue) { state.autoRunPending = true; void chrome.storage.local.remove(AUTO_RUN_KEY); maybeRunDefaultTask(); } if (changes[OUTPUT_STORAGE_KEY]?.newValue !== undefined) { if (!state.isAnalyzing || !state.port) { state.outputRaw = changes[OUTPUT_STORAGE_KEY].newValue || ""; renderOutput(); } } if (changes.theme) { applyTheme(changes.theme.newValue || "system"); } });