const DEFAULT_TASKS = [ { id: "task-generic-fit", name: "Generic Fit", text: "You should evaluate for my fit to the job. No comparisons between postings and you don't need to suggest interview prep, we'll leave those for later. a bit of tuning to your answers: please keep things more compact, a single section for the evaluation is enough, you don't need to analyze every bullet point in the posting." }, { id: "task-ratings-only", name: "Ratings Only", text: "Give ratings out of 10 with headings and do not include any other text.\n\n1. Fit evaluation: my fit to the role.\n2. Company status: how well this company offers career development for me.\n3. Pay: use $25 CAD per hour as a baseline; rate the compensation." } ]; const DEFAULT_SETTINGS = { apiKey: "", apiBaseUrl: "https://api.openai.com/v1", apiKeyHeader: "Authorization", apiKeyPrefix: "Bearer ", 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" }; const OUTPUT_STORAGE_KEY = "lastOutput"; const AUTO_RUN_KEY = "autoRunDefaultTask"; let activeAbortController = null; let keepalivePort = null; const streamState = { active: false, outputText: "", subscribers: new Set() }; function resetAbort() { if (activeAbortController) { activeAbortController.abort(); activeAbortController = null; } closeKeepalive(); } function openKeepalive(tabId) { if (!tabId || keepalivePort) return; try { keepalivePort = chrome.tabs.connect(tabId, { name: "wwcompanion-keepalive" }); keepalivePort.onDisconnect.addListener(() => { keepalivePort = null; }); } catch { keepalivePort = null; } } function closeKeepalive() { if (!keepalivePort) return; try { keepalivePort.disconnect(); } catch { // Ignore disconnect failures. } keepalivePort = null; } chrome.runtime.onInstalled.addListener(async () => { const stored = await chrome.storage.local.get(Object.keys(DEFAULT_SETTINGS)); const updates = {}; for (const [key, value] of Object.entries(DEFAULT_SETTINGS)) { const existing = stored[key]; const missing = existing === undefined || existing === null || (key === "tasks" && !Array.isArray(existing)); if (missing) updates[key] = value; } if (Object.keys(updates).length) { await chrome.storage.local.set(updates); } }); chrome.runtime.onConnect.addListener((port) => { if (port.name !== "analysis") return; streamState.subscribers.add(port); port.onDisconnect.addListener(() => { streamState.subscribers.delete(port); }); if (streamState.active) { safePost(port, { type: "SYNC", text: streamState.outputText, streaming: true }); } port.onMessage.addListener((message) => { if (message?.type === "START_ANALYSIS") { streamState.outputText = ""; resetAbort(); const controller = new AbortController(); activeAbortController = controller; const request = handleAnalysisRequest(port, message.payload, controller.signal); void request .catch((error) => { if (error?.name === "AbortError") { safePost(port, { type: "ABORTED" }); return; } safePost(port, { type: "ERROR", message: error?.message || "Unknown error during analysis." }); }) .finally(() => { if (activeAbortController === controller) { activeAbortController = null; } }); return; } if (message?.type === "ABORT_ANALYSIS") { resetAbort(); } }); }); chrome.runtime.onMessage.addListener((message) => { if (message?.type !== "RUN_DEFAULT_TASK") return; void chrome.storage.local.set({ [AUTO_RUN_KEY]: Date.now() }); if (chrome.action?.openPopup) { void chrome.action.openPopup().catch(() => {}); } }); function buildUserMessage(resume, task, posting) { return [ "=== RESUME ===", resume || "", "", "=== TASK ===", task || "", "", "=== JOB POSTING ===", posting || "" ].join("\n"); } function safePost(port, message) { try { port.postMessage(message); } catch { // Port can disconnect when the popup closes; ignore post failures. } } function broadcast(message) { for (const port of streamState.subscribers) { safePost(port, message); } } async function handleAnalysisRequest(port, payload, signal) { streamState.outputText = ""; streamState.active = true; const { apiKey, apiBaseUrl, apiKeyHeader, apiKeyPrefix, model, systemPrompt, resume, taskText, postingText, tabId } = payload || {}; if (!apiBaseUrl) { safePost(port, { type: "ERROR", message: "Missing API base URL." }); return; } if (apiKeyHeader && !apiKey) { safePost(port, { type: "ERROR", message: "Missing API key." }); return; } if (!model) { safePost(port, { type: "ERROR", message: "Missing model name." }); return; } if (!postingText) { safePost(port, { type: "ERROR", message: "No job posting text provided." }); return; } if (!taskText) { safePost(port, { type: "ERROR", message: "No task prompt selected." }); return; } const userMessage = buildUserMessage(resume, taskText, postingText); await chrome.storage.local.set({ [OUTPUT_STORAGE_KEY]: "" }); openKeepalive(tabId); try { await streamChatCompletion({ apiKey, apiBaseUrl, apiKeyHeader, apiKeyPrefix, model, systemPrompt: systemPrompt || "", userMessage, signal, onDelta: (text) => { streamState.outputText += text; broadcast({ type: "DELTA", text }); } }); broadcast({ type: "DONE" }); } finally { streamState.active = false; await chrome.storage.local.set({ [OUTPUT_STORAGE_KEY]: streamState.outputText }); closeKeepalive(); } } function buildChatUrl(apiBaseUrl) { const trimmed = (apiBaseUrl || "").trim().replace(/\/+$/, ""); if (!trimmed) return ""; if (trimmed.endsWith("/chat/completions")) return trimmed; return `${trimmed}/chat/completions`; } function buildAuthHeader(apiKeyHeader, apiKeyPrefix, apiKey) { if (!apiKeyHeader) return null; return { name: apiKeyHeader, value: `${apiKeyPrefix || ""}${apiKey || ""}` }; } async function streamChatCompletion({ apiKey, apiBaseUrl, apiKeyHeader, apiKeyPrefix, model, systemPrompt, userMessage, signal, onDelta }) { const chatUrl = buildChatUrl(apiBaseUrl); if (!chatUrl) { throw new Error("Invalid API base URL."); } const headers = { "Content-Type": "application/json" }; const authHeader = buildAuthHeader(apiKeyHeader, apiKeyPrefix, apiKey); if (authHeader) { headers[authHeader.name] = authHeader.value; } const response = await fetch(chatUrl, { method: "POST", headers, body: JSON.stringify({ model, stream: true, messages: [ { role: "system", content: systemPrompt }, { role: "user", content: userMessage } ] }), signal }); if (!response.ok) { const errorText = await response.text(); throw new Error(`OpenAI API error ${response.status}: ${errorText}`); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; // OpenAI streams Server-Sent Events; parse incremental deltas from data lines. while (true) { const { value, done } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n"); buffer = lines.pop() || ""; for (const line of lines) { const trimmed = line.trim(); if (!trimmed.startsWith("data:")) continue; const data = trimmed.slice(5).trim(); if (!data) continue; if (data === "[DONE]") return; let parsed; try { parsed = JSON.parse(data); } catch { continue; } const delta = parsed?.choices?.[0]?.delta?.content; if (delta) onDelta(delta); } } }