From 42aec81504f6d16fff20d9c58368352817c3ebb2 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Mon, 18 May 2026 01:49:47 +0300 Subject: [PATCH] Auto-deploy: 2026-05-18 01:49:47 --- background.js | 14 +- manifest.json | 30 +++- post_feed.css | 167 ++++++++++++++++++ post_feed.js | 271 ++++++++++++++++++++++++++++++ server/generate_cv.php | 55 ++++++ server/prompts/comment_prompt.txt | 22 +++ 6 files changed, 548 insertions(+), 11 deletions(-) create mode 100644 post_feed.css create mode 100644 post_feed.js create mode 100644 server/prompts/comment_prompt.txt 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 = ` +
+ 🤖 + Smart Comment + +
+ +
+ + + +
+ `; + + // Close + box.querySelector('.lja-cb-close').addEventListener('click', () => box.remove()); + + // Copy to clipboard + box.querySelector('.lja-cb-copy').addEventListener('click', function () { + const text = box.querySelector('.lja-cb-text').value; + navigator.clipboard.writeText(text).then(() => { + this.textContent = '✅ Copied!'; + setTimeout(() => { this.textContent = '📋 Copy'; }, 1500); + }); + }); + + // Paste into LinkedIn comment field + box.querySelector('.lja-cb-paste').addEventListener('click', function () { + const text = box.querySelector('.lja-cb-text').value; + + // First click the LinkedIn comment button to open the field if not open + const commentTrigger = postEl.querySelector( + '.comment-button, [aria-label*="comment" i], [data-control-name="comment"]' + ); + if (commentTrigger) commentTrigger.click(); + + setTimeout(() => { + const inputEl = findCommentInput(postEl); + if (inputEl) { + fillCommentInput(inputEl, text); + this.textContent = '✅ Done!'; + setTimeout(() => { this.textContent = '✅ Paste into Comment'; }, 1500); + } else { + // Fallback: copy to clipboard and tell user + navigator.clipboard.writeText(text); + this.textContent = '📋 Copied! Paste manually'; + } + }, 300); + }); + + // Regenerate + box.querySelector('.lja-cb-regen').addEventListener('click', async function () { + box.remove(); + await generateComment(postEl); + }); + + // Insert before the action bar + const actionBar = findActionBar(postEl); + if (actionBar) { + actionBar.parentNode.insertBefore(box, actionBar.nextSibling); + } else { + postEl.appendChild(box); + } + } + + // ─── Core: call server and generate comment ─────────────────────────────── + async function generateComment(postEl) { + const settings = await getSettings(); + if (!settings.apiKey) { + alert('Please set your Gemini API key in the extension popup first.'); + return; + } + + const postText = extractPostText(postEl); + if (!postText) { + alert('Could not read post text. The post might be an image or video.'); + return; + } + + // Show spinner on the button + const btn = postEl.querySelector('.' + BUTTON_CLASS); + if (btn) { + btn.disabled = true; + btn.innerHTML = ' Thinking...'; + } + + try { + const response = await chrome.runtime.sendMessage({ + type: 'GEMINI_REQUEST', + payload: { + apiKey: settings.apiKey, + action: 'generateComment', + postText: postText + } + }); + + if (!response.success) { + throw new Error(response.error || 'Unknown error'); + } + + const commentText = response.data.comment || response.data; + createCommentBox(postEl, commentText); + + } catch (e) { + console.error('[LJA Feed]', e); + alert('Comment generation failed: ' + e.message); + } finally { + if (btn) { + btn.disabled = false; + btn.innerHTML = '💬 Smart Comment'; + } + } + } + + // ─── Inject button into a single post ──────────────────────────────────── + function injectButton(postEl) { + if (postEl.querySelector('.' + BUTTON_CLASS)) return; // Already injected + if (!extractPostText(postEl)) return; // Skip non-text posts silently + + const btn = document.createElement('button'); + btn.className = BUTTON_CLASS; + btn.innerHTML = '💬 Smart Comment'; + btn.title = 'Generate an AI-powered comment for this post'; + + btn.addEventListener('click', (e) => { + e.stopPropagation(); + generateComment(postEl); + }); + + const actionBar = findActionBar(postEl); + if (actionBar) { + actionBar.appendChild(btn); + } + } + + // ─── Process all posts on the page ─────────────────────────────────────── + function processAllPosts() { + const posts = document.querySelectorAll( + '.feed-shared-update-v2, .occludable-update, [data-urn*="activity"]' + ); + posts.forEach(injectButton); + } + + // ─── MutationObserver: watch for new posts (infinite scroll) ───────────── + const observer = new MutationObserver((mutations) => { + for (const mutation of mutations) { + for (const node of mutation.addedNodes) { + if (node.nodeType !== Node.ELEMENT_NODE) continue; + // Check if the node itself is a post + if ( + node.classList?.contains('feed-shared-update-v2') || + node.classList?.contains('occludable-update') || + node.dataset?.urn?.includes('activity') + ) { + injectButton(node); + } + // Or if it contains posts + const innerPosts = node.querySelectorAll?.( + '.feed-shared-update-v2, .occludable-update, [data-urn*="activity"]' + ); + innerPosts?.forEach(injectButton); + } + } + }); + + // ─── Initialize ────────────────────────────────────────────────────────── + function init() { + processAllPosts(); + observer.observe(document.body, { childList: true, subtree: true }); + } + + // Wait for DOM to be ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + // Run immediately, then again after a short delay for LinkedIn's SPA + init(); + setTimeout(init, 2000); + } +})(); diff --git a/server/generate_cv.php b/server/generate_cv.php index 80993ea..9a96130 100644 --- a/server/generate_cv.php +++ b/server/generate_cv.php @@ -160,3 +160,58 @@ if ($action === 'generateText') { echo $response; exit; } + +// ========================================== +// ACTION 3: Smart Comment Generator +// ========================================== +if ($action === 'generateComment') { + $postText = substr($data['postText'] ?? '', 0, 3000); + + if (empty($postText)) { + http_response_code(400); + echo json_encode(["error" => "postText is required."]); + exit; + } + + $promptFile = __DIR__ . '/prompts/comment_prompt.txt'; + if (!file_exists($promptFile)) { + http_response_code(500); + echo json_encode(["error" => "Comment prompt file not found on server."]); + exit; + } + + $promptTemplate = file_get_contents($promptFile); + $prompt = str_replace('{{POST_TEXT}}', $postText, $promptTemplate); + + $payload = json_encode([ + "contents" => [["parts" => [["text" => $prompt]]]], + "generationConfig" => ["temperature" => 0.75, "maxOutputTokens" => 256] + ]); + + $ch = curl_init($geminiUrl); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode !== 200) { + http_response_code(500); + echo json_encode(["error" => "Gemini API Error", "details" => json_decode($response)]); + exit; + } + + $responseData = json_decode($response, true); + $commentText = trim($responseData['candidates'][0]['content']['parts'][0]['text'] ?? ''); + + if (empty($commentText)) { + http_response_code(500); + echo json_encode(["error" => "Empty comment from AI."]); + exit; + } + + echo json_encode(["success" => true, "comment" => $commentText]); + exit; +} diff --git a/server/prompts/comment_prompt.txt b/server/prompts/comment_prompt.txt new file mode 100644 index 0000000..4d97eaa --- /dev/null +++ b/server/prompts/comment_prompt.txt @@ -0,0 +1,22 @@ +You are a senior LinkedIn commentator writing on behalf of Hamza Ayed, a Senior Solutions Architect and technical leader with 20+ years of experience in enterprise software, GIS, mobility platforms, and AI systems. + +Your task is to write ONE high-quality, concise comment on a LinkedIn post. + +STRICT RULES: +1. LANGUAGE: Detect the post language automatically. + - If Arabic → respond in simple, clear, modern Arabic (not formal classical Arabic). + - If English → respond in English. +2. LENGTH: Maximum 4 lines. Never exceed this. Brevity is power. +3. STRUCTURE: Follow this pattern: + - Line 1: Acknowledge or frame the idea (1 sentence, not generic praise). + - Lines 2-3: Add a real insight, flag a gap, or extend the idea with a specific point. + - Line 4: A sharp closing thought or practical suggestion. +4. TONE: Constructive, confident, and intellectually honest. Not sycophantic. +5. VALUE: Every sentence must add something the author or readers didn't already know. +6. HONESTY: If the post has a flaw or is oversimplified, say so — diplomatically but directly. +7. NO EMOJIS inside the comment body. +8. FORBIDDEN OPENERS: Do not start with "Great post!", "Interesting!", "Well said!", "شكراً", "منشور رائع", or any generic filler. +9. OUTPUT: Return ONLY the comment text. No explanations, no labels, no quotes around it. + +POST TO COMMENT ON: +{{POST_TEXT}}