|
<!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; |
|
} |
|
|
|
|
|
.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; |
|
} |
|
|
|
|
|
.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; |
|
} |
|
|
|
|
|
.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> |
|
|
|
|
|
<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> |
|
<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">×</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> |
|
|
|
|
|
<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'">×</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 = '×'; |
|
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 { |
|
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"); |
|
|
|
|
|
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.`); |
|
} |
|
|
|
|
|
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> |
|
`; |
|
|
|
|
|
const hiddenInput = document.createElement('input'); |
|
hiddenInput.type = 'hidden'; |
|
hiddenInput.id = 'stored-answers'; |
|
hiddenInput.value = JSON.stringify(answers); |
|
container.appendChild(hiddenInput); |
|
|
|
|
|
const computeMarksBtn = document.getElementById('compute-marks-btn'); |
|
if (computeMarksBtn) { |
|
computeMarksBtn.disabled = false; |
|
} |
|
} |
|
|
|
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 { |
|
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); |
|
|
|
|
|
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 = ''; |
|
|
|
|
|
const sortedResults = results.sort((a, b) => { |
|
if (a.subfolder !== b.subfolder) { |
|
return a.subfolder.localeCompare(b.subfolder); |
|
} |
|
return a.image.localeCompare(b.image); |
|
}); |
|
|
|
|
|
const groupedResults = {}; |
|
sortedResults.forEach(result => { |
|
if (!groupedResults[result.subfolder]) { |
|
groupedResults[result.subfolder] = []; |
|
} |
|
groupedResults[result.subfolder].push(result); |
|
}); |
|
|
|
|
|
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; |
|
|
|
|
|
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>' : ''} |
|
`; |
|
|
|
|
|
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); |
|
}); |
|
}); |
|
|
|
|
|
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'); |
|
|
|
|
|
if (result.error) { |
|
|
|
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>`; |
|
} |
|
|
|
|
|
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>`; |
|
} |
|
|
|
|
|
const modalBody = document.querySelector('.modal-body'); |
|
const infoSection = modalBody.querySelector('.info-section') || document.createElement('div'); |
|
infoSection.className = 'text-section info-section'; |
|
|
|
|
|
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> |
|
`; |
|
|
|
|
|
if (!modalBody.querySelector('.info-section')) { |
|
modalBody.appendChild(infoSection); |
|
} |
|
} |
|
|
|
|
|
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 |
|
}); |
|
|
|
|
|
modal.style.display = 'block'; |
|
} |
|
|
|
|
|
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'; |
|
} |
|
|
|
|
|
window.onclick = function(event) { |
|
const modal = document.getElementById('textModal'); |
|
if (event.target == modal) { |
|
closeModal(); |
|
} |
|
} |
|
|
|
|
|
document.querySelector('.close-modal').onclick = closeModal; |
|
|
|
|
|
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(); |
|
}); |
|
|
|
|
|
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; |
|
|
|
|
|
logMessages.push({ message, type }); |
|
if (logMessages.length > 100) { |
|
logMessages.shift(); |
|
|
|
if (logContent.firstChild) { |
|
logContent.removeChild(logContent.firstChild); |
|
} |
|
} |
|
} |
|
|
|
function showLogModal() { |
|
document.getElementById('logModal').style.display = 'block'; |
|
} |
|
|
|
|
|
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; |
|
|
|
|
|
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); |
|
} |
|
} |
|
|
|
|
|
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> |