Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>ThreadStory - Fashion History Explorer</title> | |
| <style> | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { | |
| font-family: 'Georgia', serif; | |
| background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); | |
| min-height: 100vh; | |
| padding: 20px; | |
| } | |
| .container { | |
| max-width: 1000px; | |
| margin: 0 auto; | |
| background: white; | |
| border-radius: 15px; | |
| box-shadow: 0 10px 30px rgba(0,0,0,0.1); | |
| overflow: hidden; | |
| } | |
| .header { | |
| background: linear-gradient(45deg, #8B4513, #A0522D); | |
| color: white; | |
| padding: 30px; | |
| text-align: center; | |
| } | |
| .header h1 { font-size: 2.5rem; margin-bottom: 10px; } | |
| .content { padding: 30px; } | |
| .step { | |
| margin-bottom: 30px; | |
| padding: 20px; | |
| border: 2px solid #D2B48C; | |
| border-radius: 10px; | |
| background: #f9f9f9; | |
| } | |
| .step h2 { color: #8B4513; margin-bottom: 15px; } | |
| .upload-area { | |
| border: 2px dashed #8B4513; | |
| padding: 40px; | |
| text-align: center; | |
| border-radius: 10px; | |
| cursor: pointer; | |
| transition: background 0.3s; | |
| } | |
| .upload-area:hover { background: #f0f0f0; } | |
| .upload-area.dragover { background: #e8f4f8; } | |
| input[type="file"] { display: none; } | |
| .btn { | |
| background: linear-gradient(45deg, #8B4513, #A0522D); | |
| color: white; | |
| padding: 12px 25px; | |
| border: none; | |
| border-radius: 25px; | |
| cursor: pointer; | |
| font-size: 1rem; | |
| margin: 10px 5px; | |
| transition: transform 0.2s; | |
| } | |
| .btn:hover { transform: translateY(-2px); } | |
| .btn:disabled { opacity: 0.6; cursor: not-allowed; } | |
| /* Interactive Image Styles */ | |
| .image-container { | |
| position: relative; | |
| display: inline-block; | |
| max-width: 100%; | |
| margin: 20px auto; | |
| } | |
| .interactive-image { | |
| max-width: 100%; | |
| height: auto; | |
| display: block; | |
| border-radius: 10px; | |
| } | |
| .hotspot { | |
| position: absolute; | |
| width: 40px; | |
| height: 40px; | |
| background: rgba(139, 69, 19, 0.8); | |
| border: 3px solid white; | |
| border-radius: 50%; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: white; | |
| font-weight: bold; | |
| font-size: 18px; | |
| transform: translate(-50%, -50%); | |
| transition: all 0.3s; | |
| box-shadow: 0 4px 10px rgba(0,0,0,0.3); | |
| z-index: 10; | |
| } | |
| .hotspot:hover { | |
| transform: translate(-50%, -50%) scale(1.2); | |
| background: rgba(160, 82, 45, 0.9); | |
| } | |
| .hotspot.active { | |
| background: rgba(160, 82, 45, 1); | |
| animation: pulse 1s infinite; | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { transform: translate(-50%, -50%) scale(1); } | |
| 50% { transform: translate(-50%, -50%) scale(1.15); } | |
| } | |
| .popup-bubble { | |
| position: fixed; | |
| background: white; | |
| border: 3px solid #8B4513; | |
| border-radius: 15px; | |
| padding: 15px; | |
| min-width: 200px; | |
| max-width: 300px; | |
| box-shadow: 0 8px 20px rgba(0,0,0,0.2); | |
| z-index: 1000; | |
| display: none; | |
| } | |
| .popup-bubble.show { | |
| display: block; | |
| animation: popIn 0.3s ease-out; | |
| } | |
| @keyframes popIn { | |
| from { opacity: 0; transform: scale(0.8); } | |
| to { opacity: 1; transform: scale(1); } | |
| } | |
| .popup-bubble h4 { | |
| color: #8B4513; | |
| margin-bottom: 10px; | |
| font-size: 1.1rem; | |
| } | |
| .popup-bubble p { | |
| color: #555; | |
| margin-bottom: 10px; | |
| font-size: 0.9rem; | |
| } | |
| .popup-bubble button { | |
| width: 100%; | |
| background: #8B4513; | |
| color: white; | |
| border: none; | |
| padding: 8px; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| font-size: 0.9rem; | |
| } | |
| .popup-bubble button:hover { | |
| background: #A0522D; | |
| } | |
| .popup-close { | |
| position: absolute; | |
| top: 5px; | |
| right: 10px; | |
| background: none; | |
| border: none; | |
| font-size: 20px; | |
| cursor: pointer; | |
| color: #999; | |
| width: auto ; | |
| padding: 0 ; | |
| } | |
| .popup-close:hover { | |
| color: #333; | |
| background: none ; | |
| } | |
| .explanation { | |
| background: #f8f9fa; | |
| padding: 20px; | |
| border-radius: 10px; | |
| border-left: 4px solid #8B4513; | |
| white-space: pre-line; | |
| line-height: 1.6; | |
| } | |
| .loading { text-align: center; color: #8B4513; font-style: italic; } | |
| .error { | |
| background: #f8d7da; | |
| color: #721c24; | |
| padding: 15px; | |
| border-radius: 8px; | |
| border-left: 4px solid #dc3545; | |
| } | |
| .hidden { display: none; } | |
| .center { text-align: center; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <h1>🧵 ThreadStory</h1> | |
| <p>Upload an image to discover the history of fashion items</p> | |
| </div> | |
| <div class="content"> | |
| <!-- Step 1: Upload Image --> | |
| <div class="step" id="step1"> | |
| <h2>Step 1: Upload Image</h2> | |
| <div class="upload-area" onclick="document.getElementById('imageInput').click()"> | |
| <p>📸 Click here or drag & drop an image</p> | |
| <input type="file" id="imageInput" accept="image/*" onchange="uploadImage()"> | |
| </div> | |
| <div id="uploadStatus"></div> | |
| </div> | |
| <!-- Step 2: Interactive Image with Hotspots --> | |
| <div class="step hidden" id="step2"> | |
| <h2>Step 2: Click on Items in the Image</h2> | |
| <p>Click on the numbered circles to learn about each fashion item:</p> | |
| <div class="center"> | |
| <div class="image-container" id="imageContainer"> | |
| <img id="uploadedImage" class="interactive-image" src="" alt="Uploaded fashion item"> | |
| <!-- Hotspots will be added here dynamically --> | |
| </div> | |
| </div> | |
| <!-- Popup bubble template --> | |
| <div class="popup-bubble" id="popupBubble"> | |
| <button class="popup-close" onclick="closePopup()">×</button> | |
| <h4 id="popupTitle"></h4> | |
| <p id="popupDescription">Click "Learn More" for detailed history</p> | |
| <button onclick="learnMore()">Learn More</button> | |
| </div> | |
| </div> | |
| <!-- Step 3: View Detailed Explanation --> | |
| <div class="step hidden" id="step3"> | |
| <h2>Step 3: Historical Explanation</h2> | |
| <div id="explanation"></div> | |
| <button class="btn" onclick="goBackToImage()">← Back to Image</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| let currentItems = []; | |
| let currentItemIndex = null; | |
| let imageFilename = ''; | |
| function uploadImage() { | |
| const file = document.getElementById('imageInput').files[0]; | |
| if (!file) return; | |
| const status = document.getElementById('uploadStatus'); | |
| status.innerHTML = '<div class="loading">Analyzing image and detecting items...</div>'; | |
| const formData = new FormData(); | |
| formData.append('image', file); | |
| fetch('/upload', { | |
| method: 'POST', | |
| body: formData | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.success) { | |
| currentItems = data.items; | |
| imageFilename = data.image_filename; | |
| displayInteractiveImage(); | |
| document.getElementById('step1').classList.add('hidden'); | |
| document.getElementById('step2').classList.remove('hidden'); | |
| } else { | |
| status.innerHTML = `<div class="error">${data.error}</div>`; | |
| } | |
| }) | |
| .catch(error => { | |
| status.innerHTML = '<div class="error">Error uploading image</div>'; | |
| }); | |
| } | |
| function displayInteractiveImage() { | |
| const img = document.getElementById('uploadedImage'); | |
| const container = document.getElementById('imageContainer'); | |
| // Set image source | |
| img.src = `/uploads/${imageFilename}`; | |
| // Wait for image to load before adding hotspots | |
| img.onload = function() { | |
| // Remove any existing hotspots | |
| const existingHotspots = container.querySelectorAll('.hotspot'); | |
| existingHotspots.forEach(h => h.remove()); | |
| // Add hotspots for each item | |
| currentItems.forEach((item, index) => { | |
| const coords = item.coords.split(','); | |
| const x = parseFloat(coords[0]); | |
| const y = parseFloat(coords[1]); | |
| const hotspot = document.createElement('div'); | |
| hotspot.className = 'hotspot'; | |
| hotspot.textContent = index + 1; | |
| hotspot.style.left = x + '%'; | |
| hotspot.style.top = y + '%'; | |
| hotspot.onclick = (e) => showPopup(index, e); | |
| container.appendChild(hotspot); | |
| }); | |
| }; | |
| } | |
| function showPopup(itemIndex, event) { | |
| currentItemIndex = itemIndex; | |
| const item = currentItems[itemIndex]; | |
| const popup = document.getElementById('popupBubble'); | |
| const title = document.getElementById('popupTitle'); | |
| title.textContent = item.name; | |
| // Position popup near the clicked hotspot | |
| const container = document.getElementById('imageContainer'); | |
| const containerRect = container.getBoundingClientRect(); | |
| const hotspot = event.target; | |
| const hotspotRect = hotspot.getBoundingClientRect(); | |
| // Position popup near the clicked hotspot (using fixed positioning) | |
| const x = hotspotRect.left + hotspotRect.width / 2; | |
| const y = hotspotRect.top - 20; | |
| popup.style.left = x + 'px'; | |
| popup.style.top = y + 'px'; | |
| popup.style.transform = 'translate(-50%, -100%)'; | |
| popup.classList.add('show'); | |
| // Highlight active hotspot | |
| document.querySelectorAll('.hotspot').forEach(h => h.classList.remove('active')); | |
| hotspot.classList.add('active'); | |
| } | |
| function closePopup() { | |
| document.getElementById('popupBubble').classList.remove('show'); | |
| document.querySelectorAll('.hotspot').forEach(h => h.classList.remove('active')); | |
| } | |
| function learnMore() { | |
| closePopup(); | |
| const explanation = document.getElementById('explanation'); | |
| explanation.innerHTML = '<div class="loading">Getting detailed historical explanation...</div>'; | |
| fetch('/explain', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ item_index: currentItemIndex }) | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.success) { | |
| explanation.innerHTML = ` | |
| <h3>${data.item}</h3> | |
| <div class="explanation">${data.explanation}</div> | |
| `; | |
| document.getElementById('step2').classList.add('hidden'); | |
| document.getElementById('step3').classList.remove('hidden'); | |
| } else { | |
| explanation.innerHTML = `<div class="error">${data.error}</div>`; | |
| } | |
| }) | |
| .catch(error => { | |
| explanation.innerHTML = '<div class="error">Error getting explanation</div>'; | |
| }); | |
| } | |
| function goBackToImage() { | |
| document.getElementById('step3').classList.add('hidden'); | |
| document.getElementById('step2').classList.remove('hidden'); | |
| } | |
| // Drag and drop functionality | |
| const uploadArea = document.querySelector('.upload-area'); | |
| uploadArea.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| uploadArea.classList.add('dragover'); | |
| }); | |
| uploadArea.addEventListener('dragleave', () => { | |
| uploadArea.classList.remove('dragover'); | |
| }); | |
| uploadArea.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| uploadArea.classList.remove('dragover'); | |
| const files = e.dataTransfer.files; | |
| if (files.length > 0) { | |
| document.getElementById('imageInput').files = files; | |
| uploadImage(); | |
| } | |
| }); | |
| // Close popup when clicking outside | |
| document.addEventListener('click', (e) => { | |
| const popup = document.getElementById('popupBubble'); | |
| const hotspots = document.querySelectorAll('.hotspot'); | |
| let clickedHotspot = false; | |
| hotspots.forEach(h => { | |
| if (h.contains(e.target)) clickedHotspot = true; | |
| }); | |
| if (!popup.contains(e.target) && !clickedHotspot && popup.classList.contains('show')) { | |
| closePopup(); | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> |