added some UX improvements

This commit is contained in:
2026-01-16 21:08:50 -05:00
parent 33b3e414ae
commit b8cb75acf4
7 changed files with 463 additions and 65 deletions

207
popup.js
View File

@@ -12,7 +12,8 @@ const state = {
postingText: "",
tasks: [],
port: null,
isAnalyzing: false
isAnalyzing: false,
outputRaw: ""
};
function getStorage(keys) {
@@ -32,10 +33,194 @@ function buildUserMessage(resume, task, posting) {
].join("\n");
}
function escapeHtml(text) {
return text
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
}
function escapeAttribute(text) {
return text.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
}
function sanitizeUrl(url) {
const trimmed = url.trim().replace(/&amp;/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 `<a href="${escapeAttribute(
safeUrl
)}" target="_blank" rel="noreferrer">${label}</a>`;
});
output = output.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
output = output.replace(/\*([^*]+)\*/g, "<em>$1</em>");
output = output.replace(/_([^_]+)_/g, "<em>$1</em>");
output = output.replace(/@@CODESPAN(\d+)@@/g, (_match, id) => {
const code = codeSpans[Number(id)] || "";
return `<code>${code}</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(`<p>${applyInline(paragraph.join("<br>"))}</p>`);
paragraph = [];
};
const closeList = () => {
if (!listType) return;
result.push(`</${listType}>`);
listType = null;
};
const openList = (type) => {
if (listType === type) return;
if (listType) result.push(`</${listType}>`);
listType = type;
result.push(`<${type}>`);
};
const closeBlockquote = () => {
if (!inBlockquote) return;
result.push(`<blockquote>${applyInline(quoteLines.join("<br>"))}</blockquote>`);
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(`<h${level}>${applyInline(headingMatch[2])}</h${level}>`);
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(`<li>${applyInline(unorderedMatch[1])}</li>`);
continue;
}
const orderedMatch = line.match(/^\d+\.\s+(.+)$/);
if (orderedMatch) {
flushParagraph();
closeBlockquote();
openList("ol");
result.push(`<li>${applyInline(orderedMatch[1])}</li>`);
continue;
}
paragraph.push(line);
}
flushParagraph();
closeList();
closeBlockquote();
return result
.join("\n")
.replace(/@@CODEBLOCK(\d+)@@/g, (_match, id) => {
const code = blocks[Number(id)] || "";
return `<pre><code>${code}</code></pre>`;
});
}
function renderOutput() {
outputEl.innerHTML = renderMarkdown(state.outputRaw);
outputEl.scrollTop = outputEl.scrollHeight;
}
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;
@@ -100,8 +285,8 @@ function ensurePort() {
const port = chrome.runtime.connect({ name: "analysis" });
port.onMessage.addListener((message) => {
if (message?.type === "DELTA") {
outputEl.textContent += message.text;
outputEl.scrollTop = outputEl.scrollHeight;
state.outputRaw += message.text;
renderOutput();
return;
}
@@ -137,6 +322,11 @@ async function loadTasks() {
renderTasks(tasks);
}
async function loadTheme() {
const { theme = "system" } = await getStorage(["theme"]);
applyTheme(theme);
}
async function handleExtract() {
setStatus("Extracting...");
try {
@@ -188,7 +378,8 @@ async function handleAnalyze() {
const promptText = buildUserMessage(resume || "", task.text || "", state.postingText);
updatePromptCount(promptText.length);
outputEl.textContent = "";
state.outputRaw = "";
renderOutput();
setAnalyzing(true);
setStatus("Analyzing...");
@@ -220,4 +411,12 @@ settingsBtn.addEventListener("click", () => chrome.runtime.openOptionsPage());
updatePostingCount();
updatePromptCount(0);
renderOutput();
loadTasks();
loadTheme();
chrome.storage.onChanged.addListener((changes) => {
if (changes.theme) {
applyTheme(changes.theme.newValue || "system");
}
});