KindAlien's picture
Upload 32 files
3301039 verified
Raw
History Blame Contribute Delete
25.6 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VTU SGPA Calculator (with Print to PDF)</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.11.338/pdf.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Inter', sans-serif;
}
.loader {
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.fade-in {
animation: fadeIn 0.5s ease-in-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
/* Hide number input arrows */
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type=number] {
-moz-appearance: textfield;
}
@media print {
body * {
visibility: hidden;
}
#results-container, #results-container * {
visibility: visible;
}
#results-container {
position: absolute;
left: 0;
top: 0;
width: 100%;
}
}
</style>
</head>
<body class="bg-gray-50 text-gray-800">
<div class="container mx-auto p-4 sm:p-6 md:p-8 max-w-4xl">
<header class="text-center mb-8">
<h1 class="text-3xl sm:text-4xl font-bold text-gray-900">VTU SGPA Calculator (Batch Mode)</h1>
<p class="mt-2 text-md sm:text-lg text-gray-600">Calculate and export SGPA reports for multiple students.</p>
</header>
<main class="bg-white p-6 sm:p-8 rounded-2xl shadow-lg">
<!-- Step 1: Scheme and Subject Setup -->
<div id="setup-section">
<h2 class="text-2xl font-bold mb-4 text-gray-800">Step 1: Define Your Scheme & Subjects</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<div>
<label for="scheme-select" class="block text-sm font-medium text-gray-700">Scheme</label>
<select id="scheme-select" class="mt-1 block w-full py-2 px-3 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
<option>2022</option>
<option>2021</option>
<option>2018</option>
<option>Other</option>
</select>
</div>
<div>
<label for="semester-select" class="block text-sm font-medium text-gray-700">Semester</label>
<select id="semester-select" class="mt-1 block w-full py-2 px-3 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
<option>1</option><option>2</option><option>3</option><option>4</option>
<option>5</option><option>6</option><option>7</option><option>8</option>
</select>
</div>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">Subjects & Credits</label>
<div id="subject-list" class="space-y-2"></div>
<div class="mt-2 flex items-center gap-2">
<input type="text" id="new-subject-code" placeholder="Subject Code (e.g., 21CS5*)" class="flex-grow p-2 border border-gray-300 rounded-md">
<input type="number" id="new-subject-credit" placeholder="Credits" class="w-24 p-2 border border-gray-300 rounded-md">
<button id="add-subject-btn" class="bg-indigo-500 text-white font-semibold py-2 px-4 rounded-md hover:bg-indigo-600 transition-colors">Add</button>
</div>
<p class="text-xs text-gray-500 mt-2">Use * as a wildcard for optional subjects with the same credit (e.g., 21CSL5* for labs).</p>
</div>
<button id="confirm-setup-btn" class="w-full mt-6 bg-blue-600 text-white font-bold py-3 px-4 rounded-lg hover:bg-blue-700 transition-all">Confirm Subjects & Proceed</button>
</div>
<!-- Step 2: File Upload Section -->
<div id="upload-section" class="hidden">
<h2 class="text-2xl font-bold mb-4 text-gray-800">Step 2: Upload Result Files</h2>
<div class="flex items-center justify-center w-full">
<label for="result-file" class="flex flex-col items-center justify-center w-full h-48 border-2 border-gray-300 border-dashed rounded-lg cursor-pointer bg-gray-50 hover:bg-gray-100 transition-colors">
<div class="flex flex-col items-center justify-center pt-5 pb-6">
<svg class="w-10 h-10 mb-4 text-gray-500" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 16"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 13h3a3 3 0 0 0 0-6h-.025A5.56 5.56 0 0 0 16 6.5 5.5 5.5 0 0 0 5.207 5.021C5.137 5.017 5.071 5 5 5a4 4 0 0 0 0 8h2.167M10 15V6m0 0L8 8m2-2 2 2"/></svg>
<p class="mb-2 text-sm text-gray-500"><span class="font-semibold">Click to upload</span> or drag and drop</p>
<p id="file-name" class="text-xs text-gray-500">Multiple PNG, JPG, or PDF files</p>
</div>
<input id="result-file" type="file" class="hidden" accept=".pdf, .png, .jpg, .jpeg" multiple />
</label>
</div>
<button id="calculate-btn" class="w-full mt-6 bg-blue-600 text-white font-bold py-3 px-4 rounded-lg hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed" disabled>
Analyze & Calculate All
</button>
</div>
<!-- Loading and Message Section -->
<div id="message-section" class="text-center my-6 hidden">
<div class="flex justify-center items-center">
<div id="loader" class="loader"></div>
</div>
<p id="message-text" class="mt-4 text-gray-600 font-medium">Analyzing...</p>
</div>
<!-- Step 3: Results Section -->
<div id="results-section" class="hidden mt-8 fade-in">
<h2 class="text-2xl font-bold mb-4 text-gray-800">Step 3: Batch Results</h2>
<div id="results-container" class="space-y-6">
<!-- Result cards will be injected here -->
</div>
<div class="flex flex-col sm:flex-row gap-4 mt-8">
<button id="print-btn" class="w-full bg-green-600 text-white font-bold py-3 px-4 rounded-lg hover:bg-green-700 transition-all">
Export to PDF
</button>
<button id="reset-btn" class="w-full bg-gray-600 text-white font-bold py-3 px-4 rounded-lg hover:bg-gray-700 transition-all">
Start Over
</button>
</div>
</div>
</main>
<footer class="text-center mt-8 text-sm text-gray-500">
<p>Disclaimer: This tool is for estimation purposes only. Always refer to official VTU statements for your final grades.</p>
</footer>
</div>
<script>
// DOM Elements
const setupSection = document.getElementById('setup-section');
const uploadSection = document.getElementById('upload-section');
const messageSection = document.getElementById('message-section');
const resultsSection = document.getElementById('results-section');
const subjectList = document.getElementById('subject-list');
const newSubjectCodeInput = document.getElementById('new-subject-code');
const newSubjectCreditInput = document.getElementById('new-subject-credit');
const addSubjectBtn = document.getElementById('add-subject-btn');
const confirmSetupBtn = document.getElementById('confirm-setup-btn');
const fileInput = document.getElementById('result-file');
const fileNameDisplay = document.getElementById('file-name');
const calculateBtn = document.getElementById('calculate-btn');
const loader = document.getElementById('loader');
const messageText = document.getElementById('message-text');
const resultsContainer = document.getElementById('results-container');
const printBtn = document.getElementById('print-btn');
const resetBtn = document.getElementById('reset-btn');
// App State
let subjectCreditMap = {};
// Setup PDF.js worker
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.11.338/pdf.worker.min.js';
// Event Listeners
addSubjectBtn.addEventListener('click', addSubject);
confirmSetupBtn.addEventListener('click', confirmSetup);
fileInput.addEventListener('change', handleFileSelect);
calculateBtn.addEventListener('click', processBatch);
printBtn.addEventListener('click', exportToPdf);
resetBtn.addEventListener('click', () => location.reload());
function addSubject() {
const code = newSubjectCodeInput.value.trim().toUpperCase();
const credit = newSubjectCreditInput.value.trim();
if (!code || !credit) {
alert('Please enter both subject code and credit.');
return;
}
subjectCreditMap[code] = parseInt(credit);
renderSubjectList();
newSubjectCodeInput.value = '';
newSubjectCreditInput.value = '';
newSubjectCodeInput.focus();
}
function renderSubjectList() {
subjectList.innerHTML = '';
for (const code in subjectCreditMap) {
const div = document.createElement('div');
div.className = 'flex items-center gap-2 p-2 bg-gray-100 rounded-md';
div.innerHTML = `
<span class="flex-grow font-mono">${code}</span>
<span class="font-mono">${subjectCreditMap[code]} Credits</span>
<button class="text-red-500 hover:text-red-700" onclick="removeSubject('${code}')">&times;</button>
`;
subjectList.appendChild(div);
}
}
window.removeSubject = function(code) {
delete subjectCreditMap[code];
renderSubjectList();
}
function confirmSetup() {
if (Object.keys(subjectCreditMap).length === 0) {
alert('Please add at least one subject.');
return;
}
setupSection.classList.add('hidden');
uploadSection.classList.remove('hidden');
}
function handleFileSelect(event) {
const files = event.target.files;
if (files.length > 0) {
fileNameDisplay.textContent = `${files.length} file(s) selected`;
calculateBtn.disabled = false;
} else {
fileNameDisplay.textContent = 'Multiple PNG, JPG, or PDF files';
calculateBtn.disabled = true;
}
}
async function processBatch() {
const files = fileInput.files;
if (files.length === 0) {
showError('Please select one or more files.');
return;
}
uploadSection.classList.add('hidden');
messageSection.classList.remove('hidden');
resultsSection.classList.add('hidden');
resultsContainer.innerHTML = ''; // Clear previous batch results
for (let i = 0; i < files.length; i++) {
const file = files[i];
messageText.textContent = `Processing file ${i + 1} of ${files.length}: ${file.name}...`;
loader.classList.remove('hidden');
try {
await processSingleFile(file);
} catch (error) {
console.error(`Failed to process ${file.name}:`, error);
renderErrorCard(file.name, error.message);
}
}
messageSection.classList.add('hidden');
resultsSection.classList.remove('hidden');
}
async function processSingleFile(file) {
let content;
let mimeType;
if (file.type.startsWith('image/')) {
content = await fileToBase64(file);
mimeType = file.type;
} else if (file.type === 'application/pdf') {
content = await extractTextFromPdf(file);
mimeType = 'text/plain';
} else {
throw new Error('Unsupported file type.');
}
await analyzeContent(content, mimeType, file.name);
}
function fileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result.split(',')[1]);
reader.onerror = error => reject(error);
});
}
async function extractTextFromPdf(file) {
const arrayBuffer = await file.arrayBuffer();
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
let fullText = '';
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const textContent = await page.getTextContent();
fullText += textContent.items.map(item => item.str).join(' ') + '\n';
}
return fullText;
}
async function analyzeContent(content, mimeType, fileName) {
const prompt = `
You are an expert VTU (Visvesvaraya Technological University) result parser.
Analyze the provided result sheet content, which might be slightly blurry.
Extract the following details:
1. studentName: The full name of the student.
2. usn: The University Seat Number of the student.
3. subjects: An array of subject objects.
For each subject in the array, extract:
- subjectCode: The subject code (e.g., '21CSL55').
- totalMarks: The total marks obtained.
- result: The final result ('P' for Pass, 'F' for Fail, 'A' for Absent).
Ignore any summary rows or non-subject rows.
Return a single, valid JSON object with the keys "studentName", "usn", and "subjects".
Example: {"studentName": "John Doe", "usn": "1XX21CS001", "subjects": [{"subjectCode": "21CS51", "totalMarks": 75, "result": "P"}]}
`;
let payload;
if (mimeType.startsWith('image/')) {
payload = { contents: [{ parts: [{ text: prompt }, { inlineData: { mimeType: mimeType, data: content } }] }] };
} else {
payload = { contents: [{ parts: [{ text: `${prompt}\n\nHere is the result text:\n${content}` }] }] };
}
try {
const resultText = await callGeminiWithBackoff(payload);
const cleanedJsonString = resultText.replace(/^```json\n?/, '').replace(/```$/, '');
const extractedData = JSON.parse(cleanedJsonString);
if (!extractedData || !Array.isArray(extractedData.subjects) || extractedData.subjects.length === 0) {
throw new Error("AI model did not return valid data.");
}
renderResultCard(extractedData, fileName);
} catch (error) {
console.error('API or Parsing Error:', error);
throw new Error(`AI analysis failed: ${error.message}`);
}
}
async function callGeminiWithBackoff(payload) {
const apiKey = "AIzaSyDURWgVd8psg9vAMcOAs7ODVSiLUcoYNIA "; // Handled by environment
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-05-20:generateContent?key=${apiKey}`;
let delay = 1000;
for (let i = 0; i < 3; i++) {
const response = await fetch(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (response.ok) {
const result = await response.json();
if (result.candidates && result.candidates[0].content.parts.length > 0) {
return result.candidates[0].content.parts[0].text;
} else { throw new Error("Invalid API response structure."); }
} else if (response.status === 429 || response.status >= 500) {
await new Promise(resolve => setTimeout(resolve, delay));
delay *= 2;
} else {
const errorResult = await response.json();
throw new Error(errorResult.error?.message || `HTTP error! status: ${response.status}`);
}
}
throw new Error("API request failed after multiple retries.");
}
function renderResultCard(data, fileName) {
const card = document.createElement('div');
card.className = 'p-4 border border-gray-200 rounded-lg shadow-sm fade-in';
const sgpa = calculateSgpa(data.subjects);
let tableRows = '';
data.subjects.forEach(subject => {
const credits = findCreditsForSubject(subject.subjectCode);
tableRows += `
<tr class="border-b border-gray-200 last:border-b-0">
<td class="py-2 px-2 text-xs font-mono">${subject.subjectCode}</td>
<td class="py-2 px-2 text-xs text-center">${subject.totalMarks}</td>
<td class="py-2 px-2 text-xs text-center">
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${subject.result.toUpperCase() === 'P' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}">${subject.result}</span>
</td>
<td class="py-2 px-2 text-xs text-center font-medium ${credits === 'Not Found' ? 'text-red-500' : ''}">${credits}</td>
</tr>
`;
});
card.innerHTML = `
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 items-center">
<div class="md:col-span-2">
<p class="font-bold text-lg text-gray-800">${data.studentName || 'Name Not Found'}</p>
<p class="text-sm text-gray-600 font-mono">${data.usn || 'USN Not Found'}</p>
<p class="text-xs text-gray-400 mt-1">Source: ${fileName}</p>
</div>
<div class="text-center bg-blue-50 p-3 rounded-lg">
<p class="text-sm text-gray-700">SGPA</p>
<p class="text-3xl font-extrabold text-blue-600">${sgpa.toFixed(2)}</p>
</div>
</div>
<div class="mt-4 overflow-x-auto">
<table class="min-w-full">
<thead class="bg-gray-50">
<tr>
<th class="py-1 px-2 text-left text-xs font-semibold text-gray-500 uppercase">Code</th>
<th class="py-1 px-2 text-center text-xs font-semibold text-gray-500 uppercase">Marks</th>
<th class="py-1 px-2 text-center text-xs font-semibold text-gray-500 uppercase">Result</th>
<th class="py-1 px-2 text-center text-xs font-semibold text-gray-500 uppercase">Credits</th>
</tr>
</thead>
<tbody>${tableRows}</tbody>
</table>
</div>
`;
resultsContainer.appendChild(card);
}
function renderErrorCard(fileName, errorMessage) {
const card = document.createElement('div');
card.className = 'p-4 border border-red-200 bg-red-50 rounded-lg shadow-sm fade-in';
card.innerHTML = `
<p class="font-bold text-lg text-red-800">Processing Failed</p>
<p class="text-sm text-red-600 font-mono">File: ${fileName}</p>
<p class="text-xs text-red-500 mt-2">Reason: ${errorMessage}</p>
`;
resultsContainer.appendChild(card);
}
function findCreditsForSubject(subjectCode) {
if (subjectCreditMap[subjectCode]) return subjectCreditMap[subjectCode];
const wildcardKeys = Object.keys(subjectCreditMap).filter(k => k.includes('*'));
for (const key of wildcardKeys) {
const regex = new RegExp('^' + key.replace(/\*/g, '.*') + '$');
if (regex.test(subjectCode)) return subjectCreditMap[key];
}
return 'Not Found';
}
function calculateSgpa(subjects) {
let totalCredits = 0;
let weightedGradePoints = 0;
subjects.forEach(subject => {
const creditsValue = findCreditsForSubject(subject.subjectCode);
if (creditsValue !== 'Not Found') {
const credits = parseInt(creditsValue);
const gradePoint = getGradePoint(subject.totalMarks, subject.result);
totalCredits += credits;
weightedGradePoints += credits * gradePoint;
}
});
return totalCredits > 0 ? (weightedGradePoints / totalCredits) : 0;
}
function getGradePoint(marks, result) {
if (result.toUpperCase() !== 'P') return 0;
if (marks >= 90) return 10;
if (marks >= 80) return 9;
if (marks >= 70) return 8;
if (marks >= 60) return 7;
if (marks >= 50) return 6;
if (marks >= 45) return 5;
if (marks >= 40) return 4;
return 0;
}
function showError(message) {
messageText.textContent = message;
loader.classList.add('hidden');
messageSection.classList.remove('hidden');
uploadSection.classList.remove('hidden');
resultsSection.classList.add('hidden');
}
/**
* Exports the results container to a PDF file.
*/
function exportToPdf() {
const element = document.getElementById('results-container');
if (!element || element.children.length === 0) {
alert("No results to export!");
return;
}
const scheme = document.getElementById('scheme-select').value;
const semester = document.getElementById('semester-select').value;
const opt = {
margin: 0.5,
filename: `VTU_Sem${semester}_${scheme}_Results.pdf`,
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2, useCORS: true },
jsPDF: { unit: 'in', format: 'letter', orientation: 'portrait' }
};
// Use html2pdf().set(opt).from(element).save() to generate and save the PDF.
html2pdf().set(opt).from(element).save();
}
</script>
</body>
</html>