388 lines
16 KiB
JavaScript
388 lines
16 KiB
JavaScript
// search_analyzer.js — LinkedIn People Search Investor Analyzer
|
|
// Operates on linkedin.com/search/results/people/*
|
|
|
|
(function () {
|
|
'use strict';
|
|
|
|
console.log('[LJA] Search Analyzer Script Loaded');
|
|
|
|
// Prevent double injection
|
|
if (window.__linkedinSearchAnalyzerLoaded) return;
|
|
window.__linkedinSearchAnalyzerLoaded = true;
|
|
|
|
// ─── Utility: get stored settings ────────────────────────────────────────
|
|
function getSettings() {
|
|
return new Promise(resolve => {
|
|
if (!chrome || !chrome.storage) {
|
|
resolve({});
|
|
return;
|
|
}
|
|
chrome.storage.sync.get(['apiKey', 'language', 'userProfile'], (syncData) => {
|
|
if (syncData && syncData.apiKey) {
|
|
resolve(syncData);
|
|
} else {
|
|
chrome.storage.local.get(['apiKey', 'language', 'userProfile'], (localData) => {
|
|
resolve(localData || {});
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// ─── Find Result Container ───────────────────────────────────────────────
|
|
function getSearchResultsContainer() {
|
|
return document.querySelector('.search-results-container, .reusable-search__entity-result-list, ul.reusable-search__entity-result-list, .search-results__list');
|
|
}
|
|
|
|
// ─── Extract Person Data ─────────────────────────────────────────────────
|
|
function extractPersonData(cardEl) {
|
|
const data = {
|
|
name: '',
|
|
headline: '',
|
|
location: '',
|
|
summary: ''
|
|
};
|
|
|
|
try {
|
|
// 1. Get all text lines, filter out empty ones
|
|
const rawLines = cardEl.innerText.split('\n').map(s => s.trim()).filter(s => s.length > 1);
|
|
|
|
// 2. Filter out known noise (translation extensions, action buttons, connection degrees, etc)
|
|
const lines = rawLines.filter(s => {
|
|
const low = s.toLowerCase();
|
|
// Remove degree connections
|
|
if (low.includes('degree connection')) return false;
|
|
if (low.includes('• 1st') || low.includes('• 2nd') || low.includes('• 3rd')) return false;
|
|
if (['1st', '2nd', '3rd', '3rd+'].includes(low)) return false;
|
|
// Remove translation artifacts
|
|
if (low.includes('english (australia)') || low === 'auto' || low === 'translate') return false;
|
|
// Remove action buttons (exact match to avoid removing headlines like "Connect with me")
|
|
if (low === 'connect' || low === 'message' || low === 'pending' || low === 'follow' || low === 'view profile') return false;
|
|
// Remove mutual connections line
|
|
if (low.includes('mutual connection')) return false;
|
|
// Remove injected button text from this extension
|
|
if (low.includes('scan investor')) return false;
|
|
if (low.includes('تجاهله') || low.includes('تواصل معه') || low.includes('error')) return false;
|
|
return true;
|
|
});
|
|
|
|
// 3. Assign the cleaned lines
|
|
// Remove duplicates if the name appears twice (e.g., "Hamza" then "Hamza • 2nd" filtered to "Hamza")
|
|
let uniqueLines = [];
|
|
lines.forEach(l => { if (!uniqueLines.includes(l)) uniqueLines.push(l); });
|
|
|
|
if (uniqueLines.length > 0) data.name = uniqueLines[0];
|
|
if (uniqueLines.length > 1) data.headline = uniqueLines[1];
|
|
if (uniqueLines.length > 2) data.location = uniqueLines[2];
|
|
|
|
// Everything else is summary
|
|
if (uniqueLines.length > 3) {
|
|
data.summary = uniqueLines.slice(3, 8).join(' | ');
|
|
}
|
|
|
|
} catch (e) {
|
|
console.error('[LJA] Extraction failed', e);
|
|
}
|
|
|
|
if (!data.name || data.name.length < 2) data.name = 'مستثمر محتمل';
|
|
if (!data.headline) data.headline = 'لا يوجد مسمى وظيفي';
|
|
|
|
return data;
|
|
}
|
|
|
|
// ─── Find Cards Logic ──────────────────────────────────────────────────────
|
|
function findCards() {
|
|
let uniqueCards = new Set();
|
|
|
|
// Find the main Action Button (Connect/Message/Follow) which every profile card has
|
|
let allButtons = Array.from(document.querySelectorAll('button, a'));
|
|
|
|
let actionElements = allButtons.filter(el => {
|
|
if (!el.innerText) return false;
|
|
let txt = el.innerText.toLowerCase().trim();
|
|
if (txt.length === 0 || txt.length > 20) return false;
|
|
// Exact match ONLY: avoid "8K followers" matching "follow"
|
|
return txt === 'connect' || txt === 'message' || txt === 'pending' || txt === 'follow' || txt === '+ connect';
|
|
});
|
|
|
|
console.log('[LJA] Exact action buttons found:', actionElements.length, actionElements.map(e => e.innerText.trim()));
|
|
|
|
actionElements.forEach(btn => {
|
|
// Go up and find the FIRST/SMALLEST ancestor with enough text to be a profile card
|
|
let container = btn.parentElement;
|
|
for(let i=0; i<25; i++) {
|
|
if (!container || container === document.body) break;
|
|
const txt = container.innerText;
|
|
// A profile card has at least 100 chars (name + headline + location + button)
|
|
// and not too many (< 3000 so we don't grab the whole results list)
|
|
if (txt && txt.length > 100 && txt.length < 3000) {
|
|
uniqueCards.add(container);
|
|
break;
|
|
}
|
|
container = container.parentElement;
|
|
}
|
|
});
|
|
|
|
const cards = Array.from(uniqueCards);
|
|
console.log('[LJA] Found valid profile cards:', cards.length);
|
|
return cards;
|
|
}
|
|
|
|
// ─── Inject UI into Card ─────────────────────────────────────────────────
|
|
function injectScanButton(cardEl) {
|
|
if (cardEl.querySelector('.lja-scan-person-btn') || cardEl.querySelector('.lja-investor-result')) return;
|
|
|
|
const btn = document.createElement('button');
|
|
btn.className = 'lja-scan-person-btn';
|
|
btn.innerHTML = '🔍 Scan Investor';
|
|
|
|
btn.style.margin = '10px 0';
|
|
btn.style.padding = '5px 15px';
|
|
btn.style.cursor = 'pointer';
|
|
btn.style.backgroundColor = '#6C63FF';
|
|
btn.style.color = '#fff';
|
|
btn.style.border = 'none';
|
|
btn.style.borderRadius = '5px';
|
|
btn.style.fontWeight = 'bold';
|
|
btn.style.zIndex = '999';
|
|
btn.style.position = 'relative';
|
|
|
|
const resultContainer = document.createElement('div');
|
|
resultContainer.className = 'lja-result-wrapper';
|
|
resultContainer.style.width = '100%';
|
|
resultContainer.style.marginTop = '10px';
|
|
|
|
btn.addEventListener('click', async (e) => {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
await scanPerson(cardEl, btn, resultContainer);
|
|
});
|
|
|
|
// Try to append to actions area
|
|
const actionArea = cardEl.querySelector('.entity-result__actions, .search-result__actions');
|
|
if (actionArea) {
|
|
actionArea.prepend(btn);
|
|
actionArea.appendChild(resultContainer);
|
|
} else {
|
|
// Fallback: Find the name link and put it under it
|
|
const profileLink = Array.from(cardEl.querySelectorAll('a[href*="/in/"]')).find(a => a.innerText.trim().length > 0);
|
|
if (profileLink && profileLink.parentElement) {
|
|
profileLink.parentElement.appendChild(btn);
|
|
profileLink.parentElement.appendChild(resultContainer);
|
|
} else {
|
|
cardEl.appendChild(btn);
|
|
cardEl.appendChild(resultContainer);
|
|
}
|
|
}
|
|
|
|
console.log('[LJA] Injected button for a profile:', extractPersonData(cardEl).name);
|
|
}
|
|
|
|
// ─── Scan a Single Person ────────────────────────────────────────────────
|
|
async function scanPerson(cardEl, btnEl, resultContainer) {
|
|
const settings = await getSettings();
|
|
if (!settings || !settings.apiKey) {
|
|
alert('Please set your Gemini API key in the extension popup first.');
|
|
return;
|
|
}
|
|
|
|
const data = extractPersonData(cardEl);
|
|
if (!data.name && !data.headline) {
|
|
console.error('[LJA] Could not extract details, skipping.');
|
|
btnEl.innerHTML = '❌ Extraction Failed';
|
|
return;
|
|
}
|
|
|
|
btnEl.disabled = true;
|
|
btnEl.innerHTML = '<span class="lja-spinner"></span> Scanning...';
|
|
|
|
const prompt = `أنت مستشار استثماري ذكي وصارم جداً في تقييم المستثمرين للشركات الناشئة في الشرق الأوسط.
|
|
المستخدم يبحث عن مستثمرين (Angel Investors) أو شركاء لتمويل تطبيقاته "انطلق" (Intaleq) و "تريبز" (Tripz) (تطبيقات نقل ذكي Ride-hailing).
|
|
|
|
البيانات المستخرجة (من صفحة البحث فقط):
|
|
الاسم: ${data.name}
|
|
المسمى الوظيفي: ${data.headline}
|
|
الموقع: ${data.location}
|
|
نبذة/تاريخ: ${data.summary}
|
|
|
|
مهمتك: التقييم الصارم والتدقيق. الكثير من الأشخاص يكتبون "Angel Investor" الوهمية.
|
|
قواعد التقييم:
|
|
1. (green) تواصل معه: فقط إذا كان يمتلك منصباً قيادياً حقيقياً (CEO, Founder, Director) في شركة معروفة، أو يعمل في صندوق استثماري (VC)، أو لديه خبرة واضحة تدل على ملاءة مالية (مثل مسؤول سابق في بنك أو شركة كبرى).
|
|
2. (red) تجاهله: إذا كان مجرد موظف عادي، أو يكتب "Angel Investor" عند "Self-employed" بدون تاريخ مهني قوي، أو يبدو كشخص مبتدئ لا يمتلك القدرة المالية لتمويل تطبيق بحجم أوبر.
|
|
|
|
يجب أن يكون الرد بصيغة JSON فقط بهذا الشكل:
|
|
{
|
|
"status": "green" أو "red",
|
|
"reason": "سبب التقييم (كن صريحاً وقاسياً إذا كان الشخص يبدو مدعياً، سطر واحد فقط)"
|
|
}
|
|
لا تقم بإضافة أي نص آخر.`;
|
|
|
|
try {
|
|
const response = await chrome.runtime.sendMessage({
|
|
type: 'GEMINI_REQUEST',
|
|
payload: {
|
|
apiKey: settings.apiKey,
|
|
action: 'generateText',
|
|
prompt: prompt
|
|
}
|
|
});
|
|
|
|
if (!response.success) {
|
|
throw new Error(response.error || 'Unknown error');
|
|
}
|
|
|
|
let rawText = response.data.text || response.data;
|
|
rawText = rawText.replace(/```json/gi, '').replace(/```/g, '').trim();
|
|
|
|
let resultData;
|
|
try {
|
|
resultData = JSON.parse(rawText);
|
|
} catch (parseError) {
|
|
throw new Error('Failed to parse AI response. Raw: ' + rawText);
|
|
}
|
|
|
|
const isGreen = resultData.status === 'green';
|
|
const badgeIcon = isGreen ? '✅' : '❌';
|
|
const badgeText = isGreen ? 'تواصل معه' : 'تجاهله';
|
|
const colorClass = isGreen ? 'green' : 'red';
|
|
|
|
resultContainer.innerHTML = `
|
|
<div class="lja-investor-result ${colorClass}" dir="rtl" style="margin-top: 10px; padding: 10px; border-radius: 8px; font-weight: bold; font-family: system-ui; background-color: ${isGreen ? '#e6ffe6' : '#ffe6e6'}; color: ${isGreen ? '#006600' : '#cc0000'}; border: 1px solid ${isGreen ? '#00cc00' : '#ff0000'};">
|
|
<div class="lja-investor-badge">
|
|
<span>${badgeIcon}</span> ${badgeText}
|
|
</div>
|
|
<div class="lja-investor-reason" style="margin-top: 5px; font-weight: normal; font-size: 14px;">
|
|
${resultData.reason}
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
btnEl.style.display = 'none';
|
|
|
|
} catch (e) {
|
|
console.error('[LJA Search]', e);
|
|
resultContainer.innerHTML = `<div class="lja-investor-result red" style="color:red; font-weight:bold;">❌ Error: ${e.message}</div>`;
|
|
btnEl.disabled = false;
|
|
btnEl.innerHTML = '🔍 Scan Investor';
|
|
}
|
|
}
|
|
|
|
// ─── Scan All Feature ────────────────────────────────────────────────────
|
|
function injectScanAllButton() {
|
|
if (document.querySelector('.lja-scan-list-btn')) return;
|
|
|
|
let container = getSearchResultsContainer();
|
|
|
|
const btn = document.createElement('button');
|
|
btn.className = 'lja-scan-list-btn';
|
|
btn.innerHTML = '✨ Scan All Investors';
|
|
btn.title = 'Scan all loaded profiles on this page';
|
|
|
|
btn.style.margin = '20px auto';
|
|
btn.style.display = 'block';
|
|
btn.style.padding = '10px 20px';
|
|
btn.style.cursor = 'pointer';
|
|
btn.style.backgroundColor = '#6C63FF';
|
|
btn.style.color = '#fff';
|
|
btn.style.border = 'none';
|
|
btn.style.borderRadius = '8px';
|
|
btn.style.fontWeight = 'bold';
|
|
btn.style.fontSize = '16px';
|
|
btn.style.boxShadow = '0 4px 6px rgba(0,0,0,0.1)';
|
|
|
|
btn.addEventListener('click', async () => {
|
|
const cards = findCards();
|
|
|
|
if (cards.length === 0) {
|
|
alert('No profiles found to scan.');
|
|
return;
|
|
}
|
|
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<span class="lja-spinner"></span> Scanning profiles...';
|
|
|
|
// Scan sequentially
|
|
for (const card of cards) {
|
|
const scanBtn = card.querySelector('.lja-scan-person-btn');
|
|
if (scanBtn && scanBtn.style.display !== 'none') {
|
|
card.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
await new Promise(r => setTimeout(r, 500));
|
|
scanBtn.click();
|
|
while (scanBtn.disabled && scanBtn.style.display !== 'none') {
|
|
await new Promise(r => setTimeout(r, 500));
|
|
}
|
|
}
|
|
}
|
|
|
|
btn.disabled = false;
|
|
btn.innerHTML = '✨ Scan Complete!';
|
|
setTimeout(() => { btn.innerHTML = '✨ Scan All Investors'; }, 3000);
|
|
});
|
|
|
|
if (container && container.parentNode) {
|
|
container.parentNode.insertBefore(btn, container);
|
|
} else {
|
|
// Fallback: Floating button bottom right
|
|
btn.style.position = 'fixed';
|
|
btn.style.bottom = '20px';
|
|
btn.style.right = '20px';
|
|
btn.style.zIndex = '99999';
|
|
btn.style.margin = '0';
|
|
document.body.appendChild(btn);
|
|
}
|
|
console.log('[LJA] Injected Scan All button');
|
|
}
|
|
|
|
// ─── Process Page ────────────────────────────────────────────────────────
|
|
function processPage() {
|
|
if (!window.location.href.includes('linkedin.com/search/results/')) return;
|
|
|
|
console.log('[LJA] Attempting to find profile cards...');
|
|
const cards = findCards();
|
|
console.log('[LJA] processPage found cards:', cards.length);
|
|
|
|
if (cards.length > 0) {
|
|
injectScanAllButton();
|
|
cards.forEach(injectScanButton);
|
|
} else {
|
|
console.log('[LJA] No cards could be identified on this page.');
|
|
}
|
|
}
|
|
|
|
// ─── MutationObserver: watch for new results (pagination/filters) ────────
|
|
let observerTimer;
|
|
const observer = new MutationObserver(() => {
|
|
if (observerTimer) return;
|
|
observerTimer = setTimeout(() => {
|
|
processPage();
|
|
observerTimer = null;
|
|
}, 1500);
|
|
});
|
|
|
|
// ─── Initialize ──────────────────────────────────────────────────────────
|
|
function init() {
|
|
console.log('[LJA] Initializing Search Analyzer...');
|
|
processPage();
|
|
observer.observe(document.body, { childList: true, subtree: true });
|
|
}
|
|
|
|
// Handle SPA navigation by checking URL changes
|
|
let lastUrl = window.location.href;
|
|
setInterval(() => {
|
|
if (window.location.href !== lastUrl) {
|
|
lastUrl = window.location.href;
|
|
if (lastUrl.includes('linkedin.com/search/results/')) {
|
|
console.log('[LJA] SPA Navigation detected, re-processing...');
|
|
setTimeout(processPage, 2000);
|
|
}
|
|
}
|
|
}, 1000);
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
} else {
|
|
init();
|
|
setTimeout(init, 3000); // Fallback for delayed renders
|
|
}
|
|
})();
|