diff --git a/background.js b/background.js index 713ae3c..9c2658d 100644 --- a/background.js +++ b/background.js @@ -17,7 +17,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { // ─── Core API call ─────────────────────────────────────────────────────────── -async function handleGeminiRequest({ apiKey, prompt, tab, action = 'generateText', jobDescription = '', jobTitle = '' }) { +async function handleGeminiRequest({ apiKey, prompt, tab, action = 'generateText', jobDescription = '', jobTitle = '', postText = '' }) { // Rate limit check const canProceed = await checkRateLimit(); if (!canProceed) { @@ -46,7 +46,8 @@ async function handleGeminiRequest({ apiKey, prompt, tab, action = 'generateText apiKey: apiKey, prompt: trimmedPrompt, jobDescription: jobDescription, - jobTitle: jobTitle + jobTitle: jobTitle, + postText: postText }) }); @@ -57,6 +58,15 @@ async function handleGeminiRequest({ apiKey, prompt, tab, action = 'generateText if (!data.pdf) throw new Error('Empty PDF response from server.'); await incrementUsage(); return { pdf: data.pdf, filename: data.filename, fromCache: false }; + } else if (action === 'generateComment') { + const comment = data.comment; + if (!comment) { + console.error('[LJA-BG] Empty comment. Full response:', JSON.stringify(data).substring(0, 300)); + lastError = 'Empty comment from server.'; + continue; + } + await incrementUsage(); + return { comment, fromCache: false }; } else { const text = data.candidates?.[0]?.content?.parts?.[0]?.text; if (!text) { diff --git a/manifest.json b/manifest.json index 7f210d8..14e4d1e 100644 --- a/manifest.json +++ b/manifest.json @@ -1,20 +1,32 @@ { "manifest_version": 3, "name": "LinkedIn Job Analyzer", - "version": "1.3.0", - "description": "AI-powered job analysis tool for LinkedIn — personal use", - "permissions": ["storage", "activeTab", "scripting"], + "version": "1.4.0", + "description": "AI-powered job analysis + smart comment generator for LinkedIn — personal use", + "permissions": ["storage", "activeTab", "scripting", "clipboardWrite"], "host_permissions": [ "https://www.linkedin.com/*", "https://cv.intaleqapp.com/*", "https://generativelanguage.googleapis.com/*" ], - "content_scripts": [{ - "matches": ["https://www.linkedin.com/jobs/*"], - "js": ["prompts.js", "content.js"], - "css": ["overlay.css"], - "run_at": "document_idle" - }], + "content_scripts": [ + { + "matches": ["https://www.linkedin.com/jobs/*"], + "js": ["prompts.js", "content.js"], + "css": ["overlay.css"], + "run_at": "document_idle" + }, + { + "matches": [ + "https://www.linkedin.com/feed/", + "https://www.linkedin.com/feed/*", + "https://www.linkedin.com/" + ], + "js": ["post_feed.js"], + "css": ["post_feed.css"], + "run_at": "document_idle" + } + ], "action": { "default_popup": "popup.html", "default_icon": { diff --git a/post_feed.css b/post_feed.css new file mode 100644 index 0000000..519fac1 --- /dev/null +++ b/post_feed.css @@ -0,0 +1,167 @@ +/* ============================================================ + post_feed.css — Smart Comment Feature Styles + Scoped to .lja-comment-btn and .lja-comment-box + ============================================================ */ + +/* ── Smart Comment Button ─────────────────────────────────── */ +.lja-comment-btn { + display: inline-flex; + align-items: center; + gap: 5px; + margin-left: 8px; + padding: 6px 14px; + background: linear-gradient(135deg, #6c63ff, #4f46e5); + color: #fff; + border: none; + border-radius: 20px; + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + white-space: nowrap; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + box-shadow: 0 2px 8px rgba(108, 99, 255, 0.3); +} + +.lja-comment-btn:hover { + transform: translateY(-1px); + box-shadow: 0 4px 14px rgba(108, 99, 255, 0.45); + background: linear-gradient(135deg, #7c73ff, #5f56f5); +} + +.lja-comment-btn:disabled { + opacity: 0.7; + cursor: not-allowed; + transform: none; +} + +/* Spinner inside button */ +.lja-spinner { + display: inline-block; + width: 10px; + height: 10px; + border: 2px solid rgba(255, 255, 255, 0.4); + border-top-color: #fff; + border-radius: 50%; + animation: lja-spin 0.7s linear infinite; +} + +@keyframes lja-spin { + to { transform: rotate(360deg); } +} + +/* ── Comment Suggestion Box ───────────────────────────────── */ +.lja-comment-box { + margin: 10px 12px; + background: linear-gradient(145deg, #1a1a2e, #16213e); + border: 1px solid rgba(108, 99, 255, 0.35); + border-radius: 12px; + overflow: hidden; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); + animation: lja-fadeIn 0.25s ease; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; +} + +@keyframes lja-fadeIn { + from { opacity: 0; transform: translateY(-6px); } + to { opacity: 1; transform: translateY(0); } +} + +/* Header */ +.lja-cb-header { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 14px; + background: rgba(108, 99, 255, 0.15); + border-bottom: 1px solid rgba(108, 99, 255, 0.2); +} + +.lja-cb-icon { + font-size: 16px; +} + +.lja-cb-title { + flex: 1; + font-size: 13px; + font-weight: 700; + color: #a89cff; + letter-spacing: 0.3px; +} + +.lja-cb-close { + background: none; + border: none; + color: #888; + font-size: 14px; + cursor: pointer; + padding: 2px 6px; + border-radius: 4px; + transition: color 0.2s, background 0.2s; + line-height: 1; +} + +.lja-cb-close:hover { + color: #fff; + background: rgba(255, 80, 80, 0.25); +} + +/* Editable text area */ +.lja-cb-text { + display: block; + width: 100%; + min-height: 80px; + max-height: 160px; + padding: 14px 16px; + background: transparent; + border: none; + color: #e0deff; + font-size: 14px; + line-height: 1.65; + resize: vertical; + outline: none; + box-sizing: border-box; + font-family: inherit; +} + +.lja-cb-text:focus { + background: rgba(108, 99, 255, 0.05); +} + +/* Action buttons row */ +.lja-cb-actions { + display: flex; + gap: 8px; + padding: 10px 14px; + border-top: 1px solid rgba(255, 255, 255, 0.06); + flex-wrap: wrap; +} + +.lja-cb-actions button { + padding: 6px 14px; + border: 1px solid rgba(108, 99, 255, 0.4); + border-radius: 8px; + background: rgba(108, 99, 255, 0.12); + color: #b0a8ff; + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + font-family: inherit; +} + +.lja-cb-actions button:hover { + background: rgba(108, 99, 255, 0.3); + color: #fff; + border-color: rgba(108, 99, 255, 0.7); +} + +/* Paste button — primary emphasis */ +.lja-cb-paste { + background: linear-gradient(135deg, rgba(108,99,255,0.3), rgba(79,70,229,0.3)) !important; + border-color: rgba(108, 99, 255, 0.6) !important; +} + +.lja-cb-paste:hover { + background: linear-gradient(135deg, rgba(108,99,255,0.55), rgba(79,70,229,0.55)) !important; +} diff --git a/post_feed.js b/post_feed.js new file mode 100644 index 0000000..ccc712e --- /dev/null +++ b/post_feed.js @@ -0,0 +1,271 @@ +// post_feed.js — LinkedIn Feed Smart Comment Generator +// Operates ONLY on linkedin.com/feed pages — fully independent from content.js + +(function () { + 'use strict'; + + const SERVER_URL = 'https://cv.intaleqapp.com/cv/server/generate_cv.php'; + const BUTTON_CLASS = 'lja-comment-btn'; + const BOX_CLASS = 'lja-comment-box'; + + // ─── Utility: get stored settings ──────────────────────────────────────── + function getSettings() { + return new Promise(resolve => + chrome.storage.local.get(['apiKey', 'language'], resolve) + ); + } + + // ─── Extract post text from a feed item ────────────────────────────────── + function extractPostText(postEl) { + // Try multiple selectors LinkedIn uses + const selectors = [ + '.feed-shared-update-v2__description .break-words span[dir]', + '.feed-shared-text-view span[dir]', + '.update-components-text span[dir]', + '.feed-shared-update-v2__description', + '.feed-shared-text', + ]; + for (const sel of selectors) { + const el = postEl.querySelector(sel); + if (el && el.textContent.trim().length > 20) { + return el.textContent.trim(); + } + } + return null; + } + + // ─── Find the action bar in a post ─────────────────────────────────────── + function findActionBar(postEl) { + const selectors = [ + '.feed-shared-social-action-bar', + '.social-actions-bar', + '[data-urn] .feed-shared-social-action-bar', + ]; + for (const sel of selectors) { + const el = postEl.querySelector(sel); + if (el) return el; + } + return null; + } + + // ─── Find comment input field ───────────────────────────────────────────── + function findCommentInput(postEl) { + const selectors = [ + '.comments-comment-box__form-container .ql-editor', + '.comments-comment-texteditor .ql-editor', + '.ql-editor[contenteditable="true"]', + ]; + // Search inside the post first, then fall back to document + for (const sel of selectors) { + const el = postEl.querySelector(sel) || document.querySelector(sel); + if (el) return el; + } + return null; + } + + // ─── Fill comment input (handles LinkedIn's Quill editor) ──────────────── + function fillCommentInput(inputEl, text) { + inputEl.focus(); + // Clear existing content + inputEl.innerHTML = ''; + // Use execCommand for rich text editors (most reliable cross-browser) + document.execCommand('insertText', false, text); + // Fallback: dispatch input event for React to pick up + inputEl.dispatchEvent(new InputEvent('input', { bubbles: true, data: text })); + inputEl.dispatchEvent(new Event('change', { bubbles: true })); + } + + // ─── Create the comment suggestion box ─────────────────────────────────── + function createCommentBox(postEl, commentText) { + // Remove any existing box on this post + const existing = postEl.querySelector('.' + BOX_CLASS); + if (existing) existing.remove(); + + const isRTL = /[\u0600-\u06FF]/.test(commentText); + + const box = document.createElement('div'); + box.className = BOX_CLASS; + box.innerHTML = ` +