added some UX improvements
This commit is contained in:
207
popup.js
207
popup.js
@@ -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, "<")
|
||||
.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 `<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");
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user