Compare commits

...

92 Commits

Author SHA1 Message Date
Hamza-Ayed
83506f00a2 Auto-deploy: 2026-06-05 23:16:21 2026-06-05 23:16:21 +03:00
Hamza-Ayed
43c5f8d0a7 Auto-deploy: 2026-06-05 23:12:11 2026-06-05 23:12:11 +03:00
Hamza-Ayed
3de45d7f43 Auto-deploy: 2026-06-05 23:09:42 2026-06-05 23:09:42 +03:00
Hamza-Ayed
97fac48bfd Auto-deploy: 2026-06-05 23:08:11 2026-06-05 23:08:11 +03:00
Hamza-Ayed
57784c790a Auto-deploy: 2026-06-05 23:06:29 2026-06-05 23:06:29 +03:00
Hamza-Ayed
728e45c065 Auto-deploy: 2026-06-05 23:02:41 2026-06-05 23:02:41 +03:00
Hamza-Ayed
4516c2878f Auto-deploy: 2026-06-05 22:59:02 2026-06-05 22:59:02 +03:00
Hamza-Ayed
2e1df70eaf Auto-deploy: 2026-06-05 22:54:49 2026-06-05 22:54:49 +03:00
Hamza-Ayed
ae2cb47c87 Auto-deploy: 2026-06-05 22:51:59 2026-06-05 22:51:59 +03:00
Hamza-Ayed
eaca0ce6dd Auto-deploy: 2026-06-05 22:45:17 2026-06-05 22:45:17 +03:00
Hamza-Ayed
b179a63407 Auto-deploy: 2026-06-05 22:42:18 2026-06-05 22:42:18 +03:00
Hamza-Ayed
5a76223949 Auto-deploy: 2026-06-05 22:36:24 2026-06-05 22:36:24 +03:00
Hamza-Ayed
e28e4f91dc Auto-deploy: 2026-06-05 22:29:04 2026-06-05 22:29:04 +03:00
Hamza-Ayed
58798e784c Auto-deploy: 2026-06-05 22:24:06 2026-06-05 22:24:06 +03:00
Hamza-Ayed
137bf43dc8 Auto-deploy: 2026-06-05 22:19:31 2026-06-05 22:19:31 +03:00
Hamza-Ayed
a3b8732ba2 Auto-deploy: 2026-06-02 19:23:40 2026-06-02 19:23:40 +03:00
Hamza-Ayed
6d054901dc Auto-deploy: 2026-06-02 19:20:39 2026-06-02 19:20:39 +03:00
Hamza-Ayed
a615ee53af Auto-deploy: 2026-06-02 19:04:20 2026-06-02 19:04:20 +03:00
Hamza-Ayed
2f71793499 Auto-deploy: 2026-06-02 18:54:31 2026-06-02 18:54:31 +03:00
Hamza-Ayed
4bab69d2b1 Auto-deploy: 2026-06-02 18:48:35 2026-06-02 18:48:35 +03:00
Hamza-Ayed
6d4337bccf Auto-deploy: 2026-06-02 18:44:31 2026-06-02 18:44:31 +03:00
Hamza-Ayed
979a5bbdae Auto-deploy: 2026-06-02 18:39:04 2026-06-02 18:39:04 +03:00
Hamza-Ayed
8ebbedad83 Auto-deploy: 2026-06-02 18:34:02 2026-06-02 18:34:02 +03:00
Hamza-Ayed
6601d7314f Auto-deploy: 2026-06-02 18:31:11 2026-06-02 18:31:11 +03:00
Hamza-Ayed
7cf9b474bb Auto-deploy: 2026-06-02 18:28:17 2026-06-02 18:28:17 +03:00
Hamza-Ayed
96986eb302 Auto-deploy: 2026-06-02 18:22:34 2026-06-02 18:22:34 +03:00
Hamza-Ayed
6f6cec0617 Auto-deploy: 2026-06-02 18:15:27 2026-06-02 18:15:27 +03:00
Hamza-Ayed
a0c956de21 Auto-deploy: 2026-06-02 18:09:34 2026-06-02 18:09:34 +03:00
Hamza-Ayed
7bbeda0af6 Auto-deploy: 2026-06-02 18:06:52 2026-06-02 18:06:52 +03:00
Hamza-Ayed
5c975b0a6b Auto-deploy: 2026-06-02 18:05:17 2026-06-02 18:05:17 +03:00
Hamza-Ayed
36e4dd42e4 Auto-deploy: 2026-06-02 18:02:41 2026-06-02 18:02:41 +03:00
Hamza-Ayed
d5919fbf01 🐛 Fix: Use direct fetch only for Gemini (remove chrome.runtime.sendMessage which was failing) 2026-06-02 17:56:32 +03:00
Hamza-Ayed
873b965f90 🐛 Fix: Add microphone permission to manifest, add direct fetch fallback for Gemini in content.js 2026-06-02 17:55:16 +03:00
Hamza-Ayed
671b0fb927 🐛 Fix: Move voice processing handler to main background.js (service worker was not receiving messages from claude-arabic-voice) 2026-06-02 17:53:00 +03:00
Hamza-Ayed
1e16cafa74 🐛 Fix: Remove styles.css from content_scripts (was breaking Claude UI), improve background.js error handling 2026-06-02 17:49:40 +03:00
Hamza-Ayed
a6016f3f8f 🔧 Add Claude.ai to content_scripts and host_permissions for Arabic Voice extension 2026-06-02 17:44:29 +03:00
Hamza-Ayed
8f1ab9174a Add Claude Arabic Voice Input extension - Arabic speech-to-text for Claude.ai with Gemini AI processing 2026-06-02 17:42:14 +03:00
Hamza-Ayed
fa30682463 Auto-deploy: 2026-06-02 17:15:57 2026-06-02 17:15:57 +03:00
Hamza-Ayed
e153327bba Auto-deploy: 2026-06-02 16:37:46 2026-06-02 16:37:46 +03:00
Hamza-Ayed
dad9cba7db Auto-deploy: 2026-05-26 18:57:11 2026-05-26 18:57:11 +03:00
Hamza-Ayed
64bd970d9a Auto-deploy: 2026-05-26 17:51:21 2026-05-26 17:51:21 +03:00
Hamza-Ayed
1711d5ec1d Auto-deploy: 2026-05-26 17:48:55 2026-05-26 17:48:55 +03:00
Hamza-Ayed
50b021b89e Auto-deploy: 2026-05-26 17:31:08 2026-05-26 17:31:08 +03:00
Hamza-Ayed
9dbdae8684 Auto-deploy: 2026-05-26 17:30:24 2026-05-26 17:30:24 +03:00
Hamza-Ayed
76113e7d84 Auto-deploy: 2026-05-26 16:51:22 2026-05-26 16:51:22 +03:00
Hamza-Ayed
6152ff2fbe Auto-deploy: 2026-05-26 16:43:53 2026-05-26 16:43:53 +03:00
Hamza-Ayed
443b61865e Auto-deploy: 2026-05-26 16:40:53 2026-05-26 16:40:53 +03:00
Hamza-Ayed
44eeecc3e8 Auto-deploy: 2026-05-26 16:19:01 2026-05-26 16:19:01 +03:00
Hamza-Ayed
ca29b731ff Auto-deploy: 2026-05-26 15:51:42 2026-05-26 15:51:42 +03:00
Hamza-Ayed
6c46128168 Auto-deploy: 2026-05-26 15:36:18 2026-05-26 15:36:18 +03:00
Hamza-Ayed
9b7843ebb0 Auto-deploy: 2026-05-26 15:31:32 2026-05-26 15:31:32 +03:00
Hamza-Ayed
3e11488c39 Auto-deploy: 2026-05-26 14:56:07 2026-05-26 14:56:07 +03:00
Hamza-Ayed
a7356556d4 Auto-deploy: 2026-05-26 14:38:54 2026-05-26 14:38:54 +03:00
Hamza-Ayed
c1282addc2 Auto-deploy: 2026-05-26 14:23:20 2026-05-26 14:23:20 +03:00
Hamza-Ayed
3e470e4c06 Auto-deploy: 2026-05-26 14:00:23 2026-05-26 14:00:23 +03:00
Hamza-Ayed
e9c3d222fd Auto-deploy: 2026-05-26 13:31:15 2026-05-26 13:31:15 +03:00
Hamza-Ayed
bdefd51b31 Auto-deploy: 2026-05-25 23:31:23 2026-05-25 23:31:23 +03:00
Hamza-Ayed
29c6f974f0 Auto-deploy: 2026-05-25 23:30:53 2026-05-25 23:30:53 +03:00
Hamza-Ayed
3554c5b358 Auto-deploy: 2026-05-25 23:27:04 2026-05-25 23:27:04 +03:00
Hamza-Ayed
c5f8a3e356 Auto-deploy: 2026-05-25 23:13:51 2026-05-25 23:13:51 +03:00
Hamza-Ayed
8c221402e0 Auto-deploy: 2026-05-25 22:53:46 2026-05-25 22:53:46 +03:00
Hamza-Ayed
0ab5dd79b6 Auto-deploy: 2026-05-19 16:48:31 2026-05-19 16:48:31 +03:00
Hamza-Ayed
b3f5f90d0d Auto-deploy: 2026-05-19 16:31:46 2026-05-19 16:31:46 +03:00
Hamza-Ayed
6a3fdc807e Auto-deploy: 2026-05-18 22:47:30 2026-05-18 22:47:30 +03:00
Hamza-Ayed
29dac58464 Auto-deploy: 2026-05-18 03:06:29 2026-05-18 03:06:29 +03:00
Hamza-Ayed
470580ba05 Auto-deploy: 2026-05-18 02:26:39 2026-05-18 02:26:39 +03:00
Hamza-Ayed
645638010d Auto-deploy: 2026-05-18 02:23:03 2026-05-18 02:23:03 +03:00
Hamza-Ayed
a8457bf7d8 Auto-deploy: 2026-05-18 02:11:39 2026-05-18 02:11:39 +03:00
Hamza-Ayed
a238d80201 Auto-deploy: 2026-05-18 02:06:16 2026-05-18 02:06:16 +03:00
Hamza-Ayed
645b2bccf1 Auto-deploy: 2026-05-18 02:02:00 2026-05-18 02:02:00 +03:00
Hamza-Ayed
c0465e4ee4 Auto-deploy: 2026-05-18 01:59:28 2026-05-18 01:59:28 +03:00
Hamza-Ayed
42aec81504 Auto-deploy: 2026-05-18 01:49:47 2026-05-18 01:49:47 +03:00
Hamza-Ayed
6c5fd21be6 Auto-deploy: 2026-05-18 01:36:57 2026-05-18 01:36:57 +03:00
Hamza-Ayed
94d430b972 Auto-deploy: 2026-05-18 00:20:53 2026-05-18 00:20:53 +03:00
Hamza-Ayed
f3b04c1c4c Auto-deploy: 2026-05-18 00:18:20 2026-05-18 00:18:20 +03:00
Hamza-Ayed
0267dc698c Auto-deploy: 2026-05-18 00:04:43 2026-05-18 00:04:43 +03:00
Hamza-Ayed
1b930f92be Auto-deploy: 2026-05-18 00:00:24 2026-05-18 00:00:24 +03:00
Hamza-Ayed
98c890ef16 Auto-deploy: 2026-05-17 23:35:30 2026-05-17 23:35:30 +03:00
Hamza-Ayed
9bf1406796 Auto-deploy: 2026-05-17 23:23:33 2026-05-17 23:23:33 +03:00
Hamza-Ayed
3ac8260c1d Auto-deploy: 2026-05-17 23:17:07 2026-05-17 23:17:07 +03:00
Hamza-Ayed
a7a782c422 Auto-deploy: 2026-05-17 23:00:09 2026-05-17 23:00:09 +03:00
Hamza-Ayed
26fc873e85 Auto-deploy: 2026-05-17 22:54:53 2026-05-17 22:54:53 +03:00
Hamza-Ayed
2548dd1331 Auto-deploy: 2026-05-17 22:51:47 2026-05-17 22:51:47 +03:00
Hamza-Ayed
5fd6969ff8 Auto-deploy: 2026-05-17 22:45:01 2026-05-17 22:45:01 +03:00
Hamza-Ayed
8dc55c698e Auto-deploy: 2026-05-17 21:55:09 2026-05-17 21:55:09 +03:00
Hamza-Ayed
42edf6d636 Auto-deploy: 2026-05-17 21:51:31 2026-05-17 21:51:31 +03:00
Hamza-Ayed
8507032845 Auto-deploy: 2026-05-17 21:47:20 2026-05-17 21:47:20 +03:00
Hamza-Ayed
cacb756425 Auto-deploy: 2026-05-17 21:36:54 2026-05-17 21:36:54 +03:00
Hamza-Ayed
3d0b1cc36e Auto-deploy: 2026-05-17 19:00:08 2026-05-17 19:00:08 +03:00
Hamza-Ayed
acc25dfabc Auto-deploy: 2026-05-17 15:21:40 2026-05-17 15:21:40 +03:00
Hamza-Ayed
dd5d1980cf Auto-deploy: 2026-05-17 15:19:35 2026-05-17 15:19:35 +03:00
Hamza-Ayed
b65db1e20b Auto-deploy: 2026-05-17 05:01:20 2026-05-17 05:01:20 +03:00
39 changed files with 4702 additions and 203 deletions

View File

@@ -1,10 +1,10 @@
// background.js — Service Worker // background.js — Service Worker
// Handles all Gemini API calls (avoids CORS issues from content scripts) // Handles all Gemini API calls (avoids CORS issues from content scripts)
const GEMINI_MODEL = 'gemini-2.5-flash'; const GEMINI_MODEL = 'gemini-flash-lite-latest';
const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent`; const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent`;
// ─── Message listener ──────────────────────────────────────────────────────── // (Offscreen document logic removed in favor of iframe approach)
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'GEMINI_REQUEST') { if (message.type === 'GEMINI_REQUEST') {
@@ -13,29 +13,41 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
.catch(err => sendResponse({ success: false, error: err.message })); .catch(err => sendResponse({ success: false, error: err.message }));
return true; // Keep message channel open for async return true; // Keep message channel open for async
} }
if (message.type === 'GEMINI_PROCESS_VOICE') {
handleVoiceProcessing(message.payload)
.then(result => sendResponse({ success: true, data: result }))
.catch(err => sendResponse({ success: false, error: err.message }));
return true; // Keep message channel open for async
}
if (message.type === 'OPEN_PERMISSION_PAGE') {
chrome.tabs.create({ url: chrome.runtime.getURL('permission.html') });
return true;
}
}); });
// ─── Core API call ─────────────────────────────────────────────────────────── // ─── Core API call ───────────────────────────────────────────────────────────
async function handleGeminiRequest({ apiKey, prompt, tab, action = 'generateText', jobDescription = '', jobTitle = '' }) { async function handleGeminiRequest({ apiKey, prompt, tab, action = 'generateText', jobDescription = '', jobTitle = '', postText = '', template = 'default' }) {
// Rate limit check // Rate limit check
const canProceed = await checkRateLimit(); const canProceed = await checkRateLimit();
if (!canProceed) { if (!canProceed) {
throw new Error('Daily limit reached (1,000 requests). Resets at midnight PT.'); throw new Error('Daily limit reached (1,000 requests). Resets at midnight PT.');
} }
// Truncate text // Truncate text — 12k chars to fit list_analysis prompts
const maxChars = 6000; const maxChars = 12000;
const trimmedPrompt = prompt && prompt.length > maxChars const trimmedPrompt = prompt && prompt.length > maxChars
? prompt.substring(0, maxChars) + '\n\n[Truncated]' ? prompt.substring(0, maxChars) + '\n\n[Truncated]'
: prompt; : prompt;
const MAX_RETRIES = 3; const MAX_RETRIES = 3;
const RETRY_DELAYS = [2000, 20000, 30000]; const RETRY_DELAYS = [0, 15000, 30000]; // No delay on first attempt
let lastError = ''; let lastError = '';
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
await delay(RETRY_DELAYS[attempt]); if (RETRY_DELAYS[attempt] > 0) await delay(RETRY_DELAYS[attempt]);
try { try {
const response = await fetch('https://cv.intaleqapp.com/cv/server/generate_cv.php', { const response = await fetch('https://cv.intaleqapp.com/cv/server/generate_cv.php', {
@@ -46,21 +58,42 @@ async function handleGeminiRequest({ apiKey, prompt, tab, action = 'generateText
apiKey: apiKey, apiKey: apiKey,
prompt: trimmedPrompt, prompt: trimmedPrompt,
jobDescription: jobDescription, jobDescription: jobDescription,
jobTitle: jobTitle jobTitle: jobTitle,
postText: postText,
template: template
}) })
}); });
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
if (action === 'generatePdf') { if (action === 'generatePdf') {
if (!data.pdf) throw new Error('Empty PDF response from server.'); if (!data.pdf) throw new Error('Empty PDF response from server.');
await incrementUsage(); await incrementUsage();
return { pdf: data.pdf, filename: data.filename, fromCache: false }; return { pdf: data.pdf, filename: data.filename, fromCache: false };
} else if (action === 'generateComment') {
const comment = data.comment;
if (!comment) {
console.error('[LJA-BG] Empty comment. Full response:', JSON.stringify(data).substring(0, 300));
lastError = 'Empty comment from server.';
continue;
}
await incrementUsage();
return { comment, fromCache: false };
} else if (action === 'repurposePost') {
const result = data.result;
if (!result) {
console.error('[LJA-BG] Empty result. Full response:', JSON.stringify(data).substring(0, 300));
lastError = 'Empty result from server.';
continue;
}
await incrementUsage();
return { result, fromCache: false };
} else { } else {
const text = data.candidates?.[0]?.content?.parts?.[0]?.text; const text = data.candidates?.[0]?.content?.parts?.[0]?.text;
if (!text) { if (!text) {
lastError = 'Empty response from API.'; console.error('[LJA-BG] Empty text. Full response:', JSON.stringify(data).substring(0, 500));
lastError = 'Empty response from API. Server returned: ' + JSON.stringify(data).substring(0, 200);
continue; continue;
} }
await incrementUsage(); await incrementUsage();
@@ -170,3 +203,77 @@ function storageGet(keys) {
function storageSet(data) { function storageSet(data) {
return new Promise(resolve => chrome.storage.local.set(data, resolve)); return new Promise(resolve => chrome.storage.local.set(data, resolve));
} }
// ─── Voice Processing (for Claude Arabic Voice extension) ────────────────────
async function handleVoiceProcessing({ apiKey, model, text, language }) {
if (!apiKey) {
throw new Error('Gemini API key is required');
}
// Detect if text is Arabic
const isArabic = /[\u0600-\u06FF]/.test(text);
const langName = isArabic ? 'Arabic' : 'the detected language';
const prompt = `You are a text refinement assistant. Your task is to:
1. Correct any speech recognition errors in the following text
2. Fix punctuation, capitalization, and formatting
3. Keep the original meaning and content intact
4. Do NOT add any new information or commentary
5. Return ONLY the corrected text, nothing else
The text is in ${langName}. Preserve the original language.
TEXT TO REFINE:
${text}
CORRECTED TEXT:`;
const response = await fetch('https://cv.intaleqapp.com/cv/server/generate_cv.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'generateText',
apiKey: apiKey,
prompt: prompt
})
});
if (!response.ok) {
const errText = await response.text().catch(() => '');
let errMsg;
try {
const errData = JSON.parse(errText);
errMsg = errData.error?.message || `HTTP ${response.status}`;
} catch (e) {
errMsg = `HTTP ${response.status}: ${errText.substring(0, 200)}`;
}
throw new Error(`Gemini API error: ${errMsg}`);
}
const responseText = await response.text();
let data;
try {
data = JSON.parse(responseText);
} catch (e) {
return { text: responseText.trim() };
}
if (data.candidates && data.candidates[0]) {
const resultText = data.candidates[0].content?.parts?.[0]?.text;
if (resultText) {
return { text: resultText.trim() };
}
}
if (data.error) {
throw new Error(`Server error: ${data.error}${data.details ? ' - ' + JSON.stringify(data.details) : ''}`);
}
if (typeof data === 'string') {
return { text: data.trim() };
}
throw new Error('Empty response from Gemini API');
}

View File

@@ -0,0 +1,114 @@
// background.js — Service Worker for Claude Arabic Voice
// Handles Gemini API calls for voice text processing via the existing server proxy
const SERVER_URL = 'https://cv.intaleqapp.com/cv/server/generate_cv.php';
// ─── Message Listener ────────────────────────────────────────────────────────
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'GEMINI_PROCESS_VOICE') {
handleVoiceProcessing(message.payload)
.then(result => sendResponse({ success: true, data: result }))
.catch(err => sendResponse({ success: false, error: err.message }));
return true; // Keep message channel open for async
}
});
// ─── Voice Text Processing with Gemini (via server proxy) ────────────────────
async function handleVoiceProcessing({ apiKey, model, text, language }) {
if (!apiKey) {
throw new Error('Gemini API key is required');
}
// Detect if text is Arabic
const isArabic = /[\u0600-\u06FF]/.test(text);
const langName = isArabic ? 'Arabic' : 'the detected language';
const prompt = `You are a text refinement assistant. Your task is to:
1. Correct any speech recognition errors in the following text
2. Fix punctuation, capitalization, and formatting
3. Keep the original meaning and content intact
4. Do NOT add any new information or commentary
5. Return ONLY the corrected text, nothing else
The text is in ${langName}. Preserve the original language.
TEXT TO REFINE:
${text}
CORRECTED TEXT:`;
console.log('[ClaudeVoice] Sending to server:', SERVER_URL);
const response = await fetch(SERVER_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'generateText',
apiKey: apiKey,
prompt: prompt
})
});
if (!response.ok) {
const errText = await response.text().catch(() => '');
let errMsg;
try {
const errData = JSON.parse(errText);
errMsg = errData.error?.message || `HTTP ${response.status}`;
} catch (e) {
errMsg = `HTTP ${response.status}: ${errText.substring(0, 200)}`;
}
throw new Error(`Gemini API error: ${errMsg}`);
}
const responseText = await response.text();
console.log('[ClaudeVoice] Server response received, length:', responseText.length);
// Try to parse as JSON (Gemini response format from server proxy)
let data;
try {
data = JSON.parse(responseText);
} catch (e) {
// If not JSON, use the raw text
return { text: responseText.trim() };
}
// Check if it's a Gemini API response format
if (data.candidates && data.candidates[0]) {
const resultText = data.candidates[0].content?.parts?.[0]?.text;
if (resultText) {
return { text: resultText.trim() };
}
}
// Check if it's our server's error format
if (data.error) {
throw new Error(`Server error: ${data.error}${data.details ? ' - ' + JSON.stringify(data.details) : ''}`);
}
// If we got here but have some text, return it
if (typeof data === 'string') {
return { text: data.trim() };
}
throw new Error('Empty response from Gemini API');
}
// ─── Installation Handler ────────────────────────────────────────────────────
chrome.runtime.onInstalled.addListener((details) => {
if (details.reason === 'install') {
// Set default settings
chrome.storage.sync.set({
language: 'ar-SA',
autoSend: false,
useGemini: false,
geminiApiKey: '',
geminiModel: 'gemini-flash-lite-latest'
});
console.log('[ClaudeVoice] ✅ Extension installed with default settings');
}
});

View File

