516 lines
20 KiB
JavaScript
516 lines
20 KiB
JavaScript
// popup.js — Settings popup logic
|
|
|
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
|
|
function showToast(msg, type = '') {
|
|
const toast = document.getElementById('toast');
|
|
toast.textContent = msg;
|
|
toast.className = 'toast show ' + type;
|
|
setTimeout(() => { toast.className = 'toast'; }, 2500);
|
|
}
|
|
|
|
function setKeyStatus(state) {
|
|
const badge = document.getElementById('key-status');
|
|
badge.style.display = 'inline-flex';
|
|
if (state === 'ok') {
|
|
badge.className = 'status-badge success';
|
|
badge.innerHTML = '<span class="status-dot"></span> Valid';
|
|
} else if (state === 'error') {
|
|
badge.className = 'status-badge error';
|
|
badge.innerHTML = '<span class="status-dot"></span> Invalid';
|
|
} else if (state === 'checking') {
|
|
badge.className = 'status-badge checking';
|
|
badge.innerHTML = '<span class="status-dot"></span> Testing...';
|
|
}
|
|
}
|
|
|
|
function updateUsageUI(count) {
|
|
const max = 1000;
|
|
const pct = Math.min((count / max) * 100, 100).toFixed(1);
|
|
document.getElementById('usage-count').textContent = `${count} / 1,000 requests`;
|
|
document.getElementById('usage-fill').style.width = pct + '%';
|
|
}
|
|
|
|
// ─── Load saved settings ────────────────────────────────────────────────────
|
|
|
|
function loadSettings() {
|
|
chrome.storage.sync.get(['apiKey', 'userProfile', 'language'], (data) => {
|
|
if (data.apiKey) {
|
|
document.getElementById('api-key-input').value = data.apiKey;
|
|
setKeyStatus('ok');
|
|
}
|
|
let 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 =
|
|
data.language || 'auto';
|
|
});
|
|
|
|
// Usage counter from local storage
|
|
const today = new Date().toDateString();
|
|
chrome.storage.local.get(['usageDate', 'usageCount'], (data) => {
|
|
if (data.usageDate === today) {
|
|
updateUsageUI(data.usageCount || 0);
|
|
} else {
|
|
updateUsageUI(0);
|
|
}
|
|
});
|
|
}
|
|
|
|
// ─── Save settings ──────────────────────────────────────────────────────────
|
|
|
|
document.getElementById('save-btn').addEventListener('click', () => {
|
|
const apiKey = document.getElementById('api-key-input').value.trim();
|
|
const userProfile = document.getElementById('profile-textarea').value.trim();
|
|
const language = document.getElementById('lang-select').value;
|
|
|
|
if (!apiKey) {
|
|
showToast('⚠️ Please enter your API Key', 'error');
|
|
return;
|
|
}
|
|
if (!userProfile) {
|
|
showToast('⚠️ Profile cannot be empty', 'error');
|
|
return;
|
|
}
|
|
|
|
chrome.storage.sync.set({ apiKey, userProfile, language }, () => {
|
|
showToast('✅ Settings saved!', 'success');
|
|
});
|
|
});
|
|
|
|
// ─── Toggle API key visibility ───────────────────────────────────────────────
|
|
|
|
document.getElementById('toggle-key').addEventListener('click', () => {
|
|
const input = document.getElementById('api-key-input');
|
|
const btn = document.getElementById('toggle-key');
|
|
if (input.type === 'password') {
|
|
input.type = 'text';
|
|
btn.textContent = '🙈';
|
|
} else {
|
|
input.type = 'password';
|
|
btn.textContent = '👁';
|
|
}
|
|
});
|
|
|
|
// ─── Test API Key ───────────────────────────────────────────────────────────
|
|
|
|
document.getElementById('test-key-btn').addEventListener('click', async () => {
|
|
const apiKey = document.getElementById('api-key-input').value.trim();
|
|
if (!apiKey) {
|
|
showToast('⚠️ Enter your API key first', 'error');
|
|
return;
|
|
}
|
|
|
|
setKeyStatus('checking');
|
|
|
|
try {
|
|
const response = await fetch(
|
|
`https://generativelanguage.googleapis.com/v1beta/models/gemini-flash-lite-latest:generateContent?key=${apiKey}`,
|
|
{
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
contents: [{ parts: [{ text: 'Say "OK" in one word.' }] }],
|
|
generationConfig: { maxOutputTokens: 5 }
|
|
})
|
|
}
|
|
);
|
|
|
|
if (response.ok) {
|
|
setKeyStatus('ok');
|
|
showToast('✅ API Key is valid!', 'success');
|
|
} else {
|
|
const err = await response.json();
|
|
setKeyStatus('error');
|
|
showToast('❌ Invalid key: ' + (err.error?.message || response.status), 'error');
|
|
}
|
|
} catch (e) {
|
|
setKeyStatus('error');
|
|
showToast('❌ Network error: ' + e.message, 'error');
|
|
}
|
|
});
|
|
|
|
// ─── Reset profile ──────────────────────────────────────────────────────────
|
|
|
|
document.getElementById('reset-profile-btn').addEventListener('click', () => {
|
|
if (confirm('Reset profile to default?')) {
|
|
document.getElementById('profile-textarea').value = DEFAULT_PROFILE;
|
|
showToast('🔄 Profile reset to default');
|
|
}
|
|
});
|
|
|
|
// ─── Clear cache ─────────────────────────────────────────────────────────────
|
|
|
|
document.getElementById('clear-cache-btn').addEventListener('click', () => {
|
|
chrome.storage.local.remove(['analysisCache'], () => {
|
|
showToast('🗑️ Analysis cache cleared');
|
|
});
|
|
});
|
|
|
|
// ─── Clear all data ──────────────────────────────────────────────────────────
|
|
|
|
document.getElementById('clear-all-btn').addEventListener('click', () => {
|
|
if (confirm('Clear ALL data including API key and profile?')) {
|
|
chrome.storage.sync.clear(() => {
|
|
chrome.storage.local.clear(() => {
|
|
document.getElementById('api-key-input').value = '';
|
|
document.getElementById('profile-textarea').value = DEFAULT_PROFILE;
|
|
document.getElementById('lang-select').value = 'auto';
|
|
document.getElementById('key-status').style.display = 'none';
|
|
updateUsageUI(0);
|
|
showToast('🗑️ All data cleared');
|
|
});
|
|
});
|
|
}
|
|
});
|
|
|
|
// ─── 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 ────────────────────────────────────────────────────────────────────
|
|
|
|
loadSettings();
|
|
initDictation();
|
|
|