diff --git a/content.js b/content.js index f5f7838..7b87cec 100644 --- a/content.js +++ b/content.js @@ -230,70 +230,63 @@ const questions = []; const seen = new Set(); - // Strategy 1: Look inside known modal containers - const containers = document.querySelectorAll( - '.artdeco-modal, .jobs-easy-apply-modal, #artdeco-modal-outlet, ' + - '.jobs-easy-apply-content, .jobs-easy-apply-form-section__grouping, ' + - '[data-test-modal], [role="dialog"]' + // 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] + const formElements = searchRoot.querySelectorAll( + '.fb-dash-form-element, [data-test-form-element]' ); - // Strategy 2: If no modal found, scan the full page - const searchRoots = containers.length > 0 ? containers : [document.body]; + formElements.forEach(el => { + const label = el.querySelector('label'); + if (!label) return; - searchRoots.forEach(container => { - // Scan labels, legends, spans that look like question text - const candidates = container.querySelectorAll( - 'label, legend, .fb-dash-form-element__label, ' + - '.jobs-easy-apply-form-element__label, ' + - 'span.t-14, span.t-bold' - ); + // Prefer the aria-hidden span text to avoid duplication from visually-hidden span + const ariaSpan = label.querySelector('span[aria-hidden="true"]'); + let text = (ariaSpan ? ariaSpan.textContent : label.textContent) + .replace(/\*/g, '') + .replace(/required/gi, '') + .replace(/\n/g, ' ') + .replace(/\s+/g, ' ') + .trim(); - candidates.forEach(el => { - let text = el.textContent.replace(/\*/g, '').replace(/required/gi, '').trim(); - // Remove "Select an option" and similar noise - text = text.replace(/Select an option/gi, '').replace(/Show less|Show more/gi, '').trim(); + if (!text || text.length < 5 || text.length > 300 || seen.has(text.toLowerCase())) return; + seen.add(text.toLowerCase()); - if (!text || text.length < 8 || text.length > 300 || seen.has(text.toLowerCase())) return; - - // Only accept text that looks like a question or form label - const isQuestion = ( - text.endsWith('?') || - /^(how many|what|do you|are you|have you|will you|can you|would you|is your|describe|tell us)/i.test(text) || - /salary|experience|education|degree|visa|relocat|notice period|start date|certif/i.test(text) - ); + // Detect input type from this form element + let inputType = 'text'; + if (el.querySelector('select')) inputType = 'select'; + else if (el.querySelector('input[type="radio"]')) inputType = 'radio'; + else if (el.querySelector('textarea')) inputType = 'textarea'; + else if (el.querySelector('input[type="checkbox"]')) inputType = 'checkbox'; - if (!isQuestion) return; - - seen.add(text.toLowerCase()); - - // Detect input type from nearest form element - let inputType = 'text'; - const parent = el.closest('.fb-dash-form-element, .jobs-easy-apply-form-section__grouping, div') || el.parentElement; - if (parent) { - if (parent.querySelector('select')) inputType = 'select'; - else if (parent.querySelector('input[type="radio"]')) inputType = 'radio'; - else if (parent.querySelector('textarea')) inputType = 'textarea'; - else if (parent.querySelector('input[type="checkbox"]')) inputType = 'checkbox'; - else if (parent.querySelector('input[type="number"]')) inputType = 'number'; - } - - questions.push({ question: text, type: inputType }); - }); + questions.push({ question: text, type: inputType }); }); - // Strategy 3: Last resort — scan ALL visible text for question patterns + // Fallback: if the precise approach found nothing, scan broader if (questions.length === 0) { - const allSpans = document.querySelectorAll('span, label, legend, p'); - allSpans.forEach(el => { - const text = el.textContent.replace(/\*/g, '').trim(); - if (text && text.endsWith('?') && text.length > 10 && text.length < 200 && !seen.has(text.toLowerCase())) { - // Make sure this is a visible form question, not random page text - const rect = el.getBoundingClientRect(); - if (rect.width > 0 && rect.height > 0 && rect.top > 0 && rect.top < window.innerHeight) { - seen.add(text.toLowerCase()); - questions.push({ question: text, type: 'text' }); - } - } + const allLabels = searchRoot.querySelectorAll('label'); + allLabels.forEach(label => { + const ariaSpan = label.querySelector('span[aria-hidden="true"]'); + let text = (ariaSpan ? ariaSpan.textContent : label.textContent) + .replace(/\*/g, '').replace(/\n/g, ' ').replace(/\s+/g, ' ').trim(); + + if (!text || text.length < 8 || text.length > 300 || seen.has(text.toLowerCase())) return; + + const looksLikeQuestion = ( + text.endsWith('?') || + /^(how many|what|do you|are you|have you|will you|describe|tell us)/i.test(text) || + /salary|experience|education|degree|visa|relocat|notice|certif/i.test(text) + ); + if (!looksLikeQuestion) return; + + seen.add(text.toLowerCase()); + questions.push({ question: text, type: 'text' }); }); } @@ -812,13 +805,23 @@ Brief honest assessment of this opportunity for my profile` const endIdx = text.lastIndexOf('}'); if (startIdx !== -1 && endIdx !== -1) { const parsed = JSON.parse(text.substring(startIdx, endIdx + 1)); - let html = '
'; - for (const [q, a] of Object.entries(parsed)) { - html += `
-
❓ ${q}
-
💡 ${a}
+ let html = '
'; + const entries = Object.entries(parsed); + entries.forEach(([q, a], idx) => { + const safeAnswer = String(a).replace(/"/g, '"').replace(/'/g, '''); + html += `
+
❓ ${q}
+
+
💡 ${a}
+ +
`; - } + }); html += '
'; return html; } @@ -868,11 +871,14 @@ Brief honest assessment of this opportunity for my profile` ); labels.forEach(label => { - const qText = label.textContent.trim().toLowerCase(); - if (!qText) return; + // Use same clean-text logic as the scraper + const ariaSpan = label.querySelector('span[aria-hidden="true"]'); + const qText = (ariaSpan ? ariaSpan.textContent : label.textContent) + .replace(/\*/g, '').replace(/required/gi, '').replace(/\n/g, ' ').replace(/\s+/g, ' ').trim().toLowerCase(); + if (!qText || qText.length < 5) return; const matchedKey = Object.keys(answers).find(k => { - const kLower = k.toLowerCase(); + const kLower = k.toLowerCase().trim(); return kLower.includes(qText) || qText.includes(kLower); }); diff --git a/prompts.js b/prompts.js index c78dafc..d1da3f7 100644 --- a/prompts.js +++ b/prompts.js @@ -113,26 +113,35 @@ Respond EXACTLY: - [2-3 new achievement bullets ready to paste into CV — in English]`; const dynamicQuestions = job.questions && job.questions.length > 0 - ? 'Answer THESE specific application questions:\n' + job.questions.map((q, i) => `${i + 1}. ${q.question}`).join('\n') - : 'Answer these common application questions:\n1. Why are you interested in this role?\n2. What is your relevant experience?\n3. What are your salary expectations?\n4. When can you start?\n5. Do you require visa sponsorship?'; + ? 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]'; - P.qa = `You are a career coach. -IMPORTANT: ALL answers MUST be in English. Be highly concise (1 sentence max for text inputs, Yes/No for radio inputs). + P.qa = `You are a form-filling assistant. Your ONLY job is to answer application form questions. -${prof} +STRICT RULES: +- Return ONLY a raw JSON object. Nothing else. +- Do NOT write cover letters, introductions, paragraphs, or markdown. +- Do NOT use headers like "###" or "##". +- Do NOT use code blocks like \`\`\`json. +- 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 salary questions: answer with JUST a number (e.g. "25000"). +- For text questions: answer in 1 short sentence max. +- Keys must be the EXACT question text. + +MY PROFILE: +${userProfile} JOB: ${ctx} +QUESTIONS TO ANSWER: ${dynamicQuestions} -RETURN EXACTLY A RAW JSON OBJECT where keys are the exact questions above and values are your concise answers. -Do NOT use markdown code blocks like \`\`\`json. Just return the raw JSON. -Example: +RESPOND WITH ONLY THIS FORMAT (raw JSON, no wrapping): { - "Have you completed the following level of education: Bachelor's Degree?": "Yes", - "How many years of work experience do you have with Infrastructure?": "6", - "Why are you interested in this role?": "My background in mapping perfectly aligns with your needs." + "exact question text here": "concise answer", + "another question": "answer" }`; P.benefits = `You are a career analyst specializing in tech compensation in MENA.