@@ -0,0 +1,651 @@
// content.js — Arabic Voice Input for Claude.ai
// Injects a microphone button that uses Web Speech API for Arabic speech recognition
(function () {
'use strict';
// Prevent double injection
if (window.__claudeArabicVoiceLoaded) return;
window.__claudeArabicVoiceLoaded = true;
// ─── State ───────────────────────────────────────────────────────────────
let recognition = null;
let isListening = false;
let micButton = null;
let statusIndicator = null;
let interimText = '';
let finalText = '';
let autoSendTimer = null;
let silenceTimer = null;
let lastSpeechTime = 0;
let isGeminiProcessing = false;
const SILENCE_TIMEOUT = 2000; // 2 seconds of silence = auto-stop
const MAX_RECORDING_TIME = 30000; // 30 seconds max recording
// ─── Settings ────────────────────────────────────────────────────────────
let settings = {
language: 'ar-SA', // Default: Arabic (Saudi Arabia)
autoSend: false, // Auto-send after stopping
useGemini: false, // Use Gemini AI to refine text
geminiApiKey: '',
geminiModel: 'gemini-flash-lite-latest'
};
// ─── Load Settings ───────────────────────────────────────────────────────
async function loadSettings() {
return new Promise((resolve) => {
chrome.storage.sync.get(
['language', 'autoSend', 'useGemini', 'geminiApiKey', 'geminiModel'],
(data) => {
if (data.language) settings.language = data.language;
if (data.autoSend !== undefined) settings.autoSend = data.autoSend;
if (data.useGemini !== undefined) settings.useGemini = data.useGemini;
if (data.geminiApiKey) settings.geminiApiKey = data.geminiApiKey;
if (data.geminiModel) settings.geminiModel = data.geminiModel;
resolve();
}
);
});
}
// ─── Iframe Integration ──────────────────────────────────────────────────
let speechIframe = null;
function initSpeechIframe() {
if (speechIframe) return;
speechIframe = document.createElement('iframe');
speechIframe.id = 'claude-voice-speech-iframe';
speechIframe.style.cssText = `
position: absolute;
width: 1px;
height: 1px;
left: -9999px;
top: -9999px;
opacity: 0;
pointer-events: none;
border: none;
`;
speechIframe.src = chrome.runtime.getURL('claude-arabic-voice/speech.html');
speechIframe.allow = 'microphone';
document.body.appendChild(speechIframe);
window.addEventListener('message', (event) => {
if (event.source !== speechIframe.contentWindow) return;
const message = event.data;
if (message.type === 'SPEECH_START_SUCCESS') {
isListening = true;
lastSpeechTime = Date.now();
interimText = '';
finalText = '';
updateMicButton(true);
showStatus('listening', '🎤 جارٍ الاستماع...');
clearTimeout(autoSendTimer);
autoSendTimer = setTimeout(() => {
if (isListening) stopListening();
}, MAX_RECORDING_TIME);
clearTimeout(silenceTimer);
silenceTimer = setTimeout(checkSilence, SILENCE_TIMEOUT);
} else if (message.type === 'SPEECH_RESULT') {
lastSpeechTime = Date.now();
interimText = message.payload.interimText || '';
finalText = message.payload.finalText || '';
updateInputField();
clearTimeout(silenceTimer);
silenceTimer = setTimeout(checkSilence, SILENCE_TIMEOUT);
} else if (message.type === 'SPEECH_ERROR') {
console.error('[ClaudeVoice] Recognition error:', message.payload.error);
if (message.payload.error === 'no-speech') {
return;
}
stopListening();
if (message.payload.error === 'not-allowed') {
showStatus('error', '❌ الرجاء منح الصلاحية (تم فتح صفحة جديدة)');
chrome.runtime.sendMessage({ type: 'OPEN_PERMISSION_PAGE' });
} else {
showStatus('error', '❌ خطأ: ' + getArabicError(message.payload.error));
}
} else if (message.type === 'SPEECH_END') {
if (isListening) stopListening();
}
});
}
function checkSilence() {
if (isListening && Date.now() - lastSpeechTime >= SILENCE_TIMEOUT) {
stopListening();
}
}
function getArabicError(error) {
const errors = {
'no-speech': 'لم يتم اكتشاف كلام',
'aborted': 'تم الإلغاء',
'audio-capture': 'الميكروفون غير متاح',
'network': 'خطأ في الشبكة',
'not-allowed': 'الرجاء السماح باستخدام الميكروفون',
'service-not-allowed': 'الخدمة غير متاحة',
'bad-grammar': 'خطأ في القواعد'
};
return errors[error] || error;
}
// ─── Start / Stop Listening ─────────────────────────────────────────────
async function startListening() {
await loadSettings();
if (!speechIframe) initSpeechIframe();
try {
speechIframe.contentWindow.postMessage({
type: 'START_RECORDING',
payload: { language: settings.language }
}, '*');
} catch (e) {
console.error('[ClaudeVoice] Start message failed:', e);
showStatus('error', '❌ فشل الاتصال بالإضافة');
}
}
async function stopListening() {
if (!isListening) return;
isListening = false;
clearTimeout(silenceTimer);
clearTimeout(autoSendTimer);
if (speechIframe) {
speechIframe.contentWindow.postMessage({ type: 'STOP_RECORDING' }, '*');
}
updateMicButton(false);
const text = finalText.trim() || interimText.trim();
if (text) {
showStatus('processing', '⏳ جارٍ المعالجة...');
if (settings.useGemini && settings.geminiApiKey) {
await processWithGemini(text);
} else {
insertTextIntoClaude(text);
showStatus('done', '✅ تم الإدراج');
setTimeout(() => hideStatus(), 2000);
}
} else {
showStatus('idle', '🎤 اضغط للتحدث');
setTimeout(() => hideStatus(), 1500);
}
}
// ─── Update Claude Input ─────────────────────────────────────────────────
function updateInputField() {
const fullText = (finalText + interimText).trim();
if (!fullText) return;
// Find Claude's input area
const inputArea = findClaudeInput();
if (!inputArea) return;
// For interim results, we update a placeholder
// For final results, we insert the text
if (interimText) {
// Show interim in a floating preview
showInterimPreview(interimText);
} else {
hideInterimPreview();
}
}
function findClaudeInput() {
// Claude.ai uses a contenteditable div or textarea
// Try multiple selectors as Claude's UI may change
const selectors = [
'[contenteditable="true"][role="textbox"]',
'[contenteditable="true"].ProseMirror',
'div[contenteditable="true"]',
'textarea[placeholder*="Message"]',
'textarea[placeholder*="Ask"]',
'textarea[placeholder*="مراسلة"]',
'textarea[placeholder*="اسأل"]',
'.ProseMirror[contenteditable="true"]',
'div[role="textbox"][contenteditable="true"]'
];
for (const sel of selectors) {
const el = document.querySelector(sel);
if (el) return el;
}
return null;
}
function findSendButton() {
const selectors = [
'button[aria-label*="Send"]',
'button[aria-label*="إرسال"]',
'button[data-testid*="send"]',
'button[class*="send"]',
'button[class*="Send"]',
'form button[type="submit"]',
// Claude's specific send button
'button:has(svg[class*="send"])',
'button:has(svg[data-icon*="arrow"])'
];
for (const sel of selectors) {
const el = document.querySelector(sel);
if (el) return el;
}
return null;
}
function insertTextIntoClaude(text) {
const inputArea = findClaudeInput();
if (!inputArea) {
showStatus('error', '❌ لم يتم العثور على حقل الإدخال');
return false;
}
// Check if it's a contenteditable div (ProseMirror) or textarea
if (inputArea.isContentEditable || inputArea.tagName === 'DIV') {
// For contenteditable (Claude's ProseMirror editor)
// Insert text at cursor position or append
const selection = window.getSelection();
const range = document.createRange();
// Focus the input area
inputArea.focus();
// Try to place cursor at end
if (inputArea.lastChild) {
range.setStartAfter(inputArea.lastChild);
range.setEndAfter(inputArea.lastChild);
} else {
range.selectNodeContents(inputArea);
range.collapse(false);
}
selection.removeAllRanges();
selection.addRange(range);
// Insert text
document.execCommand('insertText', false, text);
// Dispatch input event for React/ProseMirror to detect
inputArea.dispatchEvent(new Event('input', { bubbles: true }));
inputArea.dispatchEvent(new Event('change', { bubbles: true }));
} else if (inputArea.tagName === 'TEXTAREA' || inputArea.tagName === 'INPUT') {
// For textarea/input
const start = inputArea.selectionStart || inputArea.value.length;
const end = inputArea.selectionEnd || inputArea.value.length;
inputArea.value = inputArea.value.substring(0, start) + text + inputArea.value.substring(end);
inputArea.selectionStart = inputArea.selectionEnd = start + text.length;
inputArea.dispatchEvent(new Event('input', { bubbles: true }));
inputArea.dispatchEvent(new Event('change', { bubbles: true }));
}
// Auto-send if enabled
if (settings.autoSend) {
setTimeout(() => {
const sendBtn = findSendButton();
if (sendBtn && !sendBtn.disabled) {
sendBtn.click();
}
}, 500);
}
return true;
}
// ─── Interim Preview ─────────────────────────────────────────────────────
let previewEl = null;
function showInterimPreview(text) {
if (!previewEl) {
previewEl = document.createElement('div');
previewEl.id = 'claude-voice-interim';
previewEl.style.cssText = `
position: fixed;
bottom: 100px;
left: 50%;
transform: translateX(-50%);
background: rgba(0,0,0,0.85);
color: #fff;
padding: 12px 20px;
border-radius: 12px;
font-size: 16px;
max-width: 600px;
width: 90%;
text-align: center;
z-index: 999999;
direction: rtl;
font-family: 'Segoe UI', Tahoma, sans-serif;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
backdrop-filter: blur(10px);
border: 1px solid rgba(255,255,255,0.1);
pointer-events: none;
`;
document.body.appendChild(previewEl);
}
previewEl.textContent = '🎤 ' + text + ' ▊';
previewEl.style.display = 'block';
}
function hideInterimPreview() {
if (previewEl) {
previewEl.style.display = 'none';
}
}
// ─── Gemini AI Processing ────────────────────────────────────────────────
async function processWithGemini(text) {
isGeminiProcessing = true;
try {
let processedText = null;
// Try direct fetch to server (more reliable than chrome.runtime.sendMessage)
try {
const isArabic = /[\u0600-\u06FF]/.test(text);
const langName = isArabic ? 'Arabic' : 'the detected language';
const prompt = `You are a text refinement assistant. Your task is to:
1. Correct any speech recognition errors in the following text
2. Fix punctuation, capitalization, and formatting
3. Keep the original meaning and content intact
4. Do NOT add any new information or commentary
5. Return ONLY the corrected text, nothing else
The text is in ${langName}. Preserve the original language.
TEXT TO REFINE:
${text}
CORRECTED TEXT:`;
const directResponse = await fetch('https://cv.intaleqapp.com/cv/server/generate_cv.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'generateText',
apiKey: settings.geminiApiKey,
prompt: prompt
})
});
if (directResponse.ok) {
const rawText = await directResponse.text();
let data;
try {
data = JSON.parse(rawText);
} catch (e) {
data = rawText;
}
if (data && data.candidates && data.candidates[0]) {
processedText = data.candidates[0].content?.parts?.[0]?.text;
} else if (typeof data === 'string') {
processedText = data;
}
} else {
console.warn('[ClaudeVoice] Server fetch failed:', directResponse.status);
}
} catch (fetchErr) {
console.warn('[ClaudeVoice] Server fetch error:', fetchErr);
}
if (processedText) {
insertTextIntoClaude(processedText);
showStatus('done', '✅ تمت المعالجة بواسطة Gemini');
} else {
insertTextIntoClaude(text);
showStatus('done', '✅ تم الإدراج (بدون معالجة)');
}
} catch (e) {
console.error('[ClaudeVoice] Gemini error:', e);
insertTextIntoClaude(text);
showStatus('done', '✅ تم الإدراج (بدون معالجة)');
}
isGeminiProcessing = false;
setTimeout(() => hideStatus(), 2000);
}
// ─── UI: Mic Button ──────────────────────────────────────────────────────
function createMicButton() {
if (micButton) return;
micButton = document.createElement('button');
micButton.id = 'claude-voice-mic-btn';
micButton.innerHTML = '🎤';
micButton.title = 'إملاء صوتي عربي - اضغط للتحدث';
micButton.setAttribute('aria-label', 'إملاء صوتي عربي');
micButton.addEventListener('click', () => {
if (isListening) {
stopListening();
} else {
startListening();
}
});
// Status indicator
statusIndicator = document.createElement('div');
statusIndicator.id = 'claude-voice-status';
statusIndicator.textContent = '🎤 اضغط للتحدث';
statusIndicator.style.display = 'none';
// Container
const container = document.createElement('div');
container.id = 'claude-voice-container';
container.appendChild(micButton);
container.appendChild(statusIndicator);
document.body.appendChild(container);
// Position the button near Claude's input area
positionMicButton();
}
function positionMicButton() {
// Try to find the input area and position near it
const observer = new MutationObserver(() => {
const inputArea = findClaudeInput();
if (inputArea && micButton) {
const rect = inputArea.getBoundingClientRect();
if (rect && rect.top > 0) {
micButton.style.bottom = (window.innerHeight - rect.top + 10) + 'px';
micButton.style.right = '20px';
}
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['style', 'class']
});
// Initial positioning
setTimeout(() => {
const inputArea = findClaudeInput();
if (inputArea && micButton) {
const rect = inputArea.getBoundingClientRect();
if (rect && rect.top > 0) {
micButton.style.bottom = (window.innerHeight - rect.top + 10) + 'px';
}
}
}, 2000);
}
function updateMicButton(listening) {
if (!micButton) return;
if (listening) {
micButton.classList.add('listening');
micButton.innerHTML = '🔴';
micButton.title = 'إيقاف التسجيل';
} else {
micButton.classList.remove('listening');
micButton.innerHTML = '🎤';
micButton.title = 'إملاء صوتي عربي - اضغط للتحدث';
}
}
function showStatus(type, message) {
if (!statusIndicator) return;
statusIndicator.textContent = message;
statusIndicator.className = 'claude-voice-status-' + type;
statusIndicator.style.display = 'block';
}
function hideStatus() {
if (!statusIndicator) return;
statusIndicator.style.display = 'none';
}
// ─── Inject Styles ───────────────────────────────────────────────────────
function injectStyles() {
// Styles are loaded from styles.css via manifest
// But we also inject some critical inline styles
const style = document.createElement('style');
style.textContent = `
#claude-voice-container {
position: fixed;
z-index: 999999;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
bottom: 120px;
right: 20px;
}
#claude-voice-mic-btn {
width: 56px;
height: 56px;
border-radius: 50%;
border: none;
background: linear-gradient(135deg, #6c63ff, #4834d4);
color: white;
font-size: 24px;
cursor: pointer;
box-shadow: 0 4px 15px rgba(108, 99, 255, 0.4);
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
#claude-voice-mic-btn:hover {
transform: scale(1.1);
box-shadow: 0 6px 20px rgba(108, 99, 255, 0.6);
}
#claude-voice-mic-btn.listening {
background: linear-gradient(135deg, #ff4444, #cc0000);
animation: claude-voice-pulse 1.5s infinite;
box-shadow: 0 4px 20px rgba(255, 68, 68, 0.6);
}
@keyframes claude-voice-pulse {
0% { box-shadow: 0 0 0 0 rgba(255, 68, 68, 0.6); }
50% { box-shadow: 0 0 0 15px rgba(255, 68, 68, 0); }
100% { box-shadow: 0 0 0 0 rgba(255, 68, 68, 0); }
}
#claude-voice-status {
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 6px 14px;
border-radius: 20px;
font-size: 12px;
font-family: 'Segoe UI', Tahoma, sans-serif;
white-space: nowrap;
direction: rtl;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
pointer-events: none;
}
.claude-voice-status-listening {
background: rgba(255, 68, 68, 0.9) !important;
}
.claude-voice-status-processing {
background: rgba(255, 165, 0, 0.9) !important;
}
.claude-voice-status-done {
background: rgba(0, 200, 83, 0.9) !important;
}
.claude-voice-status-error {
background: rgba(255, 0, 0, 0.9) !important;
}
.claude-voice-status-idle {
background: rgba(108, 99, 255, 0.9) !important;
}
`;
document.head.appendChild(style);
}
// ─── Message Listener (for popup communication) ─────────────────────────
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'GET_STATUS') {
sendResponse({ isListening });
return true;
}
if (message.type === 'SETTINGS_UPDATED') {
if (message.payload.language) settings.language = message.payload.language;
if (message.payload.autoSend !== undefined) settings.autoSend = message.payload.autoSend;
if (message.payload.useGemini !== undefined) settings.useGemini = message.payload.useGemini;
if (message.payload.geminiApiKey) settings.geminiApiKey = message.payload.geminiApiKey;
if (message.payload.geminiModel) settings.geminiModel = message.payload.geminiModel;
console.log('[ClaudeVoice] Settings updated:', settings);
sendResponse({ success: true });
return true;
}
});
// ─── Initialize ──────────────────────────────────────────────────────────
async function init() {
await loadSettings();
injectStyles();
initSpeechIframe(); // Inject iframe immediately
createMicButton();
// Re-position on scroll and resize
window.addEventListener('scroll', () => {
const inputArea = findClaudeInput();
if (inputArea && micButton) {
const rect = inputArea.getBoundingClientRect();
if (rect && rect.top > 0) {
micButton.style.bottom = (window.innerHeight - rect.top + 10) + 'px';
}
}
});
window.addEventListener('resize', () => {
const inputArea = findClaudeInput();
if (inputArea && micButton) {
const rect = inputArea.getBoundingClientRect();
if (rect && rect.top > 0) {
micButton.style.bottom = (window.innerHeight - rect.top + 10) + 'px';
}
}
});
console.log('[ClaudeVoice] ✅ Arabic voice input extension loaded');
console.log('[ClaudeVoice] Language:', settings.language);
console.log('[ClaudeVoice] Gemini:', settings.useGemini ? 'Enabled' : 'Disabled');
}
// Wait for page to be ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
<circle cx="64" cy="64" r="64" fill="#6c63ff"/>
<text x="64" y="90" text-anchor="middle" font-size="72" fill="white">🎤</text>
</svg>

After

Width:  |  Height:  |  Size: 228 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
<circle cx="8" cy="8" r="8" fill="#6c63ff"/>
<text x="8" y="12" text-anchor="middle" font-size="10" fill="white"><EFBFBD><EFBFBD></text>
</svg>

After

Width:  |  Height:  |  Size: 222 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<circle cx="24" cy="24" r="24" fill="#6c63ff"/>
<text x="24" y="34" text-anchor="middle" font-size="28" fill="white">🎤</text>
</svg>

After

Width:  |  Height:  |  Size: 224 B

View File

@@ -0,0 +1,56 @@
{
"manifest_version": 3,
"name": "كلود - إملاء صوتي عربي",
"version": "1.0.1",
"description": "يضيف زر مايكروفون لموقع Claude.ai للكتابة بالصوت باللغة العربية - Arabic speech-to-text for Claude.ai",
"permissions": [
"storage",
"offscreen"
],
"host_permissions": [
"https://claude.ai/*",
"https://cv.intaleqapp.com/*"
],
"content_scripts": [
{
"matches": [
"https://claude.ai/*"
],
"js": [
"content.js"
],
"css": [
"styles.css"
],
"run_at": "document_idle"
}
],
"background": {
"service_worker": "background.js"
},
"web_accessible_resources": [
{
"resources": [
"speech.html",
"speech.js",
"permission.html",
"permission.js",
"icons/*.svg"
],
"matches": ["https://claude.ai/*"]
}
],
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "icons/icon16.svg",
"48": "icons/icon48.svg",
"128": "icons/icon128.svg"
}
},
"icons": {
"16": "icons/icon16.svg",
"48": "icons/icon48.svg",
"128": "icons/icon128.svg"
}
}

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Claude Arabic Voice Offscreen</title>
</head>
<body>
<script src="offscreen.js"></script>
</body>
</html>

View File

@@ -0,0 +1,135 @@
// offscreen.js - Handles webkitSpeechRecognition in isolation
let recognition = null;
let isRecording = false;
// Initialize recognition early if possible, or wait until start
function initRecognition(language) {
if (recognition) return recognition;
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
if (!SpeechRecognition) {
throw new Error('Speech recognition not supported');
}
recognition = new SpeechRecognition();
recognition.continuous = true;
recognition.interimResults = true;
recognition.lang = language || 'ar-SA';
recognition.maxAlternatives = 1;
recognition.onresult = (event) => {
let interimText = '';
let finalText = '';
for (let i = event.resultIndex; i < event.results.length; i++) {
const result = event.results[i];
if (result.isFinal) {
finalText += result[0].transcript + ' ';
} else {
interimText += result[0].transcript;
}
}
// Send results back to background script
chrome.runtime.sendMessage({
type: 'OFFSCREEN_RECORDING_RESULT',
payload: { interimText, finalText }
});
};
recognition.onerror = (event) => {
console.error('[Offscreen] Recognition error:', event.error);
// Prevent infinite restart loop on fatal errors
if (event.error !== 'no-speech') {
isRecording = false;
if (typeof stopMediaTracks === 'function') stopMediaTracks();
}
chrome.runtime.sendMessage({
type: 'OFFSCREEN_RECORDING_ERROR',
payload: { error: event.error }
});
};
recognition.onend = () => {
// If we still want to be recording, restart
if (isRecording) {
try {
recognition.start();
} catch (e) {
console.warn('[Offscreen] Restart failed:', e);
}
} else {
chrome.runtime.sendMessage({ type: 'OFFSCREEN_RECORDING_END' });
}
};
return recognition;
}
let localStream = null;
function stopMediaTracks() {
if (localStream) {
localStream.getTracks().forEach(track => track.stop());
localStream = null;
}
}
// Listen for messages from background script
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'START_RECORDING') {
const lang = message.payload?.language || 'ar-SA';
// 1. Get an audio stream first. This is REQUIRED in offscreen documents
// to actually activate the microphone and ensure permissions are active.
navigator.mediaDevices.getUserMedia({ audio: true })
.then((stream) => {
localStream = stream;
try {
if (!recognition || recognition.lang !== lang) {
recognition = initRecognition(lang);
}
if (!isRecording) {
recognition.start();
isRecording = true;
sendResponse({ success: true });
} else {
sendResponse({ success: true, warning: 'Already recording' });
}
} catch (e) {
console.error('[Offscreen] Failed to start:', e);
sendResponse({ success: false, error: e.message });
}
})
.catch((err) => {
console.error('[Offscreen] getUserMedia failed:', err);
// The user hasn't granted permission yet, or hardware error
sendResponse({ success: false, error: 'not-allowed' });
chrome.runtime.sendMessage({
type: 'OFFSCREEN_RECORDING_ERROR',
payload: { error: 'not-allowed' }
});
});
return true; // Keep channel open for async sendResponse
}
if (message.type === 'STOP_RECORDING') {
if (isRecording && recognition) {
isRecording = false;
try {
recognition.stop();
} catch (e) {
console.warn('[Offscreen] Stop failed:', e);
}
}
stopMediaTracks();
sendResponse({ success: true });
return true;
}
});

View File

