Spaces:
Sleeping
Sleeping
| <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}')">×</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> |