378 lines
15 KiB
JavaScript
378 lines
15 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 {
|
|
// The most robust way to extract data on LinkedIn is reading the visible text lines.
|
|
// Profile cards always follow: Name -> Degree -> Headline -> Location -> ...
|
|
const rawLines = cardEl.innerText.split('\n').map(s => s.trim()).filter(s => s.length > 1);
|
|
|
|
// Remove lines that are just action buttons or connection degrees
|
|
const lines = rawLines.filter(s => {
|
|
const low = s.toLowerCase();
|
|
return !low.includes('degree connection') &&
|
|
!['1st', '2nd', '3rd', '3rd+'].includes(low) &&
|
|
!low.includes('connect') &&
|
|
!low.includes('message') &&
|
|
!low.includes('pending') &&
|
|
!low.includes('follow') &&
|
|
!low.includes('view profile');
|
|
});
|
|
|
|
if (lines.length > 0) data.name = lines[0];
|
|
if (lines.length > 1) data.headline = lines[1];
|
|
if (lines.length > 2) data.location = lines[2];
|
|
|
|
// Summary is everything else
|
|
if (lines.length > 3) {
|
|
data.summary = lines.slice(3, 8).join(' | ');
|
|
}
|
|
|
|
// Cleanup name if it accidentally caught something weird
|
|
if (data.name && data.name.includes('LinkedIn')) {
|
|
data.name = 'مستثمر محتمل';
|
|
}
|
|
|
|
} catch (e) {
|
|
console.error('[LJA] Extraction failed', e);
|
|
data.name = 'مستثمر محتمل';
|
|
data.summary = cardEl.innerText ? cardEl.innerText.substring(0, 300) : '';
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
// ─── Find Cards Logic ──────────────────────────────────────────────────────
|
|
function findCards() {
|
|
let cards = [];
|
|
|
|
// LinkedIn search results are usually list items with an image and an action button
|
|
let listItems = Array.from(document.querySelectorAll('li, .reusable-search__result-container, .search-entity'));
|
|
|
|
// Filter to only those that look like profile cards
|
|
let validCards = listItems.filter(el => {
|
|
const txt = el.innerText.toLowerCase();
|
|
// Must have significant text, an image, and a typical LinkedIn action button
|
|
const hasAction = txt.includes('connect') || txt.includes('message') || txt.includes('pending') || txt.includes('follow');
|
|
const hasImage = el.querySelector('img');
|
|
const isNotSidebar = !txt.includes('about') && !txt.includes('accessibility');
|
|
return txt.length > 50 && hasAction && hasImage && isNotSidebar;
|
|
});
|
|
|
|
if (validCards.length > 0) {
|
|
cards = validCards;
|
|
console.log('[LJA] Found cards via strict heuristics:', cards.length);
|
|
}
|
|
|
|
// Deduplicate cards by their text content to avoid nested matches
|
|
const uniqueCards = [];
|
|
const seenText = new Set();
|
|
for (const card of cards) {
|
|
const txt = card.innerText.trim().substring(0, 100); // use first 100 chars as signature
|
|
if (!seenText.has(txt)) {
|
|
seenText.add(txt);
|
|
uniqueCards.push(card);
|
|
}
|
|
}
|
|
|
|
return uniqueCards;
|
|
}
|
|
|
|
// ─── 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 = `أنت مستشار استثماري خبير في الشركات الناشئة (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 = `
|
|
<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
|
|
}
|
|
})();
|