Files
cv/post_feed.js
2026-05-26 17:51:21 +03:00

451 lines
16 KiB
JavaScript

// post_feed.js — LinkedIn Feed Smart Comment Generator
// Operates ONLY on linkedin.com/feed pages — fully independent from content.js
(function () {
'use strict';
const BUTTON_CLASS = 'lja-comment-btn';
const BOX_CLASS = 'lja-comment-box';
// ─── Utility: get stored settings ────────────────────────────────────────
function getSettings() {
return new Promise(resolve => {
if (!chrome || !chrome.storage) {
resolve({});
return;
}
chrome.storage.sync.get(['apiKey', 'language'], (syncData) => {
if (syncData && syncData.apiKey) {
resolve(syncData);
} else {
chrome.storage.local.get(['apiKey', 'language'], (localData) => {
resolve(localData || {});
});
}
});
});
}
// ─── Extract post text from a feed item ──────────────────────────────────
function extractPostText(postEl) {
const selectors = [
'[data-testid="expandable-text-box"]', // New LinkedIn UI
'.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) {
// Strip out the "... more" text if it exists
const text = el.textContent.replace(/…\s*more/gi, '').trim();
if (text.length > 20) return text;
}
}
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;
}
// New UI fallback: find the comment button SVG and get its parent container
const commentSvg = postEl.querySelector('svg#comment-small, svg[data-test-icon="comment-small"]');
if (commentSvg) {
const btn = commentSvg.closest('button');
if (btn && btn.parentNode) {
return btn.parentNode; // This is the action bar container
}
}
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"]',
'[contenteditable="true"][role="textbox"]',
'div[contenteditable="true"]'
];
for (const sel of selectors) {
const el = postEl.querySelector(sel);
if (el) return el;
}
return null;
}
// ─── Fill comment input (handles LinkedIn's Quill editor) ────────────────
function fillCommentInput(inputEl, text) {
inputEl.focus();
inputEl.innerHTML = '';
document.execCommand('insertText', false, text);
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, arabicSummary) {
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 & Analysis</span>
<button class="lja-cb-close" title="Close">✕</button>
</div>
<div style="padding: 10px 14px; background: rgba(108, 99, 255, 0.05); border-bottom: 1px solid rgba(255, 255, 255, 0.06);">
<div style="font-size: 11px; color: #ff8787; margin-bottom: 4px; font-weight: 600;">🕵️ التحليل والمصداقية:</div>
<div style="font-size: 12px; color: #e0deff; line-height: 1.5; text-align: right;" dir="rtl">
${arabicSummary}
</div>
</div>
<textarea
class="lja-cb-text"
dir="${isRTL ? 'rtl' : 'ltr'}"
style="text-align: ${isRTL ? 'right' : 'left'}; font-family: monospace; font-size: 13px;"
>${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>
`;
box.querySelector('.lja-cb-close').addEventListener('click', () => box.remove());
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);
});
});
box.querySelector('.lja-cb-paste').addEventListener('click', function () {
const text = box.querySelector('.lja-cb-text').value;
const commentTrigger = postEl.querySelector(
'.comment-button, [aria-label*="comment" i], [data-control-name="comment"], svg#comment-small'
);
let btnToClick = commentTrigger;
if (btnToClick && btnToClick.tagName.toLowerCase() === 'svg') {
btnToClick = btnToClick.closest('button') || btnToClick;
}
if (btnToClick) btnToClick.click();
setTimeout(() => {
const inputEl = findCommentInput(postEl);
if (inputEl) {
fillCommentInput(inputEl, text);
this.textContent = '✅ Done!';
setTimeout(() => { this.textContent = '✅ Paste into Comment'; }, 1500);
} else {
navigator.clipboard.writeText(text);
this.textContent = '📋 Copied! Paste manually';
}
}, 500);
});
box.querySelector('.lja-cb-regen').addEventListener('click', async function () {
box.remove();
await generateComment(postEl);
});
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 || !settings.apiKey) {
alert('Please set your Gemini API key in the extension popup first.\n(Debug: Key not found in storage)');
return;
}
const postText = extractPostText(postEl);
if (!postText) {
alert('Could not read post text. The post might be an image or video.');
return;
}
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');
}
// Parse the JSON returned by Gemini
let resultData;
let rawText = response.data.comment || response.data;
// Clean up markdown blocks if the AI accidentally adds them
rawText = rawText.replace(/```json/gi, '').replace(/```/g, '').trim();
try {
resultData = JSON.parse(rawText);
} catch (parseError) {
throw new Error('Failed to parse AI response. Raw output: ' + rawText);
}
const arabicSummary = resultData.arabic_summary || 'لم يتم توليد ملخص.';
const commentText = resultData.comment || '';
createCommentBox(postEl, commentText, arabicSummary);
} catch (e) {
console.error('[LJA Feed]', e);
alert('Comment generation failed: ' + e.message);
} finally {
if (btn) {
btn.disabled = false;
btn.innerHTML = '💬 Smart Comment';
}
}
}
// ─── Core: call server and repurpose post ───────────────────────────────
async function repurposePost(postEl) {
const settings = await getSettings();
if (!settings || !settings.apiKey) {
alert('Please set your Gemini API key in the extension popup first.\n(Debug: Key not found in storage)');
return;
}
const postText = extractPostText(postEl);
if (!postText) {
alert('Could not read post text. The post might be an image or video.');
return;
}
const btn = postEl.querySelector('.lja-rewrite-btn');
if (btn) {
btn.disabled = true;
btn.innerHTML = '<span class="lja-spinner"></span> Rewriting...';
}
try {
const response = await chrome.runtime.sendMessage({
type: 'GEMINI_REQUEST',
payload: {
apiKey: settings.apiKey,
action: 'repurposePost',
postText: postText
}
});
if (!response.success) {
throw new Error(response.error || 'Unknown error');
}
const resultText = response.data.result || response.data;
// We reuse the comment box UI but style it differently, or just use the same box but change the title and actions.
createRepurposeBox(postEl, resultText);
} catch (e) {
console.error('[LJA Feed]', e);
alert('Post rewriting failed: ' + e.message);
} finally {
if (btn) {
btn.disabled = false;
btn.innerHTML = '📝 Rewrite';
}
}
}
// ─── Create the repurpose result box ─────────────────────────────────────
function createRepurposeBox(postEl, resultText) {
const existing = postEl.querySelector('.' + BOX_CLASS);
if (existing) existing.remove();
const isRTL = /[\u0600-\u06FF]/.test(resultText);
// Split text into Post and Image Prompt if the separator exists
let postContent = resultText;
let imagePrompt = '';
const parts = resultText.split('--- IMAGE PROMPT ---');
if (parts.length > 1) {
postContent = parts[0].trim();
imagePrompt = parts[1].trim();
}
const box = document.createElement('div');
box.className = BOX_CLASS;
box.innerHTML = `
<div class="lja-cb-header" style="background: linear-gradient(135deg, #FF6B6B 0%, #C92A2A 100%);">
<span class="lja-cb-icon">✍️</span>
<span class="lja-cb-title">Rewritten Post</span>
<button class="lja-cb-close" title="Close">✕</button>
</div>
<div style="padding: 12px; font-size: 13px; color: #666; font-weight: 500; border-bottom: 1px solid rgba(0,0,0,0.05);">
Post Content:
</div>
<textarea
class="lja-cb-text"
dir="${isRTL ? 'rtl' : 'ltr'}"
style="text-align: ${isRTL ? 'right' : 'left'}; min-height: 120px;"
>${postContent}</textarea>
${imagePrompt ? `
<div style="padding: 12px; font-size: 13px; color: #666; font-weight: 500; border-top: 1px solid rgba(0,0,0,0.05); border-bottom: 1px solid rgba(0,0,0,0.05);">
Image Generation Prompt (Midjourney / DALL-E):
</div>
<textarea
class="lja-cb-image-prompt"
style="width: 100%; border: none; padding: 12px; background: rgba(0,0,0,0.02); resize: vertical; min-height: 80px; font-family: monospace; font-size: 12px; outline: none;"
readonly
>${imagePrompt}</textarea>
` : ''}
<div class="lja-cb-actions">
<button class="lja-cb-copy">📋 Copy Post</button>
${imagePrompt ? '<button class="lja-cb-copy-img">🎨 Copy Image Prompt</button>' : ''}
</div>
`;
box.querySelector('.lja-cb-close').addEventListener('click', () => box.remove());
box.querySelector('.lja-cb-copy').addEventListener('click', function () {
const text = box.querySelector('.lja-cb-text').value;
navigator.clipboard.writeText(text).then(() => {
this.textContent = '✅ Post Copied!';
setTimeout(() => { this.textContent = '📋 Copy Post'; }, 1500);
});
});
const copyImgBtn = box.querySelector('.lja-cb-copy-img');
if (copyImgBtn) {
copyImgBtn.addEventListener('click', function () {
const text = box.querySelector('.lja-cb-image-prompt').value;
navigator.clipboard.writeText(text).then(() => {
this.textContent = '✅ Prompt Copied!';
setTimeout(() => { this.textContent = '🎨 Copy Image Prompt'; }, 1500);
});
});
}
const actionBar = findActionBar(postEl);
if (actionBar) {
actionBar.parentNode.insertBefore(box, actionBar.nextSibling);
} else {
postEl.appendChild(box);
}
}
// ─── Inject button into a single post ────────────────────────────────────
function injectButton(postEl) {
if (postEl.querySelector('.' + BUTTON_CLASS)) return; // Already injected
const actionBar = findActionBar(postEl);
if (!actionBar) return;
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();
e.preventDefault();
generateComment(postEl);
});
const rewriteBtn = document.createElement('button');
rewriteBtn.className = 'lja-rewrite-btn';
rewriteBtn.innerHTML = '📝 Rewrite';
rewriteBtn.title = 'Rewrite this post in your own style and generate an image prompt';
// Add some inline styles for the rewrite button to differentiate it
rewriteBtn.style.background = 'linear-gradient(135deg, #FF8787 0%, #E03131 100%)';
rewriteBtn.style.color = 'white';
rewriteBtn.style.border = 'none';
rewriteBtn.style.borderRadius = '16px';
rewriteBtn.style.padding = '4px 12px';
rewriteBtn.style.fontSize = '12px';
rewriteBtn.style.fontWeight = '600';
rewriteBtn.style.cursor = 'pointer';
rewriteBtn.style.marginLeft = '8px';
rewriteBtn.style.display = 'flex';
rewriteBtn.style.alignItems = 'center';
rewriteBtn.style.gap = '4px';
rewriteBtn.addEventListener('click', (e) => {
e.stopPropagation();
e.preventDefault();
repurposePost(postEl);
});
actionBar.appendChild(btn);
actionBar.appendChild(rewriteBtn);
}
// ─── Process all posts on the page ───────────────────────────────────────
function processAllPosts() {
// Check both classic LinkedIn UI and the new React/hash-class UI
const posts = document.querySelectorAll(
'.feed-shared-update-v2, .occludable-update, [data-urn*="activity"], [role="listitem"]'
);
posts.forEach(injectButton);
}
// ─── MutationObserver: watch for new posts (infinite scroll) ─────────────
let observerTimer;
const observer = new MutationObserver(() => {
// Throttle the DOM scanning to prevent heavy performance hits on React updates
if (observerTimer) return;
observerTimer = setTimeout(() => {
processAllPosts();
observerTimer = null;
}, 500);
});
// ─── Initialize ──────────────────────────────────────────────────────────
function init() {
processAllPosts();
observer.observe(document.body, { childList: true, subtree: true });
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
setTimeout(init, 2000); // Fallback for delayed SPA renders
}
})();