added some UX improvements
This commit is contained in:
@@ -13,7 +13,8 @@ const DEFAULT_SETTINGS = {
|
|||||||
systemPrompt:
|
systemPrompt:
|
||||||
"You are a precise, honest assistant. Be concise, highlight uncertainties, and avoid inventing details.",
|
"You are a precise, honest assistant. Be concise, highlight uncertainties, and avoid inventing details.",
|
||||||
resume: "",
|
resume: "",
|
||||||
tasks: DEFAULT_TASKS
|
tasks: DEFAULT_TASKS,
|
||||||
|
theme: "system"
|
||||||
};
|
};
|
||||||
|
|
||||||
chrome.runtime.onInstalled.addListener(async () => {
|
chrome.runtime.onInstalled.addListener(async () => {
|
||||||
|
|||||||
198
popup.css
198
popup.css
@@ -6,6 +6,34 @@
|
|||||||
--panel: #fff7ec;
|
--panel: #fff7ec;
|
||||||
--border: #e4d6c5;
|
--border: #e4d6c5;
|
||||||
--glow: rgba(177, 77, 43, 0.18);
|
--glow: rgba(177, 77, 43, 0.18);
|
||||||
|
--output-bg: rgba(255, 255, 255, 0.7);
|
||||||
|
--code-bg: #f4e7d2;
|
||||||
|
--page-bg: radial-gradient(circle at top, #fdf2df, #f7ead6 60%, #f1dcc6 100%);
|
||||||
|
--input-bg: #fffdf9;
|
||||||
|
--input-fg: var(--ink);
|
||||||
|
--panel-shadow: 0 12px 30px rgba(122, 80, 47, 0.12);
|
||||||
|
color-scheme: light;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="dark"] {
|
||||||
|
--ink: #abb2bf;
|
||||||
|
--muted: #8b93a5;
|
||||||
|
--accent: #61afef;
|
||||||
|
--accent-deep: #56b6c2;
|
||||||
|
--panel: #2f343f;
|
||||||
|
--border: #3e4451;
|
||||||
|
--glow: rgba(97, 175, 239, 0.2);
|
||||||
|
--output-bg: rgba(47, 52, 63, 0.85);
|
||||||
|
--code-bg: #3a3f4b;
|
||||||
|
--page-bg: radial-gradient(circle at top, #2a2f3a, #21252b 60%, #1b1f25 100%);
|
||||||
|
--input-bg: #2b303b;
|
||||||
|
--input-fg: var(--ink);
|
||||||
|
--panel-shadow: 0 12px 30px rgba(0, 0, 0, 0.35);
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="light"] {
|
||||||
|
color-scheme: light;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@@ -14,71 +42,94 @@
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 16px;
|
padding: 12px;
|
||||||
width: 360px;
|
width: 360px;
|
||||||
font-family: "Iowan Old Style", "Palatino Linotype", "Book Antiqua", Palatino,
|
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||||
"Times New Roman", serif;
|
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
background: radial-gradient(circle at top, #fdf2df, #f7ead6 60%, #f1dcc6 100%);
|
background: var(--page-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-block {
|
.title-block {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-line {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-size: 20px;
|
font-size: 18px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 0.3px;
|
letter-spacing: 0.3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subtitle {
|
.subtitle {
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
padding: 12px;
|
padding: 10px;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
box-shadow: 0 12px 30px rgba(122, 80, 47, 0.12);
|
box-shadow: var(--panel-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls-block {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-block {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field {
|
.field {
|
||||||
margin-top: 10px;
|
margin-top: 8px;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 6px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
font-size: 12px;
|
font-size: 10px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 0.8px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 8px 10px;
|
padding: 6px 8px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
background: #fffdf9;
|
background: var(--input-bg);
|
||||||
font-size: 13px;
|
color: var(--input-fg);
|
||||||
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
.button-row {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-top: 12px;
|
}
|
||||||
|
|
||||||
|
.button-row button {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-row .ghost {
|
||||||
|
flex: 0 0 64px;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 8px 12px;
|
padding: 6px 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||||
}
|
}
|
||||||
@@ -95,7 +146,7 @@ button:active {
|
|||||||
|
|
||||||
.primary {
|
.primary {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: #fffdf9;
|
background: var(--input-bg);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
box-shadow: 0 6px 16px rgba(120, 85, 55, 0.12);
|
box-shadow: 0 6px 16px rgba(120, 85, 55, 0.12);
|
||||||
}
|
}
|
||||||
@@ -114,42 +165,113 @@ button:active {
|
|||||||
.meta {
|
.meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
font-size: 12px;
|
gap: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
font-size: 11px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
margin-top: 10px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status {
|
.status {
|
||||||
margin-top: 10px;
|
font-size: 11px;
|
||||||
font-size: 12px;
|
|
||||||
color: var(--accent-deep);
|
color: var(--accent-deep);
|
||||||
}
|
}
|
||||||
|
|
||||||
.output {
|
.output {
|
||||||
margin-top: 12px;
|
margin-top: 8px;
|
||||||
border: 1px dashed var(--border);
|
border: 1px dashed var(--border);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 10px;
|
padding: 8px;
|
||||||
background: rgba(255, 255, 255, 0.7);
|
background: var(--output-bg);
|
||||||
min-height: 160px;
|
min-height: 210px;
|
||||||
max-height: 240px;
|
max-height: 360px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
pre {
|
.output-body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
max-height: 220px;
|
max-height: 340px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
line-height: 1.4;
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-body h1,
|
||||||
|
.output-body h2,
|
||||||
|
.output-body h3,
|
||||||
|
.output-body h4 {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-body p {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-body ul,
|
||||||
|
.output-body ol {
|
||||||
|
margin: 0 0 8px 18px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-body li {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-body blockquote {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
padding-left: 10px;
|
||||||
|
border-left: 2px solid var(--border);
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-body pre {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--code-bg);
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-body code {
|
||||||
|
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
background: var(--code-bg);
|
||||||
|
padding: 1px 4px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-body a {
|
||||||
|
color: var(--accent-deep);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root:not([data-theme]),
|
||||||
|
:root[data-theme="system"] {
|
||||||
|
--ink: #abb2bf;
|
||||||
|
--muted: #8b93a5;
|
||||||
|
--accent: #61afef;
|
||||||
|
--accent-deep: #56b6c2;
|
||||||
|
--panel: #2f343f;
|
||||||
|
--border: #3e4451;
|
||||||
|
--glow: rgba(97, 175, 239, 0.2);
|
||||||
|
--output-bg: rgba(47, 52, 63, 0.85);
|
||||||
|
--code-bg: #3a3f4b;
|
||||||
|
--page-bg: radial-gradient(circle at top, #2a2f3a, #21252b 60%, #1b1f25 100%);
|
||||||
|
--input-bg: #2b303b;
|
||||||
|
--input-fg: var(--ink);
|
||||||
|
--panel-shadow: 0 12px 30px rgba(0, 0, 0, 0.35);
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
margin-top: 10px;
|
align-items: center;
|
||||||
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.link {
|
.link {
|
||||||
@@ -157,5 +279,5 @@ pre {
|
|||||||
background: none;
|
background: none;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
color: var(--accent-deep);
|
color: var(--accent-deep);
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|||||||
30
popup.html
30
popup.html
@@ -8,29 +8,35 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="title-block">
|
<header class="title-block">
|
||||||
<div class="title">WWCompanion</div>
|
<div class="title-line">
|
||||||
<div class="subtitle">Manual reasoning for WaterlooWorks</div>
|
<span class="title">WWCompanion</span>
|
||||||
|
<span class="subtitle">Manual reasoning for WaterlooWorks</span>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<button id="extractBtn" class="primary">Extract Posting</button>
|
<div class="controls-block">
|
||||||
<div class="field">
|
<div class="config-block">
|
||||||
<label for="taskSelect">Task</label>
|
<div class="field">
|
||||||
<select id="taskSelect"></select>
|
<label for="taskSelect">Task</label>
|
||||||
</div>
|
<select id="taskSelect"></select>
|
||||||
<div class="actions">
|
</div>
|
||||||
<button id="analyzeBtn" class="accent">Analyze</button>
|
<div class="button-row">
|
||||||
<button id="abortBtn" class="ghost" disabled>Stop</button>
|
<button id="extractBtn" class="primary">Extract Posting</button>
|
||||||
|
<button id="analyzeBtn" class="accent">Analyze</button>
|
||||||
|
<button id="abortBtn" class="ghost" disabled>Stop</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
<span id="postingCount">Posting: 0 chars</span>
|
<span id="postingCount">Posting: 0 chars</span>
|
||||||
<span id="promptCount">Prompt: 0 chars</span>
|
<span id="promptCount">Prompt: 0 chars</span>
|
||||||
|
<span id="status" class="status">Idle</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="status" class="status">Idle</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="output">
|
<section class="output">
|
||||||
<pre id="output" aria-live="polite"></pre>
|
<div id="output" class="output-body" aria-live="polite"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
|
|||||||
207
popup.js
207
popup.js
@@ -12,7 +12,8 @@ const state = {
|
|||||||
postingText: "",
|
postingText: "",
|
||||||
tasks: [],
|
tasks: [],
|
||||||
port: null,
|
port: null,
|
||||||
isAnalyzing: false
|
isAnalyzing: false,
|
||||||
|
outputRaw: ""
|
||||||
};
|
};
|
||||||
|
|
||||||
function getStorage(keys) {
|
function getStorage(keys) {
|
||||||
@@ -32,10 +33,194 @@ function buildUserMessage(resume, task, posting) {
|
|||||||
].join("\n");
|
].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) {
|
function setStatus(message) {
|
||||||
statusEl.textContent = message;
|
statusEl.textContent = message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyTheme(theme) {
|
||||||
|
const value = theme || "system";
|
||||||
|
document.documentElement.dataset.theme = value;
|
||||||
|
}
|
||||||
|
|
||||||
function setAnalyzing(isAnalyzing) {
|
function setAnalyzing(isAnalyzing) {
|
||||||
state.isAnalyzing = isAnalyzing;
|
state.isAnalyzing = isAnalyzing;
|
||||||
analyzeBtn.disabled = isAnalyzing;
|
analyzeBtn.disabled = isAnalyzing;
|
||||||
@@ -100,8 +285,8 @@ function ensurePort() {
|
|||||||
const port = chrome.runtime.connect({ name: "analysis" });
|
const port = chrome.runtime.connect({ name: "analysis" });
|
||||||
port.onMessage.addListener((message) => {
|
port.onMessage.addListener((message) => {
|
||||||
if (message?.type === "DELTA") {
|
if (message?.type === "DELTA") {
|
||||||
outputEl.textContent += message.text;
|
state.outputRaw += message.text;
|
||||||
outputEl.scrollTop = outputEl.scrollHeight;
|
renderOutput();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,6 +322,11 @@ async function loadTasks() {
|
|||||||
renderTasks(tasks);
|
renderTasks(tasks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadTheme() {
|
||||||
|
const { theme = "system" } = await getStorage(["theme"]);
|
||||||
|
applyTheme(theme);
|
||||||
|
}
|
||||||
|
|
||||||
async function handleExtract() {
|
async function handleExtract() {
|
||||||
setStatus("Extracting...");
|
setStatus("Extracting...");
|
||||||
try {
|
try {
|
||||||
@@ -188,7 +378,8 @@ async function handleAnalyze() {
|
|||||||
const promptText = buildUserMessage(resume || "", task.text || "", state.postingText);
|
const promptText = buildUserMessage(resume || "", task.text || "", state.postingText);
|
||||||
updatePromptCount(promptText.length);
|
updatePromptCount(promptText.length);
|
||||||
|
|
||||||
outputEl.textContent = "";
|
state.outputRaw = "";
|
||||||
|
renderOutput();
|
||||||
setAnalyzing(true);
|
setAnalyzing(true);
|
||||||
setStatus("Analyzing...");
|
setStatus("Analyzing...");
|
||||||
|
|
||||||
@@ -220,4 +411,12 @@ settingsBtn.addEventListener("click", () => chrome.runtime.openOptionsPage());
|
|||||||
|
|
||||||
updatePostingCount();
|
updatePostingCount();
|
||||||
updatePromptCount(0);
|
updatePromptCount(0);
|
||||||
|
renderOutput();
|
||||||
loadTasks();
|
loadTasks();
|
||||||
|
loadTheme();
|
||||||
|
|
||||||
|
chrome.storage.onChanged.addListener((changes) => {
|
||||||
|
if (changes.theme) {
|
||||||
|
applyTheme(changes.theme.newValue || "system");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
55
settings.css
55
settings.css
@@ -5,6 +5,29 @@
|
|||||||
--panel: #fffaf1;
|
--panel: #fffaf1;
|
||||||
--border: #eadbc8;
|
--border: #eadbc8;
|
||||||
--bg: #f5ead7;
|
--bg: #f5ead7;
|
||||||
|
--input-bg: #fffdf9;
|
||||||
|
--input-fg: var(--ink);
|
||||||
|
--card-bg: #fffefb;
|
||||||
|
--panel-shadow: 0 18px 40px rgba(120, 85, 55, 0.12);
|
||||||
|
color-scheme: light;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="dark"] {
|
||||||
|
--ink: #abb2bf;
|
||||||
|
--muted: #8b93a5;
|
||||||
|
--accent: #61afef;
|
||||||
|
--panel: #2f343f;
|
||||||
|
--border: #3e4451;
|
||||||
|
--bg: linear-gradient(160deg, #2a2f3a, #1f232b);
|
||||||
|
--input-bg: #2b303b;
|
||||||
|
--input-fg: var(--ink);
|
||||||
|
--card-bg: #2b303b;
|
||||||
|
--panel-shadow: 0 18px 40px rgba(0, 0, 0, 0.35);
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="light"] {
|
||||||
|
color-scheme: light;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@@ -14,10 +37,9 @@
|
|||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
font-family: "Iowan Old Style", "Palatino Linotype", "Book Antiqua", Palatino,
|
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||||
"Times New Roman", serif;
|
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
background: linear-gradient(160deg, #f8efe1, #efe0c9);
|
background: var(--bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-block {
|
.title-block {
|
||||||
@@ -40,7 +62,7 @@ body {
|
|||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
box-shadow: 0 18px 40px rgba(120, 85, 55, 0.12);
|
box-shadow: var(--panel-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
@@ -73,12 +95,14 @@ label {
|
|||||||
}
|
}
|
||||||
|
|
||||||
input,
|
input,
|
||||||
textarea {
|
textarea,
|
||||||
|
select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
background: #fffdf9;
|
background: var(--input-bg);
|
||||||
|
color: var(--input-fg);
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
@@ -131,11 +155,28 @@ button:active {
|
|||||||
padding: 12px;
|
padding: 12px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
background: #fffefb;
|
background: var(--card-bg);
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root:not([data-theme]),
|
||||||
|
:root[data-theme="system"] {
|
||||||
|
--ink: #abb2bf;
|
||||||
|
--muted: #8b93a5;
|
||||||
|
--accent: #61afef;
|
||||||
|
--panel: #2f343f;
|
||||||
|
--border: #3e4451;
|
||||||
|
--bg: linear-gradient(160deg, #2a2f3a, #1f232b);
|
||||||
|
--input-bg: #2b303b;
|
||||||
|
--input-fg: var(--ink);
|
||||||
|
--card-bg: #2b303b;
|
||||||
|
--panel-shadow: 0 18px 40px rgba(0, 0, 0, 0.35);
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.task-actions {
|
.task-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|||||||
@@ -31,6 +31,18 @@
|
|||||||
<div id="status" class="status"></div>
|
<div id="status" class="status"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<h2>Appearance</h2>
|
||||||
|
<div class="field">
|
||||||
|
<label for="themeSelect">Theme</label>
|
||||||
|
<select id="themeSelect">
|
||||||
|
<option value="system">System</option>
|
||||||
|
<option value="light">Light</option>
|
||||||
|
<option value="dark">Dark</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<h2>System Prompt</h2>
|
<h2>System Prompt</h2>
|
||||||
<textarea id="systemPrompt" rows="8" placeholder="Define tone and standards..."></textarea>
|
<textarea id="systemPrompt" rows="8" placeholder="Define tone and standards..."></textarea>
|
||||||
|
|||||||
23
settings.js
23
settings.js
@@ -7,6 +7,7 @@ const addTaskBtn = document.getElementById("addTaskBtn");
|
|||||||
const tasksContainer = document.getElementById("tasks");
|
const tasksContainer = document.getElementById("tasks");
|
||||||
const statusEl = document.getElementById("status");
|
const statusEl = document.getElementById("status");
|
||||||
const toggleKeyBtn = document.getElementById("toggleKey");
|
const toggleKeyBtn = document.getElementById("toggleKey");
|
||||||
|
const themeSelect = document.getElementById("themeSelect");
|
||||||
|
|
||||||
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));
|
||||||
@@ -20,6 +21,11 @@ function setStatus(message) {
|
|||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyTheme(theme) {
|
||||||
|
const value = theme || "system";
|
||||||
|
document.documentElement.dataset.theme = value;
|
||||||
|
}
|
||||||
|
|
||||||
function newTaskId() {
|
function newTaskId() {
|
||||||
if (crypto?.randomUUID) return crypto.randomUUID();
|
if (crypto?.randomUUID) return crypto.randomUUID();
|
||||||
return `task-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
return `task-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
||||||
@@ -101,13 +107,21 @@ function collectTasks() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadSettings() {
|
async function loadSettings() {
|
||||||
const { apiKey = "", model = "", systemPrompt = "", resume = "", tasks = [] } =
|
const {
|
||||||
await getStorage(["apiKey", "model", "systemPrompt", "resume", "tasks"]);
|
apiKey = "",
|
||||||
|
model = "",
|
||||||
|
systemPrompt = "",
|
||||||
|
resume = "",
|
||||||
|
tasks = [],
|
||||||
|
theme = "system"
|
||||||
|
} = await getStorage(["apiKey", "model", "systemPrompt", "resume", "tasks", "theme"]);
|
||||||
|
|
||||||
apiKeyInput.value = apiKey;
|
apiKeyInput.value = apiKey;
|
||||||
modelInput.value = model;
|
modelInput.value = model;
|
||||||
systemPromptInput.value = systemPrompt;
|
systemPromptInput.value = systemPrompt;
|
||||||
resumeInput.value = resume;
|
resumeInput.value = resume;
|
||||||
|
themeSelect.value = theme;
|
||||||
|
applyTheme(theme);
|
||||||
|
|
||||||
tasksContainer.innerHTML = "";
|
tasksContainer.innerHTML = "";
|
||||||
if (!tasks.length) {
|
if (!tasks.length) {
|
||||||
@@ -129,7 +143,8 @@ async function saveSettings() {
|
|||||||
model: modelInput.value.trim(),
|
model: modelInput.value.trim(),
|
||||||
systemPrompt: systemPromptInput.value,
|
systemPrompt: systemPromptInput.value,
|
||||||
resume: resumeInput.value,
|
resume: resumeInput.value,
|
||||||
tasks
|
tasks,
|
||||||
|
theme: themeSelect.value
|
||||||
});
|
});
|
||||||
setStatus("Saved.");
|
setStatus("Saved.");
|
||||||
}
|
}
|
||||||
@@ -145,4 +160,6 @@ toggleKeyBtn.addEventListener("click", () => {
|
|||||||
toggleKeyBtn.textContent = isPassword ? "Hide" : "Show";
|
toggleKeyBtn.textContent = isPassword ? "Hide" : "Show";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
themeSelect.addEventListener("change", () => applyTheme(themeSelect.value));
|
||||||
|
|
||||||
loadSettings();
|
loadSettings();
|
||||||
|
|||||||
Reference in New Issue
Block a user