// 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')); console.log('[LJA] Total buttons/links on page:', allButtons.length); let actionElements = allButtons.filter(el => { if (!el.innerText) return false; let txt = el.innerText.toLowerCase().trim(); // Must contain the word, but not be a huge paragraph if (txt.length === 0 || txt.length > 20) return false; return txt.includes('connect') || txt.includes('message') || txt.includes('pending') || txt.includes('follow'); }); console.log('[LJA] Action buttons found (Connect/Message/etc):', actionElements.length, actionElements.map(e => e.innerText.trim())); actionElements.forEach(btn => { let container = btn.parentElement; // Go up the DOM tree (max 15 levels) to find the list item wrapper let found = false; for(let i=0; i<15; i++) { if (!container) break; // A valid card container is usually an LI if (container.tagName === 'LI') { // Ensure it has some text (a profile has name, headline, etc) if (container.innerText && container.innerText.length > 30) { uniqueCards.add(container); found = true; break; } } // Fallback for non-LI structures if (container.classList && (container.classList.contains('reusable-search__result-container') || container.classList.contains('search-entity'))) { uniqueCards.add(container); found = true; break; } container = container.parentElement; } if (!found) { console.log('[LJA] Button found but could not find card container for:', btn.innerText.trim()); } }); 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 = ' Scanning...'; const prompt = `أنت مستشار استثماري خبير في الشركات الناشئة (Startups) في الشرق الأوسط ومصر. المستخدم يبحث عن مستثمرين (Angel Investors) أو أشخاص يمكنهم توصيله بمستثمرين لتطبيقاته "انطلق" (Intaleq) و "تريبز" (Tripz) وهي تطبيقات نقل ذكي (Ride-hailing) في مصر والشرق الأوسط. يرجى تقييم هذا الشخص بناءً على المعلومات التالية المستخرجة من لينكد إن: الاسم: ${data.name} المسمى الوظيفي: ${data.headline} الموقع: ${data.location} نبذة: ${data.summary} هل هذا الشخص مناسب للتواصل معه وعرض الاستثمار أو طلب توصية بمستثمرين؟ إذا كان الشخص مستثمرًا فعليًا أو يعمل في صندوق استثماري (VC) أو لديه شبكة علاقات قوية في ريادة الأعمال أو يدعم الشركات الناشئة، أعطه العلامة الخضراء (green). إذا كان الشخص لا يبدو مرتبطًا بالاستثمار (مثلاً مجرد موظف عادي، أو يبحث عن عمل، أو في مجال بعيد جداً عن دعم الشركات)، أعطه العلامة الحمراء (red) لكي لا يضيع المستخدم وقته معه. يجب أن يكون الرد بصيغة JSON فقط بهذا الشكل: { "status": "green" أو "red", "reason": "سبب قصير جداً باللغة العربية يشرح لماذا (سطر واحد)" } لا تقم بإضافة أي نص آخر أو Markdown.`; 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 = `
${badgeIcon} ${badgeText}
${resultData.reason}
`; btnEl.style.display = 'none'; } catch (e) { console.error('[LJA Search]', e); resultContainer.innerHTML = `
❌ Error: ${e.message}
`; 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 = ' 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 } })();