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 = ` +