|
<!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> |
|
|
|
<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; |
|
} |
|
|
|
|
|
@media (max-width: 768px) { |
|
body { |
|
padding: 1rem; |
|
} |
|
|
|
.container { |
|
padding: 1rem; |
|
} |
|
|
|
button { |
|
padding: 0.7rem 1rem; |
|
} |
|
} |
|
|
|
|
|
@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; |
|
} |
|
|
|
|
|
.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; |
|
} |
|
|
|
.upload-section.active { |
|
display: block; |
|
} |
|
|
|
@keyframes fadeIn { |
|
from { |
|
opacity: 0; |
|
transform: translateY(-10px); |
|
} |
|
to { |
|
opacity: 1; |
|
transform: translateY(0); |
|
} |
|
} |
|
|
|
|
|
.hidden { |
|
display: none !important; |
|
} |
|
|
|
.file-input-container { |
|
margin-top: 10px; |
|
} |
|
|
|
.helper-text { |
|
color: #666; |
|
font-size: 14px; |
|
margin-top: 5px; |
|
margin-bottom: 0; |
|
} |
|
|
|
|
|
.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; |
|
} |
|
|
|
|
|
@media (max-width: 768px) { |
|
.answer-container { |
|
padding: 1rem; |
|
} |
|
|
|
.answer-box { |
|
min-height: 80px; |
|
} |
|
} |
|
|
|
|
|
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-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; |
|
} |
|
|
|
|
|
.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> |
|
|
|
|
|
<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> |
|
|
|
|
|
<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 = '×'; |
|
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); |
|
|
|
|
|
pdfSection.classList.remove('active'); |
|
csvSection.classList.remove('active'); |
|
|
|
|
|
document.getElementById('pdf-files').value = ''; |
|
document.getElementById('csv-file').value = ''; |
|
|
|
|
|
if (selectedValue === 'csv') { |
|
csvSection.classList.add('active'); |
|
} else { |
|
pdfSection.classList.add('active'); |
|
} |
|
} |
|
|
|
|
|
fileTypeSelect.addEventListener('change', handleFileTypeChange); |
|
|
|
|
|
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`); |
|
} |
|
}); |
|
|
|
|
|
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 = ''; |
|
|
|
|
|
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); |
|
|
|
|
|
const answersSection = document.createElement('div'); |
|
answersSection.className = 'answers-section'; |
|
|
|
|
|
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(); |
|
let folderStructure = new Map(); |
|
|
|
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 = ''; |
|
|
|
|
|
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; |
|
} |
|
|
|
|
|
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}')">×</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') ? '▼' : '▶'; |
|
} |
|
|
|
|
|
document.getElementById('folder-upload').addEventListener('change', (event) => { |
|
const files = event.target.files; |
|
|
|
|
|
selectedFiles.clear(); |
|
|
|
|
|
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('/'); |
|
|
|
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)); |
|
|
|
|
|
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); |
|
|
|
|
|
alert(`Successfully loaded ${selectedFiles.size} valid image files`); |
|
}); |
|
|
|
function removeFile(path) { |
|
selectedFiles.delete(path); |
|
|
|
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); |
|
} |
|
} |
|
|
|
|
|
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; |
|
} |
|
|
|
|
|
let totalSize = 0; |
|
const maxSize = 15 * 1024 * 1024; |
|
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(); |
|
|
|
|
|
answerValues.forEach((answer, index) => { |
|
formData.append('correct_answers[]', answer); |
|
}); |
|
|
|
|
|
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...`); |
|
|
|
|
|
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 |
|
}); |
|
|
|
|
|
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') { |
|
|
|
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> |
|
`; |
|
} |
|
|
|
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'); |
|
} |
|
|
|
|
|
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; |
|
} |
|
} |
|
|
|
|
|
function getFileSizeMB(file) { |
|
return file.size / (1024 * 1024); |
|
} |
|
|
|
|
|
async function handleFolderUpload(event) { |
|
try { |
|
const files = event.target.files; |
|
if (!files || files.length === 0) { |
|
notificationSystem.error("No files selected"); |
|
return; |
|
} |
|
|
|
|
|
selectedFiles.clear(); |
|
|
|
let totalSize = 0; |
|
const maxTotalSize = 15; |
|
const maxFileSize = 2; |
|
|
|
|
|
for (const file of files) { |
|
const fileSize = getFileSizeMB(file); |
|
|
|
|
|
if (fileSize > maxFileSize) { |
|
notificationSystem.warning(`File ${file.name} is too large (${fileSize.toFixed(1)}MB). Maximum size is ${maxFileSize}MB.`); |
|
continue; |
|
} |
|
|
|
|
|
if (totalSize + fileSize > maxTotalSize) { |
|
notificationSystem.warning(`Total file size limit of ${maxTotalSize}MB exceeded. Some files were not added.`); |
|
break; |
|
} |
|
|
|
|
|
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.`); |
|
} |
|
} |
|
|
|
|
|
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'); |
|
} |
|
|
|
|
|
tableBody.innerHTML = ''; |
|
|
|
|
|
const sortedStudents = Object.keys(results).sort(); |
|
|
|
|
|
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> |
|
`; |
|
|
|
|
|
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'); |
|
|
|
|
|
const studentCell = document.createElement('td'); |
|
studentCell.textContent = student; |
|
row.appendChild(studentCell); |
|
|
|
|
|
const scoresCell = document.createElement('td'); |
|
const scoresList = document.createElement('ul'); |
|
scoresList.className = 'list-unstyled mb-0'; |
|
|
|
|
|
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); |
|
|
|
|
|
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); |
|
} |
|
|
|
|
|
const resultsSection = document.getElementById('results-section'); |
|
if (resultsSection) { |
|
resultsSection.style.display = 'block'; |
|
} |
|
} |
|
|
|
|
|
async function refreshLogs() { |
|
try { |
|
const logDisplay = document.getElementById('log-display'); |
|
if (!logDisplay) { |
|
console.error('Log display element not found'); |
|
return; |
|
} |
|
|
|
|
|
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'); |
|
} |
|
|
|
|
|
logDisplay.innerHTML = ''; |
|
|
|
|
|
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'; |
|
|
|
|
|
if (line.includes('ERROR')) { |
|
logEntry.classList.add('error'); |
|
} else if (line.includes('WARNING')) { |
|
logEntry.classList.add('warning'); |
|
} else { |
|
logEntry.classList.add('info'); |
|
} |
|
|
|
|
|
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); |
|
}); |
|
|
|
|
|
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); |
|
} |
|
} |
|
|
|
|
|
let logRefreshInterval; |
|
let consecutiveErrors = 0; |
|
const MAX_CONSECUTIVE_ERRORS = 3; |
|
|
|
function startLogRefresh() { |
|
|
|
refreshLogs(); |
|
|
|
|
|
logRefreshInterval = setInterval(async () => { |
|
try { |
|
await refreshLogs(); |
|
consecutiveErrors = 0; |
|
} catch (error) { |
|
consecutiveErrors++; |
|
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) { |
|
stopLogRefresh(); |
|
notificationSystem.error('Stopped auto-refreshing logs due to multiple errors'); |
|
} |
|
} |
|
}, 5000); |
|
} |
|
|
|
function stopLogRefresh() { |
|
if (logRefreshInterval) { |
|
clearInterval(logRefreshInterval); |
|
logRefreshInterval = null; |
|
} |
|
} |
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() { |
|
startLogRefresh(); |
|
}); |
|
|
|
|
|
window.addEventListener('beforeunload', function() { |
|
stopLogRefresh(); |
|
}); |
|
</script> |
|
</body> |
|
</html> |