yamanavijayavardhan's picture
printing extracted text16
7acb8dd
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Answer Generation</title>
<!-- Add Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
:root {
--primary-color: #4361ee;
--secondary-color: #3f37c9;
--accent-color: #4895ef;
--background-color: #f8f9fa;
--text-color: #2b2d42;
--border-radius: 8px;
--box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
body {
font-family: 'Poppins', sans-serif;
margin: 0;
padding: 2rem;
background-color: var(--background-color);
color: var(--text-color);
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
background: white;
border-radius: var(--border-radius);
box-shadow: var(--box-shadow);
}
h2 {
color: var(--primary-color);
margin-bottom: 1.5rem;
font-weight: 600;
position: relative;
padding-bottom: 0.5rem;
}
h2::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 50px;
height: 3px;
background-color: var(--accent-color);
border-radius: 2px;
}
.section {
background: white;
padding: 1.5rem;
border-radius: var(--border-radius);
margin-bottom: 2rem;
box-shadow: var(--box-shadow);
}
.upload-container {
margin-bottom: 1.5rem;
}
label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: var(--text-color);
}
input[type="file"] {
width: 100%;
padding: 0.5rem;
margin-bottom: 1rem;
border: 2px dashed var(--accent-color);
border-radius: var(--border-radius);
background: #f8f9fa;
cursor: pointer;
}
input[type="file"]:hover {
border-color: var(--primary-color);
}
select {
width: 100%;
padding: 0.8rem;
border: 1px solid #ddd;
border-radius: var(--border-radius);
margin-bottom: 1rem;
font-family: 'Poppins', sans-serif;
appearance: none;
background: white url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23444' viewBox='0 0 16 16'%3E%3Cpath d='M8 12L2 6h12z'/%3E%3C/svg%3E") no-repeat right 0.8rem center;
}
button {
background-color: var(--primary-color);
color: white;
border: none;
padding: 0.8rem 1.5rem;
border-radius: var(--border-radius);
cursor: pointer;
font-weight: 500;
transition: all 0.3s ease;
font-family: 'Poppins', sans-serif;
width: auto;
min-width: 200px;
margin: 1rem auto;
display: block;
}
button:hover {
background-color: var(--secondary-color);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(67, 97, 238, 0.3);
}
.answer-box {
width: 100%;
min-height: 100px;
padding: 1rem;
margin-bottom: 1rem;
border: 1px solid #ddd;
border-radius: var(--border-radius);
font-family: 'Poppins', sans-serif;
resize: vertical;
transition: border-color 0.3s ease;
}
.answer-box:focus {
outline: none;
border-color: var(--accent-color);
box-shadow: 0 0 0 3px rgba(72, 149, 239, 0.2);
}
table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
margin-top: 1.5rem;
background: white;
border-radius: var(--border-radius);
overflow: hidden;
box-shadow: var(--box-shadow);
}
th, td {
padding: 1rem;
text-align: left;
border-bottom: 1px solid #eee;
}
th {
background-color: var(--primary-color);
color: white;
font-weight: 500;
}
tr:hover {
background-color: #f8f9fa;
}
.hidden {
display: none;
}
/* Responsive Design */
@media (max-width: 768px) {
body {
padding: 1rem;
}
.container {
padding: 1rem;
}
button {
padding: 0.7rem 1rem;
}
}
/* Animation */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.section {
animation: fadeIn 0.5s ease-out;
}
.upload-methods {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
.upload-method {
flex: 1;
min-width: 300px;
}
.file-list {
margin-top: 1rem;
max-height: 200px;
overflow-y: auto;
border: 1px solid #ddd;
border-radius: var(--border-radius);
padding: 0.5rem;
}
.file-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.25rem 0;
border-bottom: 1px solid #eee;
}
.file-item:last-child {
border-bottom: none;
}
.remove-file {
color: red;
cursor: pointer;
padding: 0.25rem 0.5rem;
}
.folder-structure {
margin-top: 1rem;
padding: 1rem;
border: 1px solid #ddd;
border-radius: var(--border-radius);
background-color: #fff;
}
.folder-tree {
margin-left: 1rem;
min-height: 50px;
}
.folder {
margin: 0.5rem 0;
padding-left: 1.5rem;
position: relative;
}
.folder-name {
font-weight: 500;
color: var(--primary-color);
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
}
.folder-icon {
color: var(--primary-color);
font-size: 1.2em;
}
.folder-contents {
margin-left: 1.5rem;
padding-left: 1rem;
border-left: 2px solid var(--accent-color);
display: none;
}
.folder-contents.expanded {
display: block;
animation: fadeIn 0.3s ease-out;
}
.file-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem;
margin: 0.25rem 0;
background-color: #f8f9fa;
border-radius: 4px;
transition: background-color 0.2s;
}
.file-item:hover {
background-color: #e9ecef;
}
.file-name {
display: flex;
align-items: center;
gap: 0.5rem;
}
.file-icon {
color: #666;
}
.subfolder {
margin-left: 1.5rem;
border-left: 2px solid var(--accent-color);
padding-left: 1rem;
}
.file-count {
color: #666;
font-size: 0.9em;
margin-left: 0.5rem;
}
.file-info {
margin-top: 0.5rem;
padding: 0.5rem;
background-color: #e3f2fd;
border-radius: var(--border-radius);
display: none;
}
.no-files-message {
color: #666;
font-style: italic;
padding: 1rem;
text-align: center;
}
/* Add these CSS rules */
.csv-upload-visible {
display: block !important;
}
#csv-upload {
margin-top: 1rem;
margin-bottom: 1rem;
}
.upload-section {
margin: 20px 0;
padding: 20px;
border: 2px dashed #4361ee;
border-radius: 8px;
background: #f8f9fa;
display: none; /* Hide by default */
}
.upload-section.active {
display: block;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Remove conflicting hidden class */
.hidden {
display: none !important;
}
.file-input-container {
margin-top: 10px;
}
.helper-text {
color: #666;
font-size: 14px;
margin-top: 5px;
margin-bottom: 0;
}
/* Add these styles to your existing CSS */
.answers-header {
margin-bottom: 2rem;
border-bottom: 2px solid var(--accent-color);
padding-bottom: 1rem;
}
.answers-section {
display: flex;
flex-direction: column;
gap: 1.5rem;
margin-bottom: 2rem;
}
.answer-container {
background: #f8f9fa;
padding: 1.5rem;
border-radius: var(--border-radius);
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.answer-container label {
display: block;
margin-bottom: 0.8rem;
color: var(--primary-color);
font-weight: 500;
}
.answer-box {
width: 100%;
min-height: 100px;
padding: 1rem;
border: 1px solid #ddd;
border-radius: var(--border-radius);
font-family: 'Poppins', sans-serif;
font-size: 0.95rem;
line-height: 1.6;
resize: vertical;
transition: all 0.3s ease;
background: white;
}
.answer-box:focus {
outline: none;
border-color: var(--accent-color);
box-shadow: 0 0 0 3px rgba(72, 149, 239, 0.1);
}
.save-answers-btn {
background-color: var(--accent-color);
color: white;
border: none;
padding: 0.8rem 1.5rem;
border-radius: var(--border-radius);
cursor: pointer;
font-weight: 500;
transition: all 0.3s ease;
margin-top: 1rem;
display: block;
width: auto;
min-width: 150px;
}
.save-answers-btn:hover {
background-color: var(--primary-color);
transform: translateY(-2px);
}
.helper-text {
color: #666;
font-size: 0.9rem;
margin-top: 0.5rem;
}
/* Add responsive styles */
@media (max-width: 768px) {
.answer-container {
padding: 1rem;
}
.answer-box {
min-height: 80px;
}
}
/* Add to your existing CSS */
table {
width: 100%;
margin-top: 1rem;
border-collapse: collapse;
}
th, td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid #dee2e6;
}
th {
background-color: #4361ee;
color: white;
font-weight: 500;
}
tr:hover {
background-color: #f8f9fa;
}
.marks-summary {
margin-top: 1rem;
padding: 1rem;
background-color: #e3f2fd;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.marks-summary h4 {
margin-top: 0;
color: #4361ee;
}
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: none;
justify-content: center;
align-items: center;
z-index: 1000;
}
.loading-spinner {
width: 50px;
height: 50px;
border: 5px solid #f3f3f3;
border-top: 5px solid #4361ee;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.loading-text {
color: white;
margin-top: 20px;
font-size: 18px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
#marks-table-container {
margin-top: 20px;
overflow-x: auto;
}
#marks-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
background: white;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
#marks-table th,
#marks-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}
#marks-table th {
background-color: var(--primary-color);
color: white;
font-weight: 500;
}
#marks-table tr:hover {
background-color: #f5f5f5;
}
#marks-table tbody tr:nth-child(even) {
background-color: #f8f9fa;
}
/* Notification System Styles */
.notification-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
max-width: 400px;
max-height: 80vh;
overflow-y: auto;
}
.notification {
padding: 15px 20px;
margin-bottom: 10px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
animation: slideIn 0.3s ease-out;
position: relative;
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.notification.info {
background-color: #e3f2fd;
border-left: 4px solid #2196f3;
color: #0d47a1;
}
.notification.success {
background-color: #e8f5e9;
border-left: 4px solid #4caf50;
color: #1b5e20;
}
.notification.warning {
background-color: #fff3e0;
border-left: 4px solid #ff9800;
color: #e65100;
}
.notification.error {
background-color: #ffebee;
border-left: 4px solid #f44336;
color: #b71c1c;
}
.notification-content {
flex-grow: 1;
margin-right: 10px;
word-break: break-word;
}
.notification-close {
background: none;
border: none;
color: inherit;
cursor: pointer;
font-size: 20px;
padding: 0 5px;
opacity: 0.7;
}
.notification-close:hover {
opacity: 1;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(100%);
opacity: 0;
}
}
.notification.fade-out {
animation: slideOut 0.3s ease-out forwards;
}
/* Add these styles to your existing CSS */
.log-container {
background: #1e1e1e;
border-radius: var(--border-radius);
padding: 1rem;
margin-top: 1rem;
position: relative;
}
.log-display {
background: #1e1e1e;
color: #fff;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 14px;
line-height: 1.5;
padding: 1rem;
border-radius: 4px;
height: 300px;
overflow-y: auto;
white-space: pre-wrap;
word-wrap: break-word;
}
.refresh-logs-btn {
position: absolute;
top: 1rem;
right: 1rem;
background: var(--accent-color);
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.refresh-logs-btn:hover {
background: var(--primary-color);
}
.log-entry {
margin: 0;
padding: 2px 0;
}
.log-entry.info {
color: #4caf50;
}
.log-entry.error {
color: #f44336;
}
.log-entry.warning {
color: #ff9800;
}
.log-timestamp {
color: #888;
font-size: 0.9em;
margin-right: 8px;
}
/* Modal styles */
.modal {
display: none;
position: fixed;
z-index: 1001;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0,0,0,0.4);
}
.modal-content {
background-color: #fefefe;
margin: 5% auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
max-width: 800px;
border-radius: var(--border-radius);
position: relative;
max-height: 80vh;
overflow-y: auto;
}
.modal-header {
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #ddd;
}
.modal-body {
margin-bottom: 20px;
max-height: 60vh;
overflow-y: auto;
}
.text-section {
margin-bottom: 15px;
padding: 15px;
background-color: #f8f9fa;
border-radius: 4px;
border: 1px solid #e9ecef;
}
.text-section h4 {
margin-top: 0;
color: var(--primary-color);
margin-bottom: 10px;
}
.text-section p {
white-space: pre-wrap;
word-wrap: break-word;
margin: 0;
font-family: monospace;
line-height: 1.5;
}
/* Add these styles if not already present */
.log-entry {
padding: 5px;
margin: 2px 0;
border-radius: 4px;
}
.log-info {
background-color: #e3f2fd;
color: #0d47a1;
}
.log-error {
background-color: #ffebee;
color: #c62828;
}
.log-warning {
background-color: #fff3e0;
color: #ef6c00;
}
.log-success {
background-color: #e8f5e9;
color: #2e7d32;
}
/* Add this section for displaying answers */
.answer-set {
margin-bottom: 1rem;
padding: 1rem;
border: 1px solid #ddd;
border-radius: var(--border-radius);
}
.answer-set h4 {
margin: 0 0 0.5rem 0;
color: var(--primary-color);
}
.answers-list {
margin-top: 1rem;
}
.answer-set {
background: #f8f9fa;
padding: 1.5rem;
border-radius: var(--border-radius);
margin-bottom: 1.5rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.answer-set h4 {
color: var(--primary-color);
margin-top: 0;
margin-bottom: 1rem;
}
.answer-options {
display: flex;
flex-direction: column;
gap: 1rem;
}
.answer-option {
background: white;
padding: 1rem;
border-radius: var(--border-radius);
border: 1px solid #dee2e6;
}
.answer-option strong {
display: block;
margin-bottom: 0.5rem;
color: var(--secondary-color);
}
.answer-box {
white-space: pre-wrap;
word-break: break-word;
line-height: 1.5;
font-family: 'Poppins', sans-serif;
}
</style>
</head>
<body>
<div class="notification-container" id="notification-container"></div>
<div class="container">
<div class="section">
<h2>Upload Query CSV File</h2>
<div id="query-upload">
<div class="upload-container">
<label for="query-file">Query File:</label>
<input type="file" id="query-file" accept=".csv">
<p class="helper-text">Upload your query CSV file</p>
</div>
</div>
</div>
<div class="section">
<h2>Answer Generation</h2>
<div class="file-type-selection">
<label for="file-type">Select File Type:</label>
<select id="file-type" class="file-type-select">
<option value="pdf">PDF</option>
<option value="csv">CSV</option>
</select>
</div>
<!-- PDF Section -->
<div id="pdf-section" class="upload-section active">
<label>Upload PDF Files:</label>
<div class="file-input-container">
<input type="file" id="pdf-files" accept=".pdf" multiple>
<p class="helper-text">Please upload at least 2 PDF files</p>
</div>
</div>
<!-- CSV Section -->
<div id="csv-section" class="upload-section">
<label>Upload CSV File:</label>
<div class="file-input-container">
<input type="file" id="csv-file" accept=".csv">
<p class="helper-text">Upload a single CSV file</p>
</div>
</div>
<button id="compute-btn" onclick="computeAnswers()">Compute Answers</button>
</div>
<div class="section">
<h2>Student Answers Upload</h2>
<div class="upload-methods">
<div class="upload-method">
<label for="folder-upload">Upload Main Folder:</label>
<div class="upload-container">
<input type="file" id="folder-upload" webkitdirectory directory multiple>
<small class="help-text">Select the main folder containing student folders with answer images</small>
</div>
</div>
</div>
<div class="folder-structure">
<h3>Uploaded Files Structure:</h3>
<div id="folder-tree" class="folder-tree"></div>
</div>
<div id="file-list" class="file-list"></div>
</div>
<div class="section">
<div id="answers-container"></div>
<button id="compute-marks-btn" onclick="computeMarks()">Compute Marks</button>
<div id="marks-table-container">
<table id="marks-table">
<thead>
<tr>
<th>Student Folder</th>
<th>Image Name</th>
<th>Marks</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="marks-table-body">
</tbody>
</table>
</div>
</div>
<div class="section">
<h2>Server Logs</h2>
<div class="log-container">
<div id="log-display" class="log-display"></div>
<button onclick="refreshLogs()" class="refresh-logs-btn">Refresh Logs</button>
</div>
</div>
</div>
<div class="loading-overlay" id="loading-overlay">
<div style="text-align: center;">
<div class="loading-spinner"></div>
<div class="loading-text">Processing... This may take a few minutes.</div>
</div>
</div>
<div id="textModal" class="modal">
<div class="modal-content">
<span class="close-modal">&times;</span>
<div class="modal-header">
<h3>Extracted Text Details</h3>
</div>
<div class="modal-body">
<div class="text-section">
<h4>Extracted Text</h4>
<p id="extractedText"></p>
</div>
<div class="text-section">
<h4>Correct Answer</h4>
<p id="correctAnswer"></p>
</div>
</div>
<div class="modal-footer">
<button onclick="closeModal()" class="close-btn">Close</button>
</div>
</div>
</div>
<!-- Add this right after your existing modal -->
<div id="logModal" class="modal">
<div class="modal-content" style="width: 80%; max-height: 80vh; margin: 5% auto;">
<div class="modal-header">
<h2>Processing Logs</h2>
<span class="close" onclick="document.getElementById('logModal').style.display='none'">&times;</span>
</div>
<div class="modal-body" style="max-height: 60vh; overflow-y: auto;">
<pre id="logContent" style="white-space: pre-wrap; word-wrap: break-word; font-family: monospace; line-height: 1.5;"></pre>
</div>
</div>
</div>
<script>
const notificationSystem = {
container: null,
init() {
this.container = document.getElementById('notification-container');
},
show(message, type = 'info', duration = 5000) {
if (!this.container) this.init();
const notification = document.createElement('div');
notification.className = `notification ${type}`;
const content = document.createElement('div');
content.className = 'notification-content';
content.textContent = message;
const closeBtn = document.createElement('button');
closeBtn.className = 'notification-close';
closeBtn.innerHTML = '&times;';
closeBtn.onclick = () => this.remove(notification);
notification.appendChild(content);
notification.appendChild(closeBtn);
this.container.appendChild(notification);
if (duration > 0) {
setTimeout(() => this.remove(notification), duration);
}
return notification;
},
remove(notification) {
notification.classList.add('fade-out');
setTimeout(() => {
if (notification.parentElement === this.container) {
this.container.removeChild(notification);
}
}, 300);
},
success(message, duration = 5000) {
return this.show(message, 'success', duration);
},
error(message, duration = 8000) {
return this.show(message, 'error', duration);
},
warning(message, duration = 6000) {
return this.show(message, 'warning', duration);
},
info(message, duration = 4000) {
return this.show(message, 'info', duration);
}
};
document.addEventListener('DOMContentLoaded', function() {
const fileTypeSelect = document.getElementById('file-type');
const pdfSection = document.getElementById('pdf-section');
const csvSection = document.getElementById('csv-section');
function handleFileTypeChange() {
const selectedValue = fileTypeSelect.value;
console.log('Selected value:', selectedValue);
// Remove active class from both sections
pdfSection.classList.remove('active');
csvSection.classList.remove('active');
// Clear file inputs
document.getElementById('pdf-files').value = '';
document.getElementById('csv-file').value = '';
// Add active class to selected section
if (selectedValue === 'csv') {
csvSection.classList.add('active');
} else {
pdfSection.classList.add('active');
}
}
// Add event listener for file type change
fileTypeSelect.addEventListener('change', handleFileTypeChange);
// Add event listener for PDF files
document.getElementById('pdf-files').addEventListener('change', function(e) {
if (e.target.files.length < 2) {
alert('Please select at least 2 PDF files');
this.value = '';
} else {
console.log(`Selected ${e.target.files.length} PDF files`);
}
});
// Initial state setup
handleFileTypeChange();
});
function showLoading() {
document.getElementById('loading-overlay').style.display = 'flex';
}
function hideLoading() {
document.getElementById('loading-overlay').style.display = 'none';
}
async function computeAnswers() {
try {
showLogModal();
addLogMessage("Starting answer computation...", "info");
const fileType = document.getElementById('file-type').value;
const queryfile = document.getElementById('query-file').files[0];
if (!queryfile) {
addLogMessage("Error: Please upload a query file first!", "error");
notificationSystem.error("Please upload a query file first!");
return;
}
addLogMessage("Processing files...", "info");
const formData = new FormData();
formData.append('file_type', fileType);
formData.append('query_file', queryfile);
if (fileType === 'csv') {
const anscsvFile = document.getElementById('csv-file').files[0];
if (!anscsvFile) {
addLogMessage("Error: Please upload a CSV file for answers!", "error");
notificationSystem.error("Please upload a CSV file for answers!");
return;
}
formData.append('ans_csv_file', anscsvFile);
addLogMessage("Processing CSV file...", "info");
} else if (fileType === 'pdf') {
const pdfFiles = document.getElementById('pdf-files').files;
if (!pdfFiles || pdfFiles.length < 2) {
addLogMessage("Error: Please upload at least 2 PDF files!", "error");
notificationSystem.error("Please upload at least 2 PDF files!");
return;
}
for (let file of pdfFiles) {
formData.append('pdf_files[]', file);
}
addLogMessage(`Processing ${pdfFiles.length} PDF files...`, "info");
}
const computeBtn = document.getElementById('compute-btn');
computeBtn.disabled = true;
addLogMessage("Sending request to server...", "info");
const response = await fetch('/compute_answers', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error('Server returned an error response');
}
const result = await response.json();
if (result.error) {
throw new Error(result.error);
}
if (result.answers) {
addLogMessage("Successfully received answers from server", "success");
// Check for empty answers
const emptyAnswers = result.answers.filter(answer =>
!answer || (Array.isArray(answer) && answer.every(a => !a || a.trim() === ''))
);
if (emptyAnswers.length > 0) {
addLogMessage(`Warning: ${emptyAnswers.length} empty answer(s) detected`, "warning");
notificationSystem.warning(`Warning: ${emptyAnswers.length} empty answer(s) detected. Please check your input files.`);
}
// Show the first non-empty answer in a popup
const firstValidAnswer = result.answers.find(answer =>
answer && (Array.isArray(answer) ? answer.some(a => a && a.trim() !== '') : answer.trim() !== '')
);
if (firstValidAnswer) {
const formattedAnswer = Array.isArray(firstValidAnswer)
? firstValidAnswer.filter(a => a && a.trim() !== '').join('\n\n')
: firstValidAnswer;
showTextModal({
extracted_text: formattedAnswer,
correct_answer: Array.isArray(firstValidAnswer)
? firstValidAnswer.filter(a => a && a.trim() !== '')
: [firstValidAnswer]
});
} else {
addLogMessage("Warning: No valid answers found in the input files", "warning");
notificationSystem.warning("No valid answers found in the input files.");
}
displayAnswers(result.answers);
addLogMessage("Successfully displayed answers!", "success");
notificationSystem.success("Successfully generated answers!");
} else {
throw new Error('No answers received from server');
}
} catch (error) {
addLogMessage(`Error: ${error.message}`, "error");
notificationSystem.error('Error: ' + error.message);
} finally {
hideLoading();
const computeBtn = document.getElementById('compute-btn');
computeBtn.disabled = false;
}
}
function displayAnswers(answers) {
const answersContainer = document.getElementById('answers-container');
if (!answersContainer) {
const container = document.createElement('div');
container.id = 'answers-container';
container.className = 'section';
document.querySelector('.container').appendChild(container);
}
const container = document.getElementById('answers-container');
container.innerHTML = `
<h3>Correct Answers</h3>
<div class="answers-list">
${answers.map((answerSet, index) => `
<div class="answer-set">
<h4>Question ${index + 1}</h4>
<div class="answer-options">
${answerSet.map((answer, optionIndex) => `
<div class="answer-option">
<strong>Option ${optionIndex + 1}:</strong>
<div class="answer-box">${answer.replace(/\n/g, '<br>')}</div>
</div>
`).join('')}
</div>
</div>
`).join('')}
</div>
`;
// Store answers in hidden input for compute_marks
const hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.id = 'stored-answers';
hiddenInput.value = JSON.stringify(answers);
container.appendChild(hiddenInput);
// Enable compute marks button
const computeMarksBtn = document.getElementById('compute-marks-btn');
if (computeMarksBtn) {
computeMarksBtn.disabled = false;
}
}
let selectedFiles = new Map(); // To store all selected files
let folderStructure = new Map(); // To store folder structure
function createFolderStructure(files) {
const structure = new Map();
for (let file of files) {
if (file.type.startsWith('image/')) {
const pathParts = file.webkitRelativePath.split('/');
let currentLevel = structure;
let path = '';
// Skip the first part as it's the root folder
for (let i = 1; i < pathParts.length - 1; i++) {
const folderName = pathParts[i];
path = path ? `${path}/${folderName}` : folderName;
if (!currentLevel.has(folderName)) {
currentLevel.set(folderName, {
type: 'folder',
name: folderName,
path: path,
contents: new Map()
});
}
currentLevel = currentLevel.get(folderName).contents;
}
// Add the file
const fileName = pathParts[pathParts.length - 1];
currentLevel.set(fileName, {
type: 'file',
name: fileName,
path: `${path}/${fileName}`,
file: file
});
}
}
return structure;
}
function renderFolderStructure(structure, container, level = 0) {
structure.forEach((item, name) => {
if (item.type === 'folder') {
const folderDiv = document.createElement('div');
folderDiv.className = 'folder';
const folderHeader = document.createElement('div');
folderHeader.className = 'folder-name';
const fileCount = countFiles(item.contents);
folderHeader.innerHTML = `
<span class="folder-icon">📁</span>
${name}
<span class="file-count">(${fileCount} files)</span>
<button class="toggle-btn" onclick="toggleFolder(this)">▼</button>
`;
const contentsDiv = document.createElement('div');
contentsDiv.className = 'folder-contents expanded';
folderDiv.appendChild(folderHeader);
folderDiv.appendChild(contentsDiv);
container.appendChild(folderDiv);
renderFolderStructure(item.contents, contentsDiv, level + 1);
} else {
const fileDiv = document.createElement('div');
fileDiv.className = 'file-item';
fileDiv.innerHTML = `
<div class="file-name">
<span class="file-icon">📄</span>
${name}
</div>
<span class="remove-file" onclick="removeFile('${item.path}')">&times;</span>
`;
container.appendChild(fileDiv);
}
});
}
function countFiles(structure) {
let count = 0;
structure.forEach(item => {
if (item.type === 'file') {
count++;
} else {
count += countFiles(item.contents);
}
});
return count;
}
function toggleFolder(button) {
const contents = button.closest('.folder-name').nextElementSibling;
contents.classList.toggle('expanded');
button.textContent = contents.classList.contains('expanded') ? '▼' : '▶';
}
// Handle folder upload
document.getElementById('folder-upload').addEventListener('change', (event) => {
const files = event.target.files;
// Clear previous files
selectedFiles.clear();
// Validate file types
const validImageTypes = ['image/jpeg', 'image/jpg', 'image/png'];
let invalidFiles = [];
for (let file of files) {
if (file.type.startsWith('image/')) {
if (!validImageTypes.includes(file.type)) {
invalidFiles.push(file.name);
} else {
const pathParts = file.webkitRelativePath.split('/');
// Store the full relative path
const fullPath = file.webkitRelativePath;
selectedFiles.set(fullPath, {
file: file,
mainFolder: pathParts[0],
studentFolder: pathParts[1],
fileName: pathParts[pathParts.length - 1],
fullPath: fullPath
});
}
}
}
if (invalidFiles.length > 0) {
alert(`Warning: The following files are not valid image files (only .jpg, .jpeg, and .png are allowed):\n${invalidFiles.join('\n')}`);
}
folderStructure = createFolderStructure(Array.from(selectedFiles.values()).map(info => info.file));
// Update the visual tree
const treeContainer = document.getElementById('folder-tree');
treeContainer.innerHTML = '';
if (selectedFiles.size === 0) {
treeContainer.innerHTML = '<div class="no-files-message">No valid image files uploaded yet</div>';
return;
}
renderFolderStructure(folderStructure, treeContainer);
// Show immediate feedback
alert(`Successfully loaded ${selectedFiles.size} valid image files`);
});
function removeFile(path) {
selectedFiles.delete(path);
// Rebuild the folder structure
folderStructure = createFolderStructure(Array.from(selectedFiles.values()).map(info => info.file));
const treeContainer = document.getElementById('folder-tree');
treeContainer.innerHTML = '';
if (selectedFiles.size === 0) {
treeContainer.innerHTML = '<div class="no-files-message">No files uploaded yet</div>';
} else {
renderFolderStructure(folderStructure, treeContainer);
}
}
// Add these functions to handle file selection feedback
document.getElementById('query-file').addEventListener('change', (event) => {
const file = event.target.files[0];
const fileInfo = document.getElementById('query-file-info');
if (file) {
fileInfo.style.display = 'block';
fileInfo.textContent = `Selected file: ${file.name}`;
} else {
fileInfo.style.display = 'none';
}
});
async function computeMarks() {
try {
const storedAnswers = document.getElementById('stored-answers');
if (!storedAnswers || !storedAnswers.value) {
alert('Please compute answers first');
return;
}
if (selectedFiles.size === 0) {
alert('Please upload student answer files first');
return;
}
showLoading();
const formData = new FormData();
formData.append('answers', storedAnswers.value);
// Add files with their folder structure
selectedFiles.forEach((fileInfo, path) => {
formData.append('file', fileInfo.file, fileInfo.fullPath);
});
try {
const response = await fetch('/compute_marks', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.error) {
alert(data.error);
return;
}
displayResults(data.results);
notificationSystem.success('Successfully computed marks!');
} catch (error) {
console.error('Error:', error);
notificationSystem.error('An error occurred while computing marks: ' + error.message);
} finally {
hideLoading();
}
} catch (error) {
console.error('Error:', error);
notificationSystem.error('An error occurred: ' + error.message);
hideLoading();
}
}
function displayResults(results) {
const tableBody = document.getElementById('marks-table-body');
tableBody.innerHTML = ''; // Clear existing rows
// Sort results by subfolder and image name
const sortedResults = results.sort((a, b) => {
if (a.subfolder !== b.subfolder) {
return a.subfolder.localeCompare(b.subfolder);
}
return a.image.localeCompare(b.image);
});
// Group results by subfolder
const groupedResults = {};
sortedResults.forEach(result => {
if (!groupedResults[result.subfolder]) {
groupedResults[result.subfolder] = [];
}
groupedResults[result.subfolder].push(result);
});
// Calculate statistics
const totalStudents = Object.keys(groupedResults).length;
const totalAnswers = results.length;
const successfulExtractions = results.filter(r => r.extracted_text && !r.error).length;
const failedExtractions = results.filter(r => !r.extracted_text || r.error).length;
// Create summary section with more detailed information
const summarySection = document.createElement('div');
summarySection.className = 'marks-summary';
summarySection.innerHTML = `
<h4>Processing Summary</h4>
<p>Total students processed: ${totalStudents}</p>
<p>Total answers evaluated: ${totalAnswers}</p>
<p>Successful text extractions: ${successfulExtractions}</p>
<p>Failed text extractions: ${failedExtractions}</p>
${failedExtractions > 0 ? '<p style="color: #721c24;">⚠️ Some text extractions failed. Click "View Text" to see details.</p>' : ''}
`;
// Display results in table
Object.entries(groupedResults).forEach(([subfolder, folderResults]) => {
folderResults.forEach(result => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${escapeHtml(result.subfolder)}</td>
<td>${escapeHtml(result.image)}</td>
<td>
${result.error ?
`<span style="color: #721c24;">0.00 ⚠️</span>` :
result.marks.toFixed(2)}
</td>
<td>
<button class="view-text-btn" onclick='showTextModal(${JSON.stringify({
extracted_text: result.extracted_text,
correct_answer: result.correct_answer,
marks: result.marks,
image: result.image,
subfolder: result.subfolder,
error: result.error
})})'>
${result.error ? 'View Error' : 'View Text'}
</button>
</td>
`;
tableBody.appendChild(row);
});
});
// Insert summary before the table
const tableContainer = document.getElementById('marks-table-container');
tableContainer.insertBefore(summarySection, tableContainer.firstChild);
}
function showTextModal(result) {
const modal = document.getElementById('textModal');
const extractedText = document.getElementById('extractedText');
const correctAnswer = document.getElementById('correctAnswer');
// Handle extracted text
if (result.error) {
// If there's an error, display it with the error styling
extractedText.innerHTML = `<span style="color: #721c24; background-color: #f8d7da; padding: 10px; border-radius: 4px; display: block;">${result.error}</span>`;
} else if (!result.extracted_text || result.extracted_text.trim() === '') {
extractedText.innerHTML = '<span style="color: #721c24; background-color: #f8d7da; padding: 10px; border-radius: 4px; display: block;">No text could be extracted from the image. This might be due to poor image quality or unreadable handwriting.</span>';
} else {
extractedText.innerHTML = `<pre style="white-space: pre-wrap; word-wrap: break-word; margin: 0; font-family: monospace; line-height: 1.5;">${escapeHtml(result.extracted_text)}</pre>`;
}
// Handle correct answer
if (!result.correct_answer || (Array.isArray(result.correct_answer) && result.correct_answer.length === 0)) {
correctAnswer.innerHTML = '<span style="color: #856404; background-color: #fff3cd; padding: 10px; border-radius: 4px; display: block;">No correct answer available. Please ensure you have generated answers first.</span>';
} else {
const formattedAnswer = Array.isArray(result.correct_answer)
? result.correct_answer.filter(ans => ans && ans.trim() !== '').join('\n\n')
: result.correct_answer;
correctAnswer.innerHTML = `<pre style="white-space: pre-wrap; word-wrap: break-word; margin: 0; font-family: monospace; line-height: 1.5;">${escapeHtml(formattedAnswer)}</pre>`;
}
// Add additional information if available
const modalBody = document.querySelector('.modal-body');
const infoSection = modalBody.querySelector('.info-section') || document.createElement('div');
infoSection.className = 'text-section info-section';
// Only show additional info if we have marks or file information
if (result.marks !== undefined || result.image || result.subfolder) {
infoSection.innerHTML = `
<h4>Additional Information</h4>
<p style="font-family: sans-serif;">
${result.marks !== undefined ? `<strong>Marks:</strong> ${result.marks.toFixed(2)}<br>` : ''}
${result.image ? `<strong>Image:</strong> ${escapeHtml(result.image)}<br>` : ''}
${result.subfolder ? `<strong>Student Folder:</strong> ${escapeHtml(result.subfolder)}` : ''}
</p>
`;
// Add info section if it's not already there
if (!modalBody.querySelector('.info-section')) {
modalBody.appendChild(infoSection);
}
}
// Log the data for debugging
console.log('Modal Data:', {
extracted_text: result.extracted_text,
correct_answer: result.correct_answer,
marks: result.marks,
image: result.image,
subfolder: result.subfolder,
error: result.error
});
// Show the modal
modal.style.display = 'block';
}
// Helper function to escape HTML special characters
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function closeModal() {
const modal = document.getElementById('textModal');
modal.style.display = 'none';
}
// Close modal when clicking outside
window.onclick = function(event) {
const modal = document.getElementById('textModal');
if (event.target == modal) {
closeModal();
}
}
// Close modal when clicking the X button
document.querySelector('.close-modal').onclick = closeModal;
// Add this function to fetch and display logs
async function refreshLogs() {
try {
const logDisplay = document.getElementById('log-display');
if (!logDisplay) {
console.error('Log display element not found');
return;
}
// Add loading indicator
logDisplay.innerHTML = '<div class="log-entry info">Loading logs...</div>';
const response = await fetch('/check_logs');
let data;
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
data = await response.json();
} else {
throw new Error('Server returned non-JSON response');
}
if (!response.ok) {
throw new Error(data.message || data.error || `Failed to fetch logs: ${response.status}`);
}
if (data.status === 'error') {
throw new Error(data.error);
}
if (!data.logs) {
throw new Error('No logs received from server');
}
// Clear existing logs
logDisplay.innerHTML = '';
// Split logs into lines and format them
const logLines = data.logs.split('\n').filter(line => line.trim());
if (logLines.length === 0) {
logDisplay.innerHTML = '<div class="log-entry info">No logs available yet.</div>';
return;
}
logLines.forEach(line => {
const logEntry = document.createElement('div');
logEntry.className = 'log-entry';
// Determine log level and apply appropriate styling
if (line.includes('ERROR')) {
logEntry.classList.add('error');
} else if (line.includes('WARNING')) {
logEntry.classList.add('warning');
} else {
logEntry.classList.add('info');
}
// Add timestamp if present
const timestampMatch = line.match(/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3})/);
if (timestampMatch) {
const timestamp = timestampMatch[1];
const message = line.substring(timestamp.length + 2);
logEntry.innerHTML = `<span class="log-timestamp">${timestamp}</span> ${message}`;
} else {
logEntry.textContent = line;
}
logDisplay.appendChild(logEntry);
});
// Scroll to bottom
logDisplay.scrollTop = logDisplay.scrollHeight;
} catch (error) {
console.error('Error fetching logs:', error);
const logDisplay = document.getElementById('log-display');
if (logDisplay) {
logDisplay.innerHTML = `<div class="log-entry error">Error loading logs: ${error.message}</div>`;
}
notificationSystem.error('Failed to fetch logs: ' + error.message);
}
}
// Add auto-refresh for logs with error handling
let logRefreshInterval;
let consecutiveErrors = 0;
const MAX_CONSECUTIVE_ERRORS = 3;
function startLogRefresh() {
// Initial fetch
refreshLogs();
// Set up interval for auto-refresh
logRefreshInterval = setInterval(async () => {
try {
await refreshLogs();
consecutiveErrors = 0; // Reset error counter on success
} catch (error) {
consecutiveErrors++;
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
stopLogRefresh();
notificationSystem.error('Stopped auto-refreshing logs due to multiple errors');
}
}
}, 5000); // Refresh every 5 seconds
}
function stopLogRefresh() {
if (logRefreshInterval) {
clearInterval(logRefreshInterval);
logRefreshInterval = null;
}
}
// Start log refresh when page loads
document.addEventListener('DOMContentLoaded', function() {
startLogRefresh();
});
// Stop log refresh when page is unloaded
window.addEventListener('beforeunload', function() {
stopLogRefresh();
});
// Add this to your existing JavaScript
let logMessages = [];
function addLogMessage(message, type = 'info') {
const logContent = document.getElementById('logContent');
const entry = document.createElement('div');
entry.className = `log-entry log-${type.toLowerCase()}`;
entry.textContent = message;
logContent.appendChild(entry);
logContent.scrollTop = logContent.scrollHeight;
// Keep only last 100 messages
logMessages.push({ message, type });
if (logMessages.length > 100) {
logMessages.shift();
// Remove first child from logContent
if (logContent.firstChild) {
logContent.removeChild(logContent.firstChild);
}
}
}
function showLogModal() {
document.getElementById('logModal').style.display = 'block';
}
// Update your existing event source handling
if (typeof eventSource !== 'undefined') {
eventSource.close();
}
eventSource = new EventSource('/notifications');
eventSource.onmessage = function(event) {
const data = JSON.parse(event.data);
if (data.type === 'ping') return;
// Add message to log modal
if (data.message) {
let type = data.type;
if (typeof data.message === 'object' && data.message.message) {
addLogMessage(data.message.message, type);
} else {
addLogMessage(data.message, type);
}
}
// Show notification
if (data.type === 'error') {
showError(data.message);
} else if (data.type === 'success') {
showSuccess(data.message);
} else if (data.type === 'info') {
showInfo(data.message);
}
};
</script>
</body>
</html>