302 lines
11 KiB
JavaScript
302 lines
11 KiB
JavaScript
// Content Script for Smart Survey Filler
|
|
// Injects the Floating Bubble UI and handles page analysis and auto-filling
|
|
|
|
(function() {
|
|
// Prevent multiple injections
|
|
if (window.ssfInjected) return;
|
|
window.ssfInjected = true;
|
|
|
|
console.log('Smart Survey Filler content script active');
|
|
|
|
// --- UI CONSTRUCTION ---
|
|
const bubble = document.createElement('div');
|
|
bubble.id = 'ssf-floating-bubble';
|
|
bubble.title = "Smart Survey Filler";
|
|
bubble.innerHTML = `
|
|
<div class="ssf-bubble-icon">🤖</div>
|
|
<div class="ssf-menu" style="display: none;">
|
|
<div class="ssf-menu-header">
|
|
Smart Filler AI
|
|
<span class="ssf-close-btn">×</span>
|
|
</div>
|
|
<div style="margin-bottom: 5px; font-size: 12px; color: #555;">Select Persona:</div>
|
|
<select id="ssf-persona-select"></select>
|
|
<button id="ssf-btn-fill">Analyze & Fill Survey</button>
|
|
<div id="ssf-status">Ready</div>
|
|
</div>
|
|
`;
|
|
|
|
const style = document.createElement('style');
|
|
style.textContent = `
|
|
#ssf-floating-bubble {
|
|
position: fixed; bottom: 30px; right: 30px; width: 60px; height: 60px;
|
|
background: linear-gradient(135deg, #4A90E2, #357ABD);
|
|
border-radius: 50%; box-shadow: 0 4px 15px rgba(0,0,0,0.3);
|
|
cursor: pointer; z-index: 2147483647; display: flex; align-items: center; justify-content: center;
|
|
transition: transform 0.2s; user-select: none; color: white; font-size: 30px;
|
|
}
|
|
#ssf-floating-bubble:hover { transform: scale(1.05); }
|
|
.ssf-menu {
|
|
position: absolute; bottom: 75px; right: 0; width: 260px; background: white;
|
|
border-radius: 12px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); padding: 15px;
|
|
display: flex; flex-direction: column; gap: 8px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
cursor: default; text-align: left; color: #333; font-size: 14px;
|
|
border: 1px solid #eee;
|
|
}
|
|
.ssf-menu-header {
|
|
font-weight: bold; border-bottom: 1px solid #eee; padding-bottom: 8px; margin-bottom: 5px;
|
|
display: flex; justify-content: space-between; align-items: center; font-size: 16px;
|
|
}
|
|
.ssf-close-btn { cursor: pointer; color: #999; font-size: 20px; line-height: 1; }
|
|
.ssf-close-btn:hover { color: #333; }
|
|
#ssf-persona-select {
|
|
width: 100%; padding: 8px; border-radius: 6px; border: 1px solid #ddd; background: #fafafa;
|
|
}
|
|
#ssf-btn-fill {
|
|
background: #4A90E2; color: white; border: none; padding: 10px; border-radius: 6px;
|
|
cursor: pointer; font-weight: 600; margin-top: 5px; transition: background 0.2s;
|
|
}
|
|
#ssf-btn-fill:hover { background: #357ABD; }
|
|
#ssf-btn-fill:disabled { background: #ccc; cursor: not-allowed; }
|
|
#ssf-status {
|
|
font-size: 12px; margin-top: 5px; color: #666; text-align: center; min-height: 1.2em;
|
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
document.body.appendChild(bubble);
|
|
|
|
// --- EVENT HANDLERS ---
|
|
|
|
const menu = bubble.querySelector('.ssf-menu');
|
|
const closeBtn = bubble.querySelector('.ssf-close-btn');
|
|
const select = document.getElementById('ssf-persona-select');
|
|
const fillBtn = document.getElementById('ssf-btn-fill');
|
|
const status = document.getElementById('ssf-status');
|
|
|
|
// Toggle Menu
|
|
bubble.addEventListener('click', (e) => {
|
|
if (e.target.closest('.ssf-menu')) return; // Don't close if clicking inside menu
|
|
// Only toggle if not dragging
|
|
if (!isDragging) {
|
|
const isHidden = menu.style.display === 'none';
|
|
menu.style.display = isHidden ? 'flex' : 'none';
|
|
if (isHidden) loadPersonas();
|
|
}
|
|
});
|
|
|
|
closeBtn.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
menu.style.display = 'none';
|
|
});
|
|
|
|
// Load Personas
|
|
function loadPersonas() {
|
|
status.innerText = 'Loading profiles...';
|
|
select.innerHTML = '<option>Loading...</option>';
|
|
select.disabled = true;
|
|
fillBtn.disabled = true;
|
|
|
|
chrome.runtime.sendMessage({ type: 'FETCH_PERSONAS' }, (response) => {
|
|
select.disabled = false;
|
|
fillBtn.disabled = false;
|
|
|
|
if (chrome.runtime.lastError) {
|
|
status.innerText = 'Error: Check Extension Settings';
|
|
select.innerHTML = '<option value="">Connection Failed</option>';
|
|
return;
|
|
}
|
|
|
|
if (response && response.success && response.data && response.data.length > 0) {
|
|
status.innerText = 'Ready';
|
|
select.innerHTML = response.data.map(p => `<option value="${p.id}">${p.name} (${p.age || '?'}, ${p.job || 'N/A'})</option>`).join('');
|
|
} else if (response && !response.success) {
|
|
status.innerText = response.error || 'Connection Error';
|
|
select.innerHTML = '<option value="">Error Loading Data</option>';
|
|
} else {
|
|
status.innerText = 'No profiles found';
|
|
select.innerHTML = '<option value="">Create Profile in Dashboard</option>';
|
|
}
|
|
});
|
|
}
|
|
|
|
// Fill Button Logic
|
|
fillBtn.addEventListener('click', async (e) => {
|
|
e.stopPropagation();
|
|
const personaId = select.value;
|
|
|
|
if (!personaId) {
|
|
status.innerText = 'Please select a persona first!';
|
|
return;
|
|
}
|
|
|
|
status.innerText = 'Scanning page...';
|
|
fillBtn.disabled = true;
|
|
|
|
// 1. Map Inputs to Questions
|
|
const fields = [];
|
|
// Expanded selector for more input types
|
|
const inputs = document.querySelectorAll('input:not([type="hidden"]):not([type="submit"]):not([type="button"]), textarea, select');
|
|
|
|
inputs.forEach((input, i) => {
|
|
// Logic to find label:
|
|
// 1. Explicit <label for="id">
|
|
// 2. Parent <label>
|
|
// 3. Aria-label or Aria-labelledby
|
|
// 4. Placeholder
|
|
// 5. Preceding text node
|
|
|
|
let labelText = "";
|
|
|
|
if (input.id) {
|
|
const labelEl = document.querySelector(`label[for="${input.id}"]`);
|
|
if (labelEl) labelText = labelEl.innerText;
|
|
}
|
|
|
|
if (!labelText && input.closest('label')) {
|
|
labelText = input.closest('label').innerText;
|
|
}
|
|
|
|
if (!labelText) {
|
|
labelText = input.getAttribute('aria-label') || input.placeholder || "";
|
|
}
|
|
|
|
if (!labelText) {
|
|
// Simple proximity check (previous sibling)
|
|
let prev = input.previousElementSibling;
|
|
if (prev && prev.innerText) labelText = prev.innerText;
|
|
else if (input.parentElement && input.parentElement.previousElementSibling) {
|
|
labelText = input.parentElement.previousElementSibling.innerText;
|
|
}
|
|
}
|
|
|
|
// Clean up label
|
|
labelText = labelText.replace(/\s+/g, ' ').trim().substring(0, 150);
|
|
|
|
if (labelText || input.placeholder) {
|
|
fields.push({
|
|
id: `field_${i}`,
|
|
type: input.type || input.tagName.toLowerCase(),
|
|
label: labelText || "Unknown Question",
|
|
options: input.tagName === 'SELECT' ? [...input.options].map(o => o.text).join(',') : null
|
|
});
|
|
input.dataset.ssfId = `field_${i}`;
|
|
input.style.border = "2px solid #4A90E2"; // Visual indicator
|
|
}
|
|
});
|
|
|
|
if (fields.length === 0) {
|
|
status.innerText = 'No compatible fields found.';
|
|
fillBtn.disabled = false;
|
|
return;
|
|
}
|
|
|
|
status.innerText = `Thinking (${fields.length} fields)...`;
|
|
|
|
chrome.runtime.sendMessage({
|
|
type: 'START_FILLING',
|
|
surveyData: fields,
|
|
personaId: personaId
|
|
}, (response) => {
|
|
fillBtn.disabled = false;
|
|
|
|
// Clear visual indicators
|
|
inputs.forEach(input => input.style.border = "");
|
|
|
|
if (response && response.success) {
|
|
status.innerText = 'Injecting answers...';
|
|
applyAnswers(response.answers);
|
|
status.innerText = 'Done!';
|
|
setTimeout(() => { status.innerText = 'Ready'; }, 3000);
|
|
} else {
|
|
status.innerText = 'Failed: ' + (response ? response.error : 'Network Error');
|
|
}
|
|
});
|
|
});
|
|
|
|
function applyAnswers(answersWrapper) {
|
|
let answerMap = {};
|
|
|
|
// Parse the AI response structure
|
|
// Case A: Wrapper has .data.output (Responses API)
|
|
if (answersWrapper.data && answersWrapper.data.output) {
|
|
try {
|
|
const text = answersWrapper.data.output.find(o => o.type === 'message').content.find(c => c.type === 'output_text').text;
|
|
// Try to find JSON block
|
|
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
if (jsonMatch) {
|
|
answerMap = JSON.parse(jsonMatch[0]);
|
|
} else {
|
|
console.error("Could not find JSON in AI text:", text);
|
|
}
|
|
} catch (e) {
|
|
console.error("Parsing error A:", e);
|
|
}
|
|
}
|
|
// Case B: Direct object
|
|
else if (typeof answersWrapper === 'object') {
|
|
answerMap = answersWrapper;
|
|
}
|
|
|
|
console.log("Applying Answers:", answerMap);
|
|
|
|
for (const [id, value] of Object.entries(answerMap)) {
|
|
const input = document.querySelector(`[data-ssf-id="${id}"]`);
|
|
if (!input) continue;
|
|
|
|
try {
|
|
if (input.type === 'checkbox') {
|
|
input.checked = (String(value).toLowerCase() === 'true' || String(value).toLowerCase() === 'yes');
|
|
} else if (input.type === 'radio') {
|
|
// Complex for radios: we need to click the one that matches the value
|
|
// But here we only have the mapped ID.
|
|
// If the AI gives "Yes", we need to check if this radio input's value or label matches "Yes"
|
|
if (input.value.toLowerCase() === String(value).toLowerCase()) {
|
|
input.checked = true;
|
|
}
|
|
} else {
|
|
input.value = value;
|
|
}
|
|
|
|
// Trigger events
|
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
input.dispatchEvent(new Event('blur', { bubbles: true }));
|
|
} catch (e) {
|
|
console.error("Error setting field", id, e);
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- DRAGGING ---
|
|
let isDragging = false, startX, startY, startLeft, startTop;
|
|
|
|
bubble.addEventListener('mousedown', (e) => {
|
|
if (e.target.closest('.ssf-menu')) return;
|
|
isDragging = true;
|
|
startX = e.clientX; startY = e.clientY;
|
|
const rect = bubble.getBoundingClientRect();
|
|
startLeft = rect.left; startTop = rect.top;
|
|
|
|
// Disable transition during drag
|
|
bubble.style.transition = 'none';
|
|
e.preventDefault();
|
|
});
|
|
|
|
document.addEventListener('mousemove', (e) => {
|
|
if (!isDragging) return;
|
|
const deltaX = e.clientX - startX;
|
|
const deltaY = e.clientY - startY;
|
|
bubble.style.left = (startLeft + deltaX) + 'px';
|
|
bubble.style.top = (startTop + deltaY) + 'px';
|
|
bubble.style.right = 'auto'; bubble.style.bottom = 'auto';
|
|
});
|
|
|
|
document.addEventListener('mouseup', () => {
|
|
if (isDragging) {
|
|
isDragging = false;
|
|
bubble.style.transition = 'transform 0.2s';
|
|
}
|
|
});
|
|
|
|
})(); |