Auto-deploy: 2026-05-17 02:36:31
This commit is contained in:
134
content.js
134
content.js
@@ -230,70 +230,63 @@
|
|||||||
const questions = [];
|
const questions = [];
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
|
|
||||||
// Strategy 1: Look inside known modal containers
|
// Find the Easy Apply modal using real LinkedIn selectors
|
||||||
const containers = document.querySelectorAll(
|
const modal = document.querySelector(
|
||||||
'.artdeco-modal, .jobs-easy-apply-modal, #artdeco-modal-outlet, ' +
|
'[role="dialog"].jobs-easy-apply-modal, .artdeco-modal.jobs-easy-apply-modal, ' +
|
||||||
'.jobs-easy-apply-content, .jobs-easy-apply-form-section__grouping, ' +
|
'.artdeco-modal, [role="dialog"], #artdeco-modal-outlet'
|
||||||
'[data-test-modal], [role="dialog"]'
|
);
|
||||||
|
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
|
formElements.forEach(el => {
|
||||||
const searchRoots = containers.length > 0 ? containers : [document.body];
|
const label = el.querySelector('label');
|
||||||
|
if (!label) return;
|
||||||
|
|
||||||
searchRoots.forEach(container => {
|
// Prefer the aria-hidden span text to avoid duplication from visually-hidden span
|
||||||
// Scan labels, legends, spans that look like question text
|
const ariaSpan = label.querySelector('span[aria-hidden="true"]');
|
||||||
const candidates = container.querySelectorAll(
|
let text = (ariaSpan ? ariaSpan.textContent : label.textContent)
|
||||||
'label, legend, .fb-dash-form-element__label, ' +
|
.replace(/\*/g, '')
|
||||||
'.jobs-easy-apply-form-element__label, ' +
|
.replace(/required/gi, '')
|
||||||
'span.t-14, span.t-bold'
|
.replace(/\n/g, ' ')
|
||||||
);
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
|
||||||
candidates.forEach(el => {
|
if (!text || text.length < 5 || text.length > 300 || seen.has(text.toLowerCase())) return;
|
||||||
let text = el.textContent.replace(/\*/g, '').replace(/required/gi, '').trim();
|
seen.add(text.toLowerCase());
|
||||||
// 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 < 8 || text.length > 300 || seen.has(text.toLowerCase())) return;
|
// Detect input type from this form element
|
||||||
|
let inputType = 'text';
|
||||||
// Only accept text that looks like a question or form label
|
if (el.querySelector('select')) inputType = 'select';
|
||||||
const isQuestion = (
|
else if (el.querySelector('input[type="radio"]')) inputType = 'radio';
|
||||||
text.endsWith('?') ||
|
else if (el.querySelector('textarea')) inputType = 'textarea';
|
||||||
/^(how many|what|do you|are you|have you|will you|can you|would you|is your|describe|tell us)/i.test(text) ||
|
else if (el.querySelector('input[type="checkbox"]')) inputType = 'checkbox';
|
||||||
/salary|experience|education|degree|visa|relocat|notice period|start date|certif/i.test(text)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isQuestion) return;
|
questions.push({ question: text, type: inputType });
|
||||||
|
|
||||||
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 });
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Strategy 3: Last resort — scan ALL visible text for question patterns
|
// Fallback: if the precise approach found nothing, scan broader
|
||||||
if (questions.length === 0) {
|
if (questions.length === 0) {
|
||||||
const allSpans = document.querySelectorAll('span, label, legend, p');
|
const allLabels = searchRoot.querySelectorAll('label');
|
||||||
allSpans.forEach(el => {
|
allLabels.forEach(label => {
|
||||||
const text = el.textContent.replace(/\*/g, '').trim();
|
const ariaSpan = label.querySelector('span[aria-hidden="true"]');
|
||||||
if (text && text.endsWith('?') && text.length > 10 && text.length < 200 && !seen.has(text.toLowerCase())) {
|
let text = (ariaSpan ? ariaSpan.textContent : label.textContent)
|
||||||
// Make sure this is a visible form question, not random page text
|
.replace(/\*/g, '').replace(/\n/g, ' ').replace(/\s+/g, ' ').trim();
|
||||||
const rect = el.getBoundingClientRect();
|
|
||||||
if (rect.width > 0 && rect.height > 0 && rect.top > 0 && rect.top < window.innerHeight) {
|
if (!text || text.length < 8 || text.length > 300 || seen.has(text.toLowerCase())) return;
|
||||||
seen.add(text.toLowerCase());
|
|
||||||
questions.push({ question: text, type: 'text' });
|
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('}');
|
const endIdx = text.lastIndexOf('}');
|
||||||
if (startIdx !== -1 && endIdx !== -1) {
|
if (startIdx !== -1 && endIdx !== -1) {
|
||||||
const parsed = JSON.parse(text.substring(startIdx, endIdx + 1));
|
const parsed = JSON.parse(text.substring(startIdx, endIdx + 1));
|
||||||
let html = '<div style="display:flex;flex-direction:column;gap:12px;">';
|
let html = '<div style="display:flex;flex-direction:column;gap:10px;">';
|
||||||
for (const [q, a] of Object.entries(parsed)) {
|
const entries = Object.entries(parsed);
|
||||||
html += `<div style="background: rgba(255,255,255,0.05); padding: 10px; border-radius: 6px;">
|
entries.forEach(([q, a], idx) => {
|
||||||
<div style="font-weight: 600; font-size: 13px; color: #fff; margin-bottom: 4px;">❓ ${q}</div>
|
const safeAnswer = String(a).replace(/"/g, '"').replace(/'/g, ''');
|
||||||
<div style="color: #4caf50; font-size: 13px;">💡 ${a}</div>
|
html += `<div style="background: rgba(255,255,255,0.06); padding: 10px 12px; border-radius: 8px; border-left: 3px solid #6c63ff;">
|
||||||
|
<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="color: #4caf50; font-size: 14px; font-weight: 500; flex: 1;" id="lja-qa-answer-${idx}">💡 ${a}</div>
|
||||||
|
<button onclick="
|
||||||
|
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>`;
|
||||||
}
|
});
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
@@ -868,11 +871,14 @@ Brief honest assessment of this opportunity for my profile`
|
|||||||
);
|
);
|
||||||
|
|
||||||
labels.forEach(label => {
|
labels.forEach(label => {
|
||||||
const qText = label.textContent.trim().toLowerCase();
|
// Use same clean-text logic as the scraper
|
||||||
if (!qText) return;
|
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 matchedKey = Object.keys(answers).find(k => {
|
||||||
const kLower = k.toLowerCase();
|
const kLower = k.toLowerCase().trim();
|
||||||
return kLower.includes(qText) || qText.includes(kLower);
|
return kLower.includes(qText) || qText.includes(kLower);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
31
prompts.js
31
prompts.js
@@ -113,26 +113,35 @@ Respond EXACTLY:
|
|||||||
- [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
|
||||||
? 'Answer THESE specific application questions:\n' + job.questions.map((q, i) => `${i + 1}. ${q.question}`).join('\n')
|
? job.questions.map((q, i) => `${i + 1}. "${q.question}" [type: ${q.type}]`).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?';
|
: '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.
|
P.qa = `You are a form-filling assistant. Your ONLY job is to answer application form questions.
|
||||||
IMPORTANT: ALL answers MUST be in English. Be highly concise (1 sentence max for text inputs, Yes/No for radio inputs).
|
|
||||||
|
|
||||||
${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:
|
JOB:
|
||||||
${ctx}
|
${ctx}
|
||||||
|
|
||||||
|
QUESTIONS TO ANSWER:
|
||||||
${dynamicQuestions}
|
${dynamicQuestions}
|
||||||
|
|
||||||
RETURN EXACTLY A RAW JSON OBJECT where keys are the exact questions above and values are your concise answers.
|
RESPOND WITH ONLY THIS FORMAT (raw JSON, no wrapping):
|
||||||
Do NOT use markdown code blocks like \`\`\`json. Just return the raw JSON.
|
|
||||||
Example:
|
|
||||||
{
|
{
|
||||||
"Have you completed the following level of education: Bachelor's Degree?": "Yes",
|
"exact question text here": "concise answer",
|
||||||
"How many years of work experience do you have with Infrastructure?": "6",
|
"another question": "answer"
|
||||||
"Why are you interested in this role?": "My background in mapping perfectly aligns with your needs."
|
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
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.
|
||||||
|
|||||||
Reference in New Issue
Block a user