From 4993a68a56806dff69fe171b4b60cad02eee0cab Mon Sep 17 00:00:00 2001 From: Jorge Brunetto Date: Wed, 28 Jan 2026 21:20:47 -0300 Subject: [PATCH 1/2] feat: ia and translate --- CHANGELOG.md | 43 +- content-simple.js | 768 ++++++++++++++++++++++++---- docs/README.md | 10 +- i18n/en.js | 264 ++++++++++ i18n/es.js | 264 ++++++++++ i18n/fr.js | 264 ++++++++++ i18n/i18n.js | 205 ++++++++ i18n/patterns.js | 202 ++++++++ i18n/pt-br.js | 264 ++++++++++ manifest.json | 2 +- popup.css | 151 ++++-- popup.html | 1242 ++++++++++++++++++++++++++++++++------------- popup.js | 157 +++++- 13 files changed, 3311 insertions(+), 525 deletions(-) create mode 100644 i18n/en.js create mode 100644 i18n/es.js create mode 100644 i18n/fr.js create mode 100644 i18n/i18n.js create mode 100644 i18n/patterns.js create mode 100644 i18n/pt-br.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fdfef0..27a1401 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,18 +2,47 @@ All notable changes to EasyApplyMax will be documented in this file. +## [v1.6.0] - 2026-01-26 + +### ✨ New Features + +- **AI-Powered Answers (Groq Integration)** - Uses Groq AI to intelligently answer unknown/custom application questions +- **Smart Answer Caching** - All answers are saved to storage and reused for similar questions (no repeated API calls) +- **Similarity Matching** - Finds cached answers for similar questions using Jaccard similarity algorithm +- **Professional Profile** - Configure your professional summary and skills for better AI responses +- **Textarea Support** - Now handles long-form text fields like cover letter snippets +- **Learning System** - Saves all submitted answers to improve future applications + +### 🔧 Settings Additions + +- Groq API Key field (free at console.groq.com) +- Enable/Disable AI toggle +- Professional Summary textarea +- Skills input field +- Cached answers counter with clear button + +### 📝 How AI Works + +1. When a question is detected, first checks the cache for exact or similar matches +2. If no cache hit (>70% similarity), calls Groq API with your profile context +3. Answer is saved to cache for future use +4. All answers from successful applications are automatically cached + ## [v1.5.0] - 2026-01-15 ### ✨ New Features + - **LinkedIn Collections Support** - Now works on `/jobs/collections/` pages (saved jobs, recommended jobs, etc.) with infinite scroll - **Smart Resume Selection** - Now selects existing/previously uploaded CV instead of re-uploading for each application. Only uploads once (first application), then reuses the CV. ### 🐛 Bug Fixes + - Removed redundant "Welcome aboard!" toast message after onboarding - Fixed fadeOut animation for onboarding overlay closure - Fixed resume being uploaded for every single application (now uploads once, then selects existing) ### 🔧 Technical Improvements + - Collections support uses conditional selectors (only on collections pages, doesn't affect search pages) - Infinite scroll pagination for collections pages - Standard pagination preserved for search pages @@ -23,6 +52,7 @@ All notable changes to EasyApplyMax will be documented in this file. ### 🎉 Initial Release #### ✨ Features + - **Automated LinkedIn Easy Apply** - One-click job applications - **Smart Form Filling** - Automatically fills application forms - **Human-like Behavior** - Random delays and natural interactions @@ -34,6 +64,7 @@ All notable changes to EasyApplyMax will be documented in this file. - **Applied Jobs Tab** - View application history in extension popup #### 🎨 UI/UX + - Clean, modern interface with LinkedIn color scheme - Discord community card with direct link - Three tabs: Personal Info, Settings, Applied Jobs @@ -46,6 +77,7 @@ All notable changes to EasyApplyMax will be documented in this file. - Empty state for no applications #### 🤖 Automation + - Multi-selector element detection (XPath + CSS) - Retry mechanism (up to 3 attempts) - Automatic stuck detection (2-minute timeout) @@ -53,18 +85,21 @@ All notable changes to EasyApplyMax will be documented in this file. - Error recovery and continuation #### 🔒 Security & Privacy + - 100% local data storage - No external servers - LinkedIn-only permissions - Open source and transparent #### 💬 Community -- Discord integration (https://discord.gg/xWaCXBZbws) + +- Discord integration () - Twitter presence (@Azo92i) - Community-driven development - Feature voting and feedback #### 📚 Documentation + - Professional README with roadmap - Installation guide - Usage guide @@ -75,6 +110,7 @@ All notable changes to EasyApplyMax will be documented in this file. - Google Sheets integration guide (future) ### 🐛 Bug Fixes + - Fixed daily limit detection with multiple message patterns - Improved button state synchronization - Better error handling for failed applications @@ -82,21 +118,25 @@ All notable changes to EasyApplyMax will be documented in this file. ### 🚀 Coming Soon (Roadmap) #### v1.1.0 - AI Integration + - AI-powered job matching - Google Sheets auto-export - Success rate tracking #### v1.2.0 - CV & Cover Letters + - Dynamic CV adaptation for each job - AI-generated cover letters - Multi-format resume support #### v1.3.0 - Multi-Platform + - Indeed support - Glassdoor support - Company career page auto-apply #### v1.4.0 - Advanced Analytics + - Application success rates - Response time tracking - Industry insights @@ -113,6 +153,7 @@ All notable changes to EasyApplyMax will be documented in this file. ## Upgrade Guide ### From Nothing to v1.0.0 + Fresh install - follow [Installation Guide](README.md#installation) --- diff --git a/content-simple.js b/content-simple.js index 2bc8ce8..9ca2c24 100644 --- a/content-simple.js +++ b/content-simple.js @@ -16,6 +16,120 @@ let resumeFile = null; // Base64 data let resumeFileName = null; let resumeFileType = null; +// AI Integration variables +let groqApiKey = null; +let enableAI = false; +let professionalSummary = ''; +let skills = ''; +let aiAnswersCache = {}; // Cache for AI-generated answers + +// Current language for AI responses +let currentLanguage = 'en'; + +// ============================================ +// MULTILINGUAL PATTERNS - Button/Field Detection +// Supports: EN, PT-BR, ES, FR, DE, IT +// ============================================ +const PATTERNS = { + // Easy Apply button patterns (for aria-label detection) + easyApply: [ + 'Easy Apply', 'Candidatura simplificada', 'Candidatura Simplificada', + 'Solicitud sencilla', 'Solicitud Sencilla', 'Candidature simplifiée', + 'Candidature Simplifiée', 'Einfach bewerben', 'Candidatura semplice' + ], + + // Done/Submit application button text + doneButtons: [ + 'Done', 'Submit application', 'Submit', 'Dismiss', 'Close', 'Continue', + 'Concluído', 'Enviar candidatura', 'Enviar', 'Fechar', 'Dispensar', 'Continuar', + 'Listo', 'Enviar solicitud', 'Cerrar', 'Descartar', + 'Terminé', 'Soumettre la candidature', 'Soumettre', 'Fermer', + 'Fertig', 'Bewerbung absenden', 'Absenden', 'Schließen', + 'Fatto', 'Invia candidatura', 'Invia', 'Chiudi' + ], + + // Discard/Cancel button text + discardButtons: [ + 'discard', 'cancel', 'close', 'dismiss', + 'descartar', 'cancelar', 'fechar', 'dispensar', + 'cerrar', 'anular', 'annuler', 'abandonner', 'fermer', + 'verwerfen', 'abbrechen', 'schließen', 'scartare', 'annullare', 'chiudere' + ], + + // Next/Review/Continue button text + nextButtons: [ + 'next', 'review', 'continue', + 'próximo', 'avançar', 'continuar', 'revisar', + 'siguiente', 'suivant', 'réviser', 'continuer', + 'weiter', 'überprüfen', 'fortsetzen', 'avanti', 'rivedi', 'continua' + ], + + // Submit button text + submitButtons: [ + 'submit', 'send', 'apply', + 'enviar', 'candidatar', 'aplicar', 'postular', + 'soumettre', 'envoyer', 'postuler', 'absenden', 'bewerben', 'invia', 'candidati' + ], + + // Daily limit messages + dailyLimit: [ + "You've reached today's Easy Apply limit", "reached today's Easy Apply limit", + "Great effort applying today", "we limit daily submissions", "continue applying tomorrow", + "Save this job and continue applying tomorrow", "exceeded the daily application limit", + "daily Easy Apply limit", "limit daily submissions", + "Você atingiu o limite de Candidatura Simplificada de hoje", "limite diário de candidaturas", + "continue se candidatando amanhã", "Bom trabalho se candidatando hoje", + "Has alcanzado el límite de Solicitud Sencilla de hoy", "límite diario de solicitudes", + "continúa aplicando mañana", "Gran esfuerzo aplicando hoy", + "Vous avez atteint la limite de Candidature Simplifiée", "limite quotidienne de candidatures", + "Tägliches Bewerbungslimit erreicht" + ], + + // Field detection patterns (regex) + fields: { + yearsExperience: /experience|years|expérience|années|experiência|anos|años|jahre|anni|esperienza/i, + salary: /salary|compensation|remuneration|salaire|rémunération|salário|sueldo|salario|gehalt|stipendio/i, + email: /email|e-mail|courriel|correo/i, + firstName: /first|prénom|prenom|nombre|vorname|nome/i, + lastName: /last|nom|apellido|nachname|cognome/i, + phone: /phone|téléphone|telefono|telefon|mobile|portable|cell|móvil|cellulare|celular/i, + city: /city|ville|ciudad|stadt|città|location|localisation|ubicación|standort/i, + currentCompany: /current.*company|current.*employer|empresa.*atual|employeur.*actuel|empleador.*actual|most recent.*company|última.*empresa|recent.*employer/i, + linkedinUrl: /linkedin.*url|linkedin.*profile|perfil.*linkedin|profil.*linkedin|url.*linkedin/i, + portfolioUrl: /portfolio.*url|portfolio|website|personal.*site|site.*personnel|sitio.*personal|site.*pessoal|github|behance/i + }, + + // Yes/No patterns for radio buttons + yesPatterns: /^(yes|oui|sí|si|ja|y|sim)$/i, + noPatterns: /^(no|non|nein|n|não)$/i, + + // Language proficiency patterns + proficiency: { + native: /native|bilingual|bilingue|bilíngue|langue maternelle|lengua materna/i, + fluent: /fluent|courant|fluide|fluente|fluido/i, + professional: /professional|professionnel|profissional|profesional|advanced|avançado/i, + intermediate: /intermediate|intermediário|intermedio|moyen/i, + basic: /basic|básico|basique|elementary|elementar/i + }, + + // English level detection pattern + englishLevelQuestion: /english.*level|level.*english|english.*proficiency|proficiency.*english|inglês|inglés|anglais/i, + + // Ethnicity/Race detection pattern + ethnicityQuestion: /race|ethnicity|ethnic|racial|cor|raça|raza|etnia|origem étnica/i, + + // Ethnicity option patterns (to match dropdown options) + ethnicityOptions: { + white: /white|branco|blanco|caucasian|caucasiano/i, + black: /black|negro|preto|african|africano/i, + hispanic: /hispanic|latino|latina|hispânico|hispano/i, + asian: /asian|asiático|asiatic|amarelo/i, + indigenous: /indigenous|indígena|native american|índio|aboriginal/i, + mixed: /mixed|multiracial|pardo|mestizo|two or more|duas ou mais/i, + preferNotToSay: /prefer not|prefiro não|prefiero no|decline|não informar|no responder/i + } +}; + // Logs simples function log(msg) { console.log('[LinkedIn Bot]', msg); @@ -59,20 +173,8 @@ function isStuck() { // Check for LinkedIn's daily Easy Apply limit function checkDailyLimit() { try { - // List of limit message patterns (case-insensitive) - const limitPatterns = [ - "You've reached today's Easy Apply limit", - "You've reached today's easy apply limit", - "reached today's Easy Apply limit", - "Great effort applying today", - "we limit daily submissions", - "continue applying tomorrow", - "Save this job and continue applying tomorrow", - "exceeded the daily application limit", - "reached today\\'s easy apply limit", - "daily Easy Apply limit", - "limit daily submissions" - ]; + // Use multilingual patterns from PATTERNS constant + const limitPatterns = PATTERNS.dailyLimit; // Search in entire page text const bodyText = document.body.innerText || ''; @@ -126,7 +228,8 @@ function checkDailyLimit() { async function findAndClickDoneButton(contextElement = document, contextName = 'page', maxAttempts = 15) { log(`🔍 [${contextName}] Starting exhaustive search for Done button...`); - const doneTexts = ['Done', 'Terminé', 'Submit application', 'Soumettre la candidature', 'Dismiss', 'Close', 'Fermer']; + // Use multilingual patterns + const doneTexts = PATTERNS.doneButtons; let doneBtn = null; for (let attempt = 0; attempt < maxAttempts && !doneBtn; attempt++) { @@ -298,7 +401,8 @@ async function refreshAndReturnToSearch() { async function discardApplication() { log('🚀 DISCARD: Starting SAFE discard sequence...'); - const discardTexts = ['discard', 'annuler', 'cancel', 'abandonner', 'descarter']; + // Use multilingual patterns + const discardTexts = PATTERNS.discardButtons; try { // 🆕 DETECTION CRITIQUE: Vérifier si popup de chargement est bloqué (Python ligne 1547-1558) @@ -428,6 +532,289 @@ function fill(input, value) { input.dispatchEvent(new Event('change', { bubbles: true })); } +// ============================================ +// AI INTEGRATION - Groq API + Answer Caching +// ============================================ + +// Normalize question text for comparison (remove extra spaces, lowercase, etc.) +function normalizeQuestion(question) { + return question + .toLowerCase() + .replace(/[^\w\s]/g, '') // Remove punctuation + .replace(/\s+/g, ' ') // Normalize spaces + .trim(); +} + +// Calculate similarity between two strings (Jaccard similarity on words) +function calculateSimilarity(str1, str2) { + const words1 = new Set(normalizeQuestion(str1).split(' ').filter(w => w.length > 2)); + const words2 = new Set(normalizeQuestion(str2).split(' ').filter(w => w.length > 2)); + + if (words1.size === 0 || words2.size === 0) return 0; + + const intersection = new Set([...words1].filter(x => words2.has(x))); + const union = new Set([...words1, ...words2]); + + return intersection.size / union.size; +} + +// Find cached answer with similarity threshold +function findCachedAnswer(question, threshold = 0.7) { + const normalizedQuestion = normalizeQuestion(question); + + // First, try exact match + if (aiAnswersCache[normalizedQuestion]) { + log(`🎯 AI Cache HIT (exact): "${question.substring(0, 40)}..."`); + return aiAnswersCache[normalizedQuestion]; + } + + // Then, try similarity matching + let bestMatch = null; + let bestScore = 0; + + for (const [cachedQuestion, answer] of Object.entries(aiAnswersCache)) { + const similarity = calculateSimilarity(question, cachedQuestion); + if (similarity > bestScore && similarity >= threshold) { + bestScore = similarity; + bestMatch = { question: cachedQuestion, answer, score: similarity }; + } + } + + if (bestMatch) { + log(`🎯 AI Cache HIT (${Math.round(bestMatch.score * 100)}% similar): "${question.substring(0, 40)}..."`); + return bestMatch.answer; + } + + return null; +} + +// Save answer to cache +async function saveAnswerToCache(question, answer) { + const normalizedQuestion = normalizeQuestion(question); + aiAnswersCache[normalizedQuestion] = answer; + + // Also save to Chrome storage for persistence + try { + await chrome.storage.local.set({ aiAnswersCache }); + log(`💾 Cached answer for: "${question.substring(0, 40)}..."`); + } catch (e) { + log(`⚠️ Failed to save answer to cache: ${e.message}`); + } +} + +// Get language name for AI prompts +function getLanguageName(langCode) { + const languages = { + 'en': 'English', + 'pt-br': 'Portuguese (Brazilian)', + 'es': 'Spanish', + 'fr': 'French' + }; + return languages[langCode] || 'English'; +} + +// Call Groq API to generate answer +async function callGroqAPI(question, context = {}) { + if (!groqApiKey) { + log('⚠️ Groq API key not configured'); + return null; + } + + const responseLanguage = getLanguageName(currentLanguage); + + const systemPrompt = `You are an AI assistant helping to fill out job application forms. +You must provide SHORT, CONCISE answers suitable for form fields (usually 1-3 sentences max). +Do NOT use markdown formatting. Just plain text. + +CANDIDATE PROFILE: +- Name: ${config.firstName || ''} ${config.lastName || ''} +- Email: ${config.email || ''} +- Location: ${config.city || ''} +- Years of Experience: ${config.yearsOfExperience || ''} +- Skills: ${skills || 'Not specified'} +- Professional Summary: ${professionalSummary || 'Not provided'} + +PREVIOUS ANSWERS (use for consistency): +${Object.entries(aiAnswersCache).slice(-10).map(([q, a]) => `Q: ${q}\nA: ${a}`).join('\n\n')} + +RULES: +1. Be concise - form fields have character limits +2. Be professional and confident +3. Match the tone of the question +4. If asking about years of experience with specific tech, give a realistic number based on total experience +5. If you don't have enough info, make reasonable assumptions based on the profile +6. NEVER say "I don't know" - always provide a helpful answer +7. Always reply in ${responseLanguage}`; + + const userPrompt = `Application Question: "${question}" +${context.fieldType ? `Field Type: ${context.fieldType}` : ''} +${context.maxLength ? `Max Length: ${context.maxLength} characters` : ''} + +Provide a direct answer suitable for this form field:`; + + try { + log(`🤖 Calling Groq API for: "${question.substring(0, 50)}..."`); + + const response = await fetch('https://api.groq.com/openai/v1/chat/completions', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${groqApiKey}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + model: 'llama-3.3-70b-versatile', + messages: [ + { role: 'system', content: systemPrompt }, + { role: 'user', content: userPrompt } + ], + temperature: 0.3, + max_tokens: 200 + }) + }); + + if (!response.ok) { + const error = await response.text(); + log(`❌ Groq API error: ${response.status} - ${error}`); + return null; + } + + const data = await response.json(); + const answer = data.choices?.[0]?.message?.content?.trim(); + + if (answer) { + log(`✅ Groq response: "${answer.substring(0, 50)}..."`); + // Save to cache for future use + await saveAnswerToCache(question, answer); + return answer; + } + + return null; + } catch (error) { + log(`❌ Groq API call failed: ${error.message}`); + return null; + } +} + +// Main function to get AI answer (cache first, then API) +async function getAIAnswer(question, context = {}) { + if (!enableAI) { + return null; + } + + // Try cache first + const cachedAnswer = findCachedAnswer(question); + if (cachedAnswer) { + return cachedAnswer; + } + + // If not in cache, call API + return await callGroqAPI(question, context); +} + +// Save any filled answer to cache (for learning from user's manual inputs) +async function learnFromFilledField(question, answer) { + if (!question || !answer || answer.length < 2) return; + + // Don't cache common fields like name, email, phone + const skipPatterns = /^(email|phone|name|first.*name|last.*name|city|location|salary|experience|years)/i; + if (skipPatterns.test(normalizeQuestion(question))) return; + + await saveAnswerToCache(question, answer); +} + +// Save all answers from a modal to cache (called after successful submission) +async function saveAllAnswersFromModal(modal) { + if (!modal) return; + + log('💾 Saving all answers from application...'); + let savedCount = 0; + + // Common fields to skip (already handled by config) + const skipPatterns = /^(email|e-mail|phone|téléphone|name|nom|prénom|first|last|city|ville|location|salary|salaire|experience|années|years)/i; + + // 1. Save text input answers + const textInputs = modal.querySelectorAll('input[type="text"], input[type="number"], textarea'); + for (let input of textInputs) { + if (!input.value || input.value.length < 3) continue; + + let labelText = getFieldLabel(input, modal); + if (!labelText || labelText.length < 5) continue; + if (skipPatterns.test(labelText)) continue; + + await saveAnswerToCache(labelText, input.value); + savedCount++; + } + + // 2. Save radio button answers + const radioFieldsets = modal.querySelectorAll('fieldset[data-test-form-builder-radio-button-form-component]'); + for (let fieldset of radioFieldsets) { + const questionLabel = fieldset.querySelector('legend, span[class*="title"]'); + const questionText = questionLabel ? questionLabel.textContent.trim() : ''; + if (!questionText || questionText.length < 5) continue; + if (skipPatterns.test(questionText)) continue; + + const checkedRadio = fieldset.querySelector('input[type="radio"]:checked'); + if (checkedRadio) { + const radioLabel = fieldset.querySelector(`label[for="${checkedRadio.id}"]`); + const answer = radioLabel ? radioLabel.textContent.trim() : checkedRadio.value; + await saveAnswerToCache(questionText, answer); + savedCount++; + } + } + + // 3. Save select dropdown answers + const selects = modal.querySelectorAll('select'); + for (let select of selects) { + if (select.selectedIndex <= 0) continue; + + let labelText = getFieldLabel(select, modal); + if (!labelText || labelText.length < 5) continue; + if (skipPatterns.test(labelText)) continue; + + const selectedOption = select.options[select.selectedIndex]; + if (selectedOption && selectedOption.text) { + await saveAnswerToCache(labelText, selectedOption.text); + savedCount++; + } + } + + log(`💾 Saved ${savedCount} answers to cache (total: ${Object.keys(aiAnswersCache).length})`); +} + +// Helper function to get label text for a field +function getFieldLabel(field, modal) { + let labelText = ''; + + // aria-label + labelText += ' ' + (field.getAttribute('aria-label') || ''); + + // name attribute + labelText += ' ' + (field.getAttribute('name') || ''); + + // placeholder + labelText += ' ' + (field.getAttribute('placeholder') || ''); + + // Associated