Translaterpeed / app /static /js /intervention.js
Ruhivig65's picture
Upload 3 files
ede637d verified
/**
* ============================================
* Captcha Intervention Handler
* - Polls for active interventions
* - Shows screenshots
* - Handles click-on-screenshot to solve captcha
* ============================================
*/
// Track current interventions
let activeInterventions = {};
// ============================================
// Check for Interventions
// ============================================
async function checkInterventions() {
try {
const data = await apiCall('/api/intervention/active');
const banner = document.getElementById('interventionBanner');
const section = document.getElementById('interventionSection');
if (data.count > 0) {
// Show alert banner
banner.style.display = 'block';
section.style.display = 'block';
activeInterventions = data.interventions;
renderInterventionPanels(data.interventions);
} else {
banner.style.display = 'none';
// Only hide section if no interventions
if (Object.keys(activeInterventions).length === 0) {
section.style.display = 'none';
}
activeInterventions = {};
}
} catch (error) {
console.error('Intervention check error:', error);
}
}
// ============================================
// Render Intervention Panels
// ============================================
function renderInterventionPanels(interventions) {
const container = document.getElementById('interventionContent');
if (Object.keys(interventions).length === 0) {
container.innerHTML = '<p style="color: var(--text-muted); text-align: center; padding: 20px;">No active interventions 👍</p>';
return;
}
container.innerHTML = Object.entries(interventions).map(([novelId, info]) => {
const screenshotUrl = `/api/intervention/screenshot/${info.screenshot}`;
return `
<div class="intervention-panel" id="intervention-${novelId}">
<div class="intervention-header">
<h3>🚨 Novel #${novelId} - Captcha Detected</h3>
<span style="color: var(--text-muted); font-size: 0.8rem;">
URL: ${escapeHtml(info.page_url || 'Unknown')}
</span>
</div>
<p style="color: var(--accent-yellow); margin-bottom: 12px; font-size: 0.9rem;">
Reason: ${escapeHtml(info.reason)}
</p>
<p style="color: var(--text-secondary); margin-bottom: 12px; font-size: 0.85rem;">
👆 Click directly on the captcha in the screenshot below. Your click will be sent to the actual browser.
</p>
<div class="screenshot-container" id="screenshot-container-${novelId}"
onclick="handleScreenshotClick(event, ${novelId})">
<img src="${screenshotUrl}"
alt="Captcha Screenshot"
id="screenshot-img-${novelId}"
loading="lazy"
onerror="this.alt='Screenshot failed to load. Click Refresh.'"
>
</div>
<!-- Text input for text-based captchas -->
<div class="intervention-input">
<input type="text"
id="captcha-text-${novelId}"
placeholder="Type captcha text here (if needed)">
<button class="btn btn-sm btn-primary"
onclick="submitCaptchaText(${novelId})">
⌨️ Submit Text
</button>
</div>
<div class="intervention-actions">
<button class="btn btn-sm btn-success"
onclick="resolveIntervention(${novelId})">
✅ Captcha Solved - Resume Scraping
</button>
<button class="btn btn-sm btn-secondary"
onclick="refreshScreenshot(${novelId})">
🔄 Refresh Screenshot
</button>
<button class="btn btn-sm btn-warning"
onclick="retryClick(${novelId})">
🖱️ Retry Last Click
</button>
</div>
</div>
`;
}).join('');
}
// ============================================
// Handle Click on Screenshot
// ============================================
let lastClickCoords = {};
async function handleScreenshotClick(event, novelId) {
const container = document.getElementById(`screenshot-container-${novelId}`);
const img = document.getElementById(`screenshot-img-${novelId}`);
if (!img || !img.naturalWidth) {
showToast('Screenshot not loaded yet', 'warning');
return;
}
// Calculate click position relative to the actual image size
const rect = img.getBoundingClientRect();
const scaleX = img.naturalWidth / rect.width;
const scaleY = img.naturalHeight / rect.height;
const clickX = Math.round((event.clientX - rect.left) * scaleX);
const clickY = Math.round((event.clientY - rect.top) * scaleY);
// Store for retry
lastClickCoords[novelId] = { x: clickX, y: clickY };
// Show visual feedback - click indicator
showClickIndicator(container, event.clientX - rect.left, event.clientY - rect.top);
console.log(`Click at (${clickX}, ${clickY}) for Novel ${novelId}`);
showToast(`Clicking at (${clickX}, ${clickY})...`, 'info', 2000);
try {
const result = await apiCall('/api/intervention/click', 'POST', {
novel_id: novelId,
x: clickX,
y: clickY,
});
showToast(`Click sent! ${result.message}`, 'success');
// Auto-refresh screenshot after click to see result
setTimeout(() => refreshScreenshot(novelId), 2000);
} catch (error) {
showToast(`Click failed: ${error.message}`, 'error');
}
}
function showClickIndicator(container, x, y) {
// Remove old indicators
container.querySelectorAll('.click-indicator').forEach(el => el.remove());
// Add new indicator
const indicator = document.createElement('div');
indicator.className = 'click-indicator';
indicator.style.left = `${x}px`;
indicator.style.top = `${y}px`;
container.appendChild(indicator);
// Remove after animation
setTimeout(() => indicator.remove(), 3000);
}
// ============================================
// Retry Last Click
// ============================================
async function retryClick(novelId) {
const coords = lastClickCoords[novelId];
if (!coords) {
showToast('No previous click to retry', 'warning');
return;
}
try {
await apiCall('/api/intervention/click', 'POST', {
novel_id: novelId,
x: coords.x,
y: coords.y,
});
showToast(`Retried click at (${coords.x}, ${coords.y})`, 'info');
setTimeout(() => refreshScreenshot(novelId), 2000);
} catch (error) {
showToast(`Retry failed: ${error.message}`, 'error');
}
}
// ============================================
// Submit Captcha Text
// ============================================
async function submitCaptchaText(novelId) {
const input = document.getElementById(`captcha-text-${novelId}`);
const text = input ? input.value.trim() : '';
if (!text) {
showToast('Please enter the captcha text', 'warning');
return;
}
try {
// Try common captcha input selectors
const selectors = [
'input#captcha-input',
'input[name="captcha"]',
'input[name="cf-turnstile-response"]',
'input.captcha-input',
'input[placeholder*="captcha" i]',
'input[type="text"]:visible',
];
let submitted = false;
for (const selector of selectors) {
try {
await apiCall('/api/intervention/type', 'POST', {
novel_id: novelId,
selector: selector,
text: text,
});
submitted = true;
showToast(`Text "${text}" typed into ${selector}`, 'success');
break;
} catch (e) {
continue;
}
}
if (!submitted) {
showToast('Could not find captcha input field. Try clicking on it first.', 'warning');
}
// Clear input
if (input) input.value = '';
// Refresh screenshot
setTimeout(() => refreshScreenshot(novelId), 2000);
} catch (error) {
showToast(`Text submit failed: ${error.message}`, 'error');
}
}
// ============================================
// Refresh Screenshot
// ============================================
async function refreshScreenshot(novelId) {
try {
const result = await apiCall('/api/intervention/refresh-screenshot', 'POST', {
novel_id: novelId,
});
if (result.screenshot) {
const img = document.getElementById(`screenshot-img-${novelId}`);
if (img) {
// Add timestamp to prevent caching
img.src = `/api/intervention/screenshot/${result.screenshot}?t=${Date.now()}`;
}
showToast('Screenshot refreshed', 'info', 2000);
}
} catch (error) {
showToast(`Refresh failed: ${error.message}`, 'error');
}
}
// ============================================
// Resolve Intervention
// ============================================
async function resolveIntervention(novelId) {
try {
const result = await apiCall('/api/intervention/resolve', 'POST', {
novel_id: novelId,
});
showToast(`✅ ${result.message}`, 'success');
// Remove the panel
const panel = document.getElementById(`intervention-${novelId}`);
if (panel) {
panel.style.animation = 'toast-out 0.3s ease';
setTimeout(() => panel.remove(), 300);
}
delete activeInterventions[novelId];
// Hide section if no more interventions
if (Object.keys(activeInterventions).length === 0) {
document.getElementById('interventionBanner').style.display = 'none';
}
} catch (error) {
showToast(`Failed to resolve: ${error.message}`, 'error');
}
}