@@ -0,0 +1,70 @@
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>السماح باستخدام الميكروفون</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #0a0a0f;
color: #ffffff;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
text-align: center;
}
.container {
background: #1a1a26;
padding: 40px;
border-radius: 12px;
border: 1px solid rgba(108, 99, 255, 0.2);
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
max-width: 500px;
}
h1 {
color: #6c63ff;
margin-top: 0;
}
p {
color: #a0a0b8;
line-height: 1.6;
font-size: 16px;
margin-bottom: 30px;
}
.btn {
background: linear-gradient(135deg, #6c63ff, #4834d4);
color: white;
border: none;
padding: 12px 24px;
font-size: 16px;
border-radius: 8px;
cursor: pointer;
font-weight: bold;
transition: all 0.3s;
}
.btn:hover {
transform: scale(1.05);
box-shadow: 0 4px 15px rgba(108, 99, 255, 0.4);
}
.icon {
font-size: 48px;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="container">
<div class="icon">🎤</div>
<h1>مطلوب إذن الميكروفون</h1>
<p>ميزة الإملاء الصوتي تحتاج إلى إذن للوصول إلى الميكروفون لتعمل بشكل صحيح في الخلفية.</p>
<p>يرجى الضغط على الزر أدناه والموافقة على طلب الصلاحية من المتصفح.</p>
<button class="btn" id="grantBtn">منح صلاحية الميكروفون</button>
<p id="status" style="margin-top: 20px; font-weight: bold;"></p>
</div>
<script src="permission.js"></script>
</body>
</html>

View File

@@ -0,0 +1,19 @@
document.getElementById('grantBtn').addEventListener('click', async () => {
const status = document.getElementById('status');
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
// Stop the tracks immediately since we only needed to ask for permission
stream.getTracks().forEach(track => track.stop());
status.style.color = '#00d67e';
status.textContent = '✅ تم منح الصلاحية بنجاح! يمكنك إغلاق هذه النافذة والعودة إلى Claude.';
// Auto close after 3 seconds
setTimeout(() => {
window.close();
}, 3000);
} catch (err) {
status.style.color = '#ff4d6d';
status.textContent = '❌ حدث خطأ أو تم رفض الصلاحية: ' + err.message;
}
});

View File

@@ -0,0 +1,96 @@
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>كلود - إملاء صوتي عربي</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="popup-container">
<!-- Header -->
<div class="popup-header">
<div class="popup-icon">🎤</div>
<div class="popup-title">
<h1>الإملاء الصوتي لكلود</h1>
<p class="subtitle">Claude Arabic Voice Input</p>
</div>
</div>
<!-- Status -->
<div class="status-section">
<div class="status-indicator" id="statusIndicator">
<span class="status-dot" id="statusDot"></span>
<span id="statusText">جاهز للاستخدام</span>
</div>
</div>
<!-- Settings -->
<div class="settings-section">
<h2>⚙️ الإعدادات</h2>
<!-- Language -->
<div class="setting-group">
<label for="language">لغة التعرف على الصوت</label>
<select id="language">
<option value="ar-SA">العربية (السعودية)</option>
<option value="ar-AE">العربية (الإمارات)</option>
<option value="ar-EG">العربية (مصر)</option>
<option value="ar-JO">العربية (الأردن)</option>
<option value="ar-SY">العربية (سوريا)</option>
<option value="ar-IQ">العربية (العراق)</option>
<option value="ar">العربية (عام)</option>
<option value="en-US">English (US)</option>
</select>
</div>
<!-- Auto Send -->
<div class="setting-group checkbox-group">
<label class="checkbox-label">
<input type="checkbox" id="autoSend">
<span>إرسال تلقائي بعد الانتهاء من الكلام</span>
</label>
<p class="setting-hint">بعد التوقف عن الكلام لمدة ثانيتين، يتم إرسال النص تلقائياً</p>
</div>
<!-- Gemini AI -->
<div class="setting-group divider">
<h3>🤖 معالجة النص بـ Gemini AI</h3>
<p class="setting-hint">يحسن دقة النص المنطوق ويصحح الأخطاء الإملائية</p>
<label class="checkbox-label">
<input type="checkbox" id="useGemini">
<span>تفعيل معالجة Gemini</span>
</label>
<div id="geminiSettings" class="gemini-settings" style="display:none;">
<label for="geminiApiKey">مفتاح API</label>
<input type="password" id="geminiApiKey" placeholder="أدخل مفتاح Gemini API">
<p class="setting-hint">استخدم نفس مفتاح API الموجود في إعدادات LinkedIn Analyzer</p>
<p class="setting-hint">النموذج المستخدم: <strong>gemini-flash-lite-latest</strong></p>
</div>
</div>
</div>
<!-- How to use -->
<div class="help-section">
<h2>📖 كيفية الاستخدام</h2>
<ol>
<li>اذهب إلى <strong>claude.ai</strong></li>
<li>ستجد زر المايك 🎤 بجانب حقل الإدخال</li>
<li>اضغط على الزر وابدأ بالتحدث بالعربية</li>
<li>عند التوقف عن الكلام، سيتم إدراج النص تلقائياً</li>
<li>يمكنك تفعيل Gemini لتحسين دقة النص</li>
</ol>
</div>
<!-- Footer -->
<div class="popup-footer">
<button id="saveBtn" class="save-btn">💾 حفظ الإعدادات</button>
<p id="saveMessage" class="save-message"></p>
</div>
</div>
<script src="popup.js"></script>
</body>
</html>

View File

@@ -0,0 +1,83 @@
// popup.js — Settings UI for Claude Arabic Voice
document.addEventListener('DOMContentLoaded', () => {
// ─── Load Settings ───────────────────────────────────────────────────────
chrome.storage.sync.get(
['language', 'autoSend', 'useGemini', 'geminiApiKey', 'geminiModel'],
(data) => {
if (data.language) document.getElementById('language').value = data.language;
if (data.autoSend) document.getElementById('autoSend').checked = data.autoSend;
if (data.useGemini) {
document.getElementById('useGemini').checked = data.useGemini;
document.getElementById('geminiSettings').style.display = 'block';
}
if (data.geminiApiKey) document.getElementById('geminiApiKey').value = data.geminiApiKey;
}
);
// ─── Toggle Gemini Settings ──────────────────────────────────────────────
document.getElementById('useGemini').addEventListener('change', (e) => {
document.getElementById('geminiSettings').style.display = e.target.checked ? 'block' : 'none';
});
// ─── Save Settings ───────────────────────────────────────────────────────
document.getElementById('saveBtn').addEventListener('click', () => {
const settings = {
language: document.getElementById('language').value,
autoSend: document.getElementById('autoSend').checked,
useGemini: document.getElementById('useGemini').checked,
geminiApiKey: document.getElementById('geminiApiKey').value.trim(),
geminiModel: 'gemini-flash-lite-latest'
};
chrome.storage.sync.set(settings, () => {
const message = document.getElementById('saveMessage');
message.textContent = '✅ تم حفظ الإعدادات بنجاح!';
message.style.color = '#00c853';
setTimeout(() => {
message.textContent = '';
}, 2500);
// Notify content script of settings change
chrome.tabs.query({ url: 'https://claude.ai/*' }, (tabs) => {
tabs.forEach(tab => {
chrome.tabs.sendMessage(tab.id, {
type: 'SETTINGS_UPDATED',
payload: settings
}).catch(() => { /* tab may not have content script */ });
});
});
});
});
// ─── Update Status ──────────────────────────────────────────────────────
function updateStatus() {
const statusDot = document.getElementById('statusDot');
const statusText = document.getElementById('statusText');
chrome.tabs.query({ url: 'https://claude.ai/*' }, (tabs) => {
if (tabs.length === 0) {
statusDot.className = 'status-dot offline';
statusText.textContent = '🔴 Claude.ai غير مفتوح';
return;
}
// Check if content script is loaded
chrome.tabs.sendMessage(tabs[0].id, { type: 'GET_STATUS' }, (response) => {
if (chrome.runtime.lastError) {
statusDot.className = 'status-dot offline';
statusText.textContent = '🔴 الإضافة غير نشطة - أعد تحميل الصفحة';
} else if (response && response.isListening) {
statusDot.className = 'status-dot listening';
statusText.textContent = '🔴 جارٍ الاستماع...';
} else {
statusDot.className = 'status-dot online';
statusText.textContent = '✅ جاهز للاستخدام';
}
});
});
}
updateStatus();
setInterval(updateStatus, 3000);
});

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Speech Recognition Frame</title>
</head>
<body>
<script src="speech.js"></script>
</body>
</html>

View File

@@ -0,0 +1,121 @@
// speech.js - Handles webkitSpeechRecognition inside the iframe
let recognition = null;
let isRecording = false;
let localStream = null;
function stopMediaTracks() {
if (localStream) {
localStream.getTracks().forEach(track => track.stop());
localStream = null;
}
}
function initRecognition(language) {
if (recognition) return recognition;
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
if (!SpeechRecognition) {
throw new Error('Speech recognition not supported');
}
recognition = new SpeechRecognition();
recognition.continuous = true;
recognition.interimResults = true;
recognition.lang = language || 'ar-SA';
recognition.maxAlternatives = 1;
recognition.onresult = (event) => {
let interimText = '';
let finalText = '';
for (let i = event.resultIndex; i < event.results.length; i++) {
const result = event.results[i];
if (result.isFinal) {
finalText += result[0].transcript + ' ';
} else {
interimText += result[0].transcript;
}
}
window.parent.postMessage({
type: 'SPEECH_RESULT',
payload: { interimText, finalText }
}, '*');
};
recognition.onerror = (event) => {
console.error('[Speech Iframe] Recognition error:', event.error);
if (event.error !== 'no-speech') {
isRecording = false;
stopMediaTracks();
}
window.parent.postMessage({
type: 'SPEECH_ERROR',
payload: { error: event.error }
}, '*');
};
recognition.onend = () => {
if (isRecording) {
try {
recognition.start();
} catch (e) {
console.warn('[Speech Iframe] Restart failed:', e);
}
} else {
stopMediaTracks();
window.parent.postMessage({ type: 'SPEECH_END' }, '*');
}
};
return recognition;
}
// Listen for messages from parent window
window.addEventListener('message', (event) => {
// Basic security check (only accept messages from the parent Claude page)
if (!event.origin.includes('claude.ai')) return;
const message = event.data;
if (message.type === 'START_RECORDING') {
const lang = message.payload?.language || 'ar-SA';
navigator.mediaDevices.getUserMedia({ audio: true })
.then((stream) => {
localStream = stream;
try {
if (!recognition || recognition.lang !== lang) {
recognition = initRecognition(lang);
}
if (!isRecording) {
recognition.start();
isRecording = true;
window.parent.postMessage({ type: 'SPEECH_START_SUCCESS' }, '*');
}
} catch (e) {
console.error('[Speech Iframe] Failed to start:', e);
window.parent.postMessage({ type: 'SPEECH_ERROR', payload: { error: e.message } }, '*');
}
})
.catch((err) => {
console.error('[Speech Iframe] getUserMedia failed:', err);
window.parent.postMessage({ type: 'SPEECH_ERROR', payload: { error: 'not-allowed' } }, '*');
});
}
if (message.type === 'STOP_RECORDING') {
if (isRecording && recognition) {
isRecording = false;
try {
recognition.stop();
} catch (e) {
console.warn('[Speech Iframe] Stop failed:', e);
}
}
stopMediaTracks();
}
});

View File

@@ -0,0 +1,290 @@
/* ─── Popup Styles ─────────────────────────────────────────────────────────── */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, -apple-system, BlinkMacSystemFont, sans-serif;
background: #1a1a2e;
color: #e0e0e0;
width: 380px;
min-height: 400px;
direction: rtl;
}
.popup-container {
padding: 16px;
}
/* ─── Header ──────────────────────────────────────────────────────────────── */
.popup-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.popup-icon {
font-size: 32px;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #6c63ff, #4834d4);
border-radius: 12px;
}
.popup-title h1 {
font-size: 16px;
font-weight: 700;
color: #fff;
margin: 0;
}
.popup-title .subtitle {
font-size: 11px;
color: #888;
margin: 2px 0 0 0;
}
/* ─── Status ──────────────────────────────────────────────────────────────── */
.status-section {
margin-bottom: 16px;
}
.status-indicator {
display: flex;
align-items: center;
gap: 8px;
background: rgba(255, 255, 255, 0.05);
padding: 8px 12px;
border-radius: 8px;
font-size: 13px;
}
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: #666;
flex-shrink: 0;
}
.status-dot.online {
background: #00c853;
box-shadow: 0 0 8px rgba(0, 200, 83, 0.5);
}
.status-dot.offline {
background: #ff5252;
box-shadow: 0 0 8px rgba(255, 82, 82, 0.5);
}
.status-dot.listening {
background: #ff1744;
animation: pulse-dot 1.5s infinite;
}
@keyframes pulse-dot {
0% {
box-shadow: 0 0 0 0 rgba(255, 23, 68, 0.6);
}
50% {
box-shadow: 0 0 0 8px rgba(255, 23, 68, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(255, 23, 68, 0);
}
}
/* ─── Settings ────────────────────────────────────────────────────────────── */
.settings-section {
margin-bottom: 16px;
}
.settings-section h2 {
font-size: 14px;
font-weight: 600;
color: #ccc;
margin-bottom: 12px;
}
.settings-section h3 {
font-size: 13px;
font-weight: 600;
color: #bbb;
margin-bottom: 8px;
}
.setting-group {
margin-bottom: 14px;
}
.setting-group label {
display: block;
font-size: 12px;
font-weight: 500;
color: #aaa;
margin-bottom: 4px;
}
.setting-group select,
.setting-group input[type="password"],
.setting-group input[type="text"] {
width: 100%;
padding: 8px 10px;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 6px;
color: #fff;
font-size: 13px;
font-family: inherit;
outline: none;
transition: border-color 0.2s;
}
.setting-group select:focus,
.setting-group input:focus {
border-color: #6c63ff;
}
.setting-group select option {
background: #1a1a2e;
color: #fff;
}
.setting-hint {
font-size: 11px;
color: #777;
margin-top: 4px;
line-height: 1.4;
}
.setting-hint a {
color: #6c63ff;
text-decoration: none;
}
.setting-hint a:hover {
text-decoration: underline;
}
/* ─── Checkbox ────────────────────────────────────────────────────────────── */
.checkbox-group {
margin-bottom: 10px;
}
.checkbox-label {
display: flex !important;
align-items: center;
gap: 8px;
cursor: pointer;
font-size: 13px !important;
color: #ddd !important;
}
.checkbox-label input[type="checkbox"] {
width: 16px;
height: 16px;
accent-color: #6c63ff;
cursor: pointer;
}
/* ─── Divider ─────────────────────────────────────────────────────────────── */
.divider {
padding-top: 12px;
border-top: 1px solid rgba(255, 255, 255, 0.08);
}
/* ─── Gemini Settings ─────────────────────────────────────────────────────── */
.gemini-settings {
margin-top: 10px;
padding: 12px;
background: rgba(108, 99, 255, 0.08);
border: 1px solid rgba(108, 99, 255, 0.2);
border-radius: 8px;
}
.gemini-settings label {
margin-top: 8px;
}
.gemini-settings label:first-child {
margin-top: 0;
}
/* ─── Help Section ────────────────────────────────────────────────────────── */
.help-section {
margin-bottom: 16px;
padding: 12px;
background: rgba(255, 255, 255, 0.03);
border-radius: 8px;
}
.help-section h2 {
font-size: 13px;
font-weight: 600;
color: #ccc;
margin-bottom: 8px;
}
.help-section ol {
padding-right: 20px;
font-size: 12px;
color: #999;
line-height: 1.8;
}
.help-section ol li strong {
color: #ddd;
}
/* ─── Footer ──────────────────────────────────────────────────────────────── */
.popup-footer {
text-align: center;
}
.save-btn {
width: 100%;
padding: 10px;
background: linear-gradient(135deg, #6c63ff, #4834d4);
color: white;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
font-family: inherit;
}
.save-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 15px rgba(108, 99, 255, 0.4);
}
.save-btn:active {
transform: translateY(0);
}
.save-message {
font-size: 12px;
margin-top: 8px;
min-height: 18px;
}

View File

@@ -230,16 +230,10 @@
const questions = []; const questions = [];
const seen = new Set(); const seen = new Set();
// Find the Easy Apply modal using real LinkedIn selectors
const modal = document.querySelector(
'[role="dialog"].jobs-easy-apply-modal, .artdeco-modal.jobs-easy-apply-modal, ' +
'.artdeco-modal, [role="dialog"], #artdeco-modal-outlet'
);
const searchRoot = modal || document.body;
// LinkedIn wraps each form field in .fb-dash-form-element or [data-test-form-element] // LinkedIn wraps each form field in .fb-dash-form-element or [data-test-form-element]
const formElements = searchRoot.querySelectorAll( // Search the whole document to avoid getting trapped in the wrong dialog (like the messaging pane).
'.fb-dash-form-element, [data-test-form-element]' const formElements = document.querySelectorAll(
'.jobs-easy-apply-modal .fb-dash-form-element, .jobs-easy-apply-modal [data-test-form-element], .fb-dash-form-element, [data-test-form-element]'
); );
formElements.forEach(el => { formElements.forEach(el => {
@@ -268,25 +262,28 @@
questions.push({ question: text, type: inputType }); questions.push({ question: text, type: inputType });
}); });
// Fallback: if the precise approach found nothing, scan broader // Fallback: if the precise approach found nothing, scan broader inside modals
if (questions.length === 0) { if (questions.length === 0) {
const allLabels = searchRoot.querySelectorAll('label'); const activeModals = document.querySelectorAll('.jobs-easy-apply-modal, .artdeco-modal, [role="dialog"]');
allLabels.forEach(label => { activeModals.forEach(modal => {
const ariaSpan = label.querySelector('span[aria-hidden="true"]'); const allLabels = modal.querySelectorAll('label');
let text = (ariaSpan ? ariaSpan.textContent : label.textContent) allLabels.forEach(label => {
.replace(/\*/g, '').replace(/\n/g, ' ').replace(/\s+/g, ' ').trim(); const ariaSpan = label.querySelector('span[aria-hidden="true"]');
let text = (ariaSpan ? ariaSpan.textContent : label.textContent)
if (!text || text.length < 8 || text.length > 300 || seen.has(text.toLowerCase())) return; .replace(/\*/g, '').replace(/\n/g, ' ').replace(/\s+/g, ' ').trim();
const looksLikeQuestion = ( if (!text || text.length < 8 || text.length > 300 || seen.has(text.toLowerCase())) return;
text.endsWith('?') ||
/^(how many|what|do you|are you|have you|will you|describe|tell us)/i.test(text) || const looksLikeQuestion = (
/salary|experience|education|degree|visa|relocat|notice|certif/i.test(text) text.endsWith('?') ||
); /^(how many|what|do you|are you|have you|will you|describe|tell us)/i.test(text) ||
if (!looksLikeQuestion) return; /salary|experience|education|degree|visa|relocat|notice|certif/i.test(text)
);
if (!looksLikeQuestion) return;
seen.add(text.toLowerCase()); seen.add(text.toLowerCase());
questions.push({ question: text, type: 'text' }); questions.push({ question: text, type: 'text' });
});
}); });
} }
@@ -538,7 +535,8 @@
apiKey: settings.apiKey, apiKey: settings.apiKey,
jobDescription: jobData.description, jobDescription: jobData.description,
jobTitle: jobData.jobTitle || 'Job', jobTitle: jobData.jobTitle || 'Job',
action: 'generatePdf' action: 'generatePdf',
template: 'amman'
} }
}, resolve); }, resolve);
}); });
@@ -641,6 +639,17 @@
</div> </div>
`; `;
// Attach inline copy button listeners
if (tab === 'qa') {
pane.querySelectorAll('.lja-qa-copy-btn').forEach(btn => {
btn.addEventListener('click', function() {
navigator.clipboard.writeText(this.getAttribute('data-answer'));
this.textContent = '✅';
setTimeout(() => this.textContent = '📋', 1200);
});
});
}
copyBtn.style.display = 'block'; copyBtn.style.display = 'block';
// Show auto-fill button for Q&A tab // Show auto-fill button for Q&A tab
@@ -813,12 +822,7 @@ Brief honest assessment of this opportunity for my profile`
<div style="font-weight: 600; font-size: 12px; color: #aaa; margin-bottom: 4px;">❓ ${q}</div> <div style="font-weight: 600; font-size: 12px; color: #aaa; margin-bottom: 4px;">❓ ${q}</div>
<div style="display: flex; align-items: center; gap: 8px;"> <div style="display: flex; align-items: center; gap: 8px;">
<div style="color: #4caf50; font-size: 14px; font-weight: 500; flex: 1;" id="lja-qa-answer-${idx}">💡 ${a}</div> <div style="color: #4caf50; font-size: 14px; font-weight: 500; flex: 1;" id="lja-qa-answer-${idx}">💡 ${a}</div>
<button onclick=" <button class="lja-qa-copy-btn" data-answer="${safeAnswer}" style="background: rgba(108,99,255,0.2); border: none; border-radius: 4px; padding: 4px 8px; cursor: pointer; color: #b0b0ff; font-size: 14px; flex-shrink: 0;" title="Copy answer">📋</button>
const text = document.getElementById('lja-qa-answer-${idx}').textContent.replace('💡 ', '');
navigator.clipboard.writeText(text);
this.textContent = '✅';
setTimeout(() => this.textContent = '📋', 1200);
" style="background: rgba(108,99,255,0.2); border: none; border-radius: 4px; padding: 4px 8px; cursor: pointer; color: #b0b0ff; font-size: 14px; flex-shrink: 0;" title="Copy answer">📋</button>
</div> </div>
</div>`; </div>`;
}); });
@@ -931,7 +935,16 @@ Brief honest assessment of this opportunity for my profile`
function getSettings() { function getSettings() {
return new Promise(resolve => { return new Promise(resolve => {
chrome.storage.sync.get(['apiKey', 'userProfile', 'language'], resolve); chrome.storage.sync.get(['apiKey', 'userProfile', 'language'], (data) => {
if (data.userProfile && (data.userProfile.includes('hamzaayed@intaleqapp.com') || data.userProfile.includes('hamzaayedflutter@gmail.com') || data.userProfile.includes('hamzaayed@tripz-egypt.com'))) {
data.userProfile = data.userProfile
.replace(/hamzaayed@intaleqapp\.com/g, 'hamzaayed.dev@gmail.com')
.replace(/hamzaayedflutter@gmail\.com/g, 'hamzaayed.dev@gmail.com')
.replace(/hamzaayed@tripz-egypt\.com/g, 'hamzaayed.dev@gmail.com');
chrome.storage.sync.set({ userProfile: data.userProfile });
}
resolve(data);
});
}); });
} }
@@ -947,6 +960,155 @@ Brief honest assessment of this opportunity for my profile`
setTimeout(() => toast.classList.remove('show'), 2500); setTimeout(() => toast.classList.remove('show'), 2500);
} }
// ─── List Scanner ────────────────────────────────────────────────────────
function injectListScanner() {
if (document.getElementById('lja-scan-btn')) return;
const btn = document.createElement('button');
btn.id = 'lja-scan-btn';
btn.innerHTML = '🔍 Scan List';
btn.style.cssText = `
position: fixed;
bottom: 20px;
left: 20px;
z-index: 999999;
background: linear-gradient(135deg, #00d67e, #00a65e);
color: white;
border: none;
border-radius: 50px;
padding: 12px 24px;
font-size: 14px;
font-weight: bold;
cursor: pointer;
box-shadow: 0 4px 15px rgba(0, 214, 126, 0.4);
transition: transform 0.2s;
font-family: Arial, sans-serif;
`;
btn.onmouseover = () => btn.style.transform = 'scale(1.05)';
btn.onmouseout = () => btn.style.transform = 'scale(1)';
btn.onclick = async () => {
btn.innerHTML = '⏳ Scanning...';
try {
await scanJobList();
} catch (e) {
console.error(e);
alert('Scan failed: ' + e.message);
}
btn.innerHTML = '🔍 Scan List';
};
document.body.appendChild(btn);
}
async function scanJobList() {
const listItems = document.querySelectorAll('.jobs-search-results__list-item, .job-card-container');
if (!listItems.length) {
alert('No job items found on this page. Scroll down to load them.');
return;
}
const jobsToScan = [];
listItems.forEach((item, index) => {
const titleEl = item.querySelector('.job-card-list__title, .artdeco-entity-lockup__title');
const companyEl = item.querySelector('.job-card-container__primary-description, .artdeco-entity-lockup__subtitle');
if (titleEl && companyEl) {
jobsToScan.push({
index: index,
title: titleEl.textContent.trim().replace(/\n/g, ''),
company: companyEl.textContent.trim().replace(/\n/g, ''),
element: item
});
}
});
if (!jobsToScan.length) {
alert('Could not parse job titles.');
return;
}
document.querySelectorAll('.lja-badge').forEach(b => b.remove());
if (jobsToScan.length === 0) return;
// Limit to 25 jobs per scan to prevent AI token truncation
const jobsToProcess = jobsToScan.slice(0, 25);
const listDataStr = JSON.stringify(jobsToProcess.map(j => ({ index: j.index, title: j.title, company: j.company })));
const settings = await getSettings();
if (!settings.apiKey) {
alert('Please set your API key in the extension popup first.');
return;
}
if (typeof buildPromptV2 !== 'function') {
alert('buildPromptV2 not found. Extension error.');
return;
}
const promptStr = buildPromptV2('list_analysis', { skills: [], listData: listDataStr }, settings.userProfile, settings.language);
const response = await chrome.runtime.sendMessage({
type: 'GEMINI_REQUEST',
payload: { apiKey: settings.apiKey, prompt: promptStr, tab: 'list_analysis' }
});
if (!response.success) {
console.error('[LJA] Background Error:', response.error);
throw new Error('API Error: ' + response.error);
}
let resultText = response.data.text;
resultText = resultText.replace(/```json/gi, '').replace(/```/g, '').trim();
let results;
try {
const startIdx = resultText.indexOf('[');
const endIdx = resultText.lastIndexOf(']');
if (startIdx !== -1 && endIdx !== -1) {
results = JSON.parse(resultText.substring(startIdx, endIdx + 1));
} else {
results = JSON.parse(resultText);
}
} catch(e) {
console.error("AI JSON Parse Error. Raw Response:", resultText);
throw new Error('JSON Error: ' + resultText.substring(0, 80));
}
results.forEach(res => {
const jobItem = jobsToScan.find(j => j.index === res.index);
if (jobItem) {
const badge = document.createElement('div');
badge.className = 'lja-badge';
badge.style.cssText = `
display: inline-block;
padding: 4px 8px;
border-radius: 6px;
font-size: 11px;
font-weight: bold;
margin-top: 6px;
color: white;
width: 100%;
box-sizing: border-box;
`;
if (res.verdict === 'YES') {
badge.style.background = 'linear-gradient(135deg, #00d67e, #00a65e)';
badge.innerHTML = `✅ MATCH: ${res.reason}`;
} else if (res.verdict === 'NO') {
badge.style.background = 'linear-gradient(135deg, #ff4d6d, #d90429)';
badge.innerHTML = `❌ SKIP: ${res.reason}`;
} else {
badge.style.background = 'linear-gradient(135deg, #ffb347, #ff9200)';
badge.innerHTML = `⚠️ MAYBE: ${res.reason}`;
}
const contentContainer = jobItem.element.querySelector('.artdeco-entity-lockup__content');
if (contentContainer) contentContainer.appendChild(badge);
}
});
}
// ─── SPA Navigation Observer ───────────────────────────────────────────── // ─── SPA Navigation Observer ─────────────────────────────────────────────
let lastUrl = location.href; let lastUrl = location.href;
@@ -962,7 +1124,7 @@ Brief honest assessment of this opportunity for my profile`
const old = document.getElementById('lja-root'); const old = document.getElementById('lja-root');
if (old) old.remove(); if (old) old.remove();
window.__linkedinAnalyzerLoaded = false; window.__linkedinAnalyzerLoaded = false;
setTimeout(injectOverlay, 1200); setTimeout(() => { injectOverlay(); injectListScanner(); }, 1200);
} }
// Watch for URL changes (SPA navigation + job switches) // Watch for URL changes (SPA navigation + job switches)
@@ -998,9 +1160,9 @@ Brief honest assessment of this opportunity for my profile`
function init() { function init() {
if (document.readyState === 'loading') { if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => setTimeout(injectOverlay, 1000)); document.addEventListener('DOMContentLoaded', () => setTimeout(() => { injectOverlay(); injectListScanner(); }, 1000));
} else { } else {
setTimeout(injectOverlay, 1000); setTimeout(() => { injectOverlay(); injectListScanner(); }, 1000);
} }
} }

192
cv_market_analysis.md Normal file
View File

@@ -0,0 +1,192 @@
# تحليل سوق عمّان — استراتيجية الـ CV
## نظرة عامة على السوق
### شركات التكنولوجيا المحلية في عمّان (Target Companies)
| الشركة | المجال | الـ Stack الشائع | المسمى الوظيفي المناسب |
|--------|--------|-----------------|----------------------|
| Mawdoo3 | محتوى/AI | PHP, Python, Node.js, AWS | Senior Backend Engineer / Tech Lead |
| Abwaab | EdTech | Node.js, React, PostgreSQL, AWS | Lead Backend Engineer |
| OpenSooq | Classifieds | PHP, MySQL, Redis, AWS | Senior Backend Developer |
| ArabiaWeather | GIS/Data | Python, GIS, APIs | GIS/Backend Developer |
| Liwwa | FinTech | Node.js, Python, PostgreSQL | Senior Backend Engineer |
| JoPACC | FinTech/Payments | Java, Node.js, APIs | Systems Engineer |
| ZenHR | HR Tech/SaaS | PHP, Node.js, MySQL | Senior Software Engineer |
| Atypon | Publishing | Java, Node.js, AWS | Senior Engineer |
### ماذا تبحث عنه شركات عمّان في الـ CV؟
1. **التسليم والتنفيذ (Delivery over Design):** يريدون شخص يبني وينفذ، مش بس يرسم architecture diagrams
2. **الاستمرارية (Retention):** الخوف الأكبر: "هل حيتركنا ويروح عالخليج؟"
3. **العمل الجماعي (Team Player):** يريدون شخص يشتغل مع فريق، يمنتور junior developers
4. **التكلفة (Cost-Conscious):** يحبوا أرقام التوفير — $10K/month قصة قوية
5. **اللغة (Bilingual):** ثنائية اللغة ميزة تنافسية كبيرة
6. **السوق المحلي (Local Market):** فهم السوق الأردني والعربي مهم
7. **الـ Stack المطلوب:** PHP, Node.js, PostgreSQL, AWS, Docker — مش GIS ولا ride-hailing
---
## المشكلة الأساسية مع الـ CV الحالي
### ١. العنوان والـ Positioning
**الحالي:** "Solutions Architect" — هذا عنوان شركات enterprise في الخليج وأوروبا. شركة عمّان ما بتوظف Solutions Architect. بتوظف Senior Backend Engineer أو Tech Lead.
**المطلوب:** استبدال كل mentions الـ "Solutions Architect" بـ: `Senior Backend Engineer & Technical Lead` أو `Lead Backend Engineer — Distributed Systems` حسب الوظيفة.
### ٢. قصة الـ Founder/CTO
**المشكلة:** الـ CV الحالي بيحكي قصة "أنا بنيت شركتين" مش "أنا مهندس ممكن أضيف قيمة لشركتكم."
**الحل:** إعادة صياغة التجربة كلغة delivery و engineering، مش founding:
- ❌ "CTO & Technical Architect — Intaleq"
- ✅ "Technical Lead & Backend Engineer — Intaleq"
- ❌ "Founding Engineer & Lead Backend Architect — Tripz Egypt"
- ✅ "Lead Backend Engineer — Tripz Egypt"
- ❌ "Co-founded and architected..."
- ✅ "Built and scaled..."
### ٣. الـ AI Claim
**الحالي:** عنوان "AI Solutions Architect" مع إن الـ AI experience محدود بـ LLM API integration.
**الحل:**
- حذف "AI" من الـ positioning الأساسي
- ذكر الـ AI tools كمهارة إضافية، مش كتخصص
- إعادة تسمية: Nabih → "Automated Customer Support System" مش "AI Smart Responder"
- إعادة تسمية: Musadaq → "Document Processing Platform" مش "AI-Powered Invoice Processing"
### ٤. الـ Stack Prioritization
**الحالي:** OSM, GraphHopper, MapLibre, PostGIS في المقدمة — هذا stack شخص بنى ride-hailing.
**المطلوب لعمّان:**
1. PHP (Workerman), Node.js, NestJS — أول شيء
2. PostgreSQL, MySQL, Redis
3. Docker, AWS, CI/CD
4. REST APIs, WebSockets, Microservices
5. Flutter (ثانوي)
6. GIS (في الأخير — ميزة إضافية مش أساسية)
### ٥. السطر الـ AI-generated
**الحذف فوراً:**
> *"I leverage my expertise..."*
أي HR في عمّان شايف هاد السطر ١٠٠ مرة من ناس مستخدمين ChatGPT.
### ٦. الفريلانس بدون أسماء
**المشكلة:** "25+ production applications" بدون أسماء شركات أو صناعات.
**الحل:** ذكر الصناعات بوضوح — healthcare, logistics, HR, sports, utilities, fintech — بدون أسماء شركات إذا ما في إذن، لكن الصناعات تبني ثقة.
---
## الـ CV الجديد — الهيكل المقترح لسوق عمّان
### Header
- الاسم: Hamza Ayed
- العنوان: `{{JOB_HEADLINE}}` — يجب أن يكون `Senior Backend Engineer & Technical Lead` أو ما يناسب الوظيفة
- التواصل: عمّان، الأردن (مش Amman, Jordan — أكد الموقع)
### ١. Professional Summary
- التركيز على: delivery، scale، cost savings، MENA experience
- حذف ride-hailing hook الإجباري — استبداله بـ hook عن building production systems
- 3-4 أسطر: من أنا، ماذا بنيت، ماذا أقدم للشركة
### ٢. Technical Skills (مرتبة حسب أولوية سوق عمّان)
```
Core Focus: {{DYNAMIC_SKILLS}} ← AI-generated per job
Backend & Architecture: PHP (Workerman), Node.js, NestJS, Python (FastAPI/Flask), REST APIs, WebSockets, Microservices
Infrastructure & Cloud: Docker, Linux, Nginx, Redis, CI/CD (GitHub Actions), AWS
Databases: PostgreSQL, MySQL, Redis, Database Design & Query Optimization
Geospatial (Supplemental): PostGIS, OpenStreetMap, GraphHopper, MapLibre GL
Mobile: Flutter/Dart, GetX, BLoC/Cubit
```
### ٣. Technical Impact (جديد — يركز على delivery والإنجاز)
- Built platforms serving 6,000+ active users across two markets
- Engineered proprietary mapping infrastructure replacing Google Maps — $10K+/month savings
- Designed high-concurrency WebSocket systems for real-time operations
- Delivered 25+ production applications across healthcare, logistics, fintech, and utilities
- Published open-source SDKs on pub.dev and npm
### ٤. Professional Experience (معاد صياغته)
```
Technical Lead & Backend Engineer — Intaleq | Jan 2024Present | Amman, Jordan
• Built full-stack transportation platform from zero: PHP/Workerman backend, Flutter apps, WebSocket dispatcher — serving 1,800+ drivers
• Developed proprietary mapping platform (NestJS, GraphHopper, OSM, PostGIS) — $10K+/month cost reduction vs Google Maps
• Designed event-driven architecture for real-time matching and tracking
Lead Backend Engineer — Tripz Egypt | Jan 2023Present | Cairo / Remote
• Built complete ride-hailing system: PHP backend, Flutter apps, real-time WebSocket layer, payment integrations
• Engineered driver/rider matching system serving 4,318 drivers — $0.78 customer acquisition cost
• Integrated local payment gateways and automated payout infrastructure
Senior Backend Engineer — Freelance | Jan 2017Dec 2023 | Jordan / Remote
• Delivered 25+ applications for MENA clients: healthcare platforms, logistics systems, HR tools, sports apps, utility dashboards
• Built automated customer support platform (Flutter + PHP backend)
• Developed document processing system using vision models and async pipelines
```
### ٥. Notable Projects
- IntaleqMaps Engine (NestJS, GraphHopper, OSM, PostGIS) — proprietary mapping
- Tripz Egypt Platform — end-to-end ride-hailing
- Automated Customer Support Platform — Flutter + high-concurrency PHP
- Document Processing System — AI vision models + async pipelines
- Meta Ads Manager — multi-tenant SaaS (NestJS/React)
- WhatsApp Bridge — high-concurrency Node.js + headless Puppeteer
### ٦. Open Source (كما هو)
### ٧. Education (كما هو)
### ٨. Languages (كما هو)
### ٩. Availability (معدل لسوق عمّان)
```
Based in Amman, Jordan — Available Immediately
Open to On-site, Hybrid, and Remote roles in Jordan and MENA
```
---
## تغييرات الـ Prompt المطلوبة
### Current Prompt (generatePdf):
```
My title MUST be aligned with 'Solutions Architect', 'Senior Backend Engineer', or 'Senior Mobile Engineer'.
The summary MUST open with exactly this hook: 'Built two production ride-hailing platforms from zero to thousands of users, on proprietary infrastructure, in high-complexity and emerging markets.'
```
### New Prompt (Amman market):
```
My title MUST be aligned with 'Senior Backend Engineer', 'Technical Lead', or 'Lead Backend Engineer'.
NEVER use 'Solutions Architect' or 'AI Developer'.
The summary should open with a hook about building and scaling production systems in the MENA region.
Focus on delivery, cost optimization, and hands-on engineering — not enterprise architecture.
```
---
## ملخص التغييرات على مستوى الملفات
| الملف | التغيير |
|-------|---------|
| `server/cv_template_amman.html` | **جديد** — قالب مخصص لسوق عمّان |
| `server/cv_template.html` | يبقى كما هو (للخليج/enterprise) |
| `server/generate_cv.php` | إضافة `template` parameter + prompt مخصص |
| `prompts.js` | تحديث `cvtips` ليعكس positioning سوق عمّان |
| `linkedin-analyzer/cv_template.html` | مزامنة مع الـ server template |
---
## الخطوات التالية
1. إنشاء `server/cv_template_amman.html`
2. تعديل `generate_cv.php` لدعم template selection
3. تعديل `prompts.js` — تحديث cvtips لسوق عمّان
4. للمستقبل: إنشاء templates إضافية لـ UAE enterprise و FinTech

View File

@@ -8,19 +8,23 @@
* Avoids flexbox, uses standard block/inline styling for flawless PDF generation. * Avoids flexbox, uses standard block/inline styling for flawless PDF generation.
*/ */
body { body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: Arial, Helvetica, sans-serif;
font-variant-ligatures: none;
font-feature-settings: "liga" 0, "clig" 0;
letter-spacing: 0px;
text-rendering: geometricPrecision;
font-size: 13px; font-size: 13px;
color: #222; color: #222;
line-height: 1.5; line-height: 1.4;
margin: 0; margin: 0;
padding: 20px 40px; padding: 15px 30px;
} }
.header { .header {
text-align: center; text-align: center;
border-bottom: 2px solid #1a237e; border-bottom: 2px solid #1a237e;
padding-bottom: 15px; padding-bottom: 10px;
margin-bottom: 20px; margin-bottom: 15px;
} }
h1 { h1 {
@@ -48,40 +52,48 @@
font-weight: bold; font-weight: bold;
color: #1a237e; color: #1a237e;
border-bottom: 1px solid #ddd; border-bottom: 1px solid #ddd;
margin-top: 20px; margin-top: 15px;
margin-bottom: 10px; margin-bottom: 6px;
padding-bottom: 4px; padding-bottom: 4px;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.5px; letter-spacing: 0.5px;
} }
p { margin: 0 0 10px 0; text-align: justify; } p { margin: 0 0 6px 0; text-align: justify; }
.job-block { margin-bottom: 15px; } .job-block { margin-bottom: 10px; }
.job-header { .job-header {
width: 100%; width: 100%;
margin-bottom: 5px; margin-bottom: 5px;
display: table; /* هذا يحل مشاكل Dompdf */
} }
.job-title { .job-title {
font-weight: bold; font-weight: bold;
font-size: 14px; font-size: 13.5px; /* تصغير طفيف جداً */
color: #222; color: #222;
float: left; float: left;
width: 70%; /* إجبار العنوان على عدم تجاوز 70% من السطر */
line-height: 1.3;
} }
.job-meta { .job-meta {
font-size: 12px; font-size: 11px;
color: #1a237e; color: #1a237e;
font-weight: bold; font-weight: bold;
float: right; float: right;
width: 28%; /* تخصيص مساحة آمنة للتاريخ */
text-align: right;
} }
.clear { clear: both; } .clear { clear: both; }
ul { margin: 5px 0 10px 0; padding-left: 20px; }
li { margin-bottom: 6px; text-align: justify; }
ul { margin: 4px 0 8px 0; padding-left: 20px; }
li { margin-bottom: 4px; text-align: justify; }
</style> </style>
</head> </head>
<body> <body>
@@ -91,7 +103,10 @@
<!-- PHP WILL INJECT THE AI-GENERATED HEADLINE HERE --> <!-- PHP WILL INJECT THE AI-GENERATED HEADLINE HERE -->
<div class="headline">{{JOB_HEADLINE}}</div> <div class="headline">{{JOB_HEADLINE}}</div>
<div class="contact"> <div class="contact">
Amman, Jordan | +962 79 858 3052 | hamzaayed@intaleqapp.com | linkedin.com/in/hamza-ayed Amman, Jordan | +962 79 858 3052 | <a href="mailto:hamzaayed.dev@gmail.com" style="color: #555; text-decoration: none;">hamzaayed.dev@gmail.com</a><br>
<a href="https://linkedin.com/in/hamza-ayed" style="color: #555; text-decoration: none; font-weight: bold;">linkedin.com/in/hamza-ayed</a> |
<a href="https://github.com/Hamza-Ayed" style="color: #555; text-decoration: none; font-weight: bold;">github.com/Hamza-Ayed</a> |
<a href="https://intaleqapp.com/hamza.html" style="color: #1a237e; text-decoration: underline; font-weight: bold;">intaleqapp.com/hamza.html (Portfolio)</a>
</div> </div>
</div> </div>
@@ -99,71 +114,95 @@
<!-- PHP WILL INJECT THE AI-GENERATED SUMMARY HERE --> <!-- PHP WILL INJECT THE AI-GENERATED SUMMARY HERE -->
<p>{{TAILORED_SUMMARY}}</p> <p>{{TAILORED_SUMMARY}}</p>
<div class="section-title">Core Competencies & Skills</div>
<div class="section-title">Technical Skills</div>
<!-- PHP WILL INJECT ATS KEYWORDS HERE --> <!-- PHP WILL INJECT ATS KEYWORDS HERE -->
<p><strong>Targeted Expertise:</strong> {{DYNAMIC_SKILLS}}</p> <p><strong>Core Focus:</strong> {{DYNAMIC_SKILLS}}</p>
<p><strong>Technologies:</strong> Flutter/Dart, PHP, Python (FastAPI/Flask), Node.js, MySQL, PostgreSQL, AWS/Cloud Infrastructure, Git, Docker.</p> <p><strong>Backend & Architecture:</strong> PHP (Workerman), Node.js, NestJS, Python (FastAPI/Flask), REST APIs, WebSockets, Event-Driven Architecture</p>
<p><strong>Domains:</strong> System Architecture, Distributed Systems, GIS/Mapping (OSM), FinTech (Payment Gateways), AI/ML Integration, Zero-Trust Security.</p> <p><strong>Infrastructure & Cloud:</strong> Docker, Linux, Nginx, Redis, CI/CD (GitHub Actions), AWS</p>
<p><strong>Databases & Geospatial:</strong> PostgreSQL/PostGIS, MySQL, OpenStreetMap (OSM), GraphHopper, MapLibre GL, Spatial Queries</p>
<p><strong>Mobile:</strong> Flutter/Dart, GetX, BLoC/Cubit</p>
<div class="section-title">Selected Technical Highlights</div>
<ul>
<li>Built proprietary OSM-based mapping infrastructure replacing Google Maps APIs</li>
<li>Engineered WebSocket-based real-time ride tracking systems</li>
<li>Developed offline-first mapping SDKs published on pub.dev and npm</li>
<li>Designed async AI document-processing pipelines using vision models</li>
<li>Optimized routing infrastructure using GraphHopper and PostGIS</li>
</ul>
<div class="section-title">Professional Experience</div> <div class="section-title">Professional Experience</div>
<div class="job-block"> <div class="job-block">
<div class="job-header"> <div class="job-header">
<div class="job-title">CTO & Solutions Architect — Intaleq</div> <div class="job-title">CTO & Technical Architect | Systems & Backend Focus — Intaleq &nbsp;|&nbsp;</div>
<div class="job-meta">Jan 2025 Present | Syria / Remote</div> <div class="job-meta">January 2024 Present | Remote (MENA Region)</div>
<div class="clear"></div> <div class="clear"></div>
</div> </div>
<ul> <ul>
<li>Led the full-stack architecture of a smart transportation ecosystem supporting 1,800+ drivers and 2,500+ riders.</li> <li>Led the full-stack architecture of a smart transportation ecosystem supporting 1,800+ drivers and 2,500+ riders, building the core backend with PHP/Workerman.</li>
<li>Built a proprietary mapping platform (IntaleqMaps) on OpenStreetMap, eliminating reliance on Google Maps API and saving $10,000+/month in operational costs.</li> <li>Architected the proprietary mapping platform (IntaleqMaps), orchestrating GraphHopper routing, OSM tile serving, and PostGIS spatial queries via a NestJS API layer, saving $10,000+/month in operational costs.</li>
<li>Designed secure, custom payment infrastructure for environments lacking standard payment APIs, ensuring high-availability transaction integrity.</li>
</ul> </ul>
</div> </div>
<div class="job-block"> <div class="job-block">
<div class="job-header"> <div class="job-header">
<div class="job-title">Co-Founder & Lead Developer — Tripz Egypt</div> <div class="job-title">Founding Engineer & Lead Backend Architect — Tripz Egypt &nbsp;|&nbsp;</div>
<div class="job-meta">Jan 2024 Present | Cairo / Remote</div> <div class="job-meta">Jan 2023 Present | Cairo / Remote</div>
<div class="clear"></div> <div class="clear"></div>
</div> </div>
<ul> <ul>
<li>Architected Egypt's homegrown ride-hailing platform, managing the entire distributed ecosystem with real-time tracking and dispatching.</li> <li>Co-founded and architected Egypt's homegrown ride-hailing platform, managing the entire distributed ecosystem with real-time tracking for 4,318 drivers and 2,464 riders.</li>
<li>Implemented robust microservices for real-time driver/rider matching and route optimization using event-driven architecture.</li> <li>Implemented robust event-driven architecture for real-time driver/rider matching and WebSockets-based route optimization.</li>
<li>Integrated local digital payment gateways and driver payout systems, supporting an 8% commission structure that drove a $0.78 Customer Acquisition Cost.</li>
</ul> </ul>
</div> </div>
<div class="job-block"> <div class="job-block">
<div class="job-header"> <div class="job-header">
<div class="job-title">Mobile Solutions Architect — Freelance</div> <div class="job-title">Senior Systems Architect — Mobile & Backend (Freelance) &nbsp;|&nbsp;</div>
<div class="job-meta">Jan 2017 Dec 2023 | Jordan / Remote</div> <div class="job-meta">Jan 2017 Dec 2023 | Jordan / Remote</div>
<div class="clear"></div> <div class="clear"></div>
</div> </div>
<ul> <ul>
<li>Delivered 25+ production enterprise applications across GIS, FinTech, HR, and utilities for clients across the MENA region.</li> <li>Delivered 25+ production enterprise applications across GIS, FinTech, and utilities, leading full lifecycle architecture for clients in the MENA region.</li>
<li>Integrated AI vision models for document analysis (KYC) and automated invoice processing pipelines.</li> <li>Developed Nabih (AI Smart Responder): designed and built an intelligent virtual assistant system connecting a Flutter mobile application (GetX/Cubit) with custom PHP backend APIs.</li>
<li>Developed Musadaq (AI-Powered Invoice Processing): engineered an automated KYC and invoice document processor using AI vision models and asynchronous processing pipelines.</li>
</ul> </ul>
</div> </div>
<div class="job-block"> <div class="section-title">Notable Projects</div>
<div class="job-header"> <ul>
<div class="job-title">Operations & Logistics Officer — Jordan Armed Forces</div> <li><strong>IntaleqMaps Engine:</strong> Engineered a proprietary mapping platform orchestrating GraphHopper routing, OSM tile serving, and PostGIS spatial queries via a NestJS API layer, replacing Google Maps API across the MENA region.</li>
<div class="job-meta">Oct 2003 Nov 2023 | Jordan</div> <li><strong>Nabih AI Virtual Assistant:</strong> Architected an automated customer support solution connecting a Flutter application (GetX/Cubit) to custom high-concurrency PHP backend APIs.</li>
<div class="clear"></div> <li><strong>Musadaq AI Document Processor:</strong> Engineered an automated KYC and invoice document processing platform utilizing AI vision models and async processing pipelines.</li>
</div> <li><strong>Meta Ads Manager & SaaS Gateways:</strong> Deployed multi-tenant platforms using NestJS/React (Meta Ads Manager) and high-concurrency Node.js (WhatsApp bridge managing headless Puppeteer sessions in Docker) to automate operations.</li>
<ul> </ul>
<li>Retired Lieutenant Colonel. Directed logistics and crisis management operations, leading teams of 50+ personnel.</li>
<li>Applied rigorous, security-first methodologies to organizational leadership, disaster recovery, and operational planning.</li> <div class="section-title">Open Source Contributions</div>
</ul> <ul>
</div> <li><strong>intaleq_maps (Flutter SDK):</strong> Published on <a href="https://pub.dev/packages/intaleq_maps" style="color: #1a237e; text-decoration: none; font-weight: bold;">pub.dev/packages/intaleq_maps</a>. Custom map rendering, offline caching, and route plotting package optimized for low-bandwidth environments.</li>
<li><strong>intaleq-maps-gl (NPM Library):</strong> Published on <a href="https://libraries.io/npm/intaleq-maps-gl" style="color: #1a237e; text-decoration: none; font-weight: bold;">npmjs.com/package/intaleq-maps-gl</a>. Web-based Mapbox GL compatible library optimized for custom OSM tiles and routing integration.</li>
</ul>
<div class="section-title">Education & Certifications</div> <div class="section-title">Education & Certifications</div>
<ul> <ul>
<li><strong>BS Mathematics</strong>, Mutah University (20032007)</li> <li><strong>BS Mathematics (Applied Computing & Algorithms)</strong>, Mutah University (20032007)</li>
<li>Google Data Analytics Professional Certificate</li> <li><strong>Google Data Analytics</strong> Professional Certificate (Coursera)</li>
<li>IBM Data Science Professional Certificate</li> <li><strong>IBM Data Analyst</strong> Professional Certificate (Coursera)</li>
<li>Meta Mobile Development Certificate</li> <li><strong>Meta APIs & Django Web Framework</strong> Course Certificates (Coursera)</li>
<li><em>Total of 51 professional certifications across software engineering, AI, and enterprise architecture.</em></li> <li><strong>AWS Cloud Practitioner & Cloud Architecture Fundamentals</strong></li>
</ul> </ul>
<div class="section-title">Languages</div>
<p><strong>Arabic:</strong> Native | <strong>English:</strong> Professional Working Proficiency | <strong>Turkish:</strong> Limited Working Proficiency</p>
<div class="section-title">Availability & Work Authorization</div>
<p style="text-align: center; font-weight: bold; color: #1a237e; margin-top: 10px;">
Available Immediately | Open to Remote, Hybrid & Relocation (GCC/Europe/MENA) | Valid Passport
</p>
</body> </body>
</html> </html>

101
filler.js Normal file
View File

@@ -0,0 +1,101 @@
// filler.js — Script executed on the active tab to auto-fill form fields.
(function() {
// Retrieve the profile data passed from the popup/scripting context
const profile = window.__autofillProfileData;
if (!profile) {
console.error("Autofill profile data not found.");
return "Error: Profile data not found";
}
console.log("Starting auto-fill with profile:", profile);
// Helper to find associated label text for an input
function getLabelText(input) {
let labelText = "";
// 1. Check aria-label or placeholder
if (input.getAttribute("aria-label")) {
labelText += " " + input.getAttribute("aria-label");
}
if (input.getAttribute("placeholder")) {
labelText += " " + input.getAttribute("placeholder");
}
if (input.getAttribute("id")) {
const label = document.querySelector(`label[for="${input.getAttribute("id")}"]`);
if (label) {
labelText += " " + label.innerText;
}
}
// 2. Check parent label
let parent = input.parentElement;
while (parent && parent !== document.body) {
if (parent.tagName === "LABEL") {
labelText += " " + parent.innerText;
break;
}
parent = parent.parentElement;
}
// 3. Check name or id attributes
if (input.name) labelText += " " + input.name;
if (input.id) labelText += " " + input.id;
return labelText.toLowerCase();
}
// Find all input, textarea, and select elements
const inputs = document.querySelectorAll("input, textarea, select");
let filledCount = 0;
inputs.forEach(input => {
// Skip hidden or read-only elements
if (input.type === "hidden" || input.disabled || input.readOnly) return;
const label = getLabelText(input);
let valToSet = null;
// Matching logic based on common field names
if (label.includes("email") || label.includes("mail")) {
valToSet = profile.email;
} else if (label.includes("phone") || label.includes("tel") || label.includes("mobile") || label.includes("contact")) {
valToSet = profile.phone;
} else if (label.includes("linkedin")) {
valToSet = profile.linkedin;
} else if (label.includes("github")) {
valToSet = profile.github;
} else if (label.includes("first name") || label.includes("firstname")) {
valToSet = profile.firstName;
} else if (label.includes("last name") || label.includes("lastname")) {
valToSet = profile.lastName;
} else if (label.includes("full name") || label.includes("fullname") || (label.includes("name") && !label.includes("company") && !label.includes("school") && !label.includes("employer"))) {
valToSet = profile.fullName;
} else if (label.includes("website") || label.includes("portfolio") || label.includes("personal link")) {
valToSet = profile.portfolio || profile.linkedin;
} else if (label.includes("cover letter") || label.includes("describe") || label.includes("introduce yourself")) {
valToSet = profile.summary || "";
}
if (valToSet !== null && valToSet !== undefined) {
// Set the value
input.value = valToSet;
// Highlight the filled field briefly
const originalBg = input.style.backgroundColor;
input.style.backgroundColor = "rgba(0, 214, 126, 0.2)";
input.style.transition = "background-color 0.5s ease";
setTimeout(() => {
input.style.backgroundColor = originalBg;
}, 1500);
// Trigger change/input events so any framework (React/Angular/Vue) registers the value
input.dispatchEvent(new Event("input", { bubbles: true }));
input.dispatchEvent(new Event("change", { bubbles: true }));
filledCount++;
}
});
return `Filled ${filledCount} fields successfully!`;
})();

View File

@@ -1,20 +1,63 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "LinkedIn Job Analyzer", "name": "LinkedIn Job Analyzer",
"version": "1.2.0", "version": "1.5.0",
"description": "AI-powered job analysis tool for LinkedIn — personal use", "description": "AI-powered job analysis + smart comment generator for LinkedIn — personal use",
"permissions": ["storage", "activeTab", "scripting"], "permissions": [
"storage",
"activeTab",
"scripting",
"clipboardWrite",
"microphone"
],
"host_permissions": [ "host_permissions": [
"https://www.linkedin.com/*", "https://www.linkedin.com/*",
"https://claude.ai/*",
"https://cv.intaleqapp.com/*", "https://cv.intaleqapp.com/*",
"https://generativelanguage.googleapis.com/*" "https://generativelanguage.googleapis.com/*"
], ],
"content_scripts": [{ "content_scripts": [
"matches": ["https://www.linkedin.com/jobs/*"], {
"js": ["prompts.js", "content.js"], "matches": [
"css": ["overlay.css"], "https://www.linkedin.com/jobs/*"
"run_at": "document_idle" ],
}], "js": [
"prompts.js",
"content.js"
],
"css": [
"overlay.css"
],
"run_at": "document_idle"
},
{
"matches": [
"https://www.linkedin.com/feed/",
"https://www.linkedin.com/feed/*",
"https://www.linkedin.com/"
],
"js": [
"post_feed.js"
],
"css": [
"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": { "action": {
"default_popup": "popup.html", "default_popup": "popup.html",
"default_icon": { "default_icon": {
@@ -23,6 +66,15 @@
"128": "icons/icon128.png" "128": "icons/icon128.png"
} }
}, },
"web_accessible_resources": [
{
"resources": [
"speech.html",
"speech.js"
],
"matches": ["<all_urls>"]
}
],
"background": { "background": {
"service_worker": "background.js" "service_worker": "background.js"
}, },
@@ -31,4 +83,4 @@
"48": "icons/icon48.png", "48": "icons/icon48.png",
"128": "icons/icon128.png" "128": "icons/icon128.png"
} }
} }

70
permission.html Normal file
View File

@@ -0,0 +1,70 @@
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>السماح باستخدام الميكروفون</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #0a0a0f;
color: #ffffff;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
text-align: center;
}
.container {
background: #1a1a26;
padding: 40px;
border-radius: 12px;
border: 1px solid rgba(108, 99, 255, 0.2);
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
max-width: 500px;
}
h1 {
color: #6c63ff;
margin-top: 0;
}
p {
color: #a0a0b8;
line-height: 1.6;
font-size: 16px;
margin-bottom: 30px;
}
.btn {
background: linear-gradient(135deg, #6c63ff, #4834d4);
color: white;
border: none;
padding: 12px 24px;
font-size: 16px;
border-radius: 8px;
cursor: pointer;
font-weight: bold;
transition: all 0.3s;
}
.btn:hover {
transform: scale(1.05);
box-shadow: 0 4px 15px rgba(108, 99, 255, 0.4);
}
.icon {
font-size: 48px;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="container">
<div class="icon">🎤</div>
<h1>مطلوب إذن الميكروفون</h1>
<p>ميزة الإملاء الصوتي تحتاج إلى إذن للوصول إلى الميكروفون لتعمل بشكل صحيح في الخلفية.</p>
<p>يرجى الضغط على الزر أدناه والموافقة على طلب الصلاحية من المتصفح.</p>
<button class="btn" id="grantBtn">منح صلاحية الميكروفون</button>
<p id="status" style="margin-top: 20px; font-weight: bold;"></p>
</div>
<script src="permission.js"></script>
</body>
</html>

19
permission.js Normal file
View File

@@ -0,0 +1,19 @@
document.getElementById('grantBtn').addEventListener('click', async () => {
const status = document.getElementById('status');
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
// Stop the tracks immediately since we only needed to ask for permission
stream.getTracks().forEach(track => track.stop());
status.style.color = '#00d67e';
status.textContent = '✅ تم منح الصلاحية بنجاح! يمكنك إغلاق هذه النافذة والعودة إلى الإضافة.';
// Auto close after 3 seconds
setTimeout(() => {
window.close();
}, 3000);
} catch (err) {
status.style.color = '#ff4d6d';
status.textContent = '❌ حدث خطأ أو تم رفض الصلاحية: ' + err.message;
}
});

View File

@@ -414,6 +414,37 @@
<div class="content"> <div class="content">
<!-- VOICE DICTATION -->
<div class="section" style="background: linear-gradient(135deg, rgba(108, 99, 255, 0.15) 0%, rgba(155, 93, 229, 0.15) 100%); border-color: var(--accent);">
<div class="section-header">
<span class="section-title">🎤 إملاء صوتي ذكي</span>
<button class="btn btn-sm" id="copy-dictation-btn" style="display:none; font-family: Tahoma, sans-serif;">📋 نسخ</button>
</div>
<div style="text-align: center; margin-bottom: 12px;">
<button id="dictation-mic-btn" style="width: 60px; height: 60px; border-radius: 50%; border: none; background: linear-gradient(135deg, var(--accent), #9b5de5); color: white; font-size: 24px; cursor: pointer; transition: all 0.2s;">
🎤
</button>
<div id="dictation-status" style="font-size: 11px; color: var(--text-secondary); margin-top: 8px; font-family: Tahoma, sans-serif;">اضغط للتحدث (بالعربية)</div>
</div>
<textarea id="dictation-result" placeholder="النص سيظهر هنا..." style="min-height: 80px; direction: rtl; font-family: Tahoma, sans-serif;"></textarea>
</div>
<!-- QUICK ACTIONS -->
<div class="section" style="background: linear-gradient(135deg, rgba(108, 99, 255, 0.15) 0%, rgba(155, 93, 229, 0.15) 100%); border-color: var(--accent);">
<div class="section-header">
<span class="section-title">⚡ Quick Actions</span>
</div>
<button class="btn btn-primary" id="autofill-btn">
✨ Auto-fill Form on Current Page
</button>
<p style="font-size: 11px; color: var(--text-secondary); margin-top: 8px; text-align: center;">
Fills fields on the current page using your profile details.
</p>
</div>
<!-- API KEY SECTION --> <!-- API KEY SECTION -->
<div class="section"> <div class="section">
<div class="section-header"> <div class="section-header">

353
popup.js
View File

@@ -39,8 +39,15 @@ function loadSettings() {
document.getElementById('api-key-input').value = data.apiKey; document.getElementById('api-key-input').value = data.apiKey;
setKeyStatus('ok'); setKeyStatus('ok');
} }
document.getElementById('profile-textarea').value = let profile = data.userProfile || DEFAULT_PROFILE;
data.userProfile || DEFAULT_PROFILE; if (profile.includes('hamzaayed@intaleqapp.com') || profile.includes('hamzaayedflutter@gmail.com') || profile.includes('hamzaayed@tripz-egypt.com')) {
profile = profile
.replace(/hamzaayed@intaleqapp\.com/g, 'hamzaayed.dev@gmail.com')
.replace(/hamzaayedflutter@gmail\.com/g, 'hamzaayed.dev@gmail.com')
.replace(/hamzaayed@tripz-egypt\.com/g, 'hamzaayed.dev@gmail.com');
chrome.storage.sync.set({ userProfile: profile });
}
document.getElementById('profile-textarea').value = profile;
document.getElementById('lang-select').value = document.getElementById('lang-select').value =
data.language || 'auto'; data.language || 'auto';
}); });
@@ -104,7 +111,7 @@ document.getElementById('test-key-btn').addEventListener('click', async () => {
try { try {
const response = await fetch( const response = await fetch(
`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${apiKey}`, `https://generativelanguage.googleapis.com/v1beta/models/gemini-flash-lite-latest:generateContent?key=${apiKey}`,
{ {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@@ -163,6 +170,346 @@ document.getElementById('clear-all-btn').addEventListener('click', () => {
} }
}); });
// ─── Autofill Functionality ──────────────────────────────────────────────────
function parseProfileText(text) {
const emailRegex = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
const phoneRegex = /\+?[0-9]{1,4}[ \t.-]?[0-9]{3,4}[ \t.-]?[0-9]{3,4}/;
const linkedinRegex = /(https?:\/\/)?(www\.)?linkedin\.com\/in\/[a-zA-Z0-9_-]+/;
const githubRegex = /(https?:\/\/)?(www\.)?github\.com\/[a-zA-Z0-9_-]+/;
const emailMatch = text.match(emailRegex);
const phoneMatch = text.match(phoneRegex);
const linkedinMatch = text.match(linkedinRegex);
const githubMatch = text.match(githubRegex);
// Extract Name (first line usually)
const lines = text.split('\n').map(l => l.trim()).filter(Boolean);
let fullName = "Hamza Ayed";
if (lines.length > 0) {
const firstLine = lines[0].replace(/—.*/, '').replace(/:.*/, '').trim();
fullName = firstLine;
}
const nameParts = fullName.split(' ');
const firstName = nameParts[0] || "";
const lastName = nameParts.slice(1).join(' ') || "";
// Extract Summary
let summary = "";
const summaryIdx = text.toLowerCase().indexOf("summary:");
if (summaryIdx !== -1) {
const skillsIdx = text.toLowerCase().indexOf("core skills:", summaryIdx);
if (skillsIdx !== -1) {
summary = text.substring(summaryIdx + 8, skillsIdx).trim();
} else {
summary = text.substring(summaryIdx + 8, summaryIdx + 500).trim();
}
}
return {
fullName,
firstName,
lastName,
email: emailMatch ? emailMatch[0] : "",
phone: phoneMatch ? phoneMatch[0] : "",
linkedin: linkedinMatch ? linkedinMatch[0] : "",
github: githubMatch ? githubMatch[0] : "",
summary,
cvText: text
};
}
document.getElementById('autofill-btn').addEventListener('click', async () => {
const userProfile = document.getElementById('profile-textarea').value.trim();
if (!userProfile) {
showToast('⚠️ Please enter profile info first', 'error');
return;
}
const profileData = parseProfileText(userProfile);
try {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tab) {
showToast('❌ No active tab found', 'error');
return;
}
// Set the profile data on the tab window context
await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: (data) => {
window.__autofillProfileData = data;
},
args: [profileData]
});
// Execute the filler script
const results = await chrome.scripting.executeScript({
target: { tabId: tab.id },
files: ['filler.js']
});
if (results && results[0]) {
showToast('✨ ' + results[0].result, 'success');
} else {
showToast('✨ Autofill completed!', 'success');
}
} catch (e) {
console.error(e);
showToast('❌ Error: ' + e.message, 'error');
}
});
// ─── Voice Dictation ───────────────────────────────────────────────────────────
function initDictation() {
const statusEl = document.getElementById('dictation-status');
const micBtn = document.getElementById('dictation-mic-btn');
const resultArea = document.getElementById('dictation-result');
const copyBtn = document.getElementById('copy-dictation-btn');
let isRecording = false;
let finalTranscript = '';
let interimTranscript = '';
function updateCopyBtnVisibility() {
if (resultArea.value.trim()) {
copyBtn.style.display = 'inline-flex';
} else {
copyBtn.style.display = 'none';
}
}
resultArea.addEventListener('input', updateCopyBtnVisibility);
function stopRecording() {
if (!isRecording) return;
isRecording = false;
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
const activeTab = tabs[0];
if (activeTab && activeTab.id) {
chrome.tabs.sendMessage(activeTab.id, { type: 'STOP_RECORDING_FROM_POPUP' }, () => {
if (chrome.runtime.lastError) {
console.warn('Could not send STOP message to tab:', chrome.runtime.lastError.message);
}
});
}
});
micBtn.style.background = 'linear-gradient(135deg, var(--accent), #9b5de5)';
micBtn.innerHTML = '🎤';
updateCopyBtnVisibility();
if (!statusEl.textContent.includes('❌')) {
statusEl.textContent = 'اضغط للتحدث (بالعربية)';
}
}
// Listen to messages from the injected script
chrome.runtime.onMessage.addListener((message) => {
if (message.type === 'SPEECH_START_SUCCESS') {
isRecording = true;
micBtn.style.background = 'linear-gradient(135deg, #ff4d6d, #c9184a)';
micBtn.innerHTML = '🔴';
statusEl.textContent = 'جارٍ الاستماع... اضغط للإيقاف';
} else if (message.type === 'SPEECH_RESULT') {
interimTranscript = message.payload.interimText || '';
finalTranscript = message.payload.finalText || '';
resultArea.value = (finalTranscript + interimTranscript).trim();
updateCopyBtnVisibility();
} else if (message.type === 'SPEECH_ERROR') {
console.error('Speech recognition error', message.payload.error);
if (message.payload.error === 'not-allowed') {
statusEl.textContent = '❌ يرجى السماح للميكروفون من إعدادات المتصفح';
chrome.tabs.create({ url: chrome.runtime.getURL('permission.html') });
} else if (message.payload.error !== 'no-speech') {
statusEl.textContent = '❌ خطأ: ' + message.payload.error;
}
stopRecording();
} else if (message.type === 'SPEECH_END') {
if (isRecording) {
stopRecording();
}
}
});
micBtn.addEventListener('click', async () => {
if (isRecording) {
stopRecording();
} else {
// First, get the active tab
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
const activeTab = tabs[0];
// Cannot inject into chrome:// or chrome-extension:// pages
if (!activeTab || !activeTab.url || activeTab.url.startsWith('chrome://') || activeTab.url.startsWith('chrome-extension://') || activeTab.url.startsWith('edge://')) {
statusEl.textContent = '❌ يرجى فتح موقع عادي أولاً (مثل جوجل أو لينكدإن)';
return;
}
// Show a loading state until SPEECH_START_SUCCESS is received
micBtn.style.background = 'linear-gradient(135deg, #ffb3c1, #ff758f)';
micBtn.innerHTML = '⏳';
statusEl.textContent = 'جاري تهيئة الميكروفون...';
finalTranscript = '';
interimTranscript = '';
resultArea.value = '';
copyBtn.style.display = 'none';
chrome.scripting.executeScript({
target: { tabId: activeTab.id },
func: (lang) => {
// Clean up any existing instances in this tab first
if (window.__ljaSpeechActiveRecognition) {
try { window.__ljaSpeechActiveRecognition.stop(); } catch(e) {}
}
if (window.__ljaSpeechStopListener) {
try { chrome.runtime.onMessage.removeListener(window.__ljaSpeechStopListener); } catch(e) {}
}
window.__ljaDictationActive = true;
window.__ljaSpeechUserStopped = false;
window.__ljaSpeechAccumulatedText = '';
window.__ljaSpeechCurrentSessionFinalText = '';
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
if (!SpeechRecognition) {
chrome.runtime.sendMessage({ type: 'SPEECH_ERROR', payload: { error: 'not-supported' } });
window.__ljaDictationActive = false;
return;
}
let recognition;
function startRecognition() {
if (window.__ljaSpeechUserStopped) return;
recognition = new SpeechRecognition();
window.__ljaSpeechActiveRecognition = recognition;
recognition.lang = lang;
recognition.continuous = true;
recognition.interimResults = true;
recognition.maxAlternatives = 1;
recognition.onstart = () => {
chrome.runtime.sendMessage({ type: 'SPEECH_START_SUCCESS' });
};
recognition.onresult = (event) => {
let interimText = '';
let sessionFinalText = '';
for (let i = 0; i < event.results.length; i++) {
const result = event.results[i];
if (result.isFinal) {
sessionFinalText += result[0].transcript + ' ';
} else {
interimText += result[0].transcript;
}
}
window.__ljaSpeechCurrentSessionFinalText = sessionFinalText;
const fullFinalText = window.__ljaSpeechAccumulatedText + sessionFinalText;
chrome.runtime.sendMessage({
type: 'SPEECH_RESULT',
payload: {
interimText: interimText,
finalText: fullFinalText
}
});
};
recognition.onerror = (event) => {
console.error('[LJA Dictation] Error:', event.error);
if (event.error === 'not-allowed' || event.error === 'service-not-allowed') {
chrome.runtime.sendMessage({ type: 'SPEECH_ERROR', payload: { error: event.error } });
window.__ljaDictationActive = false;
window.__ljaSpeechUserStopped = true;
window.__ljaSpeechActiveRecognition = null;
} else {
console.warn('[LJA Dictation] Recoverable error:', event.error);
}
};
recognition.onend = () => {
if (window.__ljaSpeechUserStopped) {
// User explicitly stopped
chrome.runtime.sendMessage({ type: 'SPEECH_END' });
window.__ljaDictationActive = false;
window.__ljaSpeechActiveRecognition = null;
} else {
// Ended due to browser timeout / silence, restart it
window.__ljaSpeechAccumulatedText += window.__ljaSpeechCurrentSessionFinalText;
window.__ljaSpeechCurrentSessionFinalText = '';
console.log('[LJA Dictation] Silence/timeout. Restarting recognition...');
setTimeout(() => {
if (!window.__ljaSpeechUserStopped) {
startRecognition();
}
}, 150);
}
};
try {
recognition.start();
} catch (e) {
chrome.runtime.sendMessage({ type: 'SPEECH_ERROR', payload: { error: e.message } });
window.__ljaDictationActive = false;
window.__ljaSpeechUserStopped = true;
window.__ljaSpeechActiveRecognition = null;
}
}
// Listen for STOP message from popup
window.__ljaSpeechStopListener = (msg, sender, sendResponse) => {
if (msg.type === 'STOP_RECORDING_FROM_POPUP') {
window.__ljaSpeechUserStopped = true;
try { recognition.stop(); } catch(e) {}
window.__ljaDictationActive = false;
window.__ljaSpeechActiveRecognition = null;
if (window.__ljaSpeechStopListener) {
try { chrome.runtime.onMessage.removeListener(window.__ljaSpeechStopListener); } catch(e) {}
window.__ljaSpeechStopListener = null;
}
sendResponse({ success: true });
}
};
chrome.runtime.onMessage.addListener(window.__ljaSpeechStopListener);
startRecognition();
},
args: ['ar-SA']
}).catch(err => {
console.error('Failed to inject speech recognition:', err);
statusEl.textContent = '❌ فشل الاتصال بالصفحة الحالية';
micBtn.style.background = 'linear-gradient(135deg, var(--accent), #9b5de5)';
micBtn.innerHTML = '🎤';
});
});
}
});
copyBtn.addEventListener('click', () => {
navigator.clipboard.writeText(resultArea.value);
const originalText = copyBtn.textContent;
copyBtn.textContent = '✅ تم النسخ!';
setTimeout(() => {
copyBtn.textContent = originalText;
resultArea.value = '';
updateCopyBtnVisibility();
}, 1000);
});
}
// ─── Init ──────────────────────────────────────────────────────────────────── // ─── Init ────────────────────────────────────────────────────────────────────
loadSettings(); loadSettings();
initDictation();

