aad / Frontend.html
ajayk007's picture
Upload Frontend.html
fddc196 verified
Raw
History Blame Contribute Delete
25.3 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI CV Compatibility Scorer</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* Custom CSS from Deepsite - Keep this */
.file-input-label {
display: flex;
align-items: center;
justify-content: center;
padding: 1.5rem;
border: 2px dashed #d1d5db;
border-radius: 0.5rem;
cursor: pointer;
transition: all 0.3s ease;
}
.file-input-label:hover {
border-color: #3b82f6;
background-color: #f8fafc;
}
.file-input-label.drag-over {
border-color: #3b82f6;
background-color: #eff6ff;
}
.progress-bar {
height: 6px;
background-color: #e5e7eb;
border-radius: 3px;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
background-color: #3b82f6;
transition: width 0.3s ease;
}
.score-cell {
display: flex;
align-items: center;
}
.score-bar {
width: 60px;
height: 6px;
background-color: #e5e7eb;
border-radius: 3px;
margin-right: 8px;
overflow: hidden;
}
.score-bar-fill {
height: 100%;
background-color: #10b981;
}
@media (max-width: 768px) {
.input-section {
flex-direction: column;
}
.job-description, .cv-upload {
width: 100%;
}
}
/* NEW CSS for multi-line truncation in table explanations */
.truncate-multi-line {
display: -webkit-box;
-webkit-line-clamp: 3; /* Limit to 3 lines */
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
</style>
</head>
<body class="bg-gray-50 min-h-screen flex flex-col">
<header class="bg-white shadow-sm">
<div class="container mx-auto px-4 py-6">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-3">
<i class="fas fa-robot text-blue-500 text-2xl"></i>
<h1 class="text-2xl font-bold text-gray-800">AI CV Compatibility Scorer</h1>
</div>
<div class="hidden md:flex items-center space-x-4">
<span class="text-sm text-gray-500">Powered by AI</span>
<span class="h-8 w-8 rounded-full bg-blue-100 flex items-center justify-center">
<i class="fas fa-brain text-blue-500"></i>
</span>
</div>
</div>
</div>
</header>
<main class="flex-grow container mx-auto px-4 py-8">
<div class="bg-white rounded-xl shadow-md overflow-hidden mb-8">
<div class="p-6">
<h2 class="text-xl font-semibold text-gray-800 mb-4">Analyze CV Compatibility</h2>
<p class="text-gray-600 mb-6">Upload job description and candidate CVs to get AI-powered compatibility scores.</p>
<div class="flex input-section space-x-6">
<div class="job-description flex-1">
<label for="job-description" class="block text-sm font-medium text-gray-700 mb-2">Job Description</label>
<textarea
id="job-description"
rows="8"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500"
placeholder="Paste the job description here... Include requirements, responsibilities, and any other relevant details."></textarea>
</div>
<div class="cv-upload flex-1">
<label class="block text-sm font-medium text-gray-700 mb-2">Upload CVs</label>
<div class="file-input-container">
<input type="file" id="cv-upload" class="hidden" multiple accept=".pdf,.docx,.txt">
<label for="cv-upload" id="file-input-label" class="file-input-label">
<div class="text-center">
<i class="fas fa-cloud-upload-alt text-gray-400 text-3xl mb-2"></i>
<p class="text-sm text-gray-600">Drag & drop CV files here or click to browse</p>
<p class="text-xs text-gray-500 mt-1">Supports PDF, DOCX, TXT (Max 10 files)</p>
</div>
</label>
<div id="file-list" class="mt-3 space-y-2 max-h-40 overflow-y-auto hidden">
<p class="text-sm font-medium text-gray-700">Selected files:</p>
<div id="file-items" class="space-y-1"></div>
</div>
</div>
</div>
</div>
<div class="mt-6 flex justify-end">
<button id="score-button" class="px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg shadow-sm transition duration-150 ease-in-out flex items-center">
<i class="fas fa-calculator mr-2"></i> Score CVs
</button>
</div>
</div>
</div>
<div id="results-section" class="bg-white rounded-xl shadow-md overflow-hidden hidden">
<div class="p-6">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-semibold text-gray-800">Compatibility Results</h2>
<div class="flex items-center space-x-2">
<span class="text-sm text-gray-500">Processing:</span>
<div class="progress-bar w-32">
<div id="progress-bar-fill" class="progress-bar-fill" style="width: 0%"></div>
</div>
</div>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">CV Filename</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Score</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Explanation</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody id="results-table" class="bg-white divide-y divide-gray-200">
</tbody>
</table>
</div>
<div class="mt-4 flex justify-between items-center">
<button id="export-button" class="px-4 py-2 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-50 flex items-center">
<i class="fas fa-file-export mr-2"></i> Export Results
</button>
<button id="new-analysis-button" class="px-4 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm font-medium text-gray-700 flex items-center">
<i class="fas fa-redo mr-2"></i> New Analysis
</button>
</div>
</div>
</div>
<div id="loading-state" class="hidden fixed inset-0 bg-black bg-opacity-30 flex items-center justify-center z-50">
<div class="bg-white rounded-xl p-8 max-w-md w-full text-center">
<div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-blue-500 mx-auto mb-4"></div>
<h3 class="text-lg font-medium text-gray-900 mb-2">Analyzing CVs</h3>
<p class="text-gray-600 mb-4">Our AI is evaluating the compatibility of your CVs with the job description.</p>
<div class="w-full bg-gray-200 rounded-full h-2.5">
<div id="loading-progress" class="bg-blue-600 h-2.5 rounded-full" style="width: 0%"></div>
</div>
<p id="loading-text" class="text-sm text-gray-500 mt-2">Initializing analysis...</p>
</div>
</div>
</main>
<footer class="bg-white border-t border-gray-200 py-6">
<div class="container mx-auto px-4">
<div class="flex flex-col md:flex-row justify-between items-center">
<div class="flex items-center space-x-2 mb-4 md:mb-0">
<i class="fas fa-robot text-blue-500"></i>
<span class="text-sm font-medium text-gray-700">AI CV Compatibility Scorer</span>
</div>
<div class="flex space-x-6">
<a href="#" class="text-sm text-gray-500 hover:text-gray-700">Privacy Policy</a>
<a href="#" class="text-sm text-gray-500 hover:text-gray-700">Terms of Service</a>
<a href="#" class="text-sm text-gray-500 hover:text-gray-700">Contact Us</a>
</div>
</div>
<div class="mt-4 text-center md:text-left">
<p class="text-xs text-gray-400">© 2023 AI Talent Solutions. All rights reserved.</p>
</div>
</div>
</footer>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Get all necessary DOM elements
const fileInput = document.getElementById('cv-upload');
const fileInputLabel = document.getElementById('file-input-label');
const fileList = document.getElementById('file-list');
const fileItems = document.getElementById('file-items');
const scoreButton = document.getElementById('score-button');
const resultsSection = document.getElementById('results-section');
const resultsTable = document.getElementById('results-table');
const loadingState = document.getElementById('loading-state');
const loadingProgress = document.getElementById('loading-progress');
const loadingText = document.getElementById('loading-text');
const progressBarFill = document.getElementById('progress-bar-fill'); // Added for the small progress bar at the top
const exportButton = document.getElementById('export-button');
const newAnalysisButton = document.getElementById('new-analysis-button');
let files = []; // Stores the files selected by the user
// --- File Upload Logic (from Deepsite, remains largely the same) ---
fileInputLabel.addEventListener('dragover', (e) => {
e.preventDefault();
fileInputLabel.classList.add('drag-over');
});
fileInputLabel.addEventListener('dragleave', () => {
fileInputLabel.classList.remove('drag-over');
});
fileInputLabel.addEventListener('drop', (e) => {
e.preventDefault();
fileInputLabel.classList.remove('drag-over');
if (e.dataTransfer.files.length) {
fileInput.files = e.dataTransfer.files;
handleFiles();
}
});
fileInput.addEventListener('change', handleFiles);
function handleFiles() {
files = Array.from(fileInput.files);
if (files.length > 10) {
alert('You can upload a maximum of 10 files.');
files = files.slice(0, 10); // Truncate if too many files
}
updateFileList();
}
function updateFileList() {
fileItems.innerHTML = ''; // Clear previous file list
if (files.length > 0) {
fileList.classList.remove('hidden');
files.forEach((file, index) => {
const fileItem = document.createElement('div');
fileItem.className = 'flex items-center justify-between p-2 bg-gray-50 rounded';
const fileInfo = document.createElement('div');
fileInfo.className = 'flex items-center truncate';
let iconClass = 'fas fa-file-alt';
if (file.name.endsWith('.pdf')) iconClass = 'fas fa-file-pdf';
if (file.name.endsWith('.docx')) iconClass = 'fas fa-file-word';
fileInfo.innerHTML = `
<i class="${iconClass} text-gray-500 mr-2"></i>
<span class="text-sm text-gray-700 truncate" title="${file.name}">${file.name}</span>
<span class="text-xs text-gray-500 ml-2">(${(file.size / 1024).toFixed(1)} KB)</span>
`;
const removeButton = document.createElement('button');
removeButton.className = 'text-gray-400 hover:text-red-500 ml-2';
removeButton.innerHTML = '<i class="fas fa-times"></i>';
removeButton.addEventListener('click', () => {
files.splice(index, 1); // Remove file from array
updateFileList(); // Refresh the list
});
fileItem.appendChild(fileInfo);
fileItem.appendChild(removeButton);
fileItems.appendChild(fileItem);
});
} else {
fileList.classList.add('hidden'); // Hide the file list if no files
}
}
// --- Score Button Click Handler (MODIFIED) ---
scoreButton.addEventListener('click', async () => { // Made async to use await
const jobDescription = document.getElementById('job-description').value.trim();
if (!jobDescription) {
alert('Please enter a job description.');
return;
}
if (files.length === 0) {
alert('Please upload at least one CV file.');
return;
}
// Show loading state
loadingState.classList.remove('hidden');
document.body.style.overflow = 'hidden'; // Prevent scrolling while loading
loadingProgress.style.width = '0%'; // Reset inner loading bar
progressBarFill.style.width = '0%'; // Reset outer progress bar
loadingText.textContent = 'Sending data to AI for analysis...';
progressBarFill.style.backgroundColor = '#3b82f6'; // Reset color for outer bar
const formData = new FormData();
formData.append('job_description_text', jobDescription);
files.forEach(file => {
formData.append('cv_files', file); // Append each file to the form data
});
try {
// !!! IMPORTANT: REPLACE THIS URL with the actual URL of your deployed FastAPI Backend Space !!!
// Example: 'https://<your-username>-my-cv-scorer-backend.hf.space/score_cvs'
const backendApiUrl = 'YOUR_FASTAPI_BACKEND_API_URL_HERE/score_cvs';
// Simulate progress during API call (adjust intervals as needed)
let apiProgress = 0;
const apiProgressListener = setInterval(() => {
apiProgress += Math.random() * 5; // Simulate slower progress for API call
if (apiProgress > 95) apiProgress = 95; // Stop before 100 as final completion is after response
loadingProgress.style.width = `${apiProgress}%`;
progressBarFill.style.width = `${apiProgress}%`;
}, 500);
const response = await fetch(backendApiUrl, {
method: 'POST',
body: formData // Send as multipart/form-data
});
clearInterval(apiProgressListener); // Stop simulated progress after response
if (!response.ok) {
const errorData = await response.json();
loadingText.textContent = `Error: ${errorData.detail || 'Unknown error'}`;
loadingProgress.style.width = '100%'; // Show full progress on error
progressBarFill.style.width = '100%';
progressBarFill.style.backgroundColor = '#ef4444'; // Red for error
throw new Error(`API Error: ${response.status} - ${errorData.detail || 'Unknown error'}`);
}
loadingText.textContent = 'Analysis complete! Displaying results...';
loadingProgress.style.width = '100%';
progressBarFill.style.width = '100%';
progressBarFill.style.backgroundColor = '#10b981'; // Green for success
const results = await response.json(); // Parse the JSON response from your API
// Give a moment for the user to see "complete" message before hiding loading state
setTimeout(() => {
loadingState.classList.add('hidden');
document.body.style.overflow = 'auto'; // Re-enable scrolling
resultsSection.classList.remove('hidden');
updateResultsTable(results); // Call the function to populate results table
resultsSection.scrollIntoView({ behavior: 'smooth' }); // Scroll to results
}, 800); // 800ms delay
} catch (error) {
console.error("Failed to score CVs:", error);
clearInterval(apiProgressListener); // Ensure interval cleared on any error
loadingState.classList.add('hidden'); // Hide loading on error
document.body.style.overflow = 'auto';
alert("Error scoring CVs: " + error.message + ". Please ensure your backend API URL is correct and the server is running.");
progressBarFill.style.backgroundColor = '#3b82f6'; // Reset color
}
});
// --- Function to Update Results Table (MODIFIED) ---
function updateResultsTable(results) {
resultsTable.innerHTML = ''; // Clear existing results
if (!results || results.length === 0) {
const noResultsRow = document.createElement('tr');
noResultsRow.innerHTML = `
<td colspan="4" class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 text-center">No compatibility results found.</td>
`;
resultsTable.appendChild(noResultsRow);
return;
}
results.forEach((item, index) => {
const row = document.createElement('tr');
row.className = index % 2 === 0 ? 'bg-white' : 'bg-gray-50';
// Ensure score is a number for formatting and progress bar width
const score = parseFloat(item['Score (%)']);
const scoreDisplay = isNaN(score) ? 'N/A' : `${score.toFixed(2)}%`;
const scoreBarWidth = isNaN(score) ? 0 : Math.min(100, Math.max(0, score)); // Clamp between 0 and 100%
row.innerHTML = `
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
<div class="flex items-center">
<i class="fas ${item['CV Filename'].endsWith('.pdf') ? 'fa-file-pdf text-red-500' : item['CV Filename'].endsWith('.docx') ? 'fa-file-word text-blue-500' : 'fa-file-alt text-gray-500'} mr-2"></i>
${item['CV Filename']}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="score-cell">
<div class="score-bar">
<div class="score-bar-fill" style="width: ${scoreBarWidth}%"></div>
</div>
<span class="text-sm font-medium text-gray-800">${scoreDisplay}</span>
</div>
</td>
<td class="px-6 py-4 text-sm text-gray-700 max-w-lg truncate-multi-line" title="${item['Explanation']}">
${item['Explanation']}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<a href="#" class="text-blue-600 hover:text-blue-900">View Details</a>
</td>
`;
resultsTable.appendChild(row);
});
}
// --- New Analysis Button (ADDED) ---
newAnalysisButton.addEventListener('click', () => {
document.getElementById('job-description').value = ''; // Clear Job Description
fileInput.value = ''; // Clear file input element itself
files = []; // Clear the JavaScript files array
updateFileList(); // Update the displayed file list
resultsTable.innerHTML = ''; // Clear results table content
resultsSection.classList.add('hidden'); // Hide results section
document.body.style.overflow = 'auto'; // Re-enable body scrolling
progressBarFill.style.width = '0%'; // Reset progress bar
progressBarFill.style.backgroundColor = '#3b82f6'; // Reset progress bar color
});
// --- Export Button (ADDED) ---
exportButton.addEventListener('click', () => {
// Ensure there are results to export
if (resultsTable.rows.length === 0 || resultsTable.rows[0].cells[0].colSpan === 4) { // Check for "No results" row
alert('No results to export.');
return;
}
let csv = 'CV Filename,Score (%),Explanation\n'; // CSV header
// Iterate through table rows to build CSV content
resultsTable.querySelectorAll('tr').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3) { // Ensure row has enough cells for data
const filename = cells[0].textContent.trim();
// Get score text content, which includes percentage
const score = cells[1].querySelector('span').textContent.trim();
// Get explanation, remove line breaks and escape quotes for CSV
const explanation = cells[2].textContent.trim().replace(/\n/g, ' ').replace(/"/g, '""');
csv += `"${filename}","${score}","${explanation}"\n`;
}
});
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
// Create a temporary downloadable link
if (link.download !== undefined) { // Feature detection for download attribute
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', 'cv_compatibility_results.csv');
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} else {
// Fallback for browsers that don't support download attribute
alert('Your browser does not support direct file download. Please copy the results manually from the table.');
}
});
});
</script>
</body>
</html>