Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>AI Image Screener</title> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <link rel="icon" type="image/x-icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🔍</text></svg>"> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| :root { | |
| --primary: #1a365d; | |
| --primary-light: #2d3748; | |
| --primary-dark: #0f172a; | |
| --secondary: #718096; | |
| --accent: #3182ce; | |
| --accent-light: #63b3ed; | |
| --accent-dark: #2c5282; | |
| --success: #38a169; | |
| --warning: #d69e2e; | |
| --danger: #e53e3e; | |
| --background: #f7fafc; | |
| --card-bg: #ffffff; | |
| --border: #e2e8f0; | |
| --text: #2d3748; | |
| --text-light: #718096; | |
| --shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); | |
| --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); | |
| --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); | |
| --radius: 0.75rem; | |
| --radius-lg: 1rem; | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; | |
| background-color: var(--background); | |
| color: var(--text); | |
| line-height: 1.6; | |
| min-height: 100vh; | |
| } | |
| .container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| padding: 10px 20px; | |
| } | |
| /* Header */ | |
| header { | |
| background: linear-gradient(135deg, var(--primary-dark) 0%, var(--primary) 100%); | |
| color: white; | |
| padding: 1.5rem 0; | |
| margin-bottom: 2rem; | |
| border-radius: 0 0 var(--radius-lg) var(--radius-lg); | |
| box-shadow: var(--shadow-xl); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| header::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| right: 0; | |
| width: 300px; | |
| height: 300px; | |
| background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%); | |
| transform: translate(30%, -30%); | |
| } | |
| .header-content { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| flex-wrap: wrap; | |
| gap: 1rem; | |
| position: relative; | |
| z-index: 1; | |
| } | |
| .logo { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| } | |
| .logo-icon { | |
| width: 48px; | |
| height: 48px; | |
| background: linear-gradient(135deg, var(--accent) 0%, var(--accent-light) 100%); | |
| border-radius: 12px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 1.5rem; | |
| box-shadow: var(--shadow-lg); | |
| } | |
| .logo-text h1 { | |
| font-size: 1.75rem; | |
| font-weight: 700; | |
| letter-spacing: -0.025em; | |
| } | |
| .logo-text .tagline { | |
| font-size: 0.875rem; | |
| opacity: 0.9; | |
| margin-top: 0.125rem; | |
| font-weight: 400; | |
| } | |
| /* Hero Section */ | |
| .hero { | |
| background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%); | |
| border-radius: var(--radius-lg); | |
| padding: 2.5rem 2rem; | |
| text-align: center; | |
| margin-bottom: 2rem; | |
| color: white; | |
| box-shadow: var(--shadow-xl); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .hero::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.05'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E"); | |
| } | |
| .hero-content { | |
| position: relative; | |
| z-index: 1; | |
| } | |
| .hero h2 { | |
| font-size: 3rem; | |
| margin-bottom: 1rem; | |
| font-weight: 800; | |
| letter-spacing: -0.025em; | |
| background: linear-gradient(to right, #ffffff, #e2e8f0); | |
| -webkit-background-clip: text; | |
| background-clip: text; | |
| color: transparent; | |
| } | |
| .hero-subtitle { | |
| font-size: 1.25rem; | |
| color: rgba(255, 255, 255, 0.9); | |
| margin-bottom: 2rem; | |
| max-width: 800px; | |
| margin-left: auto; | |
| margin-right: auto; | |
| font-weight: 300; | |
| } | |
| .performance-badge { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| padding: 0.75rem 1.5rem; | |
| background-color: rgba(255, 255, 255, 0.1); | |
| color: white; | |
| border: 1px solid rgba(255, 255, 255, 0.2); | |
| border-radius: 2rem; | |
| font-size: 0.875rem; | |
| margin-bottom: 2rem; | |
| backdrop-filter: blur(10px); | |
| font-weight: 500; | |
| } | |
| .cta-button { | |
| background: linear-gradient(135deg, var(--accent) 0%, var(--accent-dark) 100%); | |
| color: white; | |
| border: none; | |
| padding: 1rem 2.5rem; | |
| font-size: 1.125rem; | |
| border-radius: 0.5rem; | |
| cursor: pointer; | |
| font-weight: 600; | |
| transition: all 0.3s; | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 0.75rem; | |
| box-shadow: 0 4px 12px rgba(49, 130, 206, 0.3); | |
| min-width: 200px; | |
| margin: 0 auto; | |
| letter-spacing: 0.025em; | |
| } | |
| .cta-button:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 8px 20px rgba(49, 130, 206, 0.4); | |
| } | |
| /* Tab Navigation */ | |
| .tabs { | |
| display: flex; | |
| gap: 0.25rem; | |
| margin-bottom: 2rem; | |
| background-color: white; | |
| border-radius: var(--radius); | |
| padding: 0.25rem; | |
| box-shadow: var(--shadow); | |
| } | |
| .tab-button { | |
| padding: 1rem 2rem; | |
| background: none; | |
| border: none; | |
| color: var(--text-light); | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| position: relative; | |
| flex: 1; | |
| text-align: center; | |
| border-radius: calc(var(--radius) - 0.25rem); | |
| letter-spacing: 0.025em; | |
| } | |
| .tab-button.active { | |
| color: var(--accent); | |
| background-color: rgba(49, 130, 206, 0.08); | |
| box-shadow: var(--shadow); | |
| } | |
| .tab-button:hover:not(.active) { | |
| color: var(--primary); | |
| background-color: rgba(0, 0, 0, 0.02); | |
| } | |
| .tab-content { | |
| display: none; | |
| animation: fadeIn 0.5s ease; | |
| } | |
| .tab-content.active { | |
| display: block; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| /* Features Grid - 5 cards in one row */ | |
| .features-grid { | |
| display: grid; | |
| grid-template-columns: repeat(5, 1fr); | |
| gap: 1.25rem; | |
| margin-bottom: 2rem; | |
| } | |
| @media (max-width: 1200px) { | |
| .features-grid { | |
| grid-template-columns: repeat(3, 1fr); | |
| } | |
| } | |
| @media (max-width: 768px) { | |
| .features-grid { | |
| grid-template-columns: repeat(2, 1fr); | |
| } | |
| } | |
| @media (max-width: 480px) { | |
| .features-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| .feature-card { | |
| background-color: white; | |
| border-radius: var(--radius); | |
| padding: 1.5rem; | |
| border: 1px solid var(--border); | |
| transition: all 0.3s; | |
| display: flex; | |
| flex-direction: column; | |
| height: 100%; | |
| } | |
| .feature-card:hover { | |
| transform: translateY(-5px); | |
| box-shadow: var(--shadow-xl); | |
| border-color: var(--accent-light); | |
| } | |
| .feature-icon { | |
| font-size: 2rem; | |
| color: var(--accent); | |
| margin-bottom: 1rem; | |
| width: 48px; | |
| height: 48px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| background: linear-gradient(135deg, rgba(49, 130, 206, 0.1) 0%, rgba(99, 179, 237, 0.1) 100%); | |
| border-radius: 12px; | |
| } | |
| .feature-card h3 { | |
| font-size: 1.125rem; | |
| font-weight: 600; | |
| margin-bottom: 0.75rem; | |
| color: var(--primary); | |
| } | |
| .feature-card p { | |
| color: var(--text-light); | |
| font-size: 0.875rem; | |
| line-height: 1.5; | |
| flex-grow: 1; | |
| } | |
| /* Metrics Tab - UPDATED with proper separation */ | |
| .metrics-section { | |
| margin-bottom: 3rem; | |
| } | |
| .metrics-section:last-child { | |
| margin-bottom: 0; | |
| } | |
| .section-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| margin-bottom: 1.5rem; | |
| padding-bottom: 1rem; | |
| border-bottom: 2px solid var(--border); | |
| } | |
| .section-icon { | |
| width: 48px; | |
| height: 48px; | |
| background: linear-gradient(135deg, var(--accent) 0%, var(--accent-light) 100%); | |
| border-radius: 12px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: white; | |
| font-size: 1.25rem; | |
| } | |
| .section-icon.evidence { | |
| background: linear-gradient(135deg, #6b46c1 0%, #b794f4 100%); | |
| } | |
| .section-title { | |
| font-size: 1.5rem; | |
| font-weight: 700; | |
| color: var(--primary); | |
| flex: 1; | |
| } | |
| .section-subtitle { | |
| font-size: 1rem; | |
| color: var(--text-light); | |
| margin-top: 0.25rem; | |
| } | |
| /* Signal Metrics Grid - 5 cards in one row */ | |
| .signal-metrics-grid { | |
| display: grid; | |
| grid-template-columns: repeat(5, 1fr); | |
| gap: 1.25rem; | |
| margin-bottom: 3rem; | |
| } | |
| @media (max-width: 1200px) { | |
| .signal-metrics-grid { | |
| grid-template-columns: repeat(3, 1fr); | |
| } | |
| } | |
| @media (max-width: 768px) { | |
| .signal-metrics-grid { | |
| grid-template-columns: repeat(2, 1fr); | |
| } | |
| } | |
| @media (max-width: 480px) { | |
| .signal-metrics-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| /* Evidence Metrics Grid - 2 cards side by side */ | |
| .evidence-metrics-grid { | |
| display: grid; | |
| grid-template-columns: repeat(2, 1fr); | |
| gap: 1.25rem; | |
| } | |
| @media (max-width: 768px) { | |
| .evidence-metrics-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| .metric-card { | |
| background-color: white; | |
| border-radius: var(--radius); | |
| padding: 1.5rem; | |
| border: 1px solid var(--border); | |
| transition: all 0.3s; | |
| display: flex; | |
| flex-direction: column; | |
| height: 100%; | |
| } | |
| .metric-card:hover { | |
| transform: translateY(-3px); | |
| box-shadow: var(--shadow-xl); | |
| } | |
| .metric-card.signal { | |
| border-top: 4px solid var(--accent); | |
| } | |
| .metric-card.evidence { | |
| border-top: 4px solid #6b46c1; | |
| } | |
| .metric-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 1rem; | |
| margin-bottom: 1rem; | |
| padding-bottom: 1rem; | |
| border-bottom: 1px solid var(--border); | |
| } | |
| .metric-icon { | |
| width: 3rem; | |
| height: 3rem; | |
| border-radius: 0.75rem; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: white; | |
| font-size: 1.5rem; | |
| flex-shrink: 0; | |
| box-shadow: var(--shadow); | |
| } | |
| .metric-title { | |
| font-size: 1.125rem; | |
| font-weight: 600; | |
| color: var(--primary); | |
| flex-grow: 1; | |
| } | |
| .metric-weight { | |
| padding: 0.25rem 0.75rem; | |
| background-color: rgba(49, 130, 206, 0.1); | |
| color: var(--accent); | |
| border-radius: 2rem; | |
| font-size: 0.75rem; | |
| font-weight: 600; | |
| white-space: nowrap; | |
| } | |
| .metric-weight.evidence { | |
| background-color: rgba(107, 70, 193, 0.1); | |
| color: #6b46c1; | |
| } | |
| .metric-description { | |
| color: var(--text-light); | |
| margin-bottom: 1rem; | |
| line-height: 1.5; | |
| font-size: 0.875rem; | |
| flex-grow: 1; | |
| } | |
| .metric-details { | |
| margin-top: auto; | |
| padding-top: 1rem; | |
| border-top: 1px solid var(--border); | |
| } | |
| .detail-item { | |
| display: flex; | |
| justify-content: space-between; | |
| margin-bottom: 0.5rem; | |
| font-size: 0.75rem; | |
| } | |
| .detail-label { | |
| color: var(--text-light); | |
| } | |
| .detail-value { | |
| color: var(--primary); | |
| font-weight: 500; | |
| } | |
| /* How-to-use Steps */ | |
| .steps-grid { | |
| display: grid; | |
| grid-template-columns: repeat(3, 1fr); | |
| gap: 1.5rem; | |
| margin-bottom: 2rem; | |
| } | |
| @media (max-width: 768px) { | |
| .steps-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| .step-card { | |
| text-align: center; | |
| padding: 2rem; | |
| background-color: white; | |
| border-radius: var(--radius); | |
| border: 1px solid var(--border); | |
| transition: all 0.3s; | |
| } | |
| .step-card:hover { | |
| transform: translateY(-5px); | |
| border-color: var(--accent); | |
| box-shadow: var(--shadow-lg); | |
| } | |
| .step-number { | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| width: 3rem; | |
| height: 3rem; | |
| background: linear-gradient(135deg, var(--accent) 0%, var(--accent-light) 100%); | |
| color: white; | |
| border-radius: 50%; | |
| font-size: 1.5rem; | |
| font-weight: bold; | |
| margin-bottom: 1rem; | |
| box-shadow: var(--shadow); | |
| } | |
| /* Cards */ | |
| .card { | |
| background-color: var(--card-bg); | |
| border-radius: var(--radius); | |
| font-size: 1rem; | |
| box-shadow: var(--shadow); | |
| padding: 1.5rem; | |
| margin-bottom: 1.5rem; | |
| border: 1px solid var(--border); | |
| } | |
| .card-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 1rem; | |
| padding-bottom: 1rem; | |
| border-bottom: 1px solid var(--border); | |
| } | |
| .card-title { | |
| font-size: 1.25rem; | |
| font-weight: 600; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| color: var(--primary); | |
| } | |
| /* Upload Section */ | |
| .upload-area { | |
| border: 2px dashed var(--border); | |
| border-radius: var(--radius); | |
| padding: 3rem 1.5rem; | |
| text-align: center; | |
| transition: all 0.3s ease; | |
| cursor: pointer; | |
| margin-bottom: 1.5rem; | |
| background-color: #f8fafc; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .upload-area::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: linear-gradient(135deg, transparent 0%, rgba(49, 130, 206, 0.02) 100%); | |
| pointer-events: none; | |
| } | |
| .upload-area:hover, .upload-area.dragover { | |
| border-color: var(--accent); | |
| background-color: rgba(49, 130, 206, 0.05); | |
| transform: translateY(-2px); | |
| box-shadow: var(--shadow-lg); | |
| } | |
| .upload-icon { | |
| font-size: 3.5rem; | |
| color: var(--accent); | |
| margin-bottom: 1rem; | |
| opacity: 0.8; | |
| } | |
| .upload-text { | |
| font-size: 1.125rem; | |
| font-weight: 500; | |
| margin-bottom: 0.5rem; | |
| color: var(--primary); | |
| } | |
| .upload-button { | |
| background-color: var(--accent); | |
| color: white; | |
| border: none; | |
| padding: 0.875rem 1.75rem; | |
| border-radius: 0.5rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 0.5rem; | |
| margin-top: 1rem; | |
| box-shadow: var(--shadow); | |
| } | |
| .upload-button:hover { | |
| background-color: var(--accent-dark); | |
| transform: translateY(-2px); | |
| box-shadow: var(--shadow-lg); | |
| } | |
| /* Thumbnail Grid */ | |
| .thumbnail-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); | |
| gap: 1rem; | |
| margin-top: 1rem; | |
| max-height: 300px; | |
| overflow-y: auto; | |
| padding: 0.5rem; | |
| border-radius: var(--radius); | |
| background-color: #f8fafc; | |
| border: 1px solid var(--border); | |
| } | |
| .thumbnail-item { | |
| position: relative; | |
| border-radius: 0.5rem; | |
| overflow: hidden; | |
| border: 2px solid var(--border); | |
| transition: all 0.3s; | |
| height: 120px; | |
| background-color: white; | |
| } | |
| .thumbnail-item:hover { | |
| border-color: var(--accent); | |
| transform: translateY(-2px); | |
| box-shadow: var(--shadow); | |
| } | |
| .thumbnail-img { | |
| width: 100%; | |
| height: 100%; | |
| object-fit: cover; | |
| } | |
| .thumbnail-overlay { | |
| position: absolute; | |
| bottom: 0; | |
| left: 0; | |
| right: 0; | |
| background: linear-gradient(transparent, rgba(0, 0, 0, 0.8)); | |
| padding: 0.5rem; | |
| color: white; | |
| font-size: 0.75rem; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .remove-thumbnail { | |
| background: rgba(229, 62, 62, 0.9); | |
| border: none; | |
| color: white; | |
| width: 24px; | |
| height: 24px; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| flex-shrink: 0; | |
| } | |
| .remove-thumbnail:hover { | |
| background: var(--danger); | |
| transform: scale(1.1); | |
| } | |
| /* Start Analysis Button */ | |
| .start-analysis-btn { | |
| background: linear-gradient(135deg, var(--accent) 0%, var(--accent-dark) 100%); | |
| color: white; | |
| border: none; | |
| padding: 1rem 2rem; | |
| font-size: 1.125rem; | |
| border-radius: 0.5rem; | |
| cursor: pointer; | |
| font-weight: 600; | |
| transition: all 0.3s; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 0.75rem; | |
| width: 100%; | |
| margin-top: 1.5rem; | |
| box-shadow: 0 4px 12px rgba(49, 130, 206, 0.3); | |
| letter-spacing: 0.025em; | |
| } | |
| .start-analysis-btn:hover:not(:disabled) { | |
| transform: translateY(-2px); | |
| box-shadow: 0 8px 20px rgba(49, 130, 206, 0.4); | |
| } | |
| .start-analysis-btn:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| transform: none ; | |
| } | |
| .start-analysis-btn .btn-content { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 0.75rem; | |
| } | |
| /* Progress Section */ | |
| .progress-container { | |
| margin-top: 1.5rem; | |
| padding: 1.5rem; | |
| background-color: white; | |
| border-radius: var(--radius); | |
| box-shadow: var(--shadow); | |
| border: 1px solid var(--border); | |
| } | |
| .progress-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 1rem; | |
| } | |
| .progress-bar { | |
| height: 0.5rem; | |
| background-color: var(--border); | |
| border-radius: 1rem; | |
| overflow: hidden; | |
| margin-bottom: 0.75rem; | |
| } | |
| .progress-fill { | |
| height: 100%; | |
| background: linear-gradient(90deg, var(--accent), var(--accent-light)); | |
| border-radius: 1rem; | |
| width: 0%; | |
| transition: width 0.5s ease; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .progress-fill::after { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| bottom: 0; | |
| width: 100px; | |
| background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent); | |
| animation: shimmer 2s infinite; | |
| } | |
| @keyframes shimmer { | |
| 0% { transform: translateX(-100%); } | |
| 100% { transform: translateX(400%); } | |
| } | |
| /* Results Section - UPDATED FOR NO SCROLLING */ | |
| .results-summary { | |
| display: grid; | |
| grid-template-columns: repeat(4, 1fr); | |
| gap: 1rem; | |
| margin-bottom: 2rem; | |
| } | |
| @media (max-width: 768px) { | |
| .results-summary { | |
| grid-template-columns: repeat(2, 1fr); | |
| } | |
| } | |
| @media (max-width: 480px) { | |
| .results-summary { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| .summary-card { | |
| text-align: center; | |
| padding: 1.5rem; | |
| border-radius: var(--radius); | |
| background-color: white; | |
| border: 1px solid var(--border); | |
| transition: all 0.3s; | |
| } | |
| .summary-card:hover { | |
| transform: translateY(-3px); | |
| box-shadow: var(--shadow-lg); | |
| } | |
| .summary-value { | |
| font-size: 2.5rem; | |
| font-weight: 800; | |
| margin-bottom: 0.5rem; | |
| background: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%); | |
| -webkit-background-clip: text; | |
| background-clip: text; | |
| color: transparent; | |
| } | |
| .summary-label { | |
| font-size: 0.875rem; | |
| font-weight: 500; | |
| color: var(--text-light); | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| } | |
| /* FIXED RESULTS TABLE - NO HORIZONTAL SCROLLING */ | |
| .results-table-container { | |
| overflow-x: auto; | |
| margin-top: 1.5rem; | |
| border-radius: var(--radius); | |
| border: 1px solid var(--border); | |
| background-color: white; | |
| box-shadow: var(--shadow); | |
| } | |
| .results-table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| min-width: 0; | |
| } | |
| .results-table th { | |
| background-color: #f8fafc; | |
| color: var(--primary); | |
| padding: 0.75rem 0.5rem; | |
| text-align: left; | |
| font-weight: 600; | |
| border-bottom: 2px solid var(--border); | |
| font-size: 0.75rem; | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| white-space: nowrap; | |
| } | |
| .results-table td { | |
| padding: 0.75rem 0.5rem; | |
| border-bottom: 1px solid var(--border); | |
| vertical-align: middle; | |
| font-size: 0.875rem; | |
| } | |
| .results-table tr:hover { | |
| background-color: #f8fafc; | |
| } | |
| .results-table tr:last-child td { | |
| border-bottom: none; | |
| } | |
| /* Fixed column widths for no scrolling */ | |
| .results-table th:nth-child(1), | |
| .results-table td:nth-child(1) { | |
| width: 180px; | |
| min-width: 180px; | |
| max-width: 180px; | |
| } | |
| .results-table th:nth-child(2), | |
| .results-table td:nth-child(2) { | |
| width: 140px; | |
| min-width: 140px; | |
| max-width: 140px; | |
| } | |
| .results-table th:nth-child(3), | |
| .results-table td:nth-child(3) { | |
| width: 150px; | |
| min-width: 150px; | |
| max-width: 150px; | |
| } | |
| .results-table th:nth-child(4), | |
| .results-table td:nth-child(4) { | |
| width: 120px; | |
| min-width: 120px; | |
| max-width: 120px; | |
| } | |
| .results-table th:nth-child(5), | |
| .results-table td:nth-child(5) { | |
| width: 120px; | |
| min-width: 120px; | |
| max-width: 120px; | |
| } | |
| .results-table th:nth-child(6), | |
| .results-table td:nth-child(6) { | |
| width: 80px; | |
| min-width: 80px; | |
| max-width: 80px; | |
| text-align: center; | |
| } | |
| .status-badge { | |
| display: inline-block; | |
| padding: 0.25rem 0.5rem; | |
| border-radius: 2rem; | |
| font-size: 0.7rem; | |
| font-weight: 600; | |
| letter-spacing: 0.025em; | |
| white-space: nowrap; | |
| } | |
| .status-authentic { | |
| background-color: rgba(56, 161, 105, 0.1); | |
| color: var(--success); | |
| border: 1px solid rgba(56, 161, 105, 0.2); | |
| } | |
| .status-review { | |
| background-color: rgba(214, 158, 46, 0.1); | |
| color: var(--warning); | |
| border: 1px solid rgba(214, 158, 46, 0.2); | |
| } | |
| .status-danger { | |
| background-color: rgba(229, 62, 62, 0.1); | |
| color: var(--danger); | |
| border: 1px solid rgba(229, 62, 62, 0.2); | |
| } | |
| .score-indicator { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| min-width: 120px; | |
| } | |
| .score-bar { | |
| flex: 1; | |
| height: 0.375rem; | |
| background-color: var(--border); | |
| border-radius: 1rem; | |
| overflow: hidden; | |
| } | |
| .score-fill { | |
| height: 100%; | |
| border-radius: 1rem; | |
| transition: width 0.5s ease; | |
| } | |
| .score-low { | |
| background: linear-gradient(90deg, var(--success), #68d391); | |
| } | |
| .score-medium { | |
| background: linear-gradient(90deg, var(--warning), #ecc94b); | |
| } | |
| .score-high { | |
| background: linear-gradient(90deg, var(--danger), #fc8181); | |
| } | |
| /* Compact badges */ | |
| .signals-badges, | |
| .evidence-badges { | |
| display: flex; | |
| gap: 0.125rem; | |
| flex-wrap: wrap; | |
| } | |
| .signal-badge { | |
| padding: 0.125rem 0.25rem; | |
| border-radius: 0.375rem; | |
| font-size: 0.65rem; | |
| font-weight: 600; | |
| border: 1px solid; | |
| white-space: nowrap; | |
| letter-spacing: 0.025em; | |
| } | |
| .signal-passed { | |
| background-color: rgba(56, 161, 105, 0.1); | |
| color: var(--success); | |
| border-color: rgba(56, 161, 105, 0.2); | |
| } | |
| .signal-warning { | |
| background-color: rgba(214, 158, 46, 0.1); | |
| color: var(--warning); | |
| border-color: rgba(214, 158, 46, 0.2); | |
| } | |
| .signal-flagged { | |
| background-color: rgba(229, 62, 62, 0.1); | |
| color: var(--danger); | |
| border-color: rgba(229, 62, 62, 0.2); | |
| } | |
| .evidence-badge { | |
| padding: 0.125rem 0.25rem; | |
| border-radius: 0.375rem; | |
| font-size: 0.65rem; | |
| font-weight: 500; | |
| white-space: nowrap; | |
| } | |
| .evidence-ai { | |
| background-color: rgba(229, 62, 62, 0.1); | |
| color: var(--danger); | |
| border: 1px solid rgba(229, 62, 62, 0.2); | |
| } | |
| .evidence-authentic { | |
| background-color: rgba(56, 161, 105, 0.1); | |
| color: var(--success); | |
| border: 1px solid rgba(56, 161, 105, 0.2); | |
| } | |
| .evidence-indeterminate { | |
| background-color: rgba(214, 158, 46, 0.1); | |
| color: var(--warning); | |
| border: 1px solid rgba(214, 158, 46, 0.2); | |
| } | |
| /* Responsive adjustments for table */ | |
| @media (max-width: 1024px) { | |
| .results-table th, | |
| .results-table td { | |
| padding: 0.5rem 0.375rem; | |
| font-size: 0.75rem; | |
| } | |
| .results-table th:nth-child(1), | |
| .results-table td:nth-child(1) { | |
| width: 160px; | |
| min-width: 160px; | |
| max-width: 160px; | |
| } | |
| .results-table th:nth-child(2), | |
| .results-table td:nth-child(2) { | |
| width: 120px; | |
| min-width: 120px; | |
| max-width: 120px; | |
| } | |
| .status-badge { | |
| padding: 0.2rem 0.4rem; | |
| font-size: 0.65rem; | |
| } | |
| } | |
| @media (max-width: 768px) { | |
| /* On smaller screens, switch to vertical scrolling but keep it minimal */ | |
| .results-table-container { | |
| overflow-x: auto; | |
| } | |
| .results-table { | |
| min-width: 700px; | |
| } | |
| } | |
| /* Detailed Analysis */ | |
| .detailed-analysis { | |
| margin-top: 2rem; | |
| padding: 1.5rem; | |
| background-color: white; | |
| border-radius: var(--radius); | |
| border: 1px solid var(--border); | |
| box-shadow: var(--shadow); | |
| } | |
| .analysis-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 0; | |
| cursor: pointer; | |
| padding: 0.5rem; | |
| border-radius: 0.5rem; | |
| transition: background-color 0.3s; | |
| } | |
| .analysis-header:hover { | |
| background-color: #f8fafc; | |
| } | |
| .analysis-content { | |
| display: none; | |
| padding-top: 1.5rem; | |
| animation: fadeIn 0.5s ease; | |
| } | |
| .analysis-content.show { | |
| display: block; | |
| } | |
| .signal-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); | |
| gap: 1rem; | |
| margin-bottom: 1.5rem; | |
| } | |
| .signal-card { | |
| padding: 1rem; | |
| border-radius: 0.5rem; | |
| border: 1px solid var(--border); | |
| background-color: #f8fafc; | |
| transition: all 0.3s; | |
| } | |
| .signal-card:hover { | |
| transform: translateY(-2px); | |
| box-shadow: var(--shadow); | |
| } | |
| /* Footer */ | |
| footer { | |
| margin-top: 3rem; | |
| padding-top: 2rem; | |
| border-top: 1px solid var(--border); | |
| color: var(--text-light); | |
| font-size: 0.875rem; | |
| text-align: center; | |
| } | |
| .footer-links { | |
| display: flex; | |
| justify-content: center; | |
| gap: 2rem; | |
| margin-bottom: 1rem; | |
| flex-wrap: wrap; | |
| } | |
| .footer-link { | |
| color: var(--accent); | |
| text-decoration: none; | |
| transition: color 0.3s; | |
| font-size: 0.875rem; | |
| font-weight: 500; | |
| } | |
| .footer-link:hover { | |
| color: var(--accent-dark); | |
| text-decoration: underline; | |
| } | |
| /* Action buttons */ | |
| .action-button { | |
| padding: 0.5rem 1rem; | |
| border: none; | |
| border-radius: 0.5rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 0.5rem; | |
| font-size: 0.875rem; | |
| letter-spacing: 0.025em; | |
| } | |
| .primary-action { | |
| background-color: var(--accent); | |
| color: white; | |
| box-shadow: var(--shadow); | |
| } | |
| .primary-action:hover { | |
| background-color: var(--accent-dark); | |
| transform: translateY(-2px); | |
| box-shadow: var(--shadow-lg); | |
| } | |
| .secondary-action { | |
| background-color: white; | |
| color: var(--accent); | |
| border: 1px solid var(--accent); | |
| box-shadow: var(--shadow); | |
| } | |
| .secondary-action:hover { | |
| background-color: rgba(49, 130, 206, 0.1); | |
| transform: translateY(-2px); | |
| box-shadow: var(--shadow-lg); | |
| } | |
| /* Loading overlay */ | |
| .loading-overlay { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background-color: rgba(0, 0, 0, 0.7); | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| z-index: 1000; | |
| opacity: 0; | |
| visibility: hidden; | |
| transition: all 0.3s; | |
| backdrop-filter: blur(4px); | |
| } | |
| .loading-overlay.active { | |
| opacity: 1; | |
| visibility: visible; | |
| } | |
| .loading-spinner { | |
| width: 60px; | |
| height: 60px; | |
| border: 4px solid rgba(255, 255, 255, 0.3); | |
| border-radius: 50%; | |
| border-top-color: white; | |
| animation: spin 1s ease-in-out infinite; | |
| margin-bottom: 1rem; | |
| } | |
| .loading-text { | |
| color: white; | |
| font-size: 1rem; | |
| font-weight: 500; | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| /* Toast notification */ | |
| .toast { | |
| position: fixed; | |
| top: 20px; | |
| right: 20px; | |
| padding: 1rem 1.5rem; | |
| background-color: white; | |
| color: var(--text); | |
| border-radius: var(--radius); | |
| box-shadow: var(--shadow-xl); | |
| z-index: 1000; | |
| transform: translateX(100%); | |
| transition: transform 0.3s ease; | |
| max-width: 300px; | |
| border-left: 4px solid var(--accent); | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| } | |
| .toast.show { | |
| transform: translateX(0); | |
| } | |
| .toast.error { | |
| border-left-color: var(--danger); | |
| } | |
| .toast.warning { | |
| border-left-color: var(--warning); | |
| } | |
| .toast.success { | |
| border-left-color: var(--success); | |
| } | |
| /* Utility classes */ | |
| .hidden { | |
| display: none ; | |
| } | |
| .visible { | |
| display: block ; | |
| } | |
| .text-center { | |
| text-align: center; | |
| } | |
| .mt-1 { margin-top: 0.5rem; } | |
| .mt-2 { margin-top: 1rem; } | |
| .mt-3 { margin-top: 1.5rem; } | |
| .mb-1 { margin-bottom: 0.5rem; } | |
| .mb-2 { margin-bottom: 1rem; } | |
| .mb-3 { margin-bottom: 1.5rem; } | |
| /* Responsive adjustments */ | |
| @media (max-width: 768px) { | |
| .hero h2 { | |
| font-size: 2rem; | |
| } | |
| .hero-subtitle { | |
| font-size: 1rem; | |
| } | |
| .tabs { | |
| flex-direction: column; | |
| } | |
| .tab-button { | |
| width: 100%; | |
| text-align: center; | |
| } | |
| .footer-links { | |
| flex-direction: column; | |
| gap: 0.75rem; | |
| } | |
| } | |
| /* Spinner for loading button */ | |
| .spinner { | |
| display: inline-block; | |
| width: 1rem; | |
| height: 1rem; | |
| border: 2px solid rgba(255, 255, 255, 0.3); | |
| border-radius: 50%; | |
| border-top-color: white; | |
| animation: spin 1s ease-in-out infinite; | |
| margin-right: 0.5rem; | |
| } | |
| /* Enhanced image preview in detailed view */ | |
| .image-preview { | |
| position: relative; | |
| border-radius: 0.5rem; | |
| overflow: hidden; | |
| border: 1px solid var(--border); | |
| background: #f8fafc; | |
| } | |
| .image-preview img { | |
| width: 100%; | |
| height: auto; | |
| display: block; | |
| } | |
| /* Info pills */ | |
| .info-pill { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 0.25rem; | |
| padding: 0.25rem 0.5rem; | |
| background-color: #f8fafc; | |
| color: var(--text-light); | |
| border-radius: 1rem; | |
| font-size: 0.75rem; | |
| font-weight: 500; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Loading Overlay --> | |
| <div class="loading-overlay" id="loadingOverlay"> | |
| <div class="loading-spinner"></div> | |
| <div class="loading-text" id="loadingText">Processing...</div> | |
| </div> | |
| <!-- Toast Notification --> | |
| <div class="toast hidden" id="toast"> | |
| <i class="fas fa-info-circle"></i> | |
| <span id="toastMessage"></span> | |
| </div> | |
| <!-- Header --> | |
| <header> | |
| <div class="container"> | |
| <div class="header-content"> | |
| <div class="logo"> | |
| <div class="logo-icon"> | |
| <i class="fas fa-filter"></i> | |
| </div> | |
| <div class="logo-text"> | |
| <h1>AI Image Screener</h1> | |
| <div class="tagline">First-pass screening for bulk workflows</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </header> | |
| <!-- Main Content --> | |
| <div class="container"> | |
| <!-- Landing Screen --> | |
| <div id="landingScreen"> | |
| <!-- Hero Section --> | |
| <section class="hero"> | |
| <div class="hero-content"> | |
| <h2>AI Image Screener</h2> | |
| <p class="hero-subtitle"> | |
| A practical first-pass AI image screening system designed to identify images that require human review based on statistical and physical patterns. | |
| </p> | |
| <div class="performance-badge"> | |
| <i class="fas fa-chart-line"></i> | |
| <span>Screening accuracy: 40-90% detection rate across AI models</span> | |
| </div> | |
| <button class="cta-button" id="tryNowBtn"> | |
| <div class="btn-content"> | |
| <i class="fas fa-play-circle"></i> | |
| Start Screening | |
| </div> | |
| </button> | |
| </div> | |
| </section> | |
| <!-- Tab Navigation --> | |
| <div class="tabs"> | |
| <button class="tab-button active" data-tab="features">Features</button> | |
| <button class="tab-button" data-tab="metrics">Detection Metrics</button> | |
| <button class="tab-button" data-tab="howto">How to Use</button> | |
| </div> | |
| <!-- Features Tab --> | |
| <div class="tab-content active" id="featuresTab"> | |
| <div class="features-grid"> | |
| <div class="feature-card"> | |
| <div class="feature-icon"> | |
| <i class="fas fa-bolt"></i> | |
| </div> | |
| <h3>Fast Processing</h3> | |
| <p>Parallel processing for batch analysis with real-time progress tracking and optimized performance.</p> | |
| </div> | |
| <div class="feature-card"> | |
| <div class="feature-icon"> | |
| <i class="fas fa-chart-bar"></i> | |
| </div> | |
| <h3>Multi-Signal Detection</h3> | |
| <p>Five independent statistical detectors with weighted ensemble aggregation for reliable results.</p> | |
| </div> | |
| <div class="feature-card"> | |
| <div class="feature-icon"> | |
| <i class="fas fa-puzzle-piece"></i> | |
| </div> | |
| <h3>Evidence Analysis</h3> | |
| <p>Aggregates detection signals and metadata into structured evidence, resolving conflicts.</p> | |
| </div> | |
| <div class="feature-card"> | |
| <div class="feature-icon"> | |
| <i class="fas fa-balance-scale"></i> | |
| </div> | |
| <h3>Decision Policy</h3> | |
| <p>Applies deterministic rules over metrics and evidence for review-aware final verdict.</p> | |
| </div> | |
| <div class="feature-card"> | |
| <div class="feature-icon"> | |
| <i class="fas fa-file-export"></i> | |
| </div> | |
| <h3>Comprehensive Reports</h3> | |
| <p>Export results in CSV and JSON formats for integration, documentation, and audit trails.</p> | |
| </div> | |
| </div> | |
| <!-- Caution Notice --> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h3 class="card-title"><i class="fas fa-exclamation-triangle" style="color: var(--warning);"></i> Important Notice</h3> | |
| </div> | |
| <p style="color: var(--text-light); font-size: 0.9375rem; line-height: 1.6;"> | |
| <strong>This is not a perfect AI detector.</strong> It's a screening tool that helps reduce manual review workload by flagging suspicious images for human verification. Always use human judgment for final decisions. | |
| </p> | |
| </div> | |
| </div> | |
| <!-- Metrics Tab - UPDATED with proper separation --> | |
| <div class="tab-content" id="metricsTab"> | |
| <!-- Signal-based Metrics Section --> | |
| <div class="metrics-section"> | |
| <div class="section-header"> | |
| <div class="section-icon"> | |
| <i class="fas fa-chart-bar"></i> | |
| </div> | |
| <div> | |
| <div class="section-title">Signal-based Detection Metrics</div> | |
| <div class="section-subtitle">Statistical analysis of image properties with weighted scoring</div> | |
| </div> | |
| </div> | |
| <div class="signal-metrics-grid"> | |
| <div class="metric-card signal"> | |
| <div class="metric-header"> | |
| <div class="metric-icon" style="background: linear-gradient(135deg, #4a5568 0%, #718096 100%);"> | |
| <i class="fas fa-wave-square"></i> | |
| </div> | |
| <div> | |
| <div class="metric-title">Gradient-Field PCA</div> | |
| </div> | |
| <span class="metric-weight">30%</span> | |
| </div> | |
| <p class="metric-description"> | |
| Detects lighting & gradient inconsistencies typical of diffusion models. Analyzes directional light patterns and shadow consistency. | |
| </p> | |
| <div class="metric-details"> | |
| <div class="detail-item"> | |
| <span class="detail-label">Method</span> | |
| <span class="detail-value">Principal Component Analysis</span> | |
| </div> | |
| <div class="detail-item"> | |
| <span class="detail-label">Detection Rate</span> | |
| <span class="detail-value">85-95%</span> | |
| </div> | |
| <div class="detail-item"> | |
| <span class="detail-label">Sensitivity</span> | |
| <span class="detail-value">High</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="metric-card signal"> | |
| <div class="metric-header"> | |
| <div class="metric-icon" style="background: linear-gradient(135deg, #718096 0%, #a0aec0 100%);"> | |
| <i class="fas fa-chart-line"></i> | |
| </div> | |
| <div> | |
| <div class="metric-title">Frequency Analysis</div> | |
| </div> | |
| <span class="metric-weight">25%</span> | |
| </div> | |
| <p class="metric-description"> | |
| Identifies unnatural spectral energy distributions via FFT analysis. AI-generated images often show characteristic frequency patterns. | |
| </p> | |
| <div class="metric-details"> | |
| <div class="detail-item"> | |
| <span class="detail-label">Method</span> | |
| <span class="detail-value">Fast Fourier Transform</span> | |
| </div> | |
| <div class="detail-item"> | |
| <span class="detail-label">Detection Rate</span> | |
| <span class="detail-value">75-85%</span> | |
| </div> | |
| <div class="detail-item"> | |
| <span class="detail-label">Sensitivity</span> | |
| <span class="detail-value">Medium-High</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="metric-card signal"> | |
| <div class="metric-header"> | |
| <div class="metric-icon" style="background: linear-gradient(135deg, #38a169 0%, #68d391 100%);"> | |
| <i class="fas fa-braille"></i> | |
| </div> | |
| <div> | |
| <div class="metric-title">Noise Pattern</div> | |
| </div> | |
| <span class="metric-weight">20%</span> | |
| </div> | |
| <p class="metric-description"> | |
| Detects missing or artificial sensor noise patterns. Real cameras produce characteristic noise while AI models generate uniform patterns. | |
| </p> | |
| <div class="metric-details"> | |
| <div class="detail-item"> | |
| <span class="detail-label">Method</span> | |
| <span class="detail-value">Noise Distribution</span> | |
| </div> | |
| <div class="detail-item"> | |
| <span class="detail-label">Detection Rate</span> | |
| <span class="detail-value">70-80%</span> | |
| </div> | |
| <div class="detail-item"> | |
| <span class="detail-label">Sensitivity</span> | |
| <span class="detail-value">Medium</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="metric-card signal"> | |
| <div class="metric-header"> | |
| <div class="metric-icon" style="background: linear-gradient(135deg, #d69e2e 0%, #ecc94b 100%);"> | |
| <i class="fas fa-text-height"></i> | |
| </div> | |
| <div> | |
| <div class="metric-title">Texture Statistics</div> | |
| </div> | |
| <span class="metric-weight">15%</span> | |
| </div> | |
| <p class="metric-description"> | |
| Identifies overly smooth or uniform texture regions. AI-generated images often lack natural texture variation found in real photographs. | |
| </p> | |
| <div class="metric-details"> | |
| <div class="detail-item"> | |
| <span class="detail-label">Method</span> | |
| <span class="detail-value">GLCM Texture</span> | |
| </div> | |
| <div class="detail-item"> | |
| <span class="detail-label">Detection Rate</span> | |
| <span class="detail-value">60-70%</span> | |
| </div> | |
| <div class="detail-item"> | |
| <span class="detail-label">Sensitivity</span> | |
| <span class="detail-value">Medium-Low</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="metric-card signal"> | |
| <div class="metric-header"> | |
| <div class="metric-icon" style="background: linear-gradient(135deg, #e53e3e 0%, #fc8181 100%);"> | |
| <i class="fas fa-palette"></i> | |
| </div> | |
| <div> | |
| <div class="metric-title">Color Distribution</div> | |
| </div> | |
| <span class="metric-weight">10%</span> | |
| </div> | |
| <p class="metric-description"> | |
| Flags unnatural saturation and color histogram patterns. AI models produce colors with distribution patterns different from real photographs. | |
| </p> | |
| <div class="metric-details"> | |
| <div class="detail-item"> | |
| <span class="detail-label">Method</span> | |
| <span class="detail-value">Color Histogram</span> | |
| </div> | |
| <div class="detail-item"> | |
| <span class="detail-label">Detection Rate</span> | |
| <span class="detail-value">50-65%</span> | |
| </div> | |
| <div class="detail-item"> | |
| <span class="detail-label">Sensitivity</span> | |
| <span class="detail-value">Low-Medium</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Evidence-based Metrics Section --> | |
| <div class="metrics-section"> | |
| <div class="section-header"> | |
| <div class="section-icon evidence"> | |
| <i class="fas fa-clipboard-check"></i> | |
| </div> | |
| <div> | |
| <div class="section-title">Evidence-based Verification</div> | |
| <div class="section-subtitle">Additional verification through metadata and watermark analysis</div> | |
| </div> | |
| </div> | |
| <div class="evidence-metrics-grid"> | |
| <div class="metric-card evidence"> | |
| <div class="metric-header"> | |
| <div class="metric-icon" style="background: linear-gradient(135deg, #2b6cb0 0%, #63b3ed 100%);"> | |
| <i class="fas fa-camera-retro"></i> | |
| </div> | |
| <div> | |
| <div class="metric-title">EXIF Analyzer</div> | |
| </div> | |
| <span class="metric-weight evidence">Evidence</span> | |
| </div> | |
| <p class="metric-description"> | |
| Analyzes image metadata for presence, completeness, and plausibility. Real camera images contain coherent EXIF data while AI-generated images often lack or have inconsistent metadata. | |
| </p> | |
| <div class="metric-details"> | |
| <div class="detail-item"> | |
| <span class="detail-label">Method</span> | |
| <span class="detail-value">Metadata Analysis</span> | |
| </div> | |
| <div class="detail-item"> | |
| <span class="detail-label">Key Signals</span> | |
| <span class="detail-value">Missing/Inconsistent EXIF</span> | |
| </div> | |
| <div class="detail-item"> | |
| <span class="detail-label">Verification</span> | |
| <span class="detail-value">Medium</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="metric-card evidence"> | |
| <div class="metric-header"> | |
| <div class="metric-icon" style="background: linear-gradient(135deg, #6b46c1 0%, #b794f4 100%);"> | |
| <i class="fas fa-fingerprint"></i> | |
| </div> | |
| <div> | |
| <div class="metric-title">Watermark Analyzer</div> | |
| </div> | |
| <span class="metric-weight evidence">Evidence</span> | |
| </div> | |
| <p class="metric-description"> | |
| Detects known and statistical watermark patterns embedded by generative models. Includes checks for frequency-domain artifacts and spatial regularities associated with AI watermarking techniques. | |
| </p> | |
| <div class="metric-details"> | |
| <div class="detail-item"> | |
| <span class="detail-label">Method</span> | |
| <span class="detail-value">Pattern Analysis</span> | |
| </div> | |
| <div class="detail-item"> | |
| <span class="detail-label">Key Signals</span> | |
| <span class="detail-value">Watermark Artifacts</span> | |
| </div> | |
| <div class="detail-item"> | |
| <span class="detail-label">Verification</span> | |
| <span class="detail-value">Model-Dependent</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- How-to-use Tab --> | |
| <div class="tab-content" id="howtoTab"> | |
| <div class="steps-grid"> | |
| <div class="step-card"> | |
| <div class="step-number">1</div> | |
| <h3>Upload Images</h3> | |
| <p>Drag & drop or select multiple images (JPG, PNG, WEBP formats supported).</p> | |
| </div> | |
| <div class="step-card"> | |
| <div class="step-number">2</div> | |
| <h3>Start Analysis</h3> | |
| <p>Click "Start Analysis" to begin screening with real-time progress tracking.</p> | |
| </div> | |
| <div class="step-card"> | |
| <div class="step-number">3</div> | |
| <h3>Review Results</h3> | |
| <p>Check flagged images, view detailed analysis, and export comprehensive reports.</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Analysis Screen (Initially Hidden) --> | |
| <div id="analysisScreen" class="hidden"> | |
| <!-- Upload Card --> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h2 class="card-title"><i class="fas fa-cloud-upload-alt"></i> Upload Images</h2> | |
| <button class="action-button secondary-action" id="backHomeBtn"> | |
| <i class="fas fa-arrow-left"></i> Back to Home | |
| </button> | |
| </div> | |
| <div class="upload-area" id="uploadArea"> | |
| <div class="upload-icon"> | |
| <i class="fas fa-cloud-upload-alt"></i> | |
| </div> | |
| <h3 class="upload-text">Drag & drop images here</h3> | |
| <p class="upload-text">or</p> | |
| <div class="upload-button" id="fileInputBtn"> | |
| <i class="fas fa-folder-open"></i> Browse Files | |
| </div> | |
| <input type="file" id="fileInput" multiple accept=".jpg,.jpeg,.png,.webp" style="display: none;"> | |
| <p class="text-center mt-2" style="color: var(--text-light); font-size: 0.875rem;"> | |
| Supports JPG, JPEG, PNG, WEBP • Maximum 10MB per file | |
| </p> | |
| </div> | |
| <!-- Thumbnail Grid --> | |
| <div class="thumbnail-grid" id="thumbnailGrid"></div> | |
| <!-- Start Analysis Button --> | |
| <div class="mt-3" id="analyzeButtonContainer" style="display: none;"> | |
| <button class="start-analysis-btn" id="analyzeBtn"> | |
| <div class="btn-content"> | |
| <i class="fas fa-play"></i> Start Analysis | |
| </div> | |
| </button> | |
| </div> | |
| <div class="progress-container hidden" id="progressContainer"> | |
| <div class="progress-header"> | |
| <span style="font-weight: 500;">Processing Images</span> | |
| <span id="progressPercent" style="font-weight: 600;">0%</span> | |
| </div> | |
| <div class="progress-bar"> | |
| <div class="progress-fill" id="progressFill"></div> | |
| </div> | |
| <div class="progress-details" style="display: flex; justify-content: space-between; font-size: 0.875rem; color: var(--text-light);"> | |
| <span id="currentFile" class="current-file">Ready to process</span> | |
| <span id="progressStats">0 / 0</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Results Section --> | |
| <div id="resultsSection" class="hidden"> | |
| <!-- Results Card --> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h2 class="card-title"><i class="fas fa-chart-bar"></i> Analysis Results</h2> | |
| <div class="results-actions" style="display: flex; gap: 0.5rem;"> | |
| <button class="action-button secondary-action" id="exportCsvBtn"> | |
| <i class="fas fa-file-csv"></i> CSV | |
| </button> | |
| <button class="action-button secondary-action" id="exportJsonBtn"> | |
| <i class="fas fa-file-code"></i> JSON | |
| </button> | |
| <button class="action-button secondary-action" id="newAnalysisBtn"> | |
| <i class="fas fa-redo"></i> New Analysis | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Results Summary --> | |
| <div class="results-summary" id="resultsSummary"> | |
| <!-- Summary cards will be populated here --> | |
| </div> | |
| <!-- Results Table - FIXED FOR NO SCROLLING --> | |
| <div class="results-table-container"> | |
| <table class="results-table" id="resultsTable"> | |
| <thead> | |
| <tr> | |
| <th>Image</th> | |
| <th>Status</th> | |
| <th>Score</th> | |
| <th>Signals</th> | |
| <th>Evidence</th> | |
| <th>Details</th> | |
| </tr> | |
| </thead> | |
| <tbody id="resultsTableBody"> | |
| <!-- Results will be populated here --> | |
| <tr id="noResultsRow"> | |
| <td colspan="6" class="text-center" style="padding: 3rem; color: var(--text-light);"> | |
| <i class="fas fa-chart-bar" style="font-size: 3rem; margin-bottom: 1rem; opacity: 0.5;"></i> | |
| <p>No analysis results yet. Upload images and click "Start Analysis" to begin.</p> | |
| </td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <!-- Detailed Analysis --> | |
| <div class="detailed-analysis"> | |
| <div class="analysis-header" id="toggleDetailedAnalysis"> | |
| <h3><i class="fas fa-search"></i> Detailed Analysis</h3> | |
| <i class="fas fa-chevron-down" id="detailedAnalysisIcon"></i> | |
| </div> | |
| <div class="analysis-content" id="detailedAnalysisContent"> | |
| <!-- Detailed analysis will be populated here --> | |
| <p id="noDetailedAnalysis" class="text-center" style="color: var(--text-light); padding: 2rem;"> | |
| <i class="fas fa-eye" style="font-size: 2rem; margin-bottom: 1rem; opacity: 0.5;"></i><br> | |
| Select an image to view detailed analysis | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Footer --> | |
| <footer> | |
| <div class="container"> | |
| <div class="footer-links"> | |
| <a href="#" class="footer-link">Documentation</a> | |
| <a href="#" class="footer-link">API Reference</a> | |
| <a href="#" class="footer-link">Privacy Policy</a> | |
| <a href="#" class="footer-link">Support</a> | |
| <a href="#" class="footer-link">GitHub</a> | |
| </div> | |
| <p style="margin-top: 0.5rem;">AI Image Screener v1.0.0 © 2025</p> | |
| </div> | |
| </footer> | |
| <script> | |
| // API Configuration | |
| const API_BASE_URL = window.location.origin; | |
| const BATCH_ENDPOINT = '/analyze/batch'; | |
| const HEALTH_ENDPOINT = '/health'; | |
| const BATCH_PROGRESS_ENDPOINT = '/batch'; | |
| const CSV_REPORT_ENDPOINT = '/report/csv'; | |
| // Global state | |
| let files = []; | |
| let fileDataUrls = {}; | |
| let currentBatchId = null; | |
| let batchResults = null; | |
| let pollingInterval = null; | |
| let selectedImageIndex = null; | |
| // DOM Elements | |
| const landingScreen = document.getElementById('landingScreen'); | |
| const analysisScreen = document.getElementById('analysisScreen'); | |
| const resultsSection = document.getElementById('resultsSection'); | |
| const loadingOverlay = document.getElementById('loadingOverlay'); | |
| const toast = document.getElementById('toast'); | |
| const toastMessage = document.getElementById('toastMessage'); | |
| const loadingText = document.getElementById('loadingText'); | |
| const tryNowBtn = document.getElementById('tryNowBtn'); | |
| const backHomeBtn = document.getElementById('backHomeBtn'); | |
| const newAnalysisBtn = document.getElementById('newAnalysisBtn'); | |
| const uploadArea = document.getElementById('uploadArea'); | |
| const fileInput = document.getElementById('fileInput'); | |
| const fileInputBtn = document.getElementById('fileInputBtn'); | |
| const thumbnailGrid = document.getElementById('thumbnailGrid'); | |
| const analyzeBtn = document.getElementById('analyzeBtn'); | |
| const analyzeButtonContainer = document.getElementById('analyzeButtonContainer'); | |
| const progressContainer = document.getElementById('progressContainer'); | |
| const progressFill = document.getElementById('progressFill'); | |
| const progressPercent = document.getElementById('progressPercent'); | |
| const currentFile = document.getElementById('currentFile'); | |
| const progressStats = document.getElementById('progressStats'); | |
| const resultsSummary = document.getElementById('resultsSummary'); | |
| const resultsTableBody = document.getElementById('resultsTableBody'); | |
| const noResultsRow = document.getElementById('noResultsRow'); | |
| const exportCsvBtn = document.getElementById('exportCsvBtn'); | |
| const exportJsonBtn = document.getElementById('exportJsonBtn'); | |
| const toggleDetailedAnalysis = document.getElementById('toggleDetailedAnalysis'); | |
| const detailedAnalysisIcon = document.getElementById('detailedAnalysisIcon'); | |
| const detailedAnalysisContent = document.getElementById('detailedAnalysisContent'); | |
| const noDetailedAnalysis = document.getElementById('noDetailedAnalysis'); | |
| const tabButtons = document.querySelectorAll('.tab-button'); | |
| const tabContents = document.querySelectorAll('.tab-content'); | |
| // Initialize | |
| document.addEventListener('DOMContentLoaded', () => { | |
| setupEventListeners(); | |
| setupTabs(); | |
| checkApiHealth(); | |
| }); | |
| // Toast notification | |
| function showToast(message, type = 'success') { | |
| toastMessage.textContent = message; | |
| toast.className = `toast ${type} show`; | |
| // Set appropriate icon | |
| const icon = toast.querySelector('i'); | |
| if (type === 'error') { | |
| icon.className = 'fas fa-exclamation-circle'; | |
| } else if (type === 'warning') { | |
| icon.className = 'fas fa-exclamation-triangle'; | |
| } else { | |
| icon.className = 'fas fa-check-circle'; | |
| } | |
| setTimeout(() => { | |
| toast.classList.remove('show'); | |
| }, 3000); | |
| } | |
| // Loading overlay | |
| function showLoading(show, message = 'Processing...') { | |
| if (show) { | |
| loadingText.textContent = message; | |
| loadingOverlay.classList.add('active'); | |
| } else { | |
| loadingOverlay.classList.remove('active'); | |
| } | |
| } | |
| // Tab functionality | |
| function setupTabs() { | |
| tabButtons.forEach(button => { | |
| button.addEventListener('click', () => { | |
| const tabId = button.dataset.tab + 'Tab'; | |
| // Remove active class from all buttons and contents | |
| tabButtons.forEach(btn => btn.classList.remove('active')); | |
| tabContents.forEach(content => content.classList.remove('active')); | |
| // Add active class to clicked button and corresponding content | |
| button.classList.add('active'); | |
| document.getElementById(tabId).classList.add('active'); | |
| }); | |
| }); | |
| } | |
| // Setup event listeners | |
| function setupEventListeners() { | |
| // Navigation | |
| tryNowBtn.addEventListener('click', showAnalysisScreen); | |
| backHomeBtn.addEventListener('click', showLandingScreen); | |
| newAnalysisBtn.addEventListener('click', resetAnalysis); | |
| // File upload | |
| fileInputBtn.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| fileInput.click(); | |
| }); | |
| // File input change handler | |
| fileInput.addEventListener('change', handleFileSelect); | |
| // Upload area drag and drop handlers | |
| uploadArea.addEventListener('dragover', handleDragOver); | |
| uploadArea.addEventListener('dragleave', handleDragLeave); | |
| uploadArea.addEventListener('drop', handleDrop); | |
| // Analysis | |
| analyzeBtn.addEventListener('click', startAnalysis); | |
| // Export | |
| exportCsvBtn.addEventListener('click', exportCsv); | |
| exportJsonBtn.addEventListener('click', exportJson); | |
| // Detailed analysis toggle | |
| toggleDetailedAnalysis.addEventListener('click', () => { | |
| detailedAnalysisContent.classList.toggle('show'); | |
| detailedAnalysisIcon.classList.toggle('fa-chevron-down'); | |
| detailedAnalysisIcon.classList.toggle('fa-chevron-up'); | |
| }); | |
| } | |
| // Screen navigation | |
| function showLandingScreen() { | |
| landingScreen.classList.remove('hidden'); | |
| analysisScreen.classList.add('hidden'); | |
| window.scrollTo({ top: 0, behavior: 'smooth' }); | |
| } | |
| function showAnalysisScreen() { | |
| landingScreen.classList.add('hidden'); | |
| analysisScreen.classList.remove('hidden'); | |
| window.scrollTo({ top: 0, behavior: 'smooth' }); | |
| } | |
| // File handling | |
| function handleDragOver(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| uploadArea.classList.add('dragover'); | |
| } | |
| function handleDragLeave(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| uploadArea.classList.remove('dragover'); | |
| } | |
| function handleDrop(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| uploadArea.classList.remove('dragover'); | |
| const droppedFiles = Array.from(e.dataTransfer.files); | |
| if (droppedFiles.length > 0) { | |
| processFiles(droppedFiles); | |
| } | |
| } | |
| function handleFileSelect(e) { | |
| const selectedFiles = Array.from(e.target.files); | |
| if (selectedFiles.length > 0) { | |
| processFiles(selectedFiles); | |
| } | |
| e.target.value = ''; | |
| } | |
| async function processFiles(newFiles) { | |
| const validFiles = []; | |
| for (const file of newFiles) { | |
| const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']; | |
| const maxSize = 10 * 1024 * 1024; | |
| if (!validTypes.includes(file.type)) { | |
| showToast(`File ${file.name} is not a supported image type.`, 'error'); | |
| continue; | |
| } | |
| if (file.size > maxSize) { | |
| showToast(`File ${file.name} exceeds the 10MB size limit.`, 'error'); | |
| continue; | |
| } | |
| validFiles.push(file); | |
| } | |
| if (validFiles.length > 0) { | |
| showLoading(true, 'Processing thumbnails...'); | |
| try { | |
| // Generate thumbnails | |
| for (const file of validFiles) { | |
| try { | |
| const dataUrl = await createThumbnail(file); | |
| fileDataUrls[file.name] = dataUrl; | |
| } catch (error) { | |
| console.error('Failed to create thumbnail:', error); | |
| fileDataUrls[file.name] = null; | |
| } | |
| } | |
| files.push(...validFiles); | |
| updateThumbnailGrid(); | |
| showToast(`Added ${validFiles.length} file${validFiles.length > 1 ? 's' : ''}`, 'success'); | |
| } catch (error) { | |
| console.error('Error processing files:', error); | |
| showToast('Error processing files. Please try again.', 'error'); | |
| } finally { | |
| showLoading(false); | |
| } | |
| } | |
| } | |
| function createThumbnail(file) { | |
| return new Promise((resolve, reject) => { | |
| const reader = new FileReader(); | |
| reader.onload = (e) => { | |
| const img = new Image(); | |
| img.onload = () => { | |
| const canvas = document.createElement('canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| // Set canvas dimensions for thumbnail | |
| const maxSize = 120; | |
| let width = img.width; | |
| let height = img.height; | |
| if (width > height) { | |
| if (width > maxSize) { | |
| height *= maxSize / width; | |
| width = maxSize; | |
| } | |
| } else { | |
| if (height > maxSize) { | |
| width *= maxSize / height; | |
| height = maxSize; | |
| } | |
| } | |
| canvas.width = width; | |
| canvas.height = height; | |
| // Draw image with better quality | |
| ctx.imageSmoothingEnabled = true; | |
| ctx.imageSmoothingQuality = 'high'; | |
| ctx.drawImage(img, 0, 0, width, height); | |
| resolve(canvas.toDataURL('image/jpeg', 0.8)); | |
| }; | |
| img.onerror = reject; | |
| img.src = e.target.result; | |
| }; | |
| reader.onerror = reject; | |
| reader.readAsDataURL(file); | |
| }); | |
| } | |
| function updateThumbnailGrid() { | |
| thumbnailGrid.innerHTML = ''; | |
| if (files.length === 0) { | |
| thumbnailGrid.style.display = 'none'; | |
| analyzeButtonContainer.style.display = 'none'; | |
| return; | |
| } | |
| thumbnailGrid.style.display = 'grid'; | |
| analyzeButtonContainer.style.display = 'block'; | |
| files.forEach((file, index) => { | |
| const thumbnailItem = document.createElement('div'); | |
| thumbnailItem.className = 'thumbnail-item'; | |
| thumbnailItem.dataset.index = index; | |
| const dataUrl = fileDataUrls[file.name] || 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><rect width="100" height="100" fill="%23f0f0f0"/><text x="50" y="50" font-family="Arial" font-size="14" text-anchor="middle" fill="%23999">No preview</text></svg>'; | |
| // Truncate filename if too long | |
| const displayName = file.name.length > 20 ? | |
| file.name.substring(0, 17) + '...' : | |
| file.name; | |
| thumbnailItem.innerHTML = ` | |
| <img src="${dataUrl}" alt="${file.name}" class="thumbnail-img" loading="lazy"> | |
| <div class="thumbnail-overlay"> | |
| <span title="${file.name}"> | |
| ${displayName} | |
| </span> | |
| <button class="remove-thumbnail" data-index="${index}" title="Remove"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| `; | |
| thumbnailGrid.appendChild(thumbnailItem); | |
| }); | |
| // Add event listeners to remove buttons | |
| document.querySelectorAll('.remove-thumbnail').forEach(btn => { | |
| btn.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| const index = parseInt(e.currentTarget.dataset.index); | |
| removeFile(index); | |
| }); | |
| }); | |
| } | |
| function removeFile(index) { | |
| const removedFile = files[index].name; | |
| files.splice(index, 1); | |
| delete fileDataUrls[removedFile]; | |
| updateThumbnailGrid(); | |
| showToast(`Removed ${removedFile}`, 'warning'); | |
| } | |
| // Analysis | |
| async function startAnalysis() { | |
| if (files.length === 0) return; | |
| showLoading(true, 'Running analysis...'); | |
| analyzeBtn.disabled = true; | |
| analyzeBtn.innerHTML = '<span class="spinner"></span> Processing...'; | |
| progressFill.style.width = '0%'; | |
| progressPercent.textContent = '0%'; | |
| currentFile.textContent = 'Preparing analysis...'; | |
| progressStats.textContent = `0 / ${files.length}`; | |
| progressContainer.classList.remove('hidden'); | |
| clearResults(); | |
| const formData = new FormData(); | |
| files.forEach(file => { | |
| formData.append('files', file); | |
| }); | |
| try { | |
| console.log('Sending batch request for', files.length, 'images...'); | |
| const response = await fetch(BATCH_ENDPOINT, { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| console.log('Response status:', response.status); | |
| if (!response.ok) { | |
| const errorText = await response.text(); | |
| throw new Error(`HTTP ${response.status}: ${errorText}`); | |
| } | |
| const apiResponse = await response.json(); | |
| console.log('API response:', apiResponse); | |
| if (!apiResponse.success) { | |
| throw new Error(apiResponse.message || 'API request failed'); | |
| } | |
| const data = apiResponse.data; | |
| console.log('Data:', data); | |
| if (data && data.batch_id) { | |
| console.log('Polling mode: batch_id =', data.batch_id); | |
| currentBatchId = data.batch_id; | |
| showToast('Analysis started. Processing in background...', 'success'); | |
| showLoading(false); | |
| startPollingProgress(); | |
| } else if (data && data.result) { | |
| console.log('Immediate results mode'); | |
| progressFill.style.width = '100%'; | |
| progressPercent.textContent = '100%'; | |
| currentFile.textContent = 'Processing complete!'; | |
| progressStats.textContent = `${files.length} / ${files.length}`; | |
| batchResults = data.result; | |
| setTimeout(() => { | |
| displayResults(); | |
| resetUI(); | |
| resultsSection.classList.remove('hidden'); | |
| document.getElementById('resultsSection').scrollIntoView({ | |
| behavior: 'smooth', | |
| block: 'start' | |
| }); | |
| showToast(`Analysis complete! Processed ${files.length} image${files.length > 1 ? 's' : ''}`, 'success'); | |
| }, 500); | |
| } else { | |
| console.error('Unexpected response format:', apiResponse); | |
| throw new Error('Invalid response format from server'); | |
| } | |
| } catch (error) { | |
| console.error('Analysis failed:', error); | |
| showLoading(false); | |
| showToast('Analysis failed: ' + error.message, 'error'); | |
| resetUI(); | |
| } | |
| } | |
| function startPollingProgress() { | |
| if (pollingInterval) clearInterval(pollingInterval); | |
| pollingInterval = setInterval(async () => { | |
| try { | |
| const response = await fetch(`${BATCH_PROGRESS_ENDPOINT}/${currentBatchId}/progress`); | |
| const data = await response.json(); | |
| const sessionData = data.data || data; | |
| if (sessionData.status === 'completed') { | |
| clearInterval(pollingInterval); | |
| if (sessionData.result) { | |
| batchResults = sessionData.result; | |
| } else { | |
| batchResults = sessionData; | |
| } | |
| displayResults(); | |
| resetUI(); | |
| resultsSection.classList.remove('hidden'); | |
| document.getElementById('resultsSection').scrollIntoView({ | |
| behavior: 'smooth', | |
| block: 'start' | |
| }); | |
| showToast('Batch analysis completed!', 'success'); | |
| } else if (sessionData.status === 'processing') { | |
| const progress = sessionData.progress; | |
| if (progress) { | |
| const percent = Math.round((progress.current / progress.total) * 100); | |
| progressFill.style.width = `${percent}%`; | |
| progressPercent.textContent = `${percent}%`; | |
| currentFile.textContent = progress.filename || 'Processing...'; | |
| progressStats.textContent = `${progress.current} / ${progress.total}`; | |
| } | |
| } else if (sessionData.status === 'failed' || sessionData.status === 'interrupted') { | |
| clearInterval(pollingInterval); | |
| showToast(`Analysis failed: ${sessionData.error || 'Unknown error'}`, 'error'); | |
| resetUI(); | |
| } | |
| } catch (error) { | |
| console.error('Progress polling failed:', error); | |
| } | |
| }, 1000); | |
| } | |
| function summarizeEvidence(evidence = []) { | |
| const summary = { | |
| AI: 0, | |
| AUTHENTIC: 0, | |
| INDETERMINATE: 0 | |
| }; | |
| evidence.forEach(ev => { | |
| switch ((ev.direction || '').toLowerCase()) { | |
| case 'ai_generated': | |
| summary.AI++; | |
| break; | |
| case 'authentic': | |
| summary.AUTHENTIC++; | |
| break; | |
| case 'indeterminate': | |
| summary.INDETERMINATE++; | |
| break; | |
| } | |
| }); | |
| return summary; | |
| } | |
| function decisionMeta(decision) { | |
| const normalized = (decision || '').toUpperCase(); | |
| switch (normalized) { | |
| case 'MOSTLY_AUTHENTIC': | |
| return { | |
| label: 'Mostly Authentic', | |
| badgeClass: 'status-authentic', | |
| recommendation: 'No immediate action required', | |
| icon: 'fa-check-circle' | |
| }; | |
| case 'AUTHENTIC_BUT_REVIEW': | |
| return { | |
| label: 'Authentic But Review', | |
| badgeClass: 'status-review', | |
| recommendation: 'Optional human review recommended', | |
| icon: 'fa-eye' | |
| }; | |
| case 'SUSPICIOUS_AI_LIKELY': | |
| return { | |
| label: 'Suspicious AI Likely', | |
| badgeClass: 'status-review', | |
| recommendation: 'Manual verification recommended', | |
| icon: 'fa-exclamation-triangle' | |
| }; | |
| case 'CONFIRMED_AI_GENERATED': | |
| return { | |
| label: 'Confirmed AI Generated', | |
| badgeClass: 'status-danger', | |
| recommendation: 'Block or audit required', | |
| icon: 'fa-times-circle' | |
| }; | |
| default: | |
| console.warn('Unknown decision:', decision); | |
| return { | |
| label: decision || 'UNKNOWN', | |
| badgeClass: 'status-review', | |
| recommendation: 'Manual review required', | |
| icon: 'fa-question-circle' | |
| }; | |
| } | |
| } | |
| function displayResults() { | |
| if (!batchResults) { | |
| console.error('No results to display:', batchResults); | |
| return; | |
| } | |
| console.log('Displaying batch results:', batchResults); | |
| const results = batchResults.results || []; | |
| console.log('Results array:', results); | |
| updateSummary(batchResults); | |
| resultsTableBody.innerHTML = ''; | |
| results.forEach((result, index) => { | |
| const row = document.createElement('tr'); | |
| row.dataset.index = index; | |
| const resultData = result; | |
| const filename = resultData.filename || 'Unknown'; | |
| const overallScore = resultData.overall_score || 0; | |
| const decision = resultData.final_decision; | |
| const meta = decisionMeta(decision); | |
| const confidence = resultData.confidence || 0; | |
| const imageSize = resultData.image_size || [0, 0]; | |
| const signals = resultData.signals || []; | |
| const evidence = resultData.evidence || []; | |
| const evidenceSummary = summarizeEvidence(evidence); | |
| const processingTime = resultData.processing_time || 0; | |
| const scorePercent = Math.round(overallScore * 100); | |
| let scoreClass = 'score-low'; | |
| if (scorePercent >= 70) scoreClass = 'score-high'; | |
| else if (scorePercent >= 50) scoreClass = 'score-medium'; | |
| const scoreWidth = `${Math.min(scorePercent, 100)}%`; | |
| const flaggedCount = signals.filter(s => s.status === 'flagged').length; | |
| const warningCount = signals.filter(s => s.status === 'warning').length; | |
| const passedCount = signals.filter(s => s.status === 'passed').length; | |
| // Get thumbnail | |
| const thumbnailSrc = fileDataUrls[filename] || 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><rect width="32" height="32" fill="%23f0f0f0"/></svg>'; | |
| // Truncate filename if too long | |
| const displayFilename = filename.length > 25 ? | |
| filename.substring(0, 22) + '...' : | |
| filename; | |
| row.innerHTML = ` | |
| <td> | |
| <div style="display: flex; align-items: center; gap: 0.5rem;"> | |
| <img src="${thumbnailSrc}" alt="${filename}" style="width: 32px; height: 32px; object-fit: cover; border-radius: 0.25rem; border: 1px solid var(--border);"> | |
| <div style="min-width: 0;"> | |
| <div style="font-weight: 600; font-size: 0.75rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="${filename}">${displayFilename}</div> | |
| <div style="font-size: 0.65rem; color: var(--text-light);"> | |
| ${imageSize[0]}×${imageSize[1]} | |
| </div> | |
| </div> | |
| </div> | |
| </td> | |
| <td> | |
| <span class="status-badge ${meta.badgeClass}"> | |
| <i class="fas ${meta.icon}" style="margin-right: 0.125rem; font-size: 0.7rem;"></i> | |
| ${meta.label} | |
| </span> | |
| </td> | |
| <td> | |
| <div class="score-indicator"> | |
| <span style="min-width: 30px; font-size: 0.75rem; font-weight: 600;">${scorePercent}%</span> | |
| <div class="score-bar"> | |
| <div class="score-fill ${scoreClass}" style="width: ${scoreWidth}"></div> | |
| </div> | |
| </div> | |
| </td> | |
| <td> | |
| <div class="signals-badges"> | |
| ${flaggedCount > 0 ? `<span class="signal-badge signal-flagged">${flaggedCount}F</span>` : ''} | |
| ${warningCount > 0 ? `<span class="signal-badge signal-warning">${warningCount}W</span>` : ''} | |
| ${passedCount > 0 ? `<span class="signal-badge signal-passed">${passedCount}P</span>` : ''} | |
| </div> | |
| </td> | |
| <td> | |
| <div class="evidence-badges"> | |
| ${evidenceSummary.AI > 0 | |
| ? `<span class="evidence-badge evidence-ai" title="AI Evidence"> | |
| <i class="fas fa-robot" style="margin-right: 0.125rem; font-size: 0.6rem;"></i> | |
| ${evidenceSummary.AI} | |
| </span>` | |
| : ''} | |
| ${evidenceSummary.AUTHENTIC > 0 | |
| ? `<span class="evidence-badge evidence-authentic" title="Authentic Evidence"> | |
| <i class="fas fa-check" style="margin-right: 0.125rem; font-size: 0.6rem;"></i> | |
| ${evidenceSummary.AUTHENTIC} | |
| </span>` | |
| : ''} | |
| ${evidenceSummary.INDETERMINATE > 0 | |
| ? `<span class="evidence-badge evidence-indeterminate" title="Indeterminate Evidence"> | |
| <i class="fas fa-question" style="margin-right: 0.125rem; font-size: 0.6rem;"></i> | |
| ${evidenceSummary.INDETERMINATE} | |
| </span>` | |
| : ''} | |
| </div> | |
| </td> | |
| <td> | |
| <button class="action-button secondary-action view-detail-btn" data-index="${index}" title="View Detailed Analysis" style="padding: 0.25rem 0.5rem;"> | |
| <i class="fas fa-search" style="font-size: 0.7rem;"></i> | |
| </button> | |
| </td> | |
| `; | |
| resultsTableBody.appendChild(row); | |
| }); | |
| noResultsRow.classList.add('hidden'); | |
| // Add event listeners to view detail buttons | |
| document.querySelectorAll('.view-detail-btn').forEach(btn => { | |
| btn.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| const index = parseInt(e.currentTarget.dataset.index); | |
| showDetailedAnalysis(index); | |
| }); | |
| }); | |
| // Make entire row clickable for detailed view | |
| document.querySelectorAll('#resultsTableBody tr').forEach(row => { | |
| row.addEventListener('click', (e) => { | |
| if (!e.target.closest('.view-detail-btn')) { | |
| const index = parseInt(row.dataset.index); | |
| showDetailedAnalysis(index); | |
| } | |
| }); | |
| }); | |
| } | |
| function updateSummary(batchResult) { | |
| if (!batchResult || !Array.isArray(batchResult.results)) { | |
| resultsSummary.innerHTML = ''; | |
| return; | |
| } | |
| // Count decisions exactly as returned by backend | |
| const counts = {}; | |
| batchResult.results.forEach(result => { | |
| const status = result.final_decision; | |
| if (!status) return; | |
| counts[status] = (counts[status] || 0) + 1; | |
| }); | |
| // Display order | |
| const ORDER = [ | |
| 'CONFIRMED_AI_GENERATED', | |
| 'SUSPICIOUS_AI_LIKELY', | |
| 'AUTHENTIC_BUT_REVIEW', | |
| 'MOSTLY_AUTHENTIC' | |
| ]; | |
| resultsSummary.innerHTML = ORDER | |
| .filter(status => counts[status]) | |
| .map(status => { | |
| const meta = decisionMeta(status); | |
| return ` | |
| <div class="summary-card"> | |
| <div class="summary-value">${counts[status]}</div> | |
| <div class="summary-label">${meta.label}</div> | |
| </div> | |
| `; | |
| }) | |
| .join(''); | |
| } | |
| function showDetailedAnalysis(index) { | |
| if (!batchResults || !batchResults.results || !batchResults.results[index]) return; | |
| selectedImageIndex = index; | |
| const result = batchResults.results[index]; | |
| const filename = result.filename || 'Unknown'; | |
| const overallScore = result.overall_score || 0; | |
| const decision = result.final_decision; | |
| const meta = decisionMeta(decision); | |
| const confidence = result.confidence != null ? Math.round(result.confidence) : 0; | |
| const imageSize = result.image_size || [0, 0]; | |
| const processingTime = result.processing_time || 0; | |
| const signals = result.signals || []; | |
| const evidence = result.evidence || []; | |
| const scorePercent = Math.round(overallScore * 100); | |
| /* ---------- Expand panel ---------- */ | |
| detailedAnalysisContent.classList.add('show'); | |
| detailedAnalysisIcon.classList.remove('fa-chevron-down'); | |
| detailedAnalysisIcon.classList.add('fa-chevron-up'); | |
| detailedAnalysisContent.scrollIntoView({ | |
| behavior: 'smooth', | |
| block: 'start' | |
| }); | |
| /* ---------- Signals (Tier-1 Metrics) ---------- */ | |
| let signalsHtml = ''; | |
| if (signals.length > 0) { | |
| signals.forEach(signal => { | |
| let statusClass = 'signal-passed'; | |
| if (signal.status === 'warning') statusClass = 'signal-warning'; | |
| if (signal.status === 'flagged') statusClass = 'signal-flagged'; | |
| const signalScore = Math.round((signal.score || 0) * 100); | |
| signalsHtml += ` | |
| <div class="signal-card"> | |
| <div class="signal-header"> | |
| <strong style="font-size: 0.9375rem;">${signal.name || 'Unknown Metric'}</strong> | |
| <span class="signal-badge ${statusClass}">${signal.status}</span> | |
| </div> | |
| <p style="font-size:0.875rem;color:var(--text-light);margin-bottom:0.5rem;"> | |
| ${signal.explanation || 'No explanation available.'} | |
| </p> | |
| <div style="display: flex; justify-content: space-between; align-items: center; margin-top: 0.5rem;"> | |
| <div class="info-pill"> | |
| <i class="fas fa-chart-line"></i> | |
| Score: ${signalScore}% | |
| </div> | |
| <div class="info-pill"> | |
| <i class="fas fa-weight-hanging"></i> | |
| Weight: ${signal.weight || 'N/A'} | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| }); | |
| } else { | |
| signalsHtml = ` | |
| <div class="signal-card"> | |
| <div class="signal-header"> | |
| <strong>Detection Signals</strong> | |
| </div> | |
| <p class="text-center" style="color:var(--text-light); padding: 1rem;"> | |
| No detection signals available for this image. | |
| </p> | |
| </div> | |
| `; | |
| } | |
| /* ---------- Evidence (Tier-2 Declarative Evidence) ---------- */ | |
| let evidenceHtml = ''; | |
| if (evidence.length > 0) { | |
| evidence.forEach(ev => { | |
| let badgeClass = 'signal-passed'; | |
| let icon = 'fa-check'; | |
| if (ev.direction === 'AI_GENERATED') { | |
| badgeClass = 'signal-flagged'; | |
| icon = 'fa-robot'; | |
| } | |
| if (ev.direction === 'INDETERMINATE') { | |
| badgeClass = 'signal-warning'; | |
| icon = 'fa-question'; | |
| } | |
| evidenceHtml += ` | |
| <div class="signal-card" style="background:#f1f5f9;"> | |
| <div class="signal-header"> | |
| <strong style="font-size: 0.9375rem;">${ev.source.toUpperCase()}</strong> | |
| <span class="signal-badge ${badgeClass}"> | |
| <i class="fas ${icon}" style="margin-right: 0.125rem;"></i> | |
| ${ev.strength} | |
| </span> | |
| </div> | |
| <p style="font-size:0.875rem;margin-bottom:0.5rem;color:var(--text);"> | |
| ${ev.finding} | |
| </p> | |
| <div style="display: flex; justify-content: space-between; align-items: center; margin-top: 0.5rem;"> | |
| <div class="info-pill"> | |
| <i class="fas fa-microscope"></i> | |
| ${ev.analyzer} | |
| </div> | |
| ${ev.confidence != null ? | |
| `<div class="info-pill"> | |
| <i class="fas fa-brain"></i> | |
| Confidence: ${Math.round(ev.confidence * 100)}% | |
| </div>` : ''} | |
| </div> | |
| </div> | |
| `; | |
| }); | |
| } else { | |
| evidenceHtml = ` | |
| <div class="signal-card" style="background:#f1f5f9;"> | |
| <div class="signal-header"> | |
| <strong>Evidence</strong> | |
| </div> | |
| <p class="text-center" style="color:var(--text-light); padding: 1rem;"> | |
| No declarative evidence detected for this image. | |
| </p> | |
| </div> | |
| `; | |
| } | |
| /* ---------- Render ---------- */ | |
| detailedAnalysisContent.innerHTML = ` | |
| <div style="margin-bottom:2rem;"> | |
| <div style="display:flex;align-items:center;gap:1.5rem;margin-bottom:1.5rem;flex-wrap:wrap;"> | |
| <div style="position:relative;"> | |
| <img src="${fileDataUrls[filename] || 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="80" height="80"><rect width="80" height="80" fill="%23f0f0f0"/></svg>'}" | |
| alt="${filename}" | |
| style="width:80px;height:80px;object-fit:cover;border-radius:0.75rem;border:2px solid var(--border);box-shadow:var(--shadow);"> | |
| <div style="position:absolute;bottom:-8px;right:-8px;background:white;border:1px solid var(--border);border-radius:0.5rem;padding:0.25rem 0.5rem;font-size:0.75rem;color:var(--text-light);box-shadow:var(--shadow);"> | |
| ${imageSize[0]}×${imageSize[1]} | |
| </div> | |
| </div> | |
| <div style="flex:1;"> | |
| <h4 style="margin-bottom:0.5rem;font-size:1.25rem;">${filename}</h4> | |
| <div style="display:flex;gap:1rem;flex-wrap:wrap;"> | |
| <div class="info-pill"> | |
| <i class="fas fa-clock"></i> | |
| ${processingTime.toFixed(2)}s | |
| </div> | |
| <div class="info-pill"> | |
| <i class="fas fa-database"></i> | |
| ${signals.length} signals | |
| </div> | |
| <div class="info-pill"> | |
| <i class="fas fa-layer-group"></i> | |
| ${evidence.length} evidence | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:1rem;"> | |
| <div style="text-align:center;padding:1.5rem;background:linear-gradient(135deg,#f8fafc,#edf2f7);border-radius:0.75rem;border:1px solid var(--border);"> | |
| <div style="font-size:2rem;font-weight:800;margin-bottom:0.25rem;">${scorePercent}%</div> | |
| <div style="font-size:0.875rem;color:var(--text-light);font-weight:500;">Score</div> | |
| </div> | |
| <div style="text-align:center;padding:1.5rem;background:linear-gradient(135deg,#f8fafc,#edf2f7);border-radius:0.75rem;border:1px solid var(--border);"> | |
| <div style="font-size:1.25rem;font-weight:700;margin-bottom:0.25rem;display:flex;align-items:center;justify-content:center;gap:0.5rem;"> | |
| <i class="fas ${meta.icon}"></i> | |
| ${meta.label} | |
| </div> | |
| <div style="font-size:0.875rem;color:var(--text-light);font-weight:500;">Verdict</div> | |
| </div> | |
| <div style="text-align:center;padding:1.5rem;background:linear-gradient(135deg,#f8fafc,#edf2f7);border-radius:0.75rem;border:1px solid var(--border);"> | |
| <div style="font-size:2rem;font-weight:800;margin-bottom:0.25rem;">${confidence}%</div> | |
| <div style="font-size:0.875rem;color:var(--text-light);font-weight:500;">Confidence</div> | |
| </div> | |
| </div> | |
| </div> | |
| <h4 style="margin-bottom:1rem;font-size:1.125rem;color:var(--primary);"> | |
| <i class="fas fa-bell"></i> Detection Signals | |
| </h4> | |
| <div class="signal-grid" style="margin-bottom:2rem;"> | |
| ${signalsHtml} | |
| </div> | |
| <h4 style="margin-bottom:1rem;font-size:1.125rem;color:var(--primary);"> | |
| <i class="fas fa-clipboard-check"></i> Evidence | |
| </h4> | |
| <div class="signal-grid" style="margin-bottom:2rem;"> | |
| ${evidenceHtml} | |
| </div> | |
| <div class="signal-card" style="background:linear-gradient(135deg,#f0f9ff,#e6f7ff);border:1px solid rgba(49,130,206,0.2);"> | |
| <div class="signal-header"> | |
| <strong style="color:var(--accent);"> | |
| <i class="fas fa-comment-alt"></i> Decision Explanation | |
| </strong> | |
| </div> | |
| <p style="font-size:0.9375rem;color:var(--text);line-height:1.6;margin-top:0.5rem;"> | |
| ${result.decision_explanation || meta.recommendation} | |
| </p> | |
| </div> | |
| `; | |
| } | |
| // Export functions | |
| async function exportCsv() { | |
| if (!currentBatchId) { | |
| showToast('No analysis results to export.', 'warning'); | |
| return; | |
| } | |
| showLoading(true, 'Generating CSV report...'); | |
| try { | |
| const response = await fetch(`${CSV_REPORT_ENDPOINT}/${currentBatchId}`); | |
| if (response.ok) { | |
| const blob = await response.blob(); | |
| const downloadLink = document.createElement('a'); | |
| downloadLink.href = URL.createObjectURL(blob); | |
| downloadLink.download = `ai_screener_report_${currentBatchId}.csv`; | |
| document.body.appendChild(downloadLink); | |
| downloadLink.click(); | |
| document.body.removeChild(downloadLink); | |
| showToast('CSV report downloaded successfully.', 'success'); | |
| } else { | |
| showToast('Failed to generate CSV report.', 'error'); | |
| } | |
| } catch (error) { | |
| console.error('CSV export failed:', error); | |
| showToast('CSV export failed. Please try again.', 'error'); | |
| } finally { | |
| showLoading(false); | |
| } | |
| } | |
| async function exportJson() { | |
| if (!batchResults) { | |
| showToast('No analysis results to export.', 'warning'); | |
| return; | |
| } | |
| showLoading(true, 'Generating JSON report...'); | |
| try { | |
| const dataStr = JSON.stringify(batchResults, null, 2); | |
| const dataBlob = new Blob([dataStr], {type: 'application/json'}); | |
| const downloadLink = document.createElement('a'); | |
| downloadLink.href = URL.createObjectURL(dataBlob); | |
| downloadLink.download = `ai_image_screener_${new Date().toISOString().split('T')[0]}_${currentBatchId || 'report'}.json`; | |
| document.body.appendChild(downloadLink); | |
| downloadLink.click(); | |
| document.body.removeChild(downloadLink); | |
| showToast('JSON report downloaded successfully.', 'success'); | |
| } catch (error) { | |
| console.error('JSON export failed:', error); | |
| showToast('JSON export failed. Please try again.', 'error'); | |
| } finally { | |
| showLoading(false); | |
| } | |
| } | |
| // Reset functions | |
| function resetUI() { | |
| analyzeBtn.disabled = false; | |
| analyzeBtn.innerHTML = '<div class="btn-content"><i class="fas fa-play"></i> Start Analysis</div>'; | |
| setTimeout(() => { | |
| progressContainer.classList.add('hidden'); | |
| }, 2000); | |
| } | |
| function resetAnalysis() { | |
| files = []; | |
| fileDataUrls = {}; | |
| batchResults = null; | |
| currentBatchId = null; | |
| selectedImageIndex = null; | |
| updateThumbnailGrid(); | |
| clearResults(); | |
| resultsSection.classList.add('hidden'); | |
| detailedAnalysisContent.innerHTML = '<p id="noDetailedAnalysis" class="text-center" style="color: var(--text-light); padding: 2rem;"><i class="fas fa-eye" style="font-size: 2rem; margin-bottom: 1rem; opacity: 0.5;"></i><br>Select an image to view detailed analysis</p>'; | |
| window.scrollTo({ top: 0, behavior: 'smooth' }); | |
| showToast('Analysis reset. Ready for new upload.', 'success'); | |
| } | |
| function clearResults() { | |
| resultsSummary.innerHTML = ''; | |
| resultsTableBody.innerHTML = ''; | |
| noResultsRow.classList.remove('hidden'); | |
| if (pollingInterval) { | |
| clearInterval(pollingInterval); | |
| pollingInterval = null; | |
| } | |
| } | |
| // API health check | |
| async function checkApiHealth() { | |
| try { | |
| const response = await fetch(HEALTH_ENDPOINT); | |
| const data = await response.json(); | |
| if (data.status === 'ok') { | |
| console.log('API connected successfully'); | |
| } | |
| } catch (error) { | |
| console.error('API health check failed:', error); | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> |