From 1d91278dcf083cf152779f360e886bd7bee1030b Mon Sep 17 00:00:00 2001 From: ydking0911 Date: Wed, 28 Jan 2026 15:44:59 -0800 Subject: [PATCH] refactor: separate HTML, CSS, and JS into modular files - Extract CSS styles to app/static/css/main.css - Extract JavaScript logic to app/static/js/app.js - Update HTML template to reference external files - Add StaticFiles mount in main.py for static asset serving - Add STATIC_DIR configuration in config.py - Maintain Jinja2 template variables for server-side data injection --- app/core/config.py | 1 + app/main.py | 6 +- app/static/css/main.css | 138 ++++++++ app/static/js/app.js | 432 +++++++++++++++++++++++++ app/templates/index.html | 684 +++++++-------------------------------- 5 files changed, 691 insertions(+), 570 deletions(-) create mode 100644 app/static/css/main.css create mode 100644 app/static/js/app.js diff --git a/app/core/config.py b/app/core/config.py index ea16355..00c8f1f 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -5,6 +5,7 @@ BASE_DIR = Path(__file__).parent.parent.parent UPLOAD_DIR = BASE_DIR / "app" / "uploads" TEMPLATES_DIR = BASE_DIR / "app" / "templates" +STATIC_DIR = BASE_DIR / "app" / "static" MODELS_DIR = BASE_DIR / "models" # Global state to keep track of training diff --git a/app/main.py b/app/main.py index a27f3eb..9907704 100644 --- a/app/main.py +++ b/app/main.py @@ -1,11 +1,12 @@ from fastapi import FastAPI, Request from fastapi.responses import HTMLResponse +from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from contextlib import asynccontextmanager import shutil import uvicorn -from .core.config import STATE, UPLOAD_DIR, TEMPLATES_DIR +from .core.config import STATE, UPLOAD_DIR, TEMPLATES_DIR, STATIC_DIR from .services.metadata_service import load_metadata from .services.dataset_service import dataset_cache from .routers import captcha, captcha_v2 @@ -34,6 +35,9 @@ async def lifespan(app: FastAPI): lifespan=lifespan ) +# Mount static files +app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static") + # Setup templates templates = Jinja2Templates(directory=str(TEMPLATES_DIR)) diff --git a/app/static/css/main.css b/app/static/css/main.css new file mode 100644 index 0000000..50e9fc6 --- /dev/null +++ b/app/static/css/main.css @@ -0,0 +1,138 @@ +/* Gotcha! AI ReCaptcha Solver - Main Stylesheet */ + +/* Base Styles */ +body { + font-family: 'Space Grotesk', sans-serif; + -webkit-tap-highlight-color: transparent; + -webkit-touch-callout: none; + overscroll-behavior: none; +} + +/* Safe area support for notched devices */ +.safe-top { padding-top: env(safe-area-inset-top); } +.safe-bottom { padding-bottom: env(safe-area-inset-bottom); } +.safe-left { padding-left: env(safe-area-inset-left); } +.safe-right { padding-right: env(safe-area-inset-right); } + +/* Terminal Scrollbar */ +.terminal-scroll::-webkit-scrollbar { + width: 6px; +} + +.terminal-scroll::-webkit-scrollbar-track { + background: #0a0c12; +} + +.terminal-scroll::-webkit-scrollbar-thumb { + background: #282e39; + border-radius: 4px; +} + +@media (max-width: 768px) { + .terminal-scroll::-webkit-scrollbar { + width: 4px; + } +} + +/* Scanner Animation */ +.scanner-line { + height: 2px; + background: linear-gradient(90deg, transparent, #00ff41, transparent); + box-shadow: 0 0 15px #00ff41; + position: absolute; + width: 100%; + top: 0; + z-index: 10; + display: none; + animation: scan 2s linear infinite; +} + +@keyframes scan { + from { top: 0%; } + to { top: 100%; } +} + +/* Typing Cursor Animation */ +.typing-cursor { + border-right: 2px solid #4d90fe; + animation: blink 0.7s infinite; +} + +@keyframes blink { + from, to { border-color: transparent; } + 50% { border-color: #4d90fe; } +} + +/* Mobile Sidebar Overlay */ +.sidebar-overlay { + opacity: 0; + visibility: hidden; + transition: opacity 0.3s ease, visibility 0.3s ease; +} + +.sidebar-overlay.active { + opacity: 1; + visibility: visible; +} + +/* Mobile Sidebar Slide */ +.mobile-sidebar { + transform: translateX(-100%); + transition: transform 0.3s ease; +} + +.mobile-sidebar.active { + transform: translateX(0); +} + +/* Touch-friendly tap states */ +.touch-active:active { + opacity: 0.7; + transform: scale(0.98); +} + +/* Prevent text selection on mobile */ +.no-select { + -webkit-user-select: none; + user-select: none; +} + +/* Touch manipulation for grid items */ +.touch-action-manipulation { + touch-action: manipulation; +} + +/* Responsive breakpoint for extra small screens */ +@media (max-width: 380px) { + .xs\:inline { display: inline; } +} + +/* Improve input on mobile */ +input[type="text"] { + -webkit-appearance: none; + appearance: none; +} + +/* Fix iOS input zoom */ +@media screen and (max-width: 768px) { + input[type="text"] { + font-size: 16px; + } +} + +/* Better button states for mobile */ +button:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +/* Hide scrollbar on very small screens */ +@media (max-width: 480px) { + .terminal-scroll { + scrollbar-width: none; + -ms-overflow-style: none; + } + .terminal-scroll::-webkit-scrollbar { + display: none; + } +} diff --git a/app/static/js/app.js b/app/static/js/app.js new file mode 100644 index 0000000..a5452c2 --- /dev/null +++ b/app/static/js/app.js @@ -0,0 +1,432 @@ +/** + * Gotcha! AI ReCaptcha Solver - Main Application + */ + +// ============================================ +// Mobile Sidebar Toggle +// ============================================ +const sidebar = document.getElementById('sidebar'); +const sidebarOverlay = document.getElementById('sidebar-overlay'); + +function toggleSidebar() { + sidebar.classList.toggle('active'); + sidebarOverlay.classList.toggle('active'); + document.body.style.overflow = sidebar.classList.contains('active') ? 'hidden' : ''; +} + +// Close sidebar on resize to desktop +window.addEventListener('resize', () => { + if (window.innerWidth >= 768) { + sidebar.classList.remove('active'); + sidebarOverlay.classList.remove('active'); + document.body.style.overflow = ''; + } +}); + +// ============================================ +// DOM Elements +// ============================================ +const terminalContent = document.getElementById('terminal-content'); +const clearTerminal = document.getElementById('clear-terminal'); +const mainStatus = document.getElementById('main-status'); + +const v1Container = document.getElementById('v1-container'); +const v2Container = document.getElementById('v2-container'); +const challengeImg = document.getElementById('challenge-img'); +const v1Scanner = document.getElementById('v1-scanner'); +const v2Scanner = document.getElementById('v2-scanner'); +const v2TargetName = document.getElementById('v2-target-name'); +const v2Grid = document.getElementById('v2-grid'); +const v1InputGroup = document.getElementById('v1-input-group'); + +const captchaInput = document.getElementById('captcha-solve-input'); +const btnAutoSolve = document.getElementById('btn-auto-solve'); +const btnVerify = document.getElementById('btn-verify-solve'); +const refreshChallenge = document.getElementById('refresh-challenge'); +const solverStatus = document.getElementById('solver-status'); + +const tabV1 = document.getElementById('tab-v1'); +const tabV2 = document.getElementById('tab-v2'); + +// ============================================ +// State Variables +// ============================================ +let currentChallengeId = null; +let currentV2Challenge = null; +let selectedV2Indices = new Set(); +let currentMode = 'v1'; + +// Metrics are injected from server-side template +// window.v1Metrics, window.v2Metrics, window.initialStatus + +// ============================================ +// Sidebar Metrics Update +// ============================================ +function updateSidebarMetrics(mode) { + const m = mode === 'v1' ? window.v1Metrics : window.v2Metrics; + + console.log(`Updating metrics for mode: ${mode}`, m); + + // Labels + document.getElementById('metric-accuracy-label').innerText = mode === 'v1' ? 'Word Accuracy' : 'Object Accuracy'; + document.getElementById('metric-char-accuracy-label').innerText = mode === 'v1' ? 'Character Accuracy' : 'Class Accuracy'; + + // Feed Description + document.getElementById('feed-description').innerText = mode === 'v1' + ? 'Native CRNN+CTC Simulation Feed' + : 'Vision-based Object Classification Feed'; + + // Values + document.getElementById('metric-accuracy').innerText = m.accuracy || 'N/A'; + document.getElementById('metric-char-accuracy').innerText = m.char_accuracy || 'N/A'; + document.getElementById('metric-precision').innerText = m.precision || 'N/A'; + document.getElementById('metric-recall').innerText = m.recall || 'N/A'; + document.getElementById('metric-f1-score').innerText = m.f1_score || 'N/A'; + document.getElementById('metric-type').innerText = m.type || 'N/A'; + document.getElementById('metric-loss').innerText = m.loss || 'N/A'; + document.getElementById('metric-loss-value').innerText = m.loss_value || 'N/A'; + + // Descriptions + document.getElementById('metric-accuracy-desc').innerText = mode === 'v1' + ? 'Entire 5-character sequence must be perfect. One wrong letter = 0% success.' + : 'Correct identification of the target object category within the grid.'; + document.getElementById('metric-char-accuracy-desc').innerText = mode === 'v1' + ? 'Percentage of individual letters correctly recognized across all samples.' + : 'Accuracy of the underlying classification model for all object classes.'; +} + +// ============================================ +// Terminal Logging +// ============================================ +function log(msg, type = 'info') { + const time = new Date().toLocaleTimeString('en-GB', { hour12: false }); + const p = document.createElement('p'); + const typeColor = type === 'success' ? 'text-accent-green' : type === 'error' ? 'text-red-500' : 'text-primary'; + p.innerHTML = `[${time}] ${type.toUpperCase()}: ${msg}`; + terminalContent.appendChild(p); + terminalContent.scrollTop = terminalContent.scrollHeight; +} + +clearTerminal.onclick = () => { + terminalContent.innerHTML = '