167
post_feed.css Normal file
View File

@@ -0,0 +1,167 @@
/* ============================================================
post_feed.css — Smart Comment Feature Styles
Scoped to .lja-comment-btn and .lja-comment-box
============================================================ */
/* ── Smart Comment Button ─────────────────────────────────── */
.lja-comment-btn {
display: inline-flex;
align-items: center;
gap: 5px;
margin-left: 8px;
padding: 6px 14px;
background: linear-gradient(135deg, #6c63ff, #4f46e5);
color: #fff;
border: none;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
box-shadow: 0 2px 8px rgba(108, 99, 255, 0.3);
}
.lja-comment-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 14px rgba(108, 99, 255, 0.45);
background: linear-gradient(135deg, #7c73ff, #5f56f5);
}
.lja-comment-btn:disabled {
opacity: 0.7;
cursor: not-allowed;
transform: none;
}
/* Spinner inside button */
.lja-spinner {
display: inline-block;
width: 10px;
height: 10px;
border: 2px solid rgba(255, 255, 255, 0.4);
border-top-color: #fff;
border-radius: 50%;
animation: lja-spin 0.7s linear infinite;
}
@keyframes lja-spin {
to { transform: rotate(360deg); }
}
/* ── Comment Suggestion Box ───────────────────────────────── */
.lja-comment-box {
margin: 10px 12px;
background: linear-gradient(145deg, #1a1a2e, #16213e);
border: 1px solid rgba(108, 99, 255, 0.35);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
animation: lja-fadeIn 0.25s ease;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
@keyframes lja-fadeIn {
from { opacity: 0; transform: translateY(-6px); }
to { opacity: 1; transform: translateY(0); }
}
/* Header */
.lja-cb-header {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 14px;
background: rgba(108, 99, 255, 0.15);
border-bottom: 1px solid rgba(108, 99, 255, 0.2);
}
.lja-cb-icon {
font-size: 16px;
}
.lja-cb-title {
flex: 1;
font-size: 13px;
font-weight: 700;
color: #a89cff;
letter-spacing: 0.3px;
}
.lja-cb-close {
background: none;
border: none;
color: #888;
font-size: 14px;
cursor: pointer;
padding: 2px 6px;
border-radius: 4px;
transition: color 0.2s, background 0.2s;
line-height: 1;
}
.lja-cb-close:hover {
color: #fff;
background: rgba(255, 80, 80, 0.25);
}
/* Editable text area */
.lja-cb-text {
display: block;
width: 100%;
min-height: 80px;
max-height: 160px;
padding: 14px 16px;
background: transparent;
border: none;
color: #e0deff;
font-size: 14px;
line-height: 1.65;
resize: vertical;
outline: none;
box-sizing: border-box;
font-family: inherit;
}
.lja-cb-text:focus {
background: rgba(108, 99, 255, 0.05);
}
/* Action buttons row */
.lja-cb-actions {
display: flex;
gap: 8px;
padding: 10px 14px;
border-top: 1px solid rgba(255, 255, 255, 0.06);
flex-wrap: wrap;
}
.lja-cb-actions button {
padding: 6px 14px;
border: 1px solid rgba(108, 99, 255, 0.4);
border-radius: 8px;
background: rgba(108, 99, 255, 0.12);
color: #b0a8ff;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
font-family: inherit;
}
.lja-cb-actions button:hover {
background: rgba(108, 99, 255, 0.3);
color: #fff;
border-color: rgba(108, 99, 255, 0.7);
}
/* Paste button — primary emphasis */
.lja-cb-paste {
background: linear-gradient(135deg, rgba(108,99,255,0.3), rgba(79,70,229,0.3)) !important;
border-color: rgba(108, 99, 255, 0.6) !important;
}
.lja-cb-paste:hover {
background: linear-gradient(135deg, rgba(108,99,255,0.55), rgba(79,70,229,0.55)) !important;
}

450
post_feed.js Normal file
View File

@@ -0,0 +1,450 @@
// post_feed.js — LinkedIn Feed Smart Comment Generator
// Operates ONLY on linkedin.com/feed pages — fully independent from content.js
(function () {
'use strict';
const BUTTON_CLASS = 'lja-comment-btn';
const BOX_CLASS = 'lja-comment-box';
// ─── Utility: get stored settings ────────────────────────────────────────
function getSettings() {
return new Promise(resolve => {
if (!chrome || !chrome.storage) {
resolve({});
return;
}
chrome.storage.sync.get(['apiKey', 'language'], (syncData) => {
if (syncData && syncData.apiKey) {
resolve(syncData);
} else {
chrome.storage.local.get(['apiKey', 'language'], (localData) => {
resolve(localData || {});
});
}
});
});
}
// ─── Extract post text from a feed item ──────────────────────────────────
function extractPostText(postEl) {
const selectors = [
'[data-testid="expandable-text-box"]', // New LinkedIn UI
'.feed-shared-update-v2__description .break-words span[dir]',
'.feed-shared-text-view span[dir]',
'.update-components-text span[dir]',
'.feed-shared-update-v2__description',
'.feed-shared-text',
];
for (const sel of selectors) {
const el = postEl.querySelector(sel);
if (el) {
// Strip out the "... more" text if it exists
const text = el.textContent.replace(/…\s*more/gi, '').trim();
if (text.length > 20) return text;
}
}
return null;
}
// ─── Find the action bar in a post ───────────────────────────────────────
function findActionBar(postEl) {
const selectors = [
'.feed-shared-social-action-bar',
'.social-actions-bar',
'[data-urn] .feed-shared-social-action-bar',
];
for (const sel of selectors) {
const el = postEl.querySelector(sel);
if (el) return el;
}
// New UI fallback: find the comment button SVG and get its parent container
const commentSvg = postEl.querySelector('svg#comment-small, svg[data-test-icon="comment-small"]');
if (commentSvg) {
const btn = commentSvg.closest('button');
if (btn && btn.parentNode) {
return btn.parentNode; // This is the action bar container
}
}
return null;
}
// ─── Find comment input field ─────────────────────────────────────────────
function findCommentInput(postEl) {
const selectors = [
'.comments-comment-box__form-container .ql-editor',
'.comments-comment-texteditor .ql-editor',
'.ql-editor[contenteditable="true"]',
'[contenteditable="true"][role="textbox"]',
'div[contenteditable="true"]'
];
for (const sel of selectors) {
const el = postEl.querySelector(sel);
if (el) return el;
}
return null;
}
// ─── Fill comment input (handles LinkedIn's Quill editor) ────────────────
function fillCommentInput(inputEl, text) {
inputEl.focus();
inputEl.innerHTML = '';
document.execCommand('insertText', false, text);
inputEl.dispatchEvent(new InputEvent('input', { bubbles: true, data: text }));
inputEl.dispatchEvent(new Event('change', { bubbles: true }));
}
// ─── Create the comment suggestion box ───────────────────────────────────
function createCommentBox(postEl, commentText, arabicSummary) {
const existing = postEl.querySelector('.' + BOX_CLASS);
if (existing) existing.remove();
const isRTL = /[\u0600-\u06FF]/.test(commentText);
const box = document.createElement('div');
box.className = BOX_CLASS;
box.innerHTML = `
<div class="lja-cb-header">
<span class="lja-cb-icon">🤖</span>
<span class="lja-cb-title">Smart Comment & Analysis</span>
<button class="lja-cb-close" title="Close">✕</button>
</div>
<div style="padding: 10px 14px; background: rgba(108, 99, 255, 0.05); border-bottom: 1px solid rgba(255, 255, 255, 0.06);">
<div style="font-size: 11px; color: #ff8787; margin-bottom: 4px; font-weight: 600;">🕵️ التحليل والمصداقية:</div>
<div style="font-size: 12px; color: #e0deff; line-height: 1.5; text-align: right;" dir="rtl">
${arabicSummary}
</div>
</div>
<textarea
class="lja-cb-text"
dir="${isRTL ? 'rtl' : 'ltr'}"
style="text-align: ${isRTL ? 'right' : 'left'}; font-family: monospace; font-size: 13px;"
>${commentText}</textarea>
<div class="lja-cb-actions">
<button class="lja-cb-copy">📋 Copy</button>
<button class="lja-cb-paste">✅ Paste into Comment</button>
<button class="lja-cb-regen">🔄 Regenerate</button>
</div>
`;
box.querySelector('.lja-cb-close').addEventListener('click', () => box.remove());
box.querySelector('.lja-cb-copy').addEventListener('click', function () {
const text = box.querySelector('.lja-cb-text').value;
navigator.clipboard.writeText(text).then(() => {
this.textContent = '✅ Copied!';
setTimeout(() => { this.textContent = '📋 Copy'; }, 1500);
});
});
box.querySelector('.lja-cb-paste').addEventListener('click', function () {
const text = box.querySelector('.lja-cb-text').value;
const commentTrigger = postEl.querySelector(
'.comment-button, [aria-label*="comment" i], [data-control-name="comment"], svg#comment-small'
);
let btnToClick = commentTrigger;
if (btnToClick && btnToClick.tagName.toLowerCase() === 'svg') {
btnToClick = btnToClick.closest('button') || btnToClick;
}
if (btnToClick) btnToClick.click();
setTimeout(() => {
const inputEl = findCommentInput(postEl);
if (inputEl) {
fillCommentInput(inputEl, text);
this.textContent = '✅ Done!';
setTimeout(() => { this.textContent = '✅ Paste into Comment'; }, 1500);
} else {
navigator.clipboard.writeText(text);
this.textContent = '📋 Copied! Paste manually';
}
}, 500);
});
box.querySelector('.lja-cb-regen').addEventListener('click', async function () {
box.remove();
await generateComment(postEl);
});
const actionBar = findActionBar(postEl);
if (actionBar) {
actionBar.parentNode.insertBefore(box, actionBar.nextSibling);
} else {
postEl.appendChild(box);
}
}
// ─── Core: call server and generate comment ───────────────────────────────
async function generateComment(postEl) {
const settings = await getSettings();
if (!settings || !settings.apiKey) {
alert('Please set your Gemini API key in the extension popup first.\n(Debug: Key not found in storage)');
return;
}
const postText = extractPostText(postEl);
if (!postText) {
alert('Could not read post text. The post might be an image or video.');
return;
}
const btn = postEl.querySelector('.' + BUTTON_CLASS);
if (btn) {
btn.disabled = true;
btn.innerHTML = '<span class="lja-spinner"></span> Thinking...';
}
try {
const response = await chrome.runtime.sendMessage({
type: 'GEMINI_REQUEST',
payload: {
apiKey: settings.apiKey,
action: 'generateComment',
postText: postText
}
});
if (!response.success) {
throw new Error(response.error || 'Unknown error');
}
// Parse the JSON returned by Gemini
let resultData;
let rawText = response.data.comment || response.data;
// Clean up markdown blocks if the AI accidentally adds them
rawText = rawText.replace(/```json/gi, '').replace(/```/g, '').trim();
try {
resultData = JSON.parse(rawText);
} catch (parseError) {
throw new Error('Failed to parse AI response. Raw output: ' + rawText);
}
const arabicSummary = resultData.arabic_summary || 'لم يتم توليد ملخص.';
const commentText = resultData.comment || '';
createCommentBox(postEl, commentText, arabicSummary);
} catch (e) {
console.error('[LJA Feed]', e);
alert('Comment generation failed: ' + e.message);
} finally {
if (btn) {
btn.disabled = false;
btn.innerHTML = '💬 Smart Comment';
}
}
}
// ─── Core: call server and repurpose post ───────────────────────────────
async function repurposePost(postEl) {
const settings = await getSettings();
if (!settings || !settings.apiKey) {
alert('Please set your Gemini API key in the extension popup first.\n(Debug: Key not found in storage)');
return;
}
const postText = extractPostText(postEl);
if (!postText) {
alert('Could not read post text. The post might be an image or video.');
return;
}
const btn = postEl.querySelector('.lja-rewrite-btn');
if (btn) {
btn.disabled = true;
btn.innerHTML = '<span class="lja-spinner"></span> Rewriting...';
}
try {
const response = await chrome.runtime.sendMessage({
type: 'GEMINI_REQUEST',
payload: {
apiKey: settings.apiKey,
action: 'repurposePost',
postText: postText
}
});
if (!response.success) {
throw new Error(response.error || 'Unknown error');
}
const resultText = response.data.result || response.data;
// We reuse the comment box UI but style it differently, or just use the same box but change the title and actions.
createRepurposeBox(postEl, resultText);
} catch (e) {
console.error('[LJA Feed]', e);
alert('Post rewriting failed: ' + e.message);
} finally {
if (btn) {
btn.disabled = false;
btn.innerHTML = '📝 Rewrite';
}
}
}
// ─── Create the repurpose result box ─────────────────────────────────────
function createRepurposeBox(postEl, resultText) {
const existing = postEl.querySelector('.' + BOX_CLASS);
if (existing) existing.remove();
const isRTL = /[\u0600-\u06FF]/.test(resultText);
// Split text into Post and Image Prompt if the separator exists
let postContent = resultText;
let imagePrompt = '';
const parts = resultText.split('--- IMAGE PROMPT ---');
if (parts.length > 1) {
postContent = parts[0].trim();
imagePrompt = parts[1].trim();
}
const box = document.createElement('div');
box.className = BOX_CLASS;
box.innerHTML = `
<div class="lja-cb-header" style="background: linear-gradient(135deg, #FF6B6B 0%, #C92A2A 100%);">
<span class="lja-cb-icon">✍️</span>
<span class="lja-cb-title">Rewritten Post</span>
<button class="lja-cb-close" title="Close">✕</button>
</div>
<div style="padding: 12px; font-size: 13px; color: #666; font-weight: 500; border-bottom: 1px solid rgba(0,0,0,0.05);">
Post Content:
</div>
<textarea
class="lja-cb-text"
dir="${isRTL ? 'rtl' : 'ltr'}"
style="text-align: ${isRTL ? 'right' : 'left'}; min-height: 120px;"
>${postContent}</textarea>
${imagePrompt ? `
<div style="padding: 12px; font-size: 13px; color: #666; font-weight: 500; border-top: 1px solid rgba(0,0,0,0.05); border-bottom: 1px solid rgba(0,0,0,0.05);">
Image Generation Prompt (Midjourney / DALL-E):
</div>
<textarea
class="lja-cb-image-prompt"
style="width: 100%; border: none; padding: 12px; background: rgba(0,0,0,0.02); resize: vertical; min-height: 80px; font-family: monospace; font-size: 12px; outline: none;"
readonly
>${imagePrompt}</textarea>
` : ''}
<div class="lja-cb-actions">
<button class="lja-cb-copy">📋 Copy Post</button>
${imagePrompt ? '<button class="lja-cb-copy-img">🎨 Copy Image Prompt</button>' : ''}
</div>
`;
box.querySelector('.lja-cb-close').addEventListener('click', () => box.remove());
box.querySelector('.lja-cb-copy').addEventListener('click', function () {
const text = box.querySelector('.lja-cb-text').value;
navigator.clipboard.writeText(text).then(() => {
this.textContent = '✅ Post Copied!';
setTimeout(() => { this.textContent = '📋 Copy Post'; }, 1500);
});
});
const copyImgBtn = box.querySelector('.lja-cb-copy-img');
if (copyImgBtn) {
copyImgBtn.addEventListener('click', function () {
const text = box.querySelector('.lja-cb-image-prompt').value;
navigator.clipboard.writeText(text).then(() => {
this.textContent = '✅ Prompt Copied!';
setTimeout(() => { this.textContent = '🎨 Copy Image Prompt'; }, 1500);
});
});
}
const actionBar = findActionBar(postEl);
if (actionBar) {
actionBar.parentNode.insertBefore(box, actionBar.nextSibling);
} else {
postEl.appendChild(box);
}
}
// ─── Inject button into a single post ────────────────────────────────────
function injectButton(postEl) {
if (postEl.querySelector('.' + BUTTON_CLASS)) return; // Already injected
const actionBar = findActionBar(postEl);
if (!actionBar) return;
if (!extractPostText(postEl)) return; // Skip non-text posts silently
const btn = document.createElement('button');
btn.className = BUTTON_CLASS;
btn.innerHTML = '💬 Smart Comment';
btn.title = 'Generate an AI-powered comment for this post';
btn.addEventListener('click', (e) => {
e.stopPropagation();
e.preventDefault();
generateComment(postEl);
});
const rewriteBtn = document.createElement('button');
rewriteBtn.className = 'lja-rewrite-btn';
rewriteBtn.innerHTML = '📝 Rewrite';
rewriteBtn.title = 'Rewrite this post in your own style and generate an image prompt';
// Add some inline styles for the rewrite button to differentiate it
rewriteBtn.style.background = 'linear-gradient(135deg, #FF8787 0%, #E03131 100%)';
rewriteBtn.style.color = 'white';
rewriteBtn.style.border = 'none';
rewriteBtn.style.borderRadius = '16px';
rewriteBtn.style.padding = '4px 12px';
rewriteBtn.style.fontSize = '12px';
rewriteBtn.style.fontWeight = '600';
rewriteBtn.style.cursor = 'pointer';
rewriteBtn.style.marginLeft = '8px';
rewriteBtn.style.display = 'flex';
rewriteBtn.style.alignItems = 'center';
rewriteBtn.style.gap = '4px';
rewriteBtn.addEventListener('click', (e) => {
e.stopPropagation();
e.preventDefault();
repurposePost(postEl);
});
actionBar.appendChild(btn);
actionBar.appendChild(rewriteBtn);
}
// ─── Process all posts on the page ───────────────────────────────────────
function processAllPosts() {
// Check both classic LinkedIn UI and the new React/hash-class UI
const posts = document.querySelectorAll(
'.feed-shared-update-v2, .occludable-update, [data-urn*="activity"], [role="listitem"]'
);
posts.forEach(injectButton);
}
// ─── MutationObserver: watch for new posts (infinite scroll) ─────────────
let observerTimer;
const observer = new MutationObserver(() => {
// Throttle the DOM scanning to prevent heavy performance hits on React updates
if (observerTimer) return;
observerTimer = setTimeout(() => {
processAllPosts();
observerTimer = null;
}, 500);
});
// ─── Initialize ──────────────────────────────────────────────────────────
function init() {
processAllPosts();
observer.observe(document.body, { childList: true, subtree: true });
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
setTimeout(init, 2000); // Fallback for delayed SPA renders
}
})();

View File

@@ -1,53 +1,49 @@
// profile.js — Default user profile (pre-filled) // profile.js — Default user profile (pre-filled)
// This is loaded into the popup and used as the default profile text. // This is loaded into the popup and used as the default profile text.
const DEFAULT_PROFILE = `HAMZA AYED — Technical Architect | Solutions Architect | Full-Stack & Cloud | GIS & FinTech const DEFAULT_PROFILE = `HAMZA AYED — Senior Backend Engineer & Systems Architect
SUMMARY: SUMMARY:
Technical Architect with 6+ years designing and building high-scale, mission-critical distributed systems. Proven track record architecting complete mobile ecosystems, proprietary mapping infrastructure (OpenStreetMap), custom FinTech payment solutions, and AI-powered document processing platforms. Led cross-functional engineering teams of 20-50+ in high-stakes, mission-critical environments across a 20-year operational leadership career. 30+ production apps on Google Play and App Store. 51 professional certifications (Google, IBM, Meta). Senior Backend Engineer and Systems Architect with experience building real-time transportation, mapping, and SaaS platforms across the MENA region. Specialized in high-concurrency PHP systems, distributed backend services, GIS infrastructure, and scalable APIs using Workerman, NestJS, Redis, PostgreSQL/PostGIS, Docker, and AWS. Built proprietary routing and mapping infrastructure integrating OpenStreetMap and GraphHopper for production ride-hailing environments.
CORE SKILLS: TECHNICAL SKILLS:
- Architecture: System Design, API Architecture, Database Design, Cloud Infrastructure, Security Architecture, Microservices, Event-Driven Architecture, Domain-Driven Design - Backend & Architecture: PHP (Workerman), Node.js, NestJS, Python (FastAPI/Flask), REST APIs, WebSockets, Event-Driven Architecture
- Mobile: Flutter/Dart (Expert), 30+ production apps, GetX/Bloc, Clean Architecture, Offline-first, OTA updates (Shorebird) - Infrastructure & Cloud: Docker, Linux, Nginx, Redis, CI/CD (GitHub Actions), AWS
- Backend: PHP (production), FastAPI, Flask, NestJS, Django, Node.js, MySQL, PostgreSQL, Firebase - Databases & Geospatial: PostgreSQL/PostGIS, MySQL, OpenStreetMap (OSM), GraphHopper, MapLibre GL, Spatial Queries
- GIS/Mapping: OpenStreetMap, Custom Tile Server, Mapping SDK (Flutter + JS, published on pub.dev & NPM), Geocoding, Route Optimization - Mobile: Flutter/Dart, GetX, BLoC/Cubit
- FinTech: Stripe, PayPal, Paymob, Custom Payment Gateway, Digital Wallets, Driver Payout Systems
- AI/ML: Gemini Vision Models, LLM Integration, Invoice Processing, KYC Verification, Document Analysis, Chatbot Systems
- Security: 3-layer Encryption, freeRASP, SSL/TLS Hardening, Server Hardening, Intrusion Monitoring
- DevOps: Linux Server Admin, CI/CD pipelines, Docker, Git, Nginx, Load Balancing, Shorebird Code Push
KEY ACHIEVEMENTS: KEY ACHIEVEMENTS:
- Built IntaleqMaps: proprietary mapping platform on OSM, saving $800$30,000/month vs Google Maps API - Architected IntaleqMaps orchestrating GraphHopper routing, OSM tile serving, and PostGIS spatial queries via a NestJS API layer, saving $10,000+/month vs Google Maps API
- Created custom Flutter Mapping SDK published on pub.dev and NPM - Created custom Flutter Mapping SDK (intaleq_maps) published on pub.dev and JS Library (intaleq-maps-gl) on NPM
- Designed and built the IntaleqMaps platform using NestJS and PostGIS, and core ecosystem backend using PHP/Workerman
- Designed custom FinTech payment infrastructure for markets with zero standard payment API access - Designed custom FinTech payment infrastructure for markets with zero standard payment API access
- Built 4-app ecosystem: Rider App, Driver App, Admin Dashboard, Service Portal — all from scratch - Built 4-app ecosystem: Rider App, Driver App, Admin Dashboard, Service Portal — all from scratch
- Architected smart mobility platform serving 1,800+ drivers and 2,500+ riders across MENA - Architected smart mobility platform serving 1,800+ drivers and 2,500+ riders across MENA
- Integrated AI vision models for invoice processing (Musadaq platform) - Integrated AI vision models for invoice processing (Musadaq platform)
- Built AI-Powered ATS Automation Chrome Extension: Real-time LinkedIn DOM scraping, Gemini AI integration, and PHP-based PDF generation - Developed Nabih: AI-powered smart responder system orchestrating a Flutter application (GetX/Cubit) with a PHP backend API for automated customer query resolution
- Built AI-Powered ATS Automation Chrome Extension: Real-time LinkedIn DOM scraping, Gemini 1.5 Pro, PHP-based PDF generation
- 51 professional certifications (Google, IBM, Meta via Coursera) - 51 professional certifications (Google, IBM, Meta via Coursera)
- $0.78 Driver Customer Acquisition Cost in the competitive Egyptian market - $0.78 Driver Customer Acquisition Cost in the competitive Egyptian market
- Government-accredited — only licensed mobility platform in its market - Government-accredited — only licensed mobility platform in its market
EXPERIENCE: EXPERIENCE:
- CTO & Solutions Architect — Intaleq | Jan 2024Present | Remote (MENA Region) - CTO & Technical Architect | Systems & Backend Focus — Intaleq | Jan 2024Present | Remote (MENA Region)
→ Led full-stack architecture of smart transportation ecosystem: 1,800+ drivers, 2,500+ riders. Designed proprietary mapping, routing, and payment infrastructure from scratch. → Led full-stack architecture of smart transportation ecosystem: 1,800+ drivers, 2,500+ riders. Designed core backend in PHP/Workerman and mapping platform backend in NestJS. Created proprietary mapping, routing, and payment infrastructure.
- Co-Founder & Lead Developer — Tripz Egypt | Jan 2024Present | Cairo/Remote - Founding Engineer & Lead Backend Architect — Tripz Egypt | Jan 2023Present | Cairo/Remote
→ Built ride-hailing platform from zero: 8 ride types, 8% driver commission (lowest in market). Full-stack: Flutter apps, PHP backend, payment integrations. → Built ride-hailing platform from zero: 8 ride types, 8% driver commission (lowest in market). Full-stack: Flutter apps, PHP backend, WebSockets, event-driven architecture, payment integrations. $0.78 CAC.
- Mobile Solutions Architect — Freelance | Jan 2017Dec 2023 | Jordan/Remote - Senior Systems Architect — Mobile & Backend (Freelance) | Jan 2017Dec 2023 | Jordan/Remote
→ 25+ production apps across MENA clients spanning healthcare, logistics, sports, HR, and utilities. → 25+ production apps across MENA clients spanning healthcare, logistics, sports, HR, and utilities.
- Operations & Logistics Leadership — Jordan Armed Forces | Oct 2003Nov 2023
→ 20 years of operational leadership. Led teams of 2050+ personnel in mission-critical environments. Crisis management, logistics planning, security clearance.
EDUCATION: EDUCATION:
- BS Mathematics (Applied Computing & Algorithms), Mutah University, 20032007 (Very Good) - BS Mathematics (Applied Computing & Algorithms), Mutah University, 20032007 (Very Good)
- Google Data Analytics Professional Certificate - Google Data Analytics Professional Certificate (Coursera)
- IBM Data Science Professional Certificate - IBM Data Analyst Professional Certificate (Coursera)
- Meta Mobile Development Certificate - Meta APIs & Django Web Framework Course Certifications (Coursera)
- 51 total certifications - AWS Cloud Practitioner & Cloud Architecture Fundamentals
LANGUAGES: Arabic (Native), English (Professional Working Proficiency) LANGUAGES: Arabic (Native), English (Professional Working Proficiency)
LOCATION: Amman, Jordan | Open to Remote, Hybrid, On-site (Middle East & Gulf) LOCATION: Amman, Jordan | Open to Remote, Hybrid, On-site (Middle East & Gulf)
CONTACT: hamzaayed@tripz-egypt.com | +962 79 858 3052 | linkedin.com/in/hamza-ayed CONTACT: hamzaayed.dev@gmail.com | +962 79 858 3052 | linkedin.com/in/hamza-ayed | github.com/Hamza-Ayed | intaleqapp.com/hamza.html (Portfolio)
TARGET ROLES: TARGET ROLES:
1. Technical Architect / Solutions Architect (distributed systems, cloud, GIS, FinTech) 1. Technical Architect / Solutions Architect (distributed systems, cloud, GIS, FinTech)
@@ -57,9 +53,10 @@ TARGET ROLES:
5. Engineering Manager / Mobile Architecture Lead 5. Engineering Manager / Mobile Architecture Lead
NOTABLE PROJECTS: NOTABLE PROJECTS:
- Intaleq Smart Mobility Platform — Real-time ride-hailing with proprietary mapping & payment infrastructure - Intaleq Smart Mobility Platform — Real-time ride-hailing with PHP/Workerman core backend, NestJS mapping engine, PostGIS spatial features, and proprietary payment infrastructure
- Tripz Egypt — Ride-hailing platform with 8 ride types, AI-powered KYC, lowest commission model - Tripz Egypt — Ride-hailing platform with 8 ride types, AI-powered KYC, lowest commission model, WebSockets dispatcher
- Musadaq — AI-powered invoice processing platform with vision models - Musadaq — AI-powered invoice processing platform with vision models
- IntaleqMaps SDK — Published mapping libraries on pub.dev + NPM - Nabih — AI-powered smart responder assistant built with Flutter (GetX/Cubit) and PHP backend APIs
- IntaleqMaps SDK & JS GL — Published mapping libraries on pub.dev + NPM (https://pub.dev/packages/intaleq_maps and https://libraries.io/npm/intaleq-maps-gl)
- LinkedIn Job Analyzer — AI Chrome extension with real-time DOM scraping, Gemini 1.5 Pro, PHP PDF generation - LinkedIn Job Analyzer — AI Chrome extension with real-time DOM scraping, Gemini 1.5 Pro, PHP PDF generation
- 25+ client apps: healthcare, sports news, HR systems, transit, utilities`; - 25+ client apps: healthcare, sports news, HR systems, transit, utilities`;

View File

@@ -2,10 +2,8 @@
// LANGUAGE RULES: Analysis = match job language. Everything else = ENGLISH ALWAYS. // LANGUAGE RULES: Analysis = match job language. Everything else = ENGLISH ALWAYS.
function buildPromptV2(tab, job, userProfile, language) { function buildPromptV2(tab, job, userProfile, language) {
// Only analysis follows job language; rest is always English // Force Arabic for the analysis tab to ensure full comprehension
const analysisLang = language === 'arabic' ? 'Respond entirely in Arabic.' const analysisLang = 'Respond ENTIRELY in Arabic. All headings, explanations, and bullets MUST be in Arabic. and RTL';
: language === 'english' ? 'Respond entirely in English.'
: 'Respond in the same language as the job posting.';
const ctx = [ const ctx = [
'Job Title: ' + (job.jobTitle || 'Not specified'), 'Job Title: ' + (job.jobTitle || 'Not specified'),
@@ -20,12 +18,23 @@ function buildPromptV2(tab, job, userProfile, language) {
const co = job.company || 'this company'; const co = job.company || 'this company';
const loc = job.location || 'Middle East'; const loc = job.location || 'Middle East';
const STRICT_RULES = `
STRICT INTEGRITY RULES — VIOLATING THESE IS FORBIDDEN:
1. NEVER invent skills, tools, frameworks, or certifications not explicitly listed in MY PROFESSIONAL PROFILE.
2. If the job requires a skill I do not have (e.g., TensorFlow, PyTorch, Scikit-learn, Spark, Hadoop, Kubernetes, deep NLP, generative AI model training), ACKNOWLEDGE THE GAP honestly. Do NOT fabricate experience.
3. My AI experience is LIMITED to: AI vision models for document processing (Musadaq), AI smart responder (Nabih), Gemini API integration (LinkedIn extension), and Python backend automation. I do NOT have MLOps pipeline experience, model training, or deep learning research.
4. My TRUE core stack is: PHP (Workerman), Node.js, NestJS, Python (FastAPI/Flask), PostgreSQL/PostGIS, Docker, Flutter — NOT data science or ML engineering.
5. Always prioritize my REAL architecture achievements (IntaleqMaps, Tripz, 25+ apps, $10K/month savings) over generic AI buzzwords.`;
const P = {}; const P = {};
P.analysis = `You are an elite career strategist. P.analysis = `You are an elite career strategist.
${analysisLang} ${analysisLang}
Evaluate this job against my profile with brutal honesty and EXTREME brevity. Evaluate this job against my profile with brutal honesty and EXTREME brevity.
DO NOT recount my history, military background, or summarize my profile. Keep it actionable and short. DO NOT recount my history, military background, or summarize my profile. Keep it actionable and short.
CRITICAL RULE: The user is a Senior Backend Engineer and Technical Lead who built and scaled production systems from zero. Position his 0-to-1, hands-on delivery experience as a massive advantage for any engineering team. Do NOT downgrade his technical leadership.
${STRICT_RULES}
${prof} ${prof}
@@ -53,6 +62,8 @@ Respond in this EXACT concise structure:
P.coverletter = `You are an expert career writer. Write a COMPLETE, READY-TO-SEND cover letter. P.coverletter = `You are an expert career writer. Write a COMPLETE, READY-TO-SEND cover letter.
IMPORTANT: Write ENTIRELY in English regardless of the job posting language. IMPORTANT: Write ENTIRELY in English regardless of the job posting language.
${STRICT_RULES}
${prof} ${prof}
JOB: JOB:
@@ -70,18 +81,21 @@ Dear ${co} Team,
Best regards, Best regards,
Hamza Ayed Hamza Ayed
hamzaayed@intaleqapp.com hamzaayed.dev@gmail.com
+962 79 858 3052 +962 79 858 3052
CRITICAL RULES: CRITICAL RULES:
- MUST be in English - MUST be in English
- NO brackets or placeholders — use ACTUAL names and data - NO brackets or placeholders — use ACTUAL names and data
- Every sentence specific to THIS job at ${co} - Every sentence specific to THIS job at ${co}
- Under 300 words total`; - Under 300 words total
- Tone MUST be professional, direct, and confident. Focus on delivery, impact, and hands-on engineering. DO NOT mention military history. DO NOT use buzzwords like "sanctioned markets", "serving millions", "disruptive".`;
P.cvtips = `You are a LinkedIn optimization expert and ATS specialist. P.cvtips = `You are a LinkedIn optimization expert and ATS specialist.
IMPORTANT: Respond ENTIRELY in English regardless of the job posting language. IMPORTANT: Respond ENTIRELY in English regardless of the job posting language.
${STRICT_RULES}
${prof} ${prof}
JOB: JOB:
@@ -90,10 +104,10 @@ ${ctx}
Respond EXACTLY: Respond EXACTLY:
## LINKEDIN HEADLINE ## LINKEDIN HEADLINE
[One headline, max 120 chars, ATS-optimized for this role] [One headline, EXACTLY under 80 characters, ATS-optimized. Format like: Senior Backend Engineer & Technical Lead | PHP, NestJS, Docker | Building for Scale]
## PROFESSIONAL SUMMARY ## PROFESSIONAL SUMMARY
[3-4 sentences tailored summary ready to paste — in English] [3-4 sentences tailored summary ready to paste — in English. CRITICAL: Focus on delivery, cost optimization, and hands-on engineering. Use a professional, direct, no-fluff tone. Emphasize impact: what you built, numbers you achieved, problems you solved. DO NOT use academic or AI-generated phrases like 'I leverage my expertise'. DO NOT invent industries like 'banking' unless they are in MY PROFILE.]
## ATS KEYWORDS TO ADD ## ATS KEYWORDS TO ADD
- [10-15 specific keywords from this job that MUST appear in CV] - [10-15 specific keywords from this job that MUST appear in CV]
@@ -112,7 +126,7 @@ Respond EXACTLY:
## NEW BULLET POINTS TO ADD ## NEW BULLET POINTS TO ADD
- [2-3 new achievement bullets ready to paste into CV — in English]`; - [2-3 new achievement bullets ready to paste into CV — in English]`;
const dynamicQuestions = job.questions && job.questions.length > 0 const dynamicQuestions = job.questions && job.questions.length > 0
? job.questions.map((q, i) => `${i + 1}. "${q.question}" [type: ${q.type}]`).join('\n') ? job.questions.map((q, i) => `${i + 1}. "${q.question}" [type: ${q.type}]`).join('\n')
: '1. "Why are you interested in this role?" [type: text]\n2. "What is your relevant experience?" [type: text]\n3. "What are your salary expectations?" [type: text]\n4. "When can you start?" [type: text]\n5. "Do you require visa sponsorship?" [type: text]'; : '1. "Why are you interested in this role?" [type: text]\n2. "What is your relevant experience?" [type: text]\n3. "What are your salary expectations?" [type: text]\n4. "When can you start?" [type: text]\n5. "Do you require visa sponsorship?" [type: text]';
@@ -125,10 +139,16 @@ STRICT RULES:
- Do NOT use code blocks like \`\`\`json. - Do NOT use code blocks like \`\`\`json.
- For number/numeric questions: answer with JUST a number (e.g. "6"). - For number/numeric questions: answer with JUST a number (e.g. "6").
- For yes/no or select questions: answer with JUST "Yes" or "No". - For yes/no or select questions: answer with JUST "Yes" or "No".
- For salary questions: answer with JUST a number (e.g. "25000"). - For salary questions: Output ONLY a number. IMPORTANT: Adapt the number intelligently to the job's likely currency (e.g., if UAE/Saudi: ~30000 AED/SAR. If remote USD: ~6000 USD).
- For text questions: answer in 1 short sentence max. - For text questions: answer in 1 short sentence max.
- Keys must be the EXACT question text. - Keys must be the EXACT question text.
MY PERSONAL DETAILS & PREFERENCES (Use these for answers):
- Location: Amman, Jordan
- Phone / Country Code: +962
- Notice Period / Start Date: Available Immediately
- Salary Expectations: Competitive / Negotiable (If forced to give a number, calculate based on the job's currency as instructed above).
MY PROFILE: MY PROFILE:
${userProfile} ${userProfile}
@@ -138,10 +158,10 @@ ${ctx}
QUESTIONS TO ANSWER: QUESTIONS TO ANSWER:
${dynamicQuestions} ${dynamicQuestions}
RESPOND WITH ONLY THIS FORMAT (raw JSON, no wrapping): RESPOND WITH ONLY THIS FORMAT (raw JSON, no wrapping, answering ONLY the questions listed above):
{ {
"exact question text here": "concise answer", "question 1 text here": "concise answer",
"another question": "answer" "question 2 text here": "concise answer"
}`; }`;
P.benefits = `You are a career analyst specializing in tech compensation in MENA. P.benefits = `You are a career analyst specializing in tech compensation in MENA.
@@ -177,6 +197,20 @@ Respond EXACTLY:
## OVERALL RATING: X/10 ## OVERALL RATING: X/10
**Worth applying?** [YES / MAYBE / NO] **Worth applying?** [YES / MAYBE / NO]
[2-3 sentence honest assessment]`; [2-3 sentence honest assessment]`;
P.list_analysis = `You are an AI pre-screening jobs.
I will give you a JSON array of jobs (Title, Company).
My stack: Flutter, Python (FastAPI), PHP, Node.js, GIS, Technical Architect.
I am actively seeking Senior Engineer, Tech Lead, or Architect roles.
Reject Java, C#, C++, .NET, or pure Product Management roles.
JOBS LIST:
${job.listData}
Respond ONLY with a raw JSON array of objects, one for each job, containing:
[
{ "index": number, "verdict": "YES" | "NO" | "MAYBE", "reason": "Short reason" }
]
Do not wrap in markdown \`\`\`json blocks.`;
return P[tab] || P.analysis; return P[tab] || P.analysis;
} }

120
search_analyzer.css Normal file
View 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); }
}

387
search_analyzer.js Normal file
View File

@@ -0,0 +1,387 @@
// 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 = '<span class="lja-spinner"></span> 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 = `
<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
}
})();

