| <!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; |
| } |
| .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; |
| } |
| |
| |
| .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; |
| font-weight: 700; |
| color: #00ff88; |
| text-transform: capitalize; |
| line-height: 1.2; |
| } |
| .prediction-confidence { |
| font-size: 1rem; |
| 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, click to upload, or press Ctrl+V</div> |
| <div class="upload-subtext">Pasted images start analysis immediately • Supports PNG, JPG, JPEG</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> |
| |
| 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(); |
| |
| |
| 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'); |
| |
| |
| let fileDataStore = []; |
| const sessionKey = (window.crypto && crypto.randomUUID) |
| ? crypto.randomUUID() |
| : `session-${Date.now()}-${Math.random().toString(16).slice(2)}`; |
| |
| |
| 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); |
| document.addEventListener('paste', handlePaste); |
| |
| function preventDefaults(e) { |
| e.preventDefault(); |
| e.stopPropagation(); |
| } |
| |
| function handleDrop(e) { |
| handleFiles(e.dataTransfer.files); |
| } |
| |
| function handleFileSelect(e) { |
| handleFiles(e.target.files); |
| } |
| |
| async function handlePaste(e) { |
| const clipboardItems = Array.from(e.clipboardData?.items || []); |
| const imageFiles = clipboardItems |
| .filter(item => item.kind === 'file' && item.type.startsWith('image/')) |
| .map((item, index) => { |
| const blob = item.getAsFile(); |
| if (!blob) return null; |
| |
| const extension = blob.type === 'image/jpeg' ? 'jpg' |
| : blob.type === 'image/png' ? 'png' |
| : blob.type === 'image/webp' ? 'webp' |
| : 'png'; |
| const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); |
| return new File( |
| [blob], |
| `clipboard-${timestamp}-${index + 1}.${extension}`, |
| { type: blob.type || `image/${extension}`, lastModified: Date.now() } |
| ); |
| }) |
| .filter(Boolean); |
| |
| if (imageFiles.length === 0) return; |
| |
| e.preventDefault(); |
| |
| if (analyzeButton.disabled) { |
| showError('An analysis is already in progress.'); |
| return; |
| } |
| |
| await handleFiles(imageFiles); |
| await analyzeImages(); |
| } |
| |
| |
| async function handleFiles(files) { |
| if (files.length === 0) return; |
| |
| |
| 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); |
| |
| |
| 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'; |
| } |
| |
| 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(); |
| formData.append('session_key', sessionKey); |
| 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 = ''; |
| } |
| } |
| |
| |
| 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 => { |
| const fileData = Number.isInteger(result.index) |
| ? fileDataStore[result.index] |
| : fileDataStore.find(item => item.file.name === result.filename); |
| if (!fileData) return; |
| |
| const card = document.createElement('div'); |
| card.className = 'result-card'; |
| |
| const partPred = result.part_prediction || { class: 'unknown', confidence: 0 }; |
| const damagePred = result.damage_prediction || { class: 'unknown', confidence: 0 }; |
| const safeFilename = escapeHtml(result.filename || fileData.file.name); |
| const safePartClass = escapeHtml(String(partPred.class || 'unknown').replace(/_/g, ' ')); |
| const safeDamageClass = escapeHtml(String(damagePred.class || 'unknown')); |
| const damageNote = damagePred.note |
| ? `<div class="damage-note">${escapeHtml(String(damagePred.note))}</div>` |
| : ''; |
| const damageColor = damagePred.class === 'correct' ? '#00ff88' : '#ff4444'; |
| |
| card.innerHTML = ` |
| <img src="${fileData.dataURL}" alt="${safeFilename}" class="result-image"> |
| <h3>${safeFilename}</h3> |
| <div class="prediction-block"> |
| <div class="prediction-title">Part Detected</div> |
| <div class="prediction-class">${safePartClass}</div> |
| <div class="prediction-confidence">Confidence: ${(Number(partPred.confidence || 0) * 100).toFixed(2)}%</div> |
| </div> |
| <div class="prediction-block"> |
| <div class="prediction-title">Damage Status</div> |
| <div class="prediction-class" style="color: ${damageColor};">${safeDamageClass}</div> |
| <div class="prediction-confidence">Confidence: ${(Number(damagePred.confidence || 0) * 100).toFixed(2)}%</div> |
| ${damageNote} |
| </div> |
| `; |
| resultsContainer.appendChild(card); |
| }); |
| } |
| |
| function escapeHtml(value) { |
| return value.replace(/[&<>"']/g, char => ({ |
| '&': '&', |
| '<': '<', |
| '>': '>', |
| '"': '"', |
| "'": ''' |
| })[char]); |
| } |
| |
| 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> |