Auto-deploy: 2026-05-18 01:49:47

This commit is contained in:
Hamza-Ayed
2026-05-18 01:49:47 +03:00
parent 6c5fd21be6
commit 42aec81504
6 changed files with 548 additions and 11 deletions

271
post_feed.js Normal file
View File

@@ -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 = `
<div class="lja-cb-header">
<span class="lja-cb-icon">🤖</span>
<span class="lja-cb-title">Smart Comment</span>
<button class="lja-cb-close" title="Close">✕</button>
</div>
<textarea
class="lja-cb-text"
dir="${isRTL ? 'rtl' : 'ltr'}"
style="text-align: ${isRTL ? 'right' : 'left'}"
>${commentText}</textarea>
<div class="lja-cb-actions">
<button class="lja-cb-copy">📋 Copy</button>
<button class="lja-cb-paste">✅ Paste into Comment</button>
<button class="lja-cb-regen">🔄 Regenerate</button>
</div>
`;
// 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 = '<span class="lja-spinner"></span> 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);
}
})();