>> Console session cleared

'; +}; + +// ============================================ +// Challenge Loading +// ============================================ +async function loadChallenge() { + if (currentMode === 'v1') { + await loadChallengeV1(); + } else { + await loadChallengeV2(); + } +} + +async function loadChallengeV1() { + log('Requesting new v1 challenge...'); + v1Scanner.style.display = 'block'; + mainStatus.innerText = 'FETCHING'; + + try { + const response = await fetch('/api/challenge'); + const data = await response.json(); + challengeImg.src = data.image; + currentChallengeId = data.challenge_id; + captchaInput.value = ""; + solverStatus.innerText = "READY"; + log(`Challenge loaded: ${currentChallengeId}`, 'success'); + } catch (err) { + log(`Failed to load v1 challenge: ${err.message}`, 'error'); + } finally { + v1Scanner.style.display = 'none'; + mainStatus.innerText = 'READY'; + } +} + +async function loadChallengeV2() { + log('Requesting new v2 challenge...'); + v2Scanner.style.display = 'block'; + mainStatus.innerText = 'FETCHING'; + selectedV2Indices.clear(); + + try { + const response = await fetch('/api/v2/challenge'); + const data = await response.json(); + currentV2Challenge = data; + v2TargetName.innerText = data.target; + renderV2Grid(data.grid); + solverStatus.innerText = "READY"; + log(`v2 Challenge loaded. Target: ${data.target}`, 'success'); + } catch (err) { + log(`Failed to load v2 challenge: ${err.message}`, 'error'); + } finally { + v2Scanner.style.display = 'none'; + mainStatus.innerText = 'READY'; + } +} + +// ============================================ +// V2 Grid Rendering +// ============================================ +function renderV2Grid(grid) { + const children = Array.from(v2Grid.children); + children.forEach(c => { if (c.id !== 'v2-scanner') v2Grid.removeChild(c); }); + + grid.forEach((item, index) => { + const div = document.createElement('div'); + div.className = "relative aspect-square bg-background-dark border border-border-dark cursor-pointer overflow-hidden group transition-all duration-300 hover:border-primary active:scale-95 touch-action-manipulation"; + div.innerHTML = ` + +
+
+ done +
+ `; + + div.addEventListener('click', (e) => { + e.preventDefault(); + toggleV2Selection(div, index); + }); + div.addEventListener('touchend', (e) => { + e.preventDefault(); + toggleV2Selection(div, index); + }, { passive: false }); + + v2Grid.appendChild(div); + }); +} + +function toggleV2Selection(el, index) { + const overlay = el.querySelector('.select-overlay'); + const check = el.querySelector('.select-check'); + + if (selectedV2Indices.has(index)) { + selectedV2Indices.delete(index); + overlay.style.opacity = '0'; + check.style.opacity = '0'; + check.style.transform = 'scale(0.5)'; + el.classList.remove('ring-4', 'ring-primary', 'ring-inset'); + } else { + selectedV2Indices.add(index); + overlay.style.opacity = '1'; + check.style.opacity = '1'; + check.style.transform = 'scale(1)'; + el.classList.add('ring-4', 'ring-primary', 'ring-inset'); + } + log(`Index ${index} ${selectedV2Indices.has(index) ? 'selected' : 'deselected'}`); +} + +// ============================================ +// AI Auto-Solve +// ============================================ +refreshChallenge.onclick = loadChallenge; + +btnAutoSolve.onclick = async () => { + if (currentMode === 'v1') { + await autoSolveV1(); + } else { + await autoSolveV2(); + } +}; + +async function autoSolveV1() { + if (!currentChallengeId) return; + btnAutoSolve.disabled = true; + log('Initializing AI solving unit...', 'info'); + solverStatus.innerText = "ANALYZING..."; + mainStatus.innerText = 'SOLVING'; + + try { + const response = await fetch('/api/solve', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ challenge_id: currentChallengeId }) + }); + const data = await response.json(); + captchaInput.value = ""; + captchaInput.classList.add('typing-cursor'); + for (let char of data.prediction) { + captchaInput.value += char; + await new Promise(r => setTimeout(r, 100)); + } + captchaInput.classList.remove('typing-cursor'); + log(`AI solve generated: "${data.prediction}"`, 'success'); + solverStatus.innerText = "SOLVE COMPLETE"; + } catch (error) { + log('AI solving unit failed', 'error'); + solverStatus.innerText = "ERROR"; + } finally { + btnAutoSolve.disabled = false; + mainStatus.innerText = 'READY'; + } +} + +async function autoSolveV2() { + if (!currentV2Challenge) return; + btnAutoSolve.disabled = true; + log('Initializing Vision AI to scan grid...', 'info'); + solverStatus.innerText = "SCANNING..."; + mainStatus.innerText = 'SOLVING'; + + try { + const response = await fetch('/api/v2/solve', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + target: currentV2Challenge.target, + grid_ids: currentV2Challenge.grid.map(g => g.id) + }) + }); + const data = await response.json(); + + selectedV2Indices.clear(); + const gridItems = Array.from(v2Grid.children).filter(c => c.id !== 'v2-scanner'); + + for (let idx of data.correct_indices) { + toggleV2Selection(gridItems[idx], idx); + await new Promise(r => setTimeout(r, 200)); + } + + log(`AI scan complete. Selected ${data.correct_indices.length} matches.`, 'success'); + solverStatus.innerText = "SCAN COMPLETE"; + } catch (error) { + log('Vision AI failed', 'error'); + solverStatus.innerText = "ERROR"; + } finally { + btnAutoSolve.disabled = false; + mainStatus.innerText = 'READY'; + } +} + +// ============================================ +// Verification +// ============================================ +btnVerify.onclick = async () => { + if (currentMode === 'v1') { + await verifyV1(); + } else { + await verifyV2(); + } +}; + +async function verifyV1() { + const userInput = captchaInput.value; + if (!userInput || !currentChallengeId) return; + log('Initiating validation check...'); + mainStatus.innerText = 'VERIFYING'; + + try { + const response = await fetch('/api/verify', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ challenge_id: currentChallengeId, user_input: userInput }) + }); + const data = await response.json(); + handleVerifyResult(data.is_correct, data.true_label); + } catch (err) { + log(`Verification failed: ${err.message}`, 'error'); + } +} + +async function verifyV2() { + if (!currentV2Challenge) return; + log('Verifying grid selection with server...'); + mainStatus.innerText = 'VERIFYING'; + + const selectedIds = Array.from(selectedV2Indices).map(idx => currentV2Challenge.grid[idx].id); + + try { + const response = await fetch('/api/v2/verify', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + target: currentV2Challenge.target, + grid_ids: currentV2Challenge.grid.map(g => g.id), + selected_ids: selectedIds + }) + }); + const data = await response.json(); + handleVerifyResult(data.is_correct, `Matches: ${data.actual_ids.length}`); + } catch (err) { + log(`v2 Verification failed: ${err.message}`, 'error'); + } +} + +function handleVerifyResult(isCorrect, infoText) { + const targetBoxes = [v1Container, v2Grid]; + + if (isCorrect) { + log(`SUCCESS: ${infoText}`, 'success'); + solverStatus.innerText = "PASSED"; + mainStatus.innerText = 'SUCCESS'; + + targetBoxes.forEach(box => { + box.classList.add('border-accent-green', 'ring-2', 'ring-accent-green/20'); + box.classList.remove('border-border-dark'); + }); + + setTimeout(() => { + targetBoxes.forEach(box => { + box.classList.remove('border-accent-green', 'ring-2', 'ring-accent-green/20'); + box.classList.add('border-border-dark'); + }); + loadChallenge(); + }, 2000); + } else { + log(`FAILURE: ${infoText}`, 'error'); + solverStatus.innerText = "FAILED"; + mainStatus.innerText = 'FAILED'; + + targetBoxes.forEach(box => { + box.classList.add('border-red-500', 'ring-2', 'ring-red-500/20'); + box.classList.remove('border-border-dark'); + }); + + setTimeout(() => { + mainStatus.innerText = 'READY'; + targetBoxes.forEach(box => { + box.classList.remove('border-red-500', 'ring-2', 'ring-red-500/20'); + box.classList.add('border-border-dark'); + }); + }, 1000); + } +} + +// ============================================ +// Tab Switching +// ============================================ +tabV1.onclick = () => { + currentMode = 'v1'; + tabV1.classList.add('bg-primary', 'text-white'); + tabV1.classList.remove('text-[#9da6b9]'); + tabV2.classList.remove('bg-primary', 'text-white'); + tabV2.classList.add('text-[#9da6b9]'); + v1Container.classList.remove('hidden'); + v2Container.classList.add('hidden'); + v1InputGroup.classList.remove('hidden'); + log('Switched to v1: Text Captcha'); + updateSidebarMetrics('v1'); + loadChallenge(); +}; + +tabV2.onclick = () => { + currentMode = 'v2'; + tabV2.classList.add('bg-primary', 'text-white'); + tabV2.classList.remove('text-[#9da6b9]'); + tabV1.classList.remove('bg-primary', 'text-white'); + tabV1.classList.add('text-[#9da6b9]'); + v2Container.classList.remove('hidden'); + v1Container.classList.add('hidden'); + v1InputGroup.classList.add('hidden'); + log('Switched to v2: Image Select'); + updateSidebarMetrics('v2'); + loadChallenge(); +}; + +// ============================================ +// Initialization +// ============================================ +window.onload = () => { + updateSidebarMetrics('v1'); + loadChallenge(); +}; diff --git a/app/templates/index.html b/app/templates/index.html index bb7b2c2..f5a307b 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -3,14 +3,20 @@ - + + + + + AI ReCaptcha Solver Dashboard + + - - - - + + + - + + + + -
-
-
+
+
+ + +
- +
-

Gotcha! v2.0

- FastAPI: - ONLINE +

Gotcha! v2.0

+
-
-
-
@@ -119,54 +71,49 @@

Gotcha!
- -