yamanavijayavardhan's picture
adding count down 1 2 3 4 and removing the null return error
8a0e8d8
raw
history blame
58.2 kB
<!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;
}
</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>
</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>
<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 {
showLoading();
const fileType = document.getElementById('file-type').value;
const queryfile = document.getElementById('query-file').files[0];
const anscsvFile = document.getElementById('csv-file').files[0];
const pdfFiles = document.getElementById('pdf-files').files;
if (!queryfile) {
notificationSystem.error("Please upload a query file first!");
hideLoading();
return;
}
notificationSystem.info("Processing files...");
const formData = new FormData();
formData.append('file_type', fileType);
formData.append('query_file', queryfile);
if (fileType === 'csv') {
if (!anscsvFile) {
notificationSystem.error("Please upload a CSV file for answers!");
hideLoading();
return;
}
formData.append('ans_csv_file', anscsvFile);
notificationSystem.info("Processing CSV file...");
} else if (fileType === 'pdf') {
if (!pdfFiles || pdfFiles.length < 2) {
notificationSystem.error("Please upload at least 2 PDF files!");
hideLoading();
return;
}
for (let file of pdfFiles) {
formData.append('pdf_files[]', file);
}
notificationSystem.info(`Processing ${pdfFiles.length} PDF files...`);
}
const computeBtn = document.getElementById('compute-btn');
computeBtn.disabled = true;
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) {
displayAnswers(result.answers);
notificationSystem.success("Successfully generated answers!");
} else {
throw new Error('No answers received from server');
}
} catch (error) {
console.error('Error:', error);
notificationSystem.error('Error: ' + error.message);
} finally {
hideLoading();
const computeBtn = document.getElementById('compute-btn');
computeBtn.disabled = false;
}
}
function displayAnswers(answers) {
const container = document.getElementById('answers-container');
container.innerHTML = '';
// Add header
const header = document.createElement('div');
header.className = 'answers-header';
header.innerHTML = `
<h3>Generated Answers</h3>
<p class="helper-text">Review and edit answers if needed</p>
`;
container.appendChild(header);
// Create answers section
const answersSection = document.createElement('div');
answersSection.className = 'answers-section';
// Handle both single and multiple answer formats
if (Array.isArray(answers[0])) {
answers.forEach((answerSet, index) => {
const answerContainer = document.createElement('div');
answerContainer.className = 'answer-container';
const label = document.createElement('label');
label.textContent = `Question ${index + 1} Answer:`;
const textBox = document.createElement('textarea');
textBox.className = 'answer-box';
textBox.value = Array.isArray(answerSet) ? answerSet.join('\n\n') : answerSet;
answerContainer.appendChild(label);
answerContainer.appendChild(textBox);
answersSection.appendChild(answerContainer);
});
} else {
const textBox = document.createElement('textarea');
textBox.className = 'answer-box';
textBox.value = answers.join('\n\n');
answersSection.appendChild(textBox);
}
container.appendChild(answersSection);
}
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 {
showLoading();
const answerBoxes = document.querySelectorAll('.answer-box');
if (answerBoxes.length === 0) {
notificationSystem.error("Please generate answers first!");
hideLoading();
return;
}
const answerValues = Array.from(answerBoxes).map(box => box.value.trim());
if (answerValues.some(answer => !answer)) {
notificationSystem.error("Please ensure all answer boxes are filled!");
hideLoading();
return;
}
if (selectedFiles.size === 0) {
notificationSystem.error("Please upload student answer files!");
hideLoading();
return;
}
// Add file size validation
let totalSize = 0;
const maxSize = 15 * 1024 * 1024; // 15MB
for (const fileInfo of selectedFiles.values()) {
totalSize += fileInfo.file.size;
if (totalSize > maxSize) {
throw new Error(`Total file size exceeds 15MB limit. Please reduce the number of files.`);
}
}
notificationSystem.info(`Processing ${selectedFiles.size} student files. This may take several minutes...`);
const computeBtn = document.getElementById('compute-marks-btn');
computeBtn.disabled = true;
const formData = new FormData();
// Add each answer as a separate correct_answers[] entry
answerValues.forEach((answer, index) => {
formData.append('correct_answers[]', answer);
});
// Add files with their folder structure
let validFiles = 0;
selectedFiles.forEach((fileInfo, path) => {
if (fileInfo.file.type.startsWith('image/')) {
formData.append('file', fileInfo.file, fileInfo.fullPath);
validFiles++;
}
});
if (validFiles === 0) {
throw new Error("No valid image files found in the uploaded folder");
}
notificationSystem.info(`Uploading and processing ${validFiles} files. Please be patient...`);
// Update loading message with more detailed progress
const loadingText = document.querySelector('.loading-text');
loadingText.innerHTML = `
Processing ${validFiles} files...<br>
This may take several minutes depending on the number of files.<br>
Please keep this window open.<br>
<small>Progress: 0/${validFiles} files processed</small>
`;
try {
const response = await fetch('/compute_marks', {
method: 'POST',
body: formData,
keepalive: true
});
// Set up event source for real-time notifications
const eventSource = new EventSource('/notifications');
let errorCount = 0;
const maxErrors = 3;
eventSource.onmessage = function(event) {
try {
const data = JSON.parse(event.data);
if (!data || typeof data !== 'object') {
console.warn('Received invalid notification data:', event.data);
return;
}
if (data.type === 'extracted_text') {
notificationSystem.info(`Extracted text from ${data.filename}:\n${data.text}`, 8000);
} else if (data.type === 'progress') {
// Update progress in loading text
const loadingText = document.querySelector('.loading-text');
if (loadingText) {
loadingText.innerHTML = `
Processing ${data.total} files...<br>
This may take several minutes depending on the number of files.<br>
Please keep this window open.<br>
<small>Progress: ${data.processed}/${data.total} files processed<br>
Current file: ${data.current_file}</small>
`;
}
// Reset error count on successful progress update
errorCount = 0;
} else if (data.type === 'error') {
console.error('Server notification error:', data.message);
errorCount++;
if (errorCount >= maxErrors) {
eventSource.close();
notificationSystem.error('Lost connection to server. Please refresh the page.');
}
}
} catch (e) {
console.error('Error parsing notification data:', e);
errorCount++;
if (errorCount >= maxErrors) {
eventSource.close();
notificationSystem.error('Error processing server updates. Please refresh the page.');
}
}
};
eventSource.onerror = function(error) {
console.error('EventSource error:', error);
eventSource.close();
notificationSystem.error('Lost connection to server. Please refresh the page.');
};
let result;
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
result = await response.json();
} else {
throw new Error('Server returned non-JSON response');
}
// Close event source after getting final response
eventSource.close();
if (!response.ok) {
throw new Error(result.message || result.error || `Server error: ${response.status}`);
}
if (result.status === 'error') {
throw new Error(result.message || result.error);
}
if (!result.results) {
throw new Error('No results found in server response');
}
displayMarks(result.results);
notificationSystem.success("Successfully computed marks!");
if (result.failed_files && result.failed_files.length > 0) {
const failedMessage = result.failed_files
.map(f => `${f.file}: ${f.error}`)
.join('\n');
notificationSystem.warning(`Some files failed to process:\n${failedMessage}`);
}
} catch (fetchError) {
console.error('Fetch error:', fetchError);
if (!navigator.onLine) {
throw new Error('No internet connection. Please check your connection and try again.');
} else {
throw new Error(`Server error: ${fetchError.message}`);
}
}
} catch (error) {
console.error('Error details:', error);
notificationSystem.error(error.message || 'Error computing marks. Please try again.');
} finally {
hideLoading();
const computeBtn = document.getElementById('compute-marks-btn');
computeBtn.disabled = false;
}
}
// Add this helper function to check file size
function getFileSizeMB(file) {
return file.size / (1024 * 1024);
}
// Add this function to handle folder uploads
async function handleFolderUpload(event) {
try {
const files = event.target.files;
if (!files || files.length === 0) {
notificationSystem.error("No files selected");
return;
}
// Clear previous files
selectedFiles.clear();
let totalSize = 0;
const maxTotalSize = 15; // Maximum total size in MB
const maxFileSize = 2; // Maximum size per file in MB
// Process each file
for (const file of files) {
const fileSize = getFileSizeMB(file);
// Check individual file size
if (fileSize > maxFileSize) {
notificationSystem.warning(`File ${file.name} is too large (${fileSize.toFixed(1)}MB). Maximum size is ${maxFileSize}MB.`);
continue;
}
// Check total size
if (totalSize + fileSize > maxTotalSize) {
notificationSystem.warning(`Total file size limit of ${maxTotalSize}MB exceeded. Some files were not added.`);
break;
}
// Add file if it's an image
if (file.type.startsWith('image/')) {
const fullPath = file.webkitRelativePath || file.name;
selectedFiles.set(fullPath, {
file: file,
fullPath: fullPath
});
totalSize += fileSize;
} else {
notificationSystem.warning(`File ${file.name} is not an image file and was skipped.`);
}
}
// Update UI with selected files
updateFileTree();
if (selectedFiles.size > 0) {
notificationSystem.success(`Successfully loaded ${selectedFiles.size} image files.`);
} else {
notificationSystem.error("No valid image files were found in the selected folder.");
}
} catch (error) {
console.error('Error handling folder upload:', error);
notificationSystem.error('Error processing folder: ' + error.message);
}
}
function displayMarks(results) {
const tableBody = document.getElementById('marks-table-body');
if (!tableBody) {
throw new Error('Table body element not found');
}
// Clear existing rows
tableBody.innerHTML = '';
// Sort student folders for consistent display
const sortedStudents = Object.keys(results).sort();
// Create a summary section
const summarySection = document.createElement('div');
summarySection.className = 'marks-summary';
summarySection.innerHTML = `
<h4>Processing Summary</h4>
<p>Total students processed: ${sortedStudents.length}</p>
<p>Total files processed: ${Object.values(results).reduce((acc, curr) => acc + Object.keys(curr).length, 0)}</p>
`;
// Insert summary before the table
const tableContainer = document.getElementById('marks-table-container');
tableContainer.insertBefore(summarySection, tableContainer.firstChild);
for (const student of sortedStudents) {
const scores = results[student];
const row = document.createElement('tr');
// Add student name cell
const studentCell = document.createElement('td');
studentCell.textContent = student;
row.appendChild(studentCell);
// Add scores cell
const scoresCell = document.createElement('td');
const scoresList = document.createElement('ul');
scoresList.className = 'list-unstyled mb-0';
// Sort filenames for consistent display
const sortedFiles = Object.keys(scores).sort();
for (const filename of sortedFiles) {
const score = scores[filename];
const scoreItem = document.createElement('li');
scoreItem.textContent = `${filename}: ${score}`;
scoresList.appendChild(scoreItem);
}
scoresCell.appendChild(scoresList);
row.appendChild(scoresCell);
// Calculate and add average score cell
const scoreValues = Object.values(scores);
const average = scoreValues.length > 0
? (scoreValues.reduce((a, b) => a + b, 0) / scoreValues.length).toFixed(2)
: 'N/A';
const averageCell = document.createElement('td');
averageCell.textContent = average;
row.appendChild(averageCell);
tableBody.appendChild(row);
}
// Show the results table
const resultsSection = document.getElementById('results-section');
if (resultsSection) {
resultsSection.style.display = 'block';
}
}
// 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();
});
</script>
</body>
</html>