Auto-deploy: 2026-06-05 22:19:31
This commit is contained in:
@@ -43,6 +43,19 @@
|
||||
"post_feed.css"
|
||||
],
|
||||
"run_at": "document_idle"
|
||||
},
|
||||
{
|
||||
"matches": [
|
||||
"https://www.linkedin.com/search/results/people/*",
|
||||
"https://www.linkedin.com/search/results/all/*"
|
||||
],
|
||||
"js": [
|
||||
"search_analyzer.js"
|
||||
],
|
||||
"css": [
|
||||
"search_analyzer.css"
|
||||
],
|
||||
"run_at": "document_idle"
|
||||
}
|
||||
],
|
||||
"action": {
|
||||
|
||||
120
search_analyzer.css
Normal file
120
search_analyzer.css
Normal file
@@ -0,0 +1,120 @@
|
||||
/* search_analyzer.css — Styles for LinkedIn Investor Scanner */
|
||||
|
||||
.lja-scan-list-btn {
|
||||
background: linear-gradient(135deg, #6C63FF 0%, #4834d4 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 10px 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
margin: 10px 0 20px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
box-shadow: 0 4px 12px rgba(108, 99, 255, 0.3);
|
||||
transition: all 0.2s ease;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.lja-scan-list-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(108, 99, 255, 0.4);
|
||||
}
|
||||
|
||||
.lja-scan-list-btn:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.lja-scan-person-btn {
|
||||
background: #f0f0f5;
|
||||
color: #6C63FF;
|
||||
border: 1px solid #dcdce6;
|
||||
border-radius: 16px;
|
||||
padding: 4px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
margin-left: 10px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.lja-scan-person-btn:hover {
|
||||
background: #e6e6ff;
|
||||
border-color: #6C63FF;
|
||||
}
|
||||
|
||||
.lja-scan-person-btn:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.lja-investor-result {
|
||||
margin-top: 12px;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
animation: ljaFadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
.lja-investor-result.green {
|
||||
background-color: rgba(0, 214, 126, 0.1);
|
||||
border-left: 4px solid #00d67e;
|
||||
}
|
||||
|
||||
.lja-investor-result.red {
|
||||
background-color: rgba(255, 107, 107, 0.1);
|
||||
border-left: 4px solid #ff6b6b;
|
||||
}
|
||||
|
||||
.lja-investor-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.lja-investor-badge.green {
|
||||
color: #00b368;
|
||||
}
|
||||
|
||||
.lja-investor-badge.red {
|
||||
color: #e03131;
|
||||
}
|
||||
|
||||
.lja-investor-reason {
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.lja-spinner {
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 2px solid rgba(255,255,255,0.3);
|
||||
border-radius: 50%;
|
||||
border-top-color: #fff;
|
||||
animation: ljaSpin 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.lja-scan-person-btn .lja-spinner {
|
||||
border-color: rgba(108,99,255,0.3);
|
||||
border-top-color: #6C63FF;
|
||||
}
|
||||
|
||||
@keyframes ljaFadeIn {
|
||||
from { opacity: 0; transform: translateY(-5px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes ljaSpin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
265
search_analyzer.js
Normal file
265
search_analyzer.js
Normal file
@@ -0,0 +1,265 @@
|
||||
// search_analyzer.js — LinkedIn People Search Investor Analyzer
|
||||
// Operates on linkedin.com/search/results/people/*
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
// ─── Extract Person Data ─────────────────────────────────────────────────
|
||||
function extractPersonData(cardEl) {
|
||||
const data = {
|
||||
name: '',
|
||||
headline: '',
|
||||
location: '',
|
||||
summary: ''
|
||||
};
|
||||
|
||||
// Name
|
||||
const nameEl = cardEl.querySelector('.entity-result__title-text a span[dir="ltr"], .entity-result__title-line a span[dir="ltr"], span.entity-result__title-text');
|
||||
if (nameEl) {
|
||||
data.name = nameEl.innerText.trim();
|
||||
}
|
||||
|
||||
// Headline
|
||||
const headlineEl = cardEl.querySelector('.entity-result__primary-subtitle');
|
||||
if (headlineEl) {
|
||||
data.headline = headlineEl.innerText.trim();
|
||||
}
|
||||
|
||||
// Location
|
||||
const locationEl = cardEl.querySelector('.entity-result__secondary-subtitle');
|
||||
if (locationEl) {
|
||||
data.location = locationEl.innerText.trim();
|
||||
}
|
||||
|
||||
// Summary/Snippet
|
||||
const summaryEl = cardEl.querySelector('.entity-result__summary');
|
||||
if (summaryEl) {
|
||||
data.summary = summaryEl.innerText.trim();
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// ─── Inject UI into Card ─────────────────────────────────────────────────
|
||||
function injectScanButton(cardEl) {
|
||||
if (cardEl.querySelector('.lja-scan-person-btn') || cardEl.querySelector('.lja-investor-result')) return;
|
||||
|
||||
// Find a good place to put the button. Usually near the title or actions.
|
||||
const actionArea = cardEl.querySelector('.entity-result__actions') || cardEl.querySelector('.entity-result__item');
|
||||
if (!actionArea) return;
|
||||
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'lja-scan-person-btn';
|
||||
btn.innerHTML = '🔍 Scan Investor';
|
||||
|
||||
// Create a container for the result
|
||||
const resultContainer = document.createElement('div');
|
||||
resultContainer.className = 'lja-result-wrapper';
|
||||
|
||||
btn.addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
await scanPerson(cardEl, btn, resultContainer);
|
||||
});
|
||||
|
||||
// Try to append button to actions area if possible, else to the item
|
||||
if (actionArea.classList.contains('entity-result__actions')) {
|
||||
actionArea.prepend(btn);
|
||||
} else {
|
||||
const titleLine = cardEl.querySelector('.entity-result__title-line') || actionArea;
|
||||
titleLine.appendChild(btn);
|
||||
}
|
||||
|
||||
cardEl.appendChild(resultContainer);
|
||||
}
|
||||
|
||||
// ─── 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) {
|
||||
alert('Could not extract person details.');
|
||||
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">
|
||||
<div class="lja-investor-badge ${colorClass}">
|
||||
<span>${badgeIcon}</span> ${badgeText}
|
||||
</div>
|
||||
<div class="lja-investor-reason">
|
||||
${resultData.reason}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
btnEl.style.display = 'none';
|
||||
|
||||
} catch (e) {
|
||||
console.error('[LJA Search]', e);
|
||||
resultContainer.innerHTML = `<div class="lja-investor-result red">❌ Error: ${e.message}</div>`;
|
||||
btnEl.disabled = false;
|
||||
btnEl.innerHTML = '🔍 Scan Investor';
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Scan All Feature ────────────────────────────────────────────────────
|
||||
function injectScanAllButton() {
|
||||
if (document.querySelector('.lja-scan-list-btn')) return;
|
||||
|
||||
const container = getSearchResultsContainer();
|
||||
if (!container) return;
|
||||
|
||||
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.addEventListener('click', async () => {
|
||||
const cards = document.querySelectorAll('.reusable-search__result-container');
|
||||
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 to avoid hammering the API
|
||||
for (const card of cards) {
|
||||
const scanBtn = card.querySelector('.lja-scan-person-btn');
|
||||
if (scanBtn && scanBtn.style.display !== 'none') {
|
||||
// Scroll to card
|
||||
card.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
await new Promise(r => setTimeout(r, 500)); // Small delay
|
||||
scanBtn.click();
|
||||
// Wait for result
|
||||
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);
|
||||
});
|
||||
|
||||
container.parentNode.insertBefore(btn, container);
|
||||
}
|
||||
|
||||
// ─── Process Page ────────────────────────────────────────────────────────
|
||||
function processPage() {
|
||||
injectScanAllButton();
|
||||
const cards = document.querySelectorAll('li.reusable-search__result-container');
|
||||
cards.forEach(injectScanButton);
|
||||
}
|
||||
|
||||
// ─── MutationObserver: watch for new results (pagination/filters) ────────
|
||||
let observerTimer;
|
||||
const observer = new MutationObserver(() => {
|
||||
if (observerTimer) return;
|
||||
observerTimer = setTimeout(() => {
|
||||
processPage();
|
||||
observerTimer = null;
|
||||
}, 800);
|
||||
});
|
||||
|
||||
// ─── Initialize ──────────────────────────────────────────────────────────
|
||||
function init() {
|
||||
processPage();
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
setTimeout(init, 2000); // Fallback for delayed renders
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user