// 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 = ' 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 = `