|
|
<!DOCTYPE html>
|
|
|
<html lang="en">
|
|
|
<head>
|
|
|
<meta charset="UTF-8">
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
<title>Multiple Classification</title>
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.9.3/min/dropzone.min.js"></script>
|
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.9.3/dropzone.min.css">
|
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
|
<style>
|
|
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
|
|
|
|
|
:root {
|
|
|
--primary-color: #4f46e5;
|
|
|
--primary-dark: #4338ca;
|
|
|
--success-color: #22c55e;
|
|
|
--error-color: #ef4444;
|
|
|
--background-color: #1a1a2e;
|
|
|
--card-background: rgba(255, 255, 255, 0.1);
|
|
|
--text-primary: #ffffff;
|
|
|
--text-secondary: #a5b4fc;
|
|
|
--border-color: rgba(255, 255, 255, 0.18);
|
|
|
}
|
|
|
|
|
|
body {
|
|
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
|
min-height: 100vh;
|
|
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
|
display: flex;
|
|
|
justify-content: center;
|
|
|
align-items: center;
|
|
|
margin: 0;
|
|
|
color: var(--text-primary);
|
|
|
overflow-x: hidden;
|
|
|
}
|
|
|
|
|
|
.container {
|
|
|
max-width: 1200px;
|
|
|
margin: 0 auto;
|
|
|
padding: 2rem;
|
|
|
position: relative;
|
|
|
z-index: 1;
|
|
|
width: 100%;
|
|
|
}
|
|
|
|
|
|
.glass-card {
|
|
|
background: var(--card-background);
|
|
|
backdrop-filter: blur(10px);
|
|
|
border-radius: 20px;
|
|
|
padding: 2rem;
|
|
|
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
|
|
|
border: 1px solid var(--border-color);
|
|
|
transform: translateY(0);
|
|
|
transition: transform 0.3s ease;
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
.glass-card:hover {
|
|
|
transform: translateY(-5px);
|
|
|
}
|
|
|
|
|
|
.header {
|
|
|
text-align: center;
|
|
|
margin-bottom: 1.5rem;
|
|
|
}
|
|
|
|
|
|
.header h1 {
|
|
|
font-size: 2rem;
|
|
|
color: var(--text-primary);
|
|
|
margin-bottom: 0.25rem;
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
.header p {
|
|
|
color: var(--text-secondary);
|
|
|
font-size: 1rem;
|
|
|
}
|
|
|
|
|
|
.upload-section {
|
|
|
background: var(--card-background);
|
|
|
border-radius: 0.75rem;
|
|
|
padding: 1.5rem;
|
|
|
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
|
|
margin-bottom: 1.5rem;
|
|
|
}
|
|
|
|
|
|
.dropzone {
|
|
|
border: 2px dashed var(--primary-color);
|
|
|
border-radius: 1rem;
|
|
|
background: rgba(255, 255, 255, 0.05);
|
|
|
padding: 2rem;
|
|
|
min-height: 200px;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
transition: all 0.3s ease;
|
|
|
}
|
|
|
|
|
|
.dropzone:hover {
|
|
|
background: rgba(255, 255, 255, 0.1);
|
|
|
border-color: var(--primary-dark);
|
|
|
}
|
|
|
|
|
|
.dropzone .dz-message {
|
|
|
margin: 1rem 0;
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
.dropzone .dz-preview {
|
|
|
background: var(--card-background);
|
|
|
border-radius: 1rem;
|
|
|
border: 1px solid var(--border-color);
|
|
|
padding: 0.5rem;
|
|
|
margin: 1rem;
|
|
|
transition: transform 0.2s ease;
|
|
|
}
|
|
|
|
|
|
.dropzone .dz-preview:hover {
|
|
|
transform: translateY(-2px);
|
|
|
}
|
|
|
|
|
|
.dropzone .dz-image {
|
|
|
width: 120px !important;
|
|
|
height: 120px !important;
|
|
|
border-radius: 0.75rem !important;
|
|
|
overflow: hidden;
|
|
|
position: relative;
|
|
|
}
|
|
|
|
|
|
.dropzone .dz-image img {
|
|
|
width: 100%;
|
|
|
height: 100%;
|
|
|
object-fit: cover;
|
|
|
}
|
|
|
|
|
|
.dropzone .dz-details {
|
|
|
padding-top: 0.5rem;
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
.dropzone .dz-filename {
|
|
|
color: var(--text-primary);
|
|
|
font-size: 0.875rem;
|
|
|
max-width: 120px;
|
|
|
overflow: hidden;
|
|
|
text-overflow: ellipsis;
|
|
|
white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
.dropzone .dz-remove {
|
|
|
color: var(--error-color);
|
|
|
text-decoration: none;
|
|
|
font-size: 0.875rem;
|
|
|
margin-top: 0.5rem;
|
|
|
display: inline-block;
|
|
|
transition: opacity 0.2s ease;
|
|
|
}
|
|
|
|
|
|
.dropzone .dz-remove:hover {
|
|
|
opacity: 0.8;
|
|
|
}
|
|
|
|
|
|
.dropzone .dz-preview .dz-progress {
|
|
|
height: 4px;
|
|
|
background: rgba(255, 255, 255, 0.1);
|
|
|
margin-top: 0.5rem;
|
|
|
border-radius: 2px;
|
|
|
}
|
|
|
|
|
|
.dropzone .dz-preview .dz-progress .dz-upload {
|
|
|
background: var(--primary-color);
|
|
|
border-radius: 2px;
|
|
|
}
|
|
|
|
|
|
.preview-container {
|
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
|
gap: 1rem;
|
|
|
margin-top: 1rem;
|
|
|
}
|
|
|
|
|
|
.control-panel {
|
|
|
display: flex;
|
|
|
gap: 1rem;
|
|
|
margin-top: 1rem;
|
|
|
justify-content: center;
|
|
|
}
|
|
|
|
|
|
.btn {
|
|
|
padding: 0.75rem 1.5rem;
|
|
|
border-radius: 0.5rem;
|
|
|
border: none;
|
|
|
color: white;
|
|
|
font-weight: 500;
|
|
|
cursor: pointer;
|
|
|
transition: all 0.2s ease;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 0.5rem;
|
|
|
font-size: 1rem;
|
|
|
}
|
|
|
|
|
|
.btn:hover {
|
|
|
transform: translateY(-1px);
|
|
|
}
|
|
|
|
|
|
.btn-clear {
|
|
|
background: var(--text-secondary);
|
|
|
}
|
|
|
|
|
|
.btn-classify {
|
|
|
background: var(--primary-color);
|
|
|
}
|
|
|
|
|
|
.btn-classify:hover {
|
|
|
background: var(--primary-dark);
|
|
|
}
|
|
|
|
|
|
.btn-download {
|
|
|
background: var(--success-color);
|
|
|
}
|
|
|
|
|
|
.btn-download:hover {
|
|
|
background: #1e9c4f;
|
|
|
}
|
|
|
|
|
|
.results-section {
|
|
|
margin-top: 1.5rem;
|
|
|
}
|
|
|
|
|
|
.section-header {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
align-items: center;
|
|
|
margin-bottom: 1rem;
|
|
|
padding: 1rem;
|
|
|
border-radius: 0.5rem;
|
|
|
box-shadow: 0 2px 4px -1px rgb(0 0 0 / 0.1);
|
|
|
background: var(--card-background);
|
|
|
}
|
|
|
|
|
|
.section-header h2 {
|
|
|
margin: 0;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 0.5rem;
|
|
|
font-size: 1.25rem;
|
|
|
color: var(--text-primary);
|
|
|
}
|
|
|
|
|
|
.pass-header {
|
|
|
background: rgba(34, 197, 94, 0.1);
|
|
|
color: var(--success-color);
|
|
|
}
|
|
|
|
|
|
.fail-header {
|
|
|
background: rgba(239, 68, 68, 0.1);
|
|
|
color: var(--error-color);
|
|
|
}
|
|
|
|
|
|
.download-btn {
|
|
|
padding: 0.75rem 1.5rem;
|
|
|
border-radius: 0.5rem;
|
|
|
color: white;
|
|
|
text-decoration: none;
|
|
|
font-weight: 500;
|
|
|
transition: all 0.2s ease;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 0.5rem;
|
|
|
font-size: 1rem;
|
|
|
}
|
|
|
|
|
|
.download-btn:hover {
|
|
|
transform: translateY(-1px);
|
|
|
}
|
|
|
|
|
|
.download-btn.pass {
|
|
|
background: var(--success-color);
|
|
|
}
|
|
|
|
|
|
.download-btn.fail {
|
|
|
background: var(--error-color);
|
|
|
}
|
|
|
|
|
|
.image-list {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
gap: 0.75rem;
|
|
|
margin-bottom: 1.5rem;
|
|
|
}
|
|
|
|
|
|
.image-ribbon {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
padding: 0.75rem;
|
|
|
background: var(--card-background);
|
|
|
border-radius: 0.5rem;
|
|
|
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
|
|
|
transition: all 0.2s ease;
|
|
|
cursor: pointer;
|
|
|
}
|
|
|
|
|
|
.image-ribbon:hover {
|
|
|
transform: translateX(4px);
|
|
|
background: rgba(255, 255, 255, 0.1);
|
|
|
}
|
|
|
|
|
|
.thumbnail {
|
|
|
width: 50px;
|
|
|
height: 50px;
|
|
|
border-radius: 0.375rem;
|
|
|
object-fit: cover;
|
|
|
margin-right: 1rem;
|
|
|
}
|
|
|
|
|
|
.image-info {
|
|
|
flex: 1;
|
|
|
}
|
|
|
|
|
|
.filename {
|
|
|
font-weight: 500;
|
|
|
color: var(--text-primary);
|
|
|
margin-top: 0.5rem;
|
|
|
font-size: 1rem;
|
|
|
white-space: normal;
|
|
|
word-break: break-word;
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
.labels {
|
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
|
gap: 0.5rem;
|
|
|
justify-content: center;
|
|
|
align-items: center;
|
|
|
margin: 0 auto;
|
|
|
position: relative;
|
|
|
transition: all 0.3s ease;
|
|
|
}
|
|
|
|
|
|
.label {
|
|
|
background: rgba(255, 255, 255, 0.1);
|
|
|
padding: 0.5rem 1rem;
|
|
|
border-radius: 9999px;
|
|
|
font-size: 0.875rem;
|
|
|
color: var(--text-secondary);
|
|
|
white-space: nowrap;
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
#summary {
|
|
|
background: var(--card-background);
|
|
|
padding: 1rem;
|
|
|
border-radius: 0.5rem;
|
|
|
text-align: center;
|
|
|
font-size: 1rem;
|
|
|
font-weight: 500;
|
|
|
margin-bottom: 1.5rem;
|
|
|
box-shadow: 0 2px 4px -1px rgb(0 0 0 / 0.1);
|
|
|
color: var(--text-primary);
|
|
|
}
|
|
|
|
|
|
.progress-container {
|
|
|
margin-top: 1rem;
|
|
|
background: rgba(255, 255, 255, 0.1);
|
|
|
border-radius: 0.5rem;
|
|
|
overflow: hidden;
|
|
|
}
|
|
|
|
|
|
.progress-bar {
|
|
|
height: 6px;
|
|
|
background: var(--primary-color);
|
|
|
width: 0;
|
|
|
transition: width 0.3s ease;
|
|
|
}
|
|
|
|
|
|
.loading-overlay {
|
|
|
display: none;
|
|
|
position: fixed;
|
|
|
top: 0;
|
|
|
left: 0;
|
|
|
width: 100%;
|
|
|
height: 100%;
|
|
|
background: rgba(0, 0, 0, 0.95);
|
|
|
z-index: 1000;
|
|
|
justify-content: center;
|
|
|
align-items: center;
|
|
|
color: white;
|
|
|
}
|
|
|
|
|
|
.loading-content {
|
|
|
background: var(--card-background);
|
|
|
padding: 2rem;
|
|
|
border-radius: 1rem;
|
|
|
width: 90%;
|
|
|
max-width: 500px;
|
|
|
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
|
|
text-align: center;
|
|
|
color: var(--text-primary);
|
|
|
}
|
|
|
|
|
|
.loading-header {
|
|
|
text-align: center;
|
|
|
margin-bottom: 1.5rem;
|
|
|
}
|
|
|
|
|
|
.loading-header h3 {
|
|
|
color: var(--primary-color);
|
|
|
margin: 0;
|
|
|
margin-bottom: 0.5rem;
|
|
|
}
|
|
|
|
|
|
.loading-spinner {
|
|
|
margin: 1rem 0;
|
|
|
font-size: 2rem;
|
|
|
color: var(--primary-color);
|
|
|
}
|
|
|
|
|
|
.loading-progress {
|
|
|
font-size: 1.1rem;
|
|
|
color: var(--text-primary);
|
|
|
}
|
|
|
|
|
|
.image-modal {
|
|
|
display: none;
|
|
|
position: fixed;
|
|
|
top: 0;
|
|
|
left: 0;
|
|
|
width: 100%;
|
|
|
height: 100%;
|
|
|
background: rgba(0, 0, 0, 0.9);
|
|
|
z-index: 2000;
|
|
|
justify-content: center;
|
|
|
align-items: center;
|
|
|
}
|
|
|
|
|
|
.modal-content {
|
|
|
position: relative;
|
|
|
max-width: 90%;
|
|
|
max-height: 90vh;
|
|
|
margin: auto;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
align-items: center;
|
|
|
}
|
|
|
|
|
|
.modal-image {
|
|
|
max-width: 100%;
|
|
|
max-height: 80vh;
|
|
|
object-fit: contain;
|
|
|
border-radius: 0.5rem;
|
|
|
}
|
|
|
|
|
|
.modal-close {
|
|
|
position: absolute;
|
|
|
top: -2rem;
|
|
|
right: 0;
|
|
|
color: white;
|
|
|
font-size: 1.5rem;
|
|
|
cursor: pointer;
|
|
|
background: none;
|
|
|
border: none;
|
|
|
padding: 0.5rem;
|
|
|
}
|
|
|
|
|
|
.results-table {
|
|
|
width: 100%;
|
|
|
border-collapse: collapse;
|
|
|
margin-top: 1.5rem;
|
|
|
background: var(--card-background);
|
|
|
border-radius: 0.5rem;
|
|
|
overflow: hidden;
|
|
|
table-layout: fixed;
|
|
|
}
|
|
|
|
|
|
.results-table th,
|
|
|
.results-table td {
|
|
|
padding: 0.75rem 0.5rem;
|
|
|
vertical-align: middle;
|
|
|
text-align: center;
|
|
|
border-bottom: 1px solid var(--border-color);
|
|
|
}
|
|
|
|
|
|
.results-table .serial {
|
|
|
width: 80px;
|
|
|
}
|
|
|
|
|
|
.results-table .image-col {
|
|
|
width: 200px;
|
|
|
}
|
|
|
|
|
|
.results-table .result-col {
|
|
|
width: 180px;
|
|
|
}
|
|
|
|
|
|
.results-table .labels-col {
|
|
|
width: auto;
|
|
|
min-width: 150px;
|
|
|
padding-left: 1rem;
|
|
|
text-align: center !important;
|
|
|
}
|
|
|
|
|
|
.results-table td {
|
|
|
padding: 0.75rem 0.5rem;
|
|
|
vertical-align: middle;
|
|
|
border-bottom: 1px solid var(--border-color);
|
|
|
text-align: left;
|
|
|
}
|
|
|
|
|
|
.results-table td.serial,
|
|
|
.results-table td.image-col,
|
|
|
.results-table td.result-col {
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
.results-table th.labels-col {
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
.results-table td.labels-col {
|
|
|
text-align: center !important;
|
|
|
padding: 1rem;
|
|
|
}
|
|
|
|
|
|
.results-table tbody tr:last-child td {
|
|
|
border-bottom: none;
|
|
|
}
|
|
|
|
|
|
.result-pass,
|
|
|
.result-fail {
|
|
|
display: inline-block;
|
|
|
padding: 0.25rem 0.75rem;
|
|
|
border-radius: 9999px;
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
.result-pass {
|
|
|
background: rgba(34, 197, 94, 0.1);
|
|
|
}
|
|
|
|
|
|
.result-fail {
|
|
|
background: rgba(239, 68, 68, 0.1);
|
|
|
}
|
|
|
|
|
|
.dropzone {
|
|
|
border: 2px dashed var(--primary-color);
|
|
|
border-radius: 0.5rem;
|
|
|
background: rgba(255, 255, 255, 0.1);
|
|
|
padding: 1rem;
|
|
|
min-height: 150px;
|
|
|
}
|
|
|
|
|
|
.dropzone .dz-preview {
|
|
|
margin: 1rem;
|
|
|
}
|
|
|
|
|
|
.dropzone .dz-preview .dz-image {
|
|
|
border-radius: 0.5rem;
|
|
|
}
|
|
|
|
|
|
.dropzone .dz-preview .dz-details {
|
|
|
color: var(--text-primary);
|
|
|
}
|
|
|
|
|
|
.dropzone .dz-preview {
|
|
|
display: inline-block;
|
|
|
margin: 1rem;
|
|
|
position: relative;
|
|
|
}
|
|
|
|
|
|
.dropzone .dz-preview:nth-child(n+5) {
|
|
|
opacity: 0.3;
|
|
|
}
|
|
|
|
|
|
.dropzone .dz-preview:nth-child(n+6) {
|
|
|
display: none !important;
|
|
|
}
|
|
|
|
|
|
.dropzone .dz-preview.dz-file-preview .dz-image,
|
|
|
.dropzone .dz-preview .dz-image {
|
|
|
border-radius: 8px;
|
|
|
width: 150px;
|
|
|
height: 150px;
|
|
|
}
|
|
|
|
|
|
.dz-more-indicator {
|
|
|
position: absolute;
|
|
|
top: 0;
|
|
|
left: 0;
|
|
|
width: 100%;
|
|
|
height: 100%;
|
|
|
background: rgba(0, 0, 0, 0.7);
|
|
|
color: white;
|
|
|
display: flex;
|
|
|
justify-content: center;
|
|
|
align-items: center;
|
|
|
font-size: 1.2rem;
|
|
|
border-radius: 8px;
|
|
|
display: none;
|
|
|
}
|
|
|
|
|
|
.dropzone .dz-preview:nth-child(5) .dz-more-indicator {
|
|
|
display: flex;
|
|
|
}
|
|
|
|
|
|
.dropzone .dz-preview {
|
|
|
display: none !important;
|
|
|
}
|
|
|
|
|
|
.upload-section {
|
|
|
background: var(--card-background);
|
|
|
border-radius: 1rem;
|
|
|
padding: 2rem;
|
|
|
margin-bottom: 2rem;
|
|
|
}
|
|
|
|
|
|
.dropzone {
|
|
|
border: 2px dashed var(--primary-color);
|
|
|
border-radius: 1rem;
|
|
|
padding: 2rem;
|
|
|
text-align: center;
|
|
|
background: rgba(255, 255, 255, 0.05);
|
|
|
transition: all 0.3s ease;
|
|
|
min-height: 200px;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
}
|
|
|
|
|
|
.dropzone .dz-message {
|
|
|
margin: 0;
|
|
|
}
|
|
|
|
|
|
.dropzone:hover {
|
|
|
background: rgba(255, 255, 255, 0.08);
|
|
|
border-color: var(--primary-dark);
|
|
|
}
|
|
|
|
|
|
#thumbnail-preview-box {
|
|
|
display: none;
|
|
|
position: absolute;
|
|
|
z-index: 9999;
|
|
|
background: var(--card-background);
|
|
|
border: 1px solid var(--border-color);
|
|
|
border-radius: 0.75rem;
|
|
|
padding: 1rem;
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
|
|
|
max-width: 300px;
|
|
|
}
|
|
|
|
|
|
#thumbnail-preview-box img {
|
|
|
max-width: 100%;
|
|
|
border-radius: 0.5rem;
|
|
|
display: block;
|
|
|
}
|
|
|
</style>
|
|
|
</head>
|
|
|
<body>
|
|
|
<div class="container">
|
|
|
<div class="glass-card">
|
|
|
<div class="header">
|
|
|
<h1>Multiple Images Classification</h1>
|
|
|
<p>Classify and segregate multiple images at once...</p>
|
|
|
</div>
|
|
|
|
|
|
<div class="upload-section">
|
|
|
<form action="/upload_multiple" class="dropzone" id="upload-form">
|
|
|
<div class="dz-message">
|
|
|
<i class="fas fa-cloud-upload-alt fa-3x" style="color: var(--primary-color); margin-bottom: 1rem;"></i>
|
|
|
<h3 style="margin: 0.5rem 0; color: var(--text-primary);">Drop files here</h3>
|
|
|
<p style="color: var(--text-secondary); margin: 0;">or click to browse</p>
|
|
|
<div id="file-count" style="margin-top: 1rem; color: var(--text-secondary);"></div>
|
|
|
</div>
|
|
|
</form>
|
|
|
<div class="progress-container">
|
|
|
<div class="progress-bar" id="upload-progress"></div>
|
|
|
</div>
|
|
|
<div class="control-panel">
|
|
|
<button class="btn btn-clear" onclick="clearAllImages()">
|
|
|
<i class="fas fa-trash-alt"></i>
|
|
|
Clear All
|
|
|
</button>
|
|
|
<button class="btn btn-classify" id="classify-btn" onclick="highlightClassification()">
|
|
|
<i class="fas fa-tags"></i>
|
|
|
Classify
|
|
|
</button>
|
|
|
|
|
|
<button class="btn btn-download" id="download-html-btn" onclick="downloadPageAsHTML()">
|
|
|
<i class="fas fa-download"></i>
|
|
|
Download HTML
|
|
|
</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div id="summary"></div>
|
|
|
|
|
|
<div class="results-section">
|
|
|
<table class="results-table" id="results-table">
|
|
|
<thead>
|
|
|
<tr>
|
|
|
<th class="serial">S.No</th>
|
|
|
<th class="image-col">Image</th>
|
|
|
<th class="result-col">Result</th>
|
|
|
<th class="labels-col">Labels</th>
|
|
|
</tr>
|
|
|
</thead>
|
|
|
<tbody id="results-tbody">
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
|
|
|
|
|
|
<div class="loading-overlay" id="loading-overlay">
|
|
|
<div class="loading-content">
|
|
|
<div class="loading-header">
|
|
|
<h3>Classifying Images</h3>
|
|
|
<p>Please wait while we process your images...</p>
|
|
|
</div>
|
|
|
<div class="loading-spinner">
|
|
|
<i class="fas fa-spinner fa-spin"></i>
|
|
|
</div>
|
|
|
<div class="loading-files" id="loading-files">
|
|
|
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
|
|
|
<div class="image-modal" id="image-modal">
|
|
|
<div class="modal-content">
|
|
|
<button class="modal-close" onclick="closeModal()">
|
|
|
<i class="fas fa-times"></i>
|
|
|
</button>
|
|
|
<img class="modal-image" id="modal-image" src="" alt="">
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div id="thumbnail-preview-box">
|
|
|
<img id="thumbnail-preview-img" src="" alt="">
|
|
|
</div>
|
|
|
|
|
|
<script>
|
|
|
let isClassifying = false;
|
|
|
|
|
|
Dropzone.options.uploadForm = {
|
|
|
paramName: "file",
|
|
|
maxFilesize: 16,
|
|
|
acceptedFiles: "image/*",
|
|
|
previewsContainer: false,
|
|
|
init: function() {
|
|
|
let myDropzone = this;
|
|
|
|
|
|
|
|
|
this.on("addedfile", function(file) {
|
|
|
|
|
|
if (myDropzone.files.length === 1) {
|
|
|
|
|
|
document.getElementById('results-tbody').innerHTML = '';
|
|
|
document.getElementById('summary').innerHTML = '';
|
|
|
document.getElementById('upload-progress').style.width = '0';
|
|
|
|
|
|
|
|
|
fetch('/clear_uploads', { method: 'POST' })
|
|
|
.then(response => response.json())
|
|
|
.catch(error => console.error('Error:', error));
|
|
|
}
|
|
|
updateFileCount(myDropzone.files.length);
|
|
|
});
|
|
|
|
|
|
this.on("success", function(file, response) {
|
|
|
updateFileCount(this.files.length);
|
|
|
});
|
|
|
|
|
|
this.on("removedfile", function() {
|
|
|
updateFileCount(this.files.length);
|
|
|
});
|
|
|
|
|
|
this.on("totaluploadprogress", function(progress) {
|
|
|
document.getElementById('upload-progress').style.width = progress + "%";
|
|
|
});
|
|
|
|
|
|
|
|
|
this.element.querySelector(".dz-message").addEventListener("click", function() {
|
|
|
|
|
|
myDropzone.removeAllFiles(true);
|
|
|
document.getElementById('results-tbody').innerHTML = '';
|
|
|
document.getElementById('summary').innerHTML = '';
|
|
|
document.getElementById('upload-progress').style.width = '0';
|
|
|
|
|
|
|
|
|
fetch('/clear_uploads', { method: 'POST' })
|
|
|
.then(response => response.json())
|
|
|
.catch(error => console.error('Error:', error));
|
|
|
});
|
|
|
}
|
|
|
};
|
|
|
|
|
|
function updateFileCount(count) {
|
|
|
const fileCount = document.getElementById('file-count');
|
|
|
if (count > 0) {
|
|
|
fileCount.textContent = `${count} file${count === 1 ? '' : 's'} selected`;
|
|
|
} else {
|
|
|
fileCount.textContent = '';
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function updateSummary() {
|
|
|
const passCount = document.querySelectorAll('.result-pass').length;
|
|
|
const failCount = document.querySelectorAll('.result-fail').length;
|
|
|
const totalFiles = passCount + failCount;
|
|
|
|
|
|
document.getElementById('summary').innerHTML = `
|
|
|
<i class="fas fa-chart-pie"></i> Results:
|
|
|
<span style="color: var(--success-color)">${passCount} passed</span>,
|
|
|
<span style="color: var(--error-color)">${failCount} failed</span>
|
|
|
out of ${totalFiles} total images
|
|
|
`;
|
|
|
}
|
|
|
|
|
|
function clearAllImages() {
|
|
|
fetch('/clear_uploads', { method: 'POST' })
|
|
|
.then(response => response.json())
|
|
|
.then(data => {
|
|
|
|
|
|
document.getElementById('results-tbody').innerHTML = '';
|
|
|
document.getElementById('summary').innerHTML = '';
|
|
|
document.getElementById('upload-progress').style.width = '0';
|
|
|
|
|
|
|
|
|
let myDropzone = Dropzone.forElement("#upload-form");
|
|
|
myDropzone.removeAllFiles(true);
|
|
|
|
|
|
|
|
|
document.querySelectorAll('.dz-preview').forEach(el => el.remove());
|
|
|
})
|
|
|
.catch(error => console.error('Error:', error));
|
|
|
}
|
|
|
|
|
|
function createTableRow(result, index) {
|
|
|
const labels = result.status === 'Fail' ? result.labels : [];
|
|
|
|
|
|
return `
|
|
|
<tr>
|
|
|
<td class="serial">${index + 1}</td>
|
|
|
<td class="image-col">
|
|
|
<img src="data:image/jpeg;base64,${result.image}"
|
|
|
alt="${result.filename}"
|
|
|
class="thumbnail"
|
|
|
onclick="openModal(this)"
|
|
|
style="cursor: pointer;">
|
|
|
<div class="filename">${result.filename}</div>
|
|
|
</td>
|
|
|
<td class="result-col">
|
|
|
<span class="result-${result.status.toLowerCase()}">${result.status}</span>
|
|
|
</td>
|
|
|
<td class="labels-col">
|
|
|
${result.status === 'Fail' ? `
|
|
|
<div class="labels">
|
|
|
${labels.map(label => `
|
|
|
<span class="label">${label}</span>
|
|
|
`).join('')}
|
|
|
</div>
|
|
|
` : '-'}
|
|
|
</td>
|
|
|
</tr>
|
|
|
`;
|
|
|
}
|
|
|
|
|
|
function openModal(imageElement) {
|
|
|
const modal = document.getElementById('image-modal');
|
|
|
const modalImage = document.getElementById('modal-image');
|
|
|
|
|
|
|
|
|
modalImage.src = imageElement.src;
|
|
|
modal.style.display = 'flex';
|
|
|
|
|
|
|
|
|
document.addEventListener('click', closeModalOnClickOutside);
|
|
|
}
|
|
|
|
|
|
function closeModal() {
|
|
|
const modal = document.getElementById('image-modal');
|
|
|
modal.style.display = 'none';
|
|
|
|
|
|
|
|
|
document.removeEventListener('click', closeModalOnClickOutside);
|
|
|
}
|
|
|
|
|
|
function closeModalOnClickOutside(event) {
|
|
|
const modal = document.getElementById('image-modal');
|
|
|
const modalContent = document.querySelector('.modal-content');
|
|
|
|
|
|
|
|
|
if (!modalContent.contains(event.target)) {
|
|
|
closeModal();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function handleEscKey(event) {
|
|
|
if (event.key === 'Escape') {
|
|
|
closeModal();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
document.addEventListener('keydown', handleEscKey);
|
|
|
|
|
|
async function processNextImage(filesProcessed, totalFiles) {
|
|
|
try {
|
|
|
const response = await fetch('/classify_multiple', { method: 'POST' });
|
|
|
const result = await response.json();
|
|
|
|
|
|
if (result.error) {
|
|
|
throw new Error(result.error);
|
|
|
}
|
|
|
|
|
|
if (result.filename) {
|
|
|
|
|
|
const row = document.getElementById(`result-row-${filesProcessed}`);
|
|
|
if (row) {
|
|
|
row.outerHTML = createTableRow(result, filesProcessed);
|
|
|
updateSummary();
|
|
|
}
|
|
|
|
|
|
|
|
|
if (filesProcessed + 1 < totalFiles) {
|
|
|
setTimeout(() => {
|
|
|
processNextImage(filesProcessed + 1, totalFiles);
|
|
|
}, 100);
|
|
|
} else {
|
|
|
finishClassification();
|
|
|
}
|
|
|
} else {
|
|
|
finishClassification();
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error('Error processing image:', error);
|
|
|
showError(filesProcessed);
|
|
|
finishClassification();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function finishClassification() {
|
|
|
isClassifying = false;
|
|
|
const classifyBtn = document.getElementById('classify-btn');
|
|
|
classifyBtn.disabled = false;
|
|
|
classifyBtn.innerHTML = '<i class="fas fa-tags"></i> Classify';
|
|
|
}
|
|
|
|
|
|
async function highlightClassification() {
|
|
|
if (isClassifying) {
|
|
|
alert('Classification in progress...');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
const resultsBody = document.getElementById('results-tbody');
|
|
|
const myDropzone = Dropzone.forElement("#upload-form");
|
|
|
|
|
|
if (!myDropzone.files.length) {
|
|
|
alert("Please upload some images first!");
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
|
|
|
isClassifying = true;
|
|
|
const classifyBtn = document.getElementById('classify-btn');
|
|
|
classifyBtn.disabled = true;
|
|
|
classifyBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Classifying...';
|
|
|
|
|
|
|
|
|
resultsBody.innerHTML = '';
|
|
|
|
|
|
|
|
|
try {
|
|
|
await fetch('/clear_uploads', { method: 'POST' });
|
|
|
|
|
|
|
|
|
for (const file of myDropzone.files) {
|
|
|
const formData = new FormData();
|
|
|
formData.append('file', file);
|
|
|
await fetch('/upload_multiple', {
|
|
|
method: 'POST',
|
|
|
body: formData
|
|
|
});
|
|
|
}
|
|
|
|
|
|
|
|
|
myDropzone.files.forEach((file, index) => {
|
|
|
resultsBody.insertAdjacentHTML('beforeend', `
|
|
|
<tr id="result-row-${index}">
|
|
|
<td class="serial">${index + 1}</td>
|
|
|
<td class="image-col">
|
|
|
<div class="filename">${file.name}</div>
|
|
|
<div style="color: var(--text-secondary);">Waiting...</div>
|
|
|
</td>
|
|
|
<td class="result-col">
|
|
|
<i class="fas fa-clock"></i>
|
|
|
</td>
|
|
|
<td class="labels-col">
|
|
|
<i class="fas fa-clock"></i>
|
|
|
</td>
|
|
|
</tr>
|
|
|
`);
|
|
|
});
|
|
|
|
|
|
|
|
|
processNextImage(0, myDropzone.files.length);
|
|
|
|
|
|
} catch (error) {
|
|
|
console.error('Error during classification:', error);
|
|
|
alert('Error during classification. Please try again.');
|
|
|
finishClassification();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function showError(index) {
|
|
|
const row = document.getElementById(`result-row-${index}`);
|
|
|
if (row) {
|
|
|
row.innerHTML = `
|
|
|
<td class="serial">${index + 1}</td>
|
|
|
<td colspan="3" style="color: var(--error-color);">
|
|
|
Error processing image. Please try again.
|
|
|
</td>
|
|
|
`;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function toggleLabels(labelsDiv) {
|
|
|
labelsDiv.classList.toggle('expanded');
|
|
|
}
|
|
|
|
|
|
document.getElementById('image-modal').addEventListener('click', function(e) {
|
|
|
if (e.target === this) {
|
|
|
closeModal();
|
|
|
}
|
|
|
});
|
|
|
|
|
|
document.addEventListener('click', function(e) {
|
|
|
if (e.target.classList.contains('thumbnail')) {
|
|
|
e.preventDefault();
|
|
|
const box = document.getElementById('thumbnail-preview-box');
|
|
|
const img = document.getElementById('thumbnail-preview-img');
|
|
|
img.src = e.target.src;
|
|
|
box.style.display = 'block';
|
|
|
box.style.left = (e.pageX + 10) + 'px';
|
|
|
box.style.top = (e.pageY + 10) + 'px';
|
|
|
|
|
|
document.addEventListener('keydown', hideOnEscape);
|
|
|
} else if (!e.target.closest('#thumbnail-preview-box')) {
|
|
|
hidePreview();
|
|
|
}
|
|
|
});
|
|
|
|
|
|
function hideOnEscape(e) {
|
|
|
if (e.key === 'Escape') {
|
|
|
hidePreview();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function hidePreview() {
|
|
|
const box = document.getElementById('thumbnail-preview-box');
|
|
|
box.style.display = 'none';
|
|
|
document.removeEventListener('keydown', hideOnEscape);
|
|
|
}
|
|
|
|
|
|
|
|
|
function downloadPageAsHTML() {
|
|
|
const now = new Date();
|
|
|
const date = now.toLocaleDateString('en-CA');
|
|
|
const time = now.toLocaleTimeString('en-GB')
|
|
|
.replace(/:/g, '-')
|
|
|
.split('.')[0];
|
|
|
|
|
|
|
|
|
const btn = document.getElementById('download-html-btn');
|
|
|
const originalBtnText = btn.innerHTML;
|
|
|
btn.disabled = true;
|
|
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Preparing report...';
|
|
|
|
|
|
try {
|
|
|
|
|
|
const summaryContent = document.getElementById('summary').innerHTML;
|
|
|
|
|
|
|
|
|
const rows = document.querySelectorAll('#results-tbody tr');
|
|
|
if (!rows.length) {
|
|
|
throw new Error('No data to save');
|
|
|
}
|
|
|
|
|
|
const tableData = Array.from(rows)
|
|
|
.filter(row => row.querySelector('.result-col span').textContent === 'Fail')
|
|
|
.map(row => ({
|
|
|
serialNo: row.querySelector('.serial').textContent,
|
|
|
image: row.querySelector('.thumbnail').src,
|
|
|
filename: row.querySelector('.filename').textContent,
|
|
|
status: row.querySelector('.result-col span').textContent,
|
|
|
labels: row.querySelector('.labels') ?
|
|
|
Array.from(row.querySelector('.labels').querySelectorAll('.label'))
|
|
|
.map(label => label.textContent) : []
|
|
|
}));
|
|
|
|
|
|
if (tableData.length === 0) {
|
|
|
alert('No failed images to save!');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
|
|
|
const CHUNK_SIZE = 5;
|
|
|
const chunks = [];
|
|
|
for (let i = 0; i < tableData.length; i += CHUNK_SIZE) {
|
|
|
chunks.push(tableData.slice(i, i + CHUNK_SIZE));
|
|
|
}
|
|
|
|
|
|
|
|
|
let currentChunk = 0;
|
|
|
const totalChunks = chunks.length;
|
|
|
|
|
|
async function sendChunk() {
|
|
|
try {
|
|
|
|
|
|
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> Saving ${currentChunk + 1}/${totalChunks}`;
|
|
|
|
|
|
const response = await fetch('/save_report_chunk', {
|
|
|
method: 'POST',
|
|
|
headers: {
|
|
|
'Content-Type': 'application/json'
|
|
|
},
|
|
|
body: JSON.stringify({
|
|
|
chunkNumber: currentChunk,
|
|
|
totalChunks: totalChunks,
|
|
|
date: date,
|
|
|
time: time,
|
|
|
summary: currentChunk === 0 ? summaryContent : '',
|
|
|
chunk: chunks[currentChunk]
|
|
|
})
|
|
|
});
|
|
|
|
|
|
if (!response.ok) {
|
|
|
const error = await response.json();
|
|
|
throw new Error(error.error || 'Failed to save report');
|
|
|
}
|
|
|
|
|
|
const result = await response.json();
|
|
|
|
|
|
if (result.error) {
|
|
|
throw new Error(result.error);
|
|
|
}
|
|
|
|
|
|
if (currentChunk < totalChunks - 1) {
|
|
|
currentChunk++;
|
|
|
await sendChunk();
|
|
|
} else {
|
|
|
alert(`Report saved successfully in Reports/${date}/Report_${date}_${time}.html`);
|
|
|
btn.innerHTML = originalBtnText;
|
|
|
btn.disabled = false;
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error('Error:', error);
|
|
|
alert('Error saving report: ' + error.message);
|
|
|
btn.innerHTML = originalBtnText;
|
|
|
btn.disabled = false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
sendChunk().catch(error => {
|
|
|
console.error('Error in chunk processing:', error);
|
|
|
alert('Error saving report: ' + error.message);
|
|
|
btn.innerHTML = originalBtnText;
|
|
|
btn.disabled = false;
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
console.error('Error:', error);
|
|
|
alert('Error preparing report: ' + error.message);
|
|
|
btn.innerHTML = originalBtnText;
|
|
|
btn.disabled = false;
|
|
|
}
|
|
|
}
|
|
|
</script>
|
|
|
</body>
|
|
|
</html> |