ImageForensics-AI / ui /index.html
satyaki-mitra's picture
ui updated at one place
37bf0cf
<!DOCTYPE html>
<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 !important;
}
.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 !important;
}
.visible {
display: block !important;
}
.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>