37819-vm/chrome-extension/content-script.js
2026-01-25 23:01:49 +00:00

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">&times;</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';
}
});
})();