// 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(); // The most bulletproof way to find a profile card is to find the main Action Button (Connect/Message/Follow) // and then go up the DOM tree until we find the container that holds the whole profile let buttons = Array.from(document.querySelectorAll('button')).filter(btn => { let txt = btn.innerText.toLowerCase().trim(); return txt === 'connect' || txt === 'message' || txt === 'pending' || txt === 'follow'; }); buttons.forEach(btn => { let container = btn.parentElement; // Go up the DOM tree (max 10 levels) to find the list item for(let i=0; i<10; i++) { if (!container) break; // A valid card container is usually an LI and contains an image if (container.tagName === 'LI' && container.querySelector('img')) { 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 = ' 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 = `