SakibAhmed's picture
Upload 4 files
ac8c4df verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>YOLO Vision AI - Multi-Image Analysis</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #0f0f23 0%, #1a1a2e 50%, #16213e 100%);
min-height: 100vh;
overflow-x: hidden;
position: relative;
color: #e0e0e0;
}
.particles {
position: absolute; width: 100%; height: 100%; overflow: hidden; z-index: 0;
}
.particle {
position: absolute; width: 2px; height: 2px; background: #00d4ff; border-radius: 50%; animation: float 6s ease-in-out infinite; opacity: 0.6;
}
@keyframes float {
0%, 100% { transform: translateY(0px) rotate(0deg); }
50% { transform: translateY(-20px) rotate(180deg); }
}
.container {
position: relative; z-index: 1; max-width: 800px; margin: 0 auto; padding: 2rem; min-height: 100vh; display: flex; flex-direction: column; justify-content: flex-start; align-items: center;
}
.header {
text-align: center; margin-bottom: 2rem; animation: slideDown 1s ease-out; width: 100%;
}
.title {
font-size: 3.5rem; font-weight: 700; background: linear-gradient(45deg, #00d4ff, #ff00ff, #00ff88); background-size: 200% 200%; -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; animation: gradientShift 3s ease-in-out infinite; margin-bottom: 1rem; text-shadow: 0 0 30px rgba(0, 212, 255, 0.5);
}
.subtitle {
font-size: 1.2rem; color: #a0a0a0; font-weight: 300;
}
.upload-area {
width: 100%; max-width: 550px; min-height: 300px; border: 2px dashed #00d4ff; border-radius: 20px; background: rgba(0, 212, 255, 0.05); backdrop-filter: blur(10px); display: flex; flex-direction: column; justify-content: center; align-items: center; cursor: pointer; transition: all 0.3s ease; position: relative; overflow: hidden; animation: slideUp 1s ease-out 0.3s both;
}
.upload-area:hover {
border-color: #ff00ff; background: rgba(255, 0, 255, 0.05); transform: translateY(-5px); box-shadow: 0 20px 40px rgba(0, 212, 255, 0.2);
}
.upload-area.dragover {
border-color: #00ff88; background: rgba(0, 255, 136, 0.1); transform: scale(1.02);
}
.upload-icon {
font-size: 4rem; color: #00d4ff; margin-bottom: 1rem; transition: all 0.3s ease;
}
.upload-area:hover .upload-icon {
color: #ff00ff; transform: scale(1.1);
}
.upload-text {
color: #ffffff; font-size: 1.1rem; margin-bottom: 0.5rem; font-weight: 500;
}
.upload-subtext {
color: #a0a0a0; font-size: 0.9rem;
}
.file-input {
display: none;
}
.file-list-container {
display: none;
width: 100%;
max-width: 550px;
margin-top: 2rem;
animation: fadeIn 0.5s ease;
}
#fileList {
list-style: none;
background: rgba(0, 212, 255, 0.05);
border-radius: 10px;
padding: 1rem;
max-height: 200px;
overflow-y: auto;
border: 1px solid rgba(0, 212, 255, 0.2);
}
#fileList li {
padding: 0.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
color: #c0c0c0;
}
#fileList li:last-child {
border-bottom: none;
}
.analyze-button {
display: block;
width: 100%;
background: linear-gradient(45deg, #00d4ff, #0099cc); border: none; color: white; padding: 15px 40px; font-size: 1.1rem; font-weight: 600; border-radius: 50px; cursor: pointer; margin-top: 1.5rem; transition: all 0.3s ease; text-transform: uppercase; letter-spacing: 1px;
}
.analyze-button:hover {
transform: translateY(-2px); box-shadow: 0 10px 25px rgba(0, 212, 255, 0.4); background: linear-gradient(45deg, #ff00ff, #cc0099);
}
.analyze-button:disabled {
opacity: 0.6; cursor: not-allowed; transform: none; box-shadow: none; background: #555;
}
.loading {
display: none; margin-top: 2rem; text-align: center;
}
.spinner {
width: 40px; height: 40px; border: 4px solid rgba(0, 212, 255, 0.3); border-top: 4px solid #00d4ff; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto;
}
#results-container {
margin-top: 2rem;
width: 100%;
max-width: 550px; /* Adjusted max-width */
}
.result-card {
padding: 1.5rem;
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(15px);
border: 1px solid rgba(0, 212, 255, 0.3);
animation: slideUp 0.5s ease-out;
margin-bottom: 2rem;
border-radius: 15px; /* Unified border radius */
}
/* --- NEW: Image style within the card --- */
.result-image {
width: 100%;
height: auto;
max-height: 400px;
object-fit: contain;
border-radius: 10px;
margin-bottom: 1.5rem;
background-color: rgba(0,0,0,0.2);
}
.result-card h3 {
font-size: 1.2rem;
color: #00d4ff;
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 1px solid rgba(0, 212, 255, 0.2);
font-weight: 600;
word-wrap: break-word;
}
.prediction-block {
margin-bottom: 1.5rem;
}
.prediction-block:last-child {
margin-bottom: 0;
}
.prediction-title {
font-size: 0.9rem;
color: #a0a0a0;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 0.5rem;
}
.prediction-class {
font-size: 1.8rem; /* Made class name larger */
font-weight: 700;
color: #00ff88;
text-transform: capitalize;
line-height: 1.2;
}
.prediction-confidence {
font-size: 1rem; /* Slightly larger confidence text */
color: #e0e0e0;
}
.damage-note {
font-size: 0.8rem;
color: #aaa;
font-style: italic;
margin-top: 4px;
}
.error {
color: #ff4444; background: rgba(255, 68, 68, 0.1); padding: 1rem; border-radius: 10px; border: 1px solid #ff4444; margin-top: 2rem; width: 100%; max-width: 550px;
}
@keyframes slideDown { from { opacity: 0; transform: translateY(-50px); } to { opacity: 1; transform: translateY(0); } }
@keyframes slideUp { from { opacity: 0; transform: translateY(50px); } to { opacity: 1; transform: translateY(0); } }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
@keyframes gradientShift { 0%, 100% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } }
@media (max-width: 768px) {
.title { font-size: 2.5rem; } .container { padding: 1rem; } .upload-area { min-height: 250px; }
}
</style>
</head>
<body>
<div class="particles" id="particles"></div>
<div class="container">
<div class="header">
<h1 class="title">YOLO Vision AI</h1>
<p class="subtitle">Multi-Image Vehicle Part & Damage Analysis</p>
</div>
<div class="upload-area" id="uploadArea">
<div class="upload-icon">🔮</div>
<div class="upload-text">Drop your images here or click to upload</div>
<div class="upload-subtext">Supports PNG, JPG, JPEG formats</div>
<input type="file" id="fileInput" class="file-input" accept=".png,.jpg,.jpeg" multiple>
</div>
<div class="file-list-container" id="fileListContainer">
<ul id="fileList"></ul>
<button class="analyze-button" id="analyzeButton">🚀 Analyze Images</button>
</div>
<div class="loading" id="loading">
<div class="spinner"></div>
<p style="color: #00d4ff; margin-top: 1rem;">Processing your images...</p>
</div>
<div id="results-container"></div>
<div class="error" id="errorContainer" style="display: none;"></div>
</div>
<script>
// Create animated particles
function createParticles() {
const container = document.getElementById('particles');
if (container.children.length > 0) return;
for (let i = 0; i < 50; i++) {
const particle = document.createElement('div');
particle.className = 'particle';
particle.style.left = Math.random() * 100 + '%';
particle.style.top = Math.random() * 100 + '%';
particle.style.animationDelay = Math.random() * 6 + 's';
particle.style.animationDuration = (3 + Math.random() * 3) + 's';
container.appendChild(particle);
}
}
createParticles();
// DOM elements
const uploadArea = document.getElementById('uploadArea');
const fileInput = document.getElementById('fileInput');
const fileListContainer = document.getElementById('fileListContainer');
const fileList = document.getElementById('fileList');
const analyzeButton = document.getElementById('analyzeButton');
const loading = document.getElementById('loading');
const errorContainer = document.getElementById('errorContainer');
const resultsContainer = document.getElementById('results-container');
// --- NEW: Store for file objects and their data URLs for preview ---
let fileDataStore = [];
// Event Listeners
uploadArea.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', handleFileSelect);
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
uploadArea.addEventListener(eventName, preventDefaults, false);
});
['dragenter', 'dragover'].forEach(eventName => {
uploadArea.addEventListener(eventName, () => uploadArea.classList.add('dragover'), false);
});
['dragleave', 'drop'].forEach(eventName => {
uploadArea.addEventListener(eventName, () => uploadArea.classList.remove('dragover'), false);
});
uploadArea.addEventListener('drop', handleDrop, false);
analyzeButton.addEventListener('click', analyzeImages);
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
function handleDrop(e) {
handleFiles(e.dataTransfer.files);
}
function handleFileSelect(e) {
handleFiles(e.target.files);
}
// --- UPDATED: Reads files and generates data URLs for previews ---
async function handleFiles(files) {
if (files.length === 0) return;
// Clear previous selections and results
resetUI();
fileDataStore = [];
const filePromises = Array.from(files).map(file => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
fileDataStore.push({ file: file, dataURL: e.target.result });
resolve();
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
});
await Promise.all(filePromises);
// Update the UI list
fileDataStore.forEach(item => {
const listItem = document.createElement('li');
listItem.textContent = `${item.file.name} (${(item.file.size / 1024).toFixed(1)} KB)`;
fileList.appendChild(listItem);
});
fileListContainer.style.display = 'block';
uploadArea.style.display = 'none'; // Hide upload area after selection
}
async function analyzeImages() {
if (fileDataStore.length === 0) {
showError('Please select one or more images first');
return;
}
loading.style.display = 'block';
analyzeButton.disabled = true;
hideError();
resultsContainer.innerHTML = '';
try {
const formData = new FormData();
fileDataStore.forEach(item => {
formData.append('file', item.file);
});
const response = await fetch('/predict', { method: 'POST', body: formData });
const data = await response.json();
if (response.ok) {
displayResults(data);
} else {
showError(data.error || 'An unknown error occurred during prediction');
}
} catch (error) {
showError('Failed to connect to the server. Please check your connection and try again.');
console.error('Error:', error);
} finally {
loading.style.display = 'none';
analyzeButton.disabled = false;
fileInput.value = '';
}
}
// --- UPDATED: Displays results with image previews ---
function displayResults(results) {
if (!Array.isArray(results) || results.length === 0) {
resultsContainer.innerHTML = '<p>No results were returned from the server.</p>';
return;
}
results.forEach(result => {
// Find the corresponding image data URL from our store
const fileData = fileDataStore.find(item => item.file.name === result.filename);
if (!fileData) return; // Skip if no matching image found
const card = document.createElement('div');
card.className = 'result-card';
const partPred = result.part_prediction;
const damagePred = result.damage_prediction;
const damageNote = damagePred.note ? `<div class="damage-note">${damagePred.note}</div>` : '';
card.innerHTML = `
<img src="${fileData.dataURL}" alt="${result.filename}" class="result-image">
<h3>${result.filename}</h3>
<div class="prediction-block">
<div class="prediction-title">Part Detected</div>
<div class="prediction-class">${partPred.class.replace(/_/g, ' ')}</div>
<div class="prediction-confidence">Confidence: ${(partPred.confidence * 100).toFixed(2)}%</div>
</div>
<div class="prediction-block">
<div class="prediction-title">Damage Status</div>
<div class="prediction-class" style="color: ${damagePred.class === 'correct' ? '#00ff88' : '#ff4444'};">${damagePred.class}</div>
<div class="prediction-confidence">Confidence: ${(damagePred.confidence * 100).toFixed(2)}%</div>
${damageNote}
</div>
`;
resultsContainer.appendChild(card);
});
}
function resetUI() {
fileList.innerHTML = '';
resultsContainer.innerHTML = '';
fileListContainer.style.display = 'none';
uploadArea.style.display = 'flex';
hideError();
}
function showError(message) {
errorContainer.textContent = message;
errorContainer.style.display = 'block';
}
function hideError() {
errorContainer.style.display = 'none';
}
</script>
</body>
</html>