View File

@@ -8,19 +8,23 @@
* Avoids flexbox, uses standard block/inline styling for flawless PDF generation. * Avoids flexbox, uses standard block/inline styling for flawless PDF generation.
*/ */
body { body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: Arial, Helvetica, sans-serif;
font-variant-ligatures: none;
font-feature-settings: "liga" 0, "clig" 0;
letter-spacing: 0px;
text-rendering: geometricPrecision;
font-size: 13px; font-size: 13px;
color: #222; color: #222;
line-height: 1.5; line-height: 1.4;
margin: 0; margin: 0;
padding: 20px 40px; padding: 15px 30px;
} }
.header { .header {
text-align: center; text-align: center;
border-bottom: 2px solid #1a237e; border-bottom: 2px solid #1a237e;
padding-bottom: 15px; padding-bottom: 10px;
margin-bottom: 20px; margin-bottom: 15px;
} }
h1 { h1 {
@@ -48,40 +52,48 @@
font-weight: bold; font-weight: bold;
color: #1a237e; color: #1a237e;
border-bottom: 1px solid #ddd; border-bottom: 1px solid #ddd;
margin-top: 20px; margin-top: 15px;
margin-bottom: 10px; margin-bottom: 6px;
padding-bottom: 4px; padding-bottom: 4px;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.5px; letter-spacing: 0.5px;
} }
p { margin: 0 0 10px 0; text-align: justify; } p { margin: 0 0 6px 0; text-align: justify; }
.job-block { margin-bottom: 15px; } .job-block { margin-bottom: 10px; }
.job-header { .job-header {
width: 100%; width: 100%;
margin-bottom: 5px; margin-bottom: 5px;
display: table; /* هذا يحل مشاكل Dompdf */
} }
.job-title { .job-title {
font-weight: bold; font-weight: bold;
font-size: 14px; font-size: 13.5px; /* تصغير طفيف جداً */
color: #222; color: #222;
float: left; float: left;
width: 70%; /* إجبار العنوان على عدم تجاوز 70% من السطر */
line-height: 1.3;
} }
.job-meta { .job-meta {
font-size: 12px; font-size: 11px;
color: #1a237e; color: #1a237e;
font-weight: bold; font-weight: bold;
float: right; float: right;
width: 28%; /* تخصيص مساحة آمنة للتاريخ */
text-align: right;
} }
.clear { clear: both; } .clear { clear: both; }
ul { margin: 5px 0 10px 0; padding-left: 20px; }
li { margin-bottom: 6px; text-align: justify; }
ul { margin: 4px 0 8px 0; padding-left: 20px; }
li { margin-bottom: 4px; text-align: justify; }
</style> </style>
</head> </head>
<body> <body>
@@ -91,7 +103,10 @@
<!-- PHP WILL INJECT THE AI-GENERATED HEADLINE HERE --> <!-- PHP WILL INJECT THE AI-GENERATED HEADLINE HERE -->
<div class="headline">{{JOB_HEADLINE}}</div> <div class="headline">{{JOB_HEADLINE}}</div>
<div class="contact"> <div class="contact">
Amman, Jordan | +962 79 858 3052 | hamzaayed@intaleqapp.com | linkedin.com/in/hamza-ayed Amman, Jordan | +962 79 858 3052 | <a href="mailto:hamzaayed.dev@gmail.com" style="color: #555; text-decoration: none;">hamzaayed.dev@gmail.com</a><br>
<a href="https://linkedin.com/in/hamza-ayed" style="color: #555; text-decoration: none; font-weight: bold;">linkedin.com/in/hamza-ayed</a> |
<a href="https://github.com/Hamza-Ayed" style="color: #555; text-decoration: none; font-weight: bold;">github.com/Hamza-Ayed</a> |
<a href="https://intaleqapp.com/hamza.html" style="color: #1a237e; text-decoration: underline; font-weight: bold;">intaleqapp.com/hamza.html (Portfolio)</a>
</div> </div>
</div> </div>
@@ -99,71 +114,95 @@
<!-- PHP WILL INJECT THE AI-GENERATED SUMMARY HERE --> <!-- PHP WILL INJECT THE AI-GENERATED SUMMARY HERE -->
<p>{{TAILORED_SUMMARY}}</p> <p>{{TAILORED_SUMMARY}}</p>
<div class="section-title">Core Competencies & Skills</div>
<div class="section-title">Technical Skills</div>
<!-- PHP WILL INJECT ATS KEYWORDS HERE --> <!-- PHP WILL INJECT ATS KEYWORDS HERE -->
<p><strong>Targeted Expertise:</strong> {{DYNAMIC_SKILLS}}</p> <p><strong>Core Focus:</strong> {{DYNAMIC_SKILLS}}</p>
<p><strong>Technologies:</strong> Flutter/Dart, PHP, Python (FastAPI/Flask), Node.js, MySQL, PostgreSQL, AWS/Cloud Infrastructure, Git, Docker.</p> <p><strong>Backend & Architecture:</strong> PHP (Workerman), Node.js, NestJS, Python (FastAPI/Flask), REST APIs, WebSockets, Event-Driven Architecture</p>
<p><strong>Domains:</strong> System Architecture, Distributed Systems, GIS/Mapping (OSM), FinTech (Payment Gateways), AI/ML Integration, Zero-Trust Security.</p> <p><strong>Infrastructure & Cloud:</strong> Docker, Linux, Nginx, Redis, CI/CD (GitHub Actions), AWS</p>
<p><strong>Databases & Geospatial:</strong> PostgreSQL/PostGIS, MySQL, OpenStreetMap (OSM), GraphHopper, MapLibre GL, Spatial Queries</p>
<p><strong>Mobile:</strong> Flutter/Dart, GetX, BLoC/Cubit</p>
<div class="section-title">Selected Technical Highlights</div>
<ul>
<li>Built proprietary OSM-based mapping infrastructure replacing Google Maps APIs</li>
<li>Engineered WebSocket-based real-time ride tracking systems</li>
<li>Developed offline-first mapping SDKs published on pub.dev and npm</li>
<li>Designed async AI document-processing pipelines using vision models</li>
<li>Optimized routing infrastructure using GraphHopper and PostGIS</li>
</ul>
<div class="section-title">Professional Experience</div> <div class="section-title">Professional Experience</div>
<div class="job-block"> <div class="job-block">
<div class="job-header"> <div class="job-header">
<div class="job-title">CTO & Solutions Architect — Intaleq</div> <div class="job-title">CTO & Technical Architect | Systems & Backend Focus — Intaleq &nbsp;|&nbsp;</div>
<div class="job-meta">Jan 2025 Present | Syria / Remote</div> <div class="job-meta">January 2024 Present | Remote (MENA Region)</div>
<div class="clear"></div> <div class="clear"></div>
</div> </div>
<ul> <ul>
<li>Led the full-stack architecture of a smart transportation ecosystem supporting 1,800+ drivers and 2,500+ riders.</li> <li>Led the full-stack architecture of a smart transportation ecosystem supporting 1,800+ drivers and 2,500+ riders, building the core backend with PHP/Workerman.</li>
<li>Built a proprietary mapping platform (IntaleqMaps) on OpenStreetMap, eliminating reliance on Google Maps API and saving $10,000+/month in operational costs.</li> <li>Architected the proprietary mapping platform (IntaleqMaps), orchestrating GraphHopper routing, OSM tile serving, and PostGIS spatial queries via a NestJS API layer, saving $10,000+/month in operational costs.</li>
<li>Designed secure, custom payment infrastructure for environments lacking standard payment APIs, ensuring high-availability transaction integrity.</li>
</ul> </ul>
</div> </div>
<div class="job-block"> <div class="job-block">
<div class="job-header"> <div class="job-header">
<div class="job-title">Co-Founder & Lead Developer — Tripz Egypt</div> <div class="job-title">Founding Engineer & Lead Backend Architect — Tripz Egypt &nbsp;|&nbsp;</div>
<div class="job-meta">Jan 2024 Present | Cairo / Remote</div> <div class="job-meta">Jan 2023 Present | Cairo / Remote</div>
<div class="clear"></div> <div class="clear"></div>
</div> </div>
<ul> <ul>
<li>Architected Egypt's homegrown ride-hailing platform, managing the entire distributed ecosystem with real-time tracking and dispatching.</li> <li>Co-founded and architected Egypt's homegrown ride-hailing platform, managing the entire distributed ecosystem with real-time tracking for 4,318 drivers and 2,464 riders.</li>
<li>Implemented robust microservices for real-time driver/rider matching and route optimization using event-driven architecture.</li> <li>Implemented robust event-driven architecture for real-time driver/rider matching and WebSockets-based route optimization.</li>
<li>Integrated local digital payment gateways and driver payout systems, supporting an 8% commission structure that drove a $0.78 Customer Acquisition Cost.</li>
</ul> </ul>
</div> </div>
<div class="job-block"> <div class="job-block">
<div class="job-header"> <div class="job-header">
<div class="job-title">Mobile Solutions Architect — Freelance</div> <div class="job-title">Senior Systems Architect — Mobile & Backend (Freelance) &nbsp;|&nbsp;</div>
<div class="job-meta">Jan 2017 Dec 2023 | Jordan / Remote</div> <div class="job-meta">Jan 2017 Dec 2023 | Jordan / Remote</div>
<div class="clear"></div> <div class="clear"></div>
</div> </div>
<ul> <ul>
<li>Delivered 25+ production enterprise applications across GIS, FinTech, HR, and utilities for clients across the MENA region.</li> <li>Delivered 25+ production enterprise applications across GIS, FinTech, and utilities, leading full lifecycle architecture for clients in the MENA region.</li>
<li>Integrated AI vision models for document analysis (KYC) and automated invoice processing pipelines.</li> <li>Developed Nabih (AI Smart Responder): designed and built an intelligent virtual assistant system connecting a Flutter mobile application (GetX/Cubit) with custom PHP backend APIs.</li>
<li>Developed Musadaq (AI-Powered Invoice Processing): engineered an automated KYC and invoice document processor using AI vision models and asynchronous processing pipelines.</li>
</ul> </ul>
</div> </div>
<div class="job-block"> <div class="section-title">Notable Projects</div>
<div class="job-header"> <ul>
<div class="job-title">Operations & Logistics Officer — Jordan Armed Forces</div> <li><strong>IntaleqMaps Engine:</strong> Engineered a proprietary mapping platform orchestrating GraphHopper routing, OSM tile serving, and PostGIS spatial queries via a NestJS API layer, replacing Google Maps API across the MENA region.</li>
<div class="job-meta">Oct 2003 Nov 2023 | Jordan</div> <li><strong>Nabih AI Virtual Assistant:</strong> Architected an automated customer support solution connecting a Flutter application (GetX/Cubit) to custom high-concurrency PHP backend APIs.</li>
<div class="clear"></div> <li><strong>Musadaq AI Document Processor:</strong> Engineered an automated KYC and invoice document processing platform utilizing AI vision models and async processing pipelines.</li>
</div> <li><strong>Meta Ads Manager & SaaS Gateways:</strong> Deployed multi-tenant platforms using NestJS/React (Meta Ads Manager) and high-concurrency Node.js (WhatsApp bridge managing headless Puppeteer sessions in Docker) to automate operations.</li>
<ul> </ul>
<li>Retired Lieutenant Colonel. Directed logistics and crisis management operations, leading teams of 50+ personnel.</li>
<li>Applied rigorous, security-first methodologies to organizational leadership, disaster recovery, and operational planning.</li> <div class="section-title">Open Source Contributions</div>
</ul> <ul>
</div> <li><strong>intaleq_maps (Flutter SDK):</strong> Published on <a href="https://pub.dev/packages/intaleq_maps" style="color: #1a237e; text-decoration: none; font-weight: bold;">pub.dev/packages/intaleq_maps</a>. Custom map rendering, offline caching, and route plotting package optimized for low-bandwidth environments.</li>
<li><strong>intaleq-maps-gl (NPM Library):</strong> Published on <a href="https://libraries.io/npm/intaleq-maps-gl" style="color: #1a237e; text-decoration: none; font-weight: bold;">npmjs.com/package/intaleq-maps-gl</a>. Web-based Mapbox GL compatible library optimized for custom OSM tiles and routing integration.</li>
</ul>
<div class="section-title">Education & Certifications</div> <div class="section-title">Education & Certifications</div>
<ul> <ul>
<li><strong>BS Mathematics</strong>, Mutah University (20032007)</li> <li><strong>BS Mathematics (Applied Computing & Algorithms)</strong>, Mutah University (20032007)</li>
<li>Google Data Analytics Professional Certificate</li> <li><strong>Google Data Analytics</strong> Professional Certificate (Coursera)</li>
<li>IBM Data Science Professional Certificate</li> <li><strong>IBM Data Analyst</strong> Professional Certificate (Coursera)</li>
<li>Meta Mobile Development Certificate</li> <li><strong>Meta APIs & Django Web Framework</strong> Course Certificates (Coursera)</li>
<li><em>Total of 51 professional certifications across software engineering, AI, and enterprise architecture.</em></li> <li><strong>AWS Cloud Practitioner & Cloud Architecture Fundamentals</strong></li>
</ul> </ul>
<div class="section-title">Languages</div>
<p><strong>Arabic:</strong> Native | <strong>English:</strong> Professional Working Proficiency | <strong>Turkish:</strong> Limited Working Proficiency</p>
<div class="section-title">Availability & Work Authorization</div>
<p style="text-align: center; font-weight: bold; color: #1a237e; margin-top: 10px;">
Available Immediately | Open to Remote, Hybrid & Relocation (GCC/Europe/MENA) | Valid Passport
</p>
</body> </body>
</html> </html>

View File

@@ -0,0 +1,200 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body {
font-family: Arial, Helvetica, sans-serif;
font-variant-ligatures: none;
font-feature-settings: "liga" 0, "clig" 0;
letter-spacing: 0px;
text-rendering: geometricPrecision;
font-size: 13px;
color: #222;
line-height: 1.4;
margin: 0;
padding: 15px 30px;
}
.header {
text-align: center;
border-bottom: 2px solid #1a237e;
padding-bottom: 10px;
margin-bottom: 15px;
}
h1 {
font-size: 28px;
color: #1a237e;
margin: 0 0 5px 0;
text-transform: uppercase;
letter-spacing: 1px;
}
.headline {
font-size: 16px;
font-weight: bold;
color: #424242;
margin-bottom: 5px;
}
.contact {
font-size: 12px;
color: #555;
}
.section-title {
font-size: 15px;
font-weight: bold;
color: #1a237e;
border-bottom: 1px solid #ddd;
margin-top: 15px;
margin-bottom: 6px;
padding-bottom: 4px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
p { margin: 0 0 6px 0; text-align: justify; }
.job-block { margin-bottom: 10px; }
.job-header {
width: 100%;
margin-bottom: 5px;
display: table;
}
.job-title {
font-weight: bold;
font-size: 13.5px;
color: #222;
float: left;
width: 70%;
line-height: 1.3;
}
.job-meta {
font-size: 11px;
color: #1a237e;
font-weight: bold;
float: right;
width: 28%;
text-align: right;
}
.clear { clear: both; }
ul { margin: 4px 0 8px 0; padding-left: 20px; }
li { margin-bottom: 4px; text-align: justify; }
</style>
</head>
<body>
<div class="header">
<h1>Hamza Ayed</h1>
<div class="headline">{{JOB_HEADLINE}}</div>
<div class="contact">
Amman, Jordan | +962 79 858 3052 | <a href="mailto:hamzaayed.dev@gmail.com" style="color: #555; text-decoration: none;">hamzaayed.dev@gmail.com</a><br>
<a href="https://linkedin.com/in/hamza-ayed" style="color: #555; text-decoration: none; font-weight: bold;">linkedin.com/in/hamza-ayed</a> |
<a href="https://github.com/Hamza-Ayed" style="color: #555; text-decoration: none; font-weight: bold;">github.com/Hamza-Ayed</a> |
<a href="https://intaleqapp.com/hamza.html" style="color: #1a237e; text-decoration: underline; font-weight: bold;">intaleqapp.com/hamza.html (Portfolio)</a>
</div>
</div>
<div class="section-title">Professional Summary</div>
<p>{{TAILORED_SUMMARY}}</p>
<div class="section-title">Technical Skills</div>
<p><strong>Core Focus:</strong> {{DYNAMIC_SKILLS}}</p>
<p><strong>Backend &amp; Architecture:</strong> PHP (Workerman), Node.js, NestJS, Python (FastAPI/Flask), REST APIs, WebSockets, Microservices</p>
<p><strong>Infrastructure &amp; Cloud:</strong> Docker, Linux, Nginx, Redis, CI/CD (GitHub Actions), AWS</p>
<p><strong>Databases:</strong> PostgreSQL, MySQL, Redis, Database Design &amp; Query Optimization</p>
<p><strong>Geospatial (Supplemental):</strong> PostGIS, OpenStreetMap (OSM), GraphHopper, MapLibre GL, Spatial Queries</p>
<p><strong>Mobile:</strong> Flutter/Dart, GetX, BLoC/Cubit</p>
<div class="section-title">Impact &amp; Delivery</div>
<ul>
<li>Built and scaled platforms serving 6,000+ active users across two markets with zero downtime deployments</li>
<li>Engineered proprietary OSM-based mapping infrastructure replacing Google Maps APIs across the MENA region — saving $10,000+/month in operational costs</li>
<li>Designed high-concurrency WebSocket systems handling real-time operations at scale</li>
<li>Delivered 25+ production applications across healthcare, logistics, fintech, and utilities for MENA clients</li>
<li>Published open-source mapping SDKs on pub.dev and npm, used in production environments</li>
</ul>
<div class="section-title">Professional Experience</div>
<div class="job-block">
<div class="job-header">
<div class="job-title">Technical Lead &amp; Backend Engineer — Intaleq &nbsp;|&nbsp;</div>
<div class="job-meta">January 2024 Present | Amman, Jordan</div>
<div class="clear"></div>
</div>
<ul>
<li>Built full-stack transportation platform from zero to production: PHP/Workerman backend, Flutter mobile apps, WebSocket-based real-time dispatcher — serving 1,800+ active drivers.</li>
<li>Developed proprietary mapping platform (IntaleqMaps) orchestrating GraphHopper routing, OSM tile serving, and PostGIS spatial queries via a NestJS API layer — reducing mapping API costs by $10,000+/month.</li>
<li>Designed event-driven architecture for real-time driver/rider matching, live tracking, and automated fare calculation.</li>
</ul>
</div>
<div class="job-block">
<div class="job-header">
<div class="job-title">Lead Backend Engineer — Tripz Egypt &nbsp;|&nbsp;</div>
<div class="job-meta">Jan 2023 Present | Cairo / Remote</div>
<div class="clear"></div>
</div>
<ul>
<li>Built complete ride-hailing platform: PHP backend system, Flutter rider and driver apps, real-time WebSocket communication layer, and local payment gateway integrations.</li>
<li>Engineered event-driven driver/rider matching system serving 4,318 drivers with a $0.78 customer acquisition cost — the lowest in the Egyptian market.</li>
<li>Integrated local digital payment gateways and designed automated driver payout infrastructure supporting an 8% commission model.</li>
</ul>
</div>
<div class="job-block">
<div class="job-header">
<div class="job-title">Senior Backend Engineer — Freelance &nbsp;|&nbsp;</div>
<div class="job-meta">Jan 2017 Dec 2023 | Jordan / Remote</div>
<div class="clear"></div>
</div>
<ul>
<li>Delivered 25+ applications for MENA clients across healthcare, logistics, HR technology, sports platforms, and utility management systems — full lifecycle from architecture to deployment.</li>
<li>Built automated customer support platform: designed Flutter application (GetX/Cubit) connected to high-concurrency PHP backend APIs handling real-time query resolution.</li>
<li>Developed document processing system: engineered async processing pipelines using vision models for automated KYC and invoice data extraction.</li>
</ul>
</div>
<div class="section-title">Notable Projects</div>
<ul>
<li><strong>IntaleqMaps Engine:</strong> Proprietary mapping platform orchestrating GraphHopper routing, OSM tile serving, and PostGIS spatial queries via a NestJS API layer — replacing Google Maps API across the MENA region.</li>
<li><strong>Tripz Egypt Platform:</strong> End-to-end ride-hailing system with 8 ride types, real-time matching, custom payment infrastructure, and AI-powered KYC verification.</li>
<li><strong>Customer Support Automation Platform:</strong> Built an automated query resolution system connecting a Flutter application (GetX/Cubit) to high-concurrency PHP backend APIs.</li>
<li><strong>Document Processing Platform:</strong> Engineered automated KYC and invoice processing using vision models and asynchronous processing pipelines.</li>
<li><strong>Meta Ads Manager (SaaS):</strong> Deployed multi-tenant platform using NestJS/React for automated ad campaign management.</li>
<li><strong>WhatsApp Bridge:</strong> High-concurrency Node.js service managing headless Puppeteer sessions in Docker for automated messaging operations.</li>
</ul>
<div class="section-title">Open Source Contributions</div>
<ul>
<li><strong>intaleq_maps (Flutter SDK):</strong> Published on <a href="https://pub.dev/packages/intaleq_maps" style="color: #1a237e; text-decoration: none; font-weight: bold;">pub.dev/packages/intaleq_maps</a>. Custom map rendering, offline caching, and route plotting package optimized for low-bandwidth environments.</li>
<li><strong>intaleq-maps-gl (NPM Library):</strong> Published on <a href="https://libraries.io/npm/intaleq-maps-gl" style="color: #1a237e; text-decoration: none; font-weight: bold;">npmjs.com/package/intaleq-maps-gl</a>. Web-based Mapbox GL compatible library optimized for custom OSM tiles and routing integration.</li>
</ul>
<div class="section-title">Education &amp; Certifications</div>
<ul>
<li><strong>BS Mathematics (Applied Computing &amp; Algorithms)</strong>, Mutah University (20032007)</li>
<li><strong>Google Data Analytics</strong> Professional Certificate (Coursera)</li>
<li><strong>IBM Data Analyst</strong> Professional Certificate (Coursera)</li>
<li><strong>Meta APIs &amp; Django Web Framework</strong> Course Certificates (Coursera)</li>
<li><strong>AWS Cloud Practitioner &amp; Cloud Architecture Fundamentals</strong></li>
</ul>
<div class="section-title">Languages</div>
<p><strong>Arabic:</strong> Native | <strong>English:</strong> Professional Working Proficiency | <strong>Turkish:</strong> Limited Working Proficiency</p>
<div class="section-title">Availability</div>
<p style="text-align: center; font-weight: bold; color: #1a237e; margin-top: 10px;">
Based in Amman, Jordan — Available Immediately | Open to On-site, Hybrid &amp; Remote Roles in Jordan &amp; MENA
</p>
</body>
</html>

View File

@@ -48,7 +48,8 @@ if (empty($apiKey)) {
exit; exit;
} }
$model = "gemini-2.5-flash"; // Standardized on gemini-flash-lite-latest due to quota limits
$model = "gemini-flash-lite-latest";
$geminiUrl = "https://generativelanguage.googleapis.com/v1beta/models/{$model}:generateContent?key=" . $apiKey; $geminiUrl = "https://generativelanguage.googleapis.com/v1beta/models/{$model}:generateContent?key=" . $apiKey;
// ========================================== // ==========================================
@@ -56,16 +57,44 @@ $geminiUrl = "https://generativelanguage.googleapis.com/v1beta/models/{$model}:g
// ========================================== // ==========================================
if ($action === 'generatePdf') { if ($action === 'generatePdf') {
$jobDescription = $data['jobDescription'] ?? ''; $jobDescription = $data['jobDescription'] ?? '';
$template = $data['template'] ?? 'default'; // 'amman' or 'default'
$prompt = "You are an expert ATS CV tailor. Read the following job description and generate tailored content for my CV to maximize my chances of getting an interview.
// --- Amman Market Prompt (Senior Backend Engineer & Technical Lead) ---
if ($template === 'amman') {
$prompt = "You are an expert ATS CV tailor for the Jordan/Amman local tech market. Read the following job description and generate tailored content.
STRICT INTEGRITY RULES:
1. NEVER invent skills I do not have. My TRUE technical stack is: PHP (Workerman), Node.js, NestJS, Python (FastAPI/Flask), PostgreSQL/PostGIS, Docker, Flutter, GetX, BLoC, WebSockets, OpenStreetMap.
2. Do NOT add data science skills like TensorFlow, PyTorch, Scikit-learn, Hadoop, or MLOps.
3. My title MUST be aligned with 'Senior Backend Engineer', 'Technical Lead', or 'Lead Backend Engineer'. NEVER use 'Solutions Architect', 'AI Developer', or 'CTO'.
Return ONLY a valid JSON object with EXACTLY three keys: 'headline', 'summary', and 'skills'. Return ONLY a valid JSON object with EXACTLY three keys: 'headline', 'summary', and 'skills'.
The 'headline' should be a 5-6 word professional title relevant to the job. The 'headline' should be a clean, confident title based on my TRUE skills — matching Amman market expectations (Senior/Lead Backend Engineer).
The 'summary' should be a 3-sentence powerful paragraph highlighting skills relevant to the job. The 'summary' should open with a hook about building and scaling production systems in the MENA region, emphasizing delivery, cost optimization, and hands-on engineering. Connect my real achievements to the job requirements. Keep it professional and direct — NO academic fluff, NO 'I leverage my expertise' phrases.
The 'skills' should be a comma-separated list of 10 highly relevant ATS keywords. The 'skills' should be a comma-separated list of 10 highly relevant ATS keywords from MY ACTUAL SKILLS that match the job. Prioritize backend, API, database, and infrastructure skills over GIS.
Do NOT use markdown blocks like ```json, just return raw JSON text. Do NOT use markdown blocks like ```json, just return raw JSON text.
Job Description: Job Description:
" . substr($jobDescription, 0, 4000); " . substr($jobDescription, 0, 4000);
}
// --- Default Prompt (Current — Enterprise/GCC) ---
else {
$prompt = "You are an expert ATS CV tailor. Read the following job description and generate tailored content for my CV to maximize my chances of getting an interview.
STRICT INTEGRITY RULES:
1. NEVER invent skills I do not have. My TRUE technical stack is: PHP (Workerman), Node.js, NestJS, Python (FastAPI/Flask), PostgreSQL/PostGIS, Docker, Flutter, GetX, BLoC, WebSockets, OpenStreetMap.
2. Do NOT add data science skills like TensorFlow, PyTorch, Scikit-learn, Hadoop, or MLOps.
3. My title MUST be aligned with 'Solutions Architect', 'Senior Backend Engineer', or 'Senior Mobile Engineer'. NEVER title me 'Senior AI Developer'.
Return ONLY a valid JSON object with EXACTLY three keys: 'headline', 'summary', and 'skills'.
The 'headline' should be a clean, confident title based on my TRUE skills.
The 'summary' MUST open with exactly this hook: 'Built two production ride-hailing platforms from zero to thousands of users, on proprietary infrastructure, in high-complexity and emerging markets.' Then use the next 2 sentences to seamlessly tie my relevant TRUE skills to the job description requirements.
The 'skills' should be a comma-separated list of 10 highly relevant ATS keywords from MY ACTUAL SKILLS that match the job.
Do NOT use markdown blocks like ```json, just return raw JSON text.
Job Description:
" . substr($jobDescription, 0, 4000);
}
$payload = json_encode([ $payload = json_encode([
"contents" => [["parts" => [["text" => $prompt]]]], "contents" => [["parts" => [["text" => $prompt]]]],
@@ -92,12 +121,17 @@ Job Description:
$aiText = str_replace(['```json', '```'], '', $aiText); $aiText = str_replace(['```json', '```'], '', $aiText);
$parsedJson = json_decode(trim($aiText), true); $parsedJson = json_decode(trim($aiText), true);
$headline = $parsedJson['headline'] ?? "Solutions Architect & Technical Leader"; $defaultHeadline = ($template === 'amman')
? "Senior Backend Engineer & Technical Lead"
: "Solutions Architect & Technical Leader";
$headline = $parsedJson['headline'] ?? $defaultHeadline;
$summary = $parsedJson['summary'] ?? "Experienced professional with a strong background in software engineering."; $summary = $parsedJson['summary'] ?? "Experienced professional with a strong background in software engineering.";
$skills = $parsedJson['skills'] ?? "Architecture, APIs, Cloud, Backend Systems, System Design"; $skills = $parsedJson['skills'] ?? "Architecture, APIs, Cloud, Backend Systems, System Design";
$templatePath = __DIR__ . '/cv_template.html'; $templateFile = ($template === 'amman')
$html = file_get_contents($templatePath); ? __DIR__ . '/cv_template_amman.html'
: __DIR__ . '/cv_template.html';
$html = file_get_contents($templateFile);
$html = str_replace('{{JOB_HEADLINE}}', htmlspecialchars($headline), $html); $html = str_replace('{{JOB_HEADLINE}}', htmlspecialchars($headline), $html);
$html = str_replace('{{TAILORED_SUMMARY}}', htmlspecialchars($summary), $html); $html = str_replace('{{TAILORED_SUMMARY}}', htmlspecialchars($summary), $html);
$html = str_replace('{{DYNAMIC_SKILLS}}', htmlspecialchars($skills), $html); $html = str_replace('{{DYNAMIC_SKILLS}}', htmlspecialchars($skills), $html);
@@ -159,3 +193,113 @@ if ($action === 'generateText') {
echo $response; echo $response;
exit; exit;
} }
// ==========================================
// ACTION 3: Smart Comment Generator
// ==========================================
if ($action === 'generateComment') {
$postText = substr($data['postText'] ?? '', 0, 3000);
if (empty($postText)) {
http_response_code(400);
echo json_encode(["error" => "postText is required."]);
exit;
}
$promptFile = __DIR__ . '/prompts/comment_prompt.txt';
if (!file_exists($promptFile)) {
http_response_code(500);
echo json_encode(["error" => "Comment prompt file not found on server."]);
exit;
}
$promptTemplate = file_get_contents($promptFile);
$prompt = str_replace('{{POST_TEXT}}', $postText, $promptTemplate);
$payload = json_encode([
"contents" => [["parts" => [["text" => $prompt]]]],
"generationConfig" => ["temperature" => 0.75, "maxOutputTokens" => 256]
]);
$ch = curl_init($geminiUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
http_response_code(500);
echo json_encode(["error" => "Gemini API Error", "details" => json_decode($response)]);
exit;
}
$responseData = json_decode($response, true);
$commentText = trim($responseData['candidates'][0]['content']['parts'][0]['text'] ?? '');
if (empty($commentText)) {
http_response_code(500);
echo json_encode(["error" => "Empty comment from AI."]);
exit;
}
echo json_encode(["success" => true, "comment" => $commentText]);
exit;
}
// ==========================================
// ACTION 4: Repurpose Post Generator
// ==========================================
if ($action === 'repurposePost') {
$postText = substr($data['postText'] ?? '', 0, 3000);
if (empty($postText)) {
http_response_code(400);
echo json_encode(["error" => "postText is required."]);
exit;
}
$promptFile = __DIR__ . '/prompts/repurpose_prompt.txt';
if (!file_exists($promptFile)) {
http_response_code(500);
echo json_encode(["error" => "Repurpose prompt file not found on server."]);
exit;
}
$promptTemplate = file_get_contents($promptFile);
$prompt = str_replace('{{POST_TEXT}}', $postText, $promptTemplate);
$payload = json_encode([
"contents" => [["parts" => [["text" => $prompt]]]],
"generationConfig" => ["temperature" => 0.75, "maxOutputTokens" => 800]
]);
$ch = curl_init($geminiUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
http_response_code(500);
echo json_encode(["error" => "Gemini API Error", "details" => json_decode($response)]);
exit;
}
$responseData = json_decode($response, true);
$resultText = trim($responseData['candidates'][0]['content']['parts'][0]['text'] ?? '');
if (empty($resultText)) {
http_response_code(500);
echo json_encode(["error" => "Empty result from AI."]);
exit;
}
echo json_encode(["success" => true, "result" => $resultText]);
exit;
}

View File

@@ -0,0 +1,21 @@
You are a Solutions Architect and Engineering Leader. Read the following LinkedIn post and generate a JSON response.
STRICT RULES FOR THE COMMENT:
- Max 3 sentences. It MUST flow naturally as a single, coherent, human-written comment. Do NOT output disconnected, bullet-point-like statements.
- Tone: Calm, authoritative, direct, and operator-level. Avoid corporate jargon, fluff, ego, preaching, or generic praise clichés (never start with "Great post", "Well said", etc.).
- INDUSTRY RELEVANCE (CRITICAL): Do NOT mention ride-hailing, mobility, GIS, or transportation unless the post is explicitly and directly about maps, routing, logistics, or transportation. For general posts (like team management, system design, workplace culture, innovation), comment from a general systems architect/tech lead perspective.
- TONE & STYLE: Write a comment that encourages professional discussion and attracts profile visits.
- LANGUAGE: Match the language of the post. If the post is in Arabic, write the comment in Arabic. If the post is in English, write the comment in English.
- Output the comment exactly as plain text in the JSON string (no markdown formatting inside).
STRICT RULES FOR THE ARABIC SUMMARY & CREDIBILITY CHECK:
- Analyze the post objectively. No flattery. No automatic agreement.
- Detect any exaggerated metrics, vanity metrics, logical flaws, or emotional manipulation (e.g., VC/startup hustle culture tropes).
- Provide a 2-3 sentence Arabic response. The first sentence should summarize the post. The following sentences MUST be a brutal, honest credibility assessment pointing out risks or flaws in the author's logic. (Note: The summary and credibility check MUST ALWAYS be in Arabic, regardless of the post's language).
Return ONLY a valid JSON object with EXACTLY two keys:
1. "arabic_summary": The objective summary and credibility assessment in Arabic.
2. "comment": The comment following the rules above (matching the post's language: Arabic if the post is Arabic, English if the post is English).
POST TEXT:
{{POST_TEXT}}

View File

@@ -0,0 +1,21 @@
You are Hamza Ayed, a Senior Solutions Architect and technical leader.
Your task is to REPURPOSE an existing LinkedIn post into an entirely new, original post written in your unique voice.
YOUR BACKGROUND & PHILOSOPHY:
- Built "Intaleq" and "Tripz" platforms from scratch.
- Created "Intaleq Maps" to bypass Google Maps, saving massive costs and achieving independence.
- You believe in building independent infrastructure, cost-efficiency, and tech sovereignty rather than relying entirely on 3rd parties.
STRICT RULES:
1. CORE CONCEPT: Extract the main idea from the provided post, but write a COMPLETELY NEW post. Do not just summarize. Reframe it from your Architect/Leadership perspective.
2. LANGUAGE: Match the language of the original post. If Arabic, use conversational, engaging, professional Arabic (AVOID AI clichés like "في العصر الرقمي", "مما لا شك فيه", "يعد", "ختاماً"). Write naturally like a seasoned expert.
3. STRUCTURE:
- Hook: A strong opening statement (no emojis in the very first sentence).
- Body: Your perspective on the topic. Subtly weave in your philosophy if relevant (without revealing secrets).
- Conclusion: End with a strong closing thought or an open question to encourage engagement.
4. IMAGE PROMPT: At the very end of your response, leave two blank lines, then output EXACTLY this header: "--- IMAGE PROMPT ---", followed by a highly detailed, descriptive prompt in English for an AI image generator (like Midjourney or DALL-E) to create a stunning, professional accompanying visual for your post.
5. NO EMOJIS in the image prompt section. You may use a few appropriate emojis in the main post text.
6. OUTPUT FORMAT: Return the new post text, then the image prompt section. Nothing else.
POST TO REPURPOSE:
{{POST_TEXT}}

10
speech.html Normal file
View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Speech Recognition Frame</title>
</head>
<body>
<script src="speech.js"></script>
</body>
</html>

120
speech.js Normal file
View File

@@ -0,0 +1,120 @@
// speech.js - Handles webkitSpeechRecognition inside the injected iframe
let recognition = null;
let isRecording = false;
let localStream = null;
function stopMediaTracks() {
if (localStream) {
localStream.getTracks().forEach(track => track.stop());
localStream = null;
}
}
function initRecognition(language) {
if (recognition) return recognition;
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
if (!SpeechRecognition) {
throw new Error('Speech recognition not supported');
}
recognition = new SpeechRecognition();
recognition.continuous = true;
recognition.interimResults = true;
recognition.lang = language || 'ar-SA';
recognition.maxAlternatives = 1;
recognition.onresult = (event) => {
let interimText = '';
let finalText = '';
for (let i = 0; i < event.results.length; i++) {
const result = event.results[i];
if (result.isFinal) {
finalText += result[0].transcript + ' ';
} else {
interimText += result[0].transcript;
}
}
window.parent.postMessage({
type: 'SPEECH_RESULT',
payload: { interimText, finalText }
}, '*');
};
recognition.onerror = (event) => {
console.error('[Speech Iframe] Recognition error:', event.error);
if (event.error !== 'no-speech') {
isRecording = false;
stopMediaTracks();
}
window.parent.postMessage({
type: 'SPEECH_ERROR',
payload: { error: event.error }
}, '*');
};
recognition.onend = () => {
if (isRecording) {
try {
recognition.start();
} catch (e) {
console.warn('[Speech Iframe] Restart failed:', e);
}
} else {
stopMediaTracks();
window.parent.postMessage({ type: 'SPEECH_END' }, '*');
}
};
return recognition;
}
// Listen for messages broadcasted across the extension
window.addEventListener('message', (event) => {
const message = event.data;
if (!message || !message.type) return;
if (message.type === 'START_RECORDING_FROM_POPUP') {
const lang = message.payload?.language || 'ar-SA';
navigator.mediaDevices.getUserMedia({ audio: true })
.then((stream) => {
localStream = stream;
try {
if (!recognition || recognition.lang !== lang) {
recognition = initRecognition(lang);
}
if (!isRecording) {
recognition.start();
isRecording = true;
// Tell popup it started successfully
window.parent.postMessage({ type: 'SPEECH_START_SUCCESS' }, '*');
}
} catch (e) {
console.error('[Speech Iframe] Failed to start:', e);
window.parent.postMessage({ type: 'SPEECH_ERROR', payload: { error: e.message } }, '*');
}
})
.catch((err) => {
console.error('[Speech Iframe] getUserMedia failed:', err);
window.parent.postMessage({ type: 'SPEECH_ERROR', payload: { error: 'not-allowed' } }, '*');
});
}
if (message.type === 'STOP_RECORDING_FROM_POPUP') {
if (isRecording && recognition) {
isRecording = false;
try {
recognition.stop();
} catch (e) {
console.warn('[Speech Iframe] Stop failed:', e);
}
}
stopMediaTracks();
}
});

2
test_gemini.js Normal file
View File

@@ -0,0 +1,2 @@
const fs = require('fs');
const settings = JSON.parse(fs.readFileSync('/Users/hamzaaleghwairyeen/.gemini/antigravity/brain/1fc80e71-ad9e-4306-9d23-0fcef50f8b3e/scratch/settings.json', 'utf8').catch ? '{}' : '{}'); // wait, I don't have his API key here...