Auto-deploy: 2026-05-18 01:49:47
This commit is contained in:
@@ -17,7 +17,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||||||
|
|
||||||
// ─── Core API call ───────────────────────────────────────────────────────────
|
// ─── 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
|
// Rate limit check
|
||||||
const canProceed = await checkRateLimit();
|
const canProceed = await checkRateLimit();
|
||||||
if (!canProceed) {
|
if (!canProceed) {
|
||||||
@@ -46,7 +46,8 @@ async function handleGeminiRequest({ apiKey, prompt, tab, action = 'generateText
|
|||||||
apiKey: apiKey,
|
apiKey: apiKey,
|
||||||
prompt: trimmedPrompt,
|
prompt: trimmedPrompt,
|
||||||
jobDescription: jobDescription,
|
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.');
|
if (!data.pdf) throw new Error('Empty PDF response from server.');
|
||||||
await incrementUsage();
|
await incrementUsage();
|
||||||
return { pdf: data.pdf, filename: data.filename, fromCache: false };
|
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 {
|
} else {
|
||||||
const text = data.candidates?.[0]?.content?.parts?.[0]?.text;
|
const text = data.candidates?.[0]?.content?.parts?.[0]?.text;
|
||||||
if (!text) {
|
if (!text) {
|
||||||
|
|||||||
@@ -1,20 +1,32 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "LinkedIn Job Analyzer",
|
"name": "LinkedIn Job Analyzer",
|
||||||
"version": "1.3.0",
|
"version": "1.4.0",
|
||||||
"description": "AI-powered job analysis tool for LinkedIn — personal use",
|
"description": "AI-powered job analysis + smart comment generator for LinkedIn — personal use",
|
||||||
"permissions": ["storage", "activeTab", "scripting"],
|
"permissions": ["storage", "activeTab", "scripting", "clipboardWrite"],
|
||||||
"host_permissions": [
|
"host_permissions": [
|
||||||
"https://www.linkedin.com/*",
|
"https://www.linkedin.com/*",
|
||||||
"https://cv.intaleqapp.com/*",
|
"https://cv.intaleqapp.com/*",
|
||||||
"https://generativelanguage.googleapis.com/*"
|
"https://generativelanguage.googleapis.com/*"
|
||||||
],
|
],
|
||||||
"content_scripts": [{
|
"content_scripts": [
|
||||||
"matches": ["https://www.linkedin.com/jobs/*"],
|
{
|
||||||
"js": ["prompts.js", "content.js"],
|
"matches": ["https://www.linkedin.com/jobs/*"],
|
||||||
"css": ["overlay.css"],
|
"js": ["prompts.js", "content.js"],
|
||||||
"run_at": "document_idle"
|
"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": {
|
"action": {
|
||||||
"default_popup": "popup.html",
|
"default_popup": "popup.html",
|
||||||
"default_icon": {
|
"default_icon": {
|
||||||
|
|||||||
167
post_feed.css
Normal file
167
post_feed.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
271
post_feed.js
Normal file
271
post_feed.js
Normal 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);
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -160,3 +160,58 @@ if ($action === 'generateText') {
|
|||||||
echo $response;
|
echo $response;
|
||||||
exit;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
22
server/prompts/comment_prompt.txt
Normal file
22
server/prompts/comment_prompt.txt
Normal file
@@ -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}}
|
||||||
Reference in New Issue
Block a user