| | |
| |
|
| | class MarineDashboard { |
| | constructor() { |
| | this.currentImage = null; |
| | this.currentImageFile = null; |
| | this.isProcessing = false; |
| | |
| | this.initializeElements(); |
| | this.bindEvents(); |
| | this.checkAPIStatus(); |
| | this.loadSampleImages(); |
| | } |
| |
|
| | initializeElements() { |
| | |
| | this.uploadArea = document.getElementById('uploadArea'); |
| | this.fileInput = document.getElementById('fileInput'); |
| | this.identifyBtn = document.getElementById('identifyBtn'); |
| | |
| | |
| | this.confidenceSlider = document.getElementById('confidenceSlider'); |
| | this.confidenceValue = document.getElementById('confidenceValue'); |
| | this.iouSlider = document.getElementById('iouSlider'); |
| | this.iouValue = document.getElementById('iouValue'); |
| | |
| | |
| | this.annotatedImageContainer = document.getElementById('annotatedImageContainer'); |
| | |
| | |
| | this.metadataSection = document.getElementById('metadataSection'); |
| | this.speciesSection = document.getElementById('speciesSection'); |
| | this.processingTime = document.getElementById('processingTime'); |
| | this.speciesCount = document.getElementById('speciesCount'); |
| | this.imageSize = document.getElementById('imageSize'); |
| | this.speciesList = document.getElementById('speciesList'); |
| | |
| | |
| | this.statusDot = document.getElementById('statusDot'); |
| | this.statusText = document.getElementById('statusText'); |
| | this.modelInfo = document.getElementById('modelInfo'); |
| |
|
| | |
| | this.totalSpecies = document.getElementById('totalSpecies'); |
| | this.deviceInfo = document.getElementById('deviceInfo'); |
| |
|
| | |
| | this.sampleImagesSlider = document.getElementById('sampleImagesSlider'); |
| | this.sliderPrev = document.getElementById('sliderPrev'); |
| | this.sliderNext = document.getElementById('sliderNext'); |
| | } |
| |
|
| | bindEvents() { |
| | |
| | this.uploadArea.addEventListener('click', () => this.fileInput.click()); |
| | this.uploadArea.addEventListener('dragover', this.handleDragOver.bind(this)); |
| | this.uploadArea.addEventListener('dragleave', this.handleDragLeave.bind(this)); |
| | this.uploadArea.addEventListener('drop', this.handleDrop.bind(this)); |
| | |
| | |
| | this.fileInput.addEventListener('change', this.handleFileSelect.bind(this)); |
| | |
| | |
| | this.confidenceSlider.addEventListener('input', this.updateConfidenceValue.bind(this)); |
| | this.iouSlider.addEventListener('input', this.updateIouValue.bind(this)); |
| | |
| | |
| | this.identifyBtn.addEventListener('click', this.identifySpecies.bind(this)); |
| |
|
| | |
| | this.sliderPrev.addEventListener('click', this.scrollSliderLeft.bind(this)); |
| | this.sliderNext.addEventListener('click', this.scrollSliderRight.bind(this)); |
| | } |
| |
|
| | |
| | async checkAPIStatus() { |
| | try { |
| | const response = await fetch('/api/v1/health'); |
| | const data = await response.json(); |
| | |
| | if (data.model_loaded) { |
| | this.statusDot.className = 'status-dot healthy'; |
| | this.statusText.textContent = 'API Ready'; |
| | |
| | if (data.model_info) { |
| | this.modelInfo.textContent = `Model: ${data.model_info.model_name} (${data.model_info.total_classes} species)`; |
| | this.totalSpecies.textContent = data.model_info.total_classes; |
| | this.deviceInfo.textContent = data.model_info.device; |
| | } |
| | } else { |
| | this.statusDot.className = 'status-dot error'; |
| | this.statusText.textContent = 'Model Loading...'; |
| | this.modelInfo.textContent = 'Please wait while the model loads'; |
| | } |
| | } catch (error) { |
| | this.statusDot.className = 'status-dot error'; |
| | this.statusText.textContent = 'API Unavailable'; |
| | this.modelInfo.textContent = 'Unable to connect to API'; |
| | console.error('API status check failed:', error); |
| | } |
| | } |
| |
|
| | |
| | handleDragOver(e) { |
| | e.preventDefault(); |
| | this.uploadArea.classList.add('dragover'); |
| | } |
| |
|
| | handleDragLeave(e) { |
| | e.preventDefault(); |
| | this.uploadArea.classList.remove('dragover'); |
| | } |
| |
|
| | handleDrop(e) { |
| | e.preventDefault(); |
| | this.uploadArea.classList.remove('dragover'); |
| | |
| | const files = e.dataTransfer.files; |
| | if (files.length > 0) { |
| | this.processFile(files[0]); |
| | } |
| | } |
| |
|
| | |
| | handleFileSelect(e) { |
| | const file = e.target.files[0]; |
| | if (file) { |
| | this.processFile(file); |
| | } |
| | } |
| |
|
| | |
| | processFile(file) { |
| | |
| | if (!file.type.startsWith('image/')) { |
| | window.MarineAPI.utils.showNotification('Please select an image file', 'error'); |
| | return; |
| | } |
| |
|
| | |
| | if (file.size > 10 * 1024 * 1024) { |
| | window.MarineAPI.utils.showNotification('File size must be less than 10MB', 'error'); |
| | return; |
| | } |
| |
|
| | this.currentImageFile = file; |
| | this.displayUploadedImage(file); |
| | this.identifyBtn.disabled = false; |
| | |
| | |
| | this.uploadArea.classList.add('has-image'); |
| | } |
| |
|
| | |
| | displayUploadedImage(file) { |
| | const reader = new FileReader(); |
| | reader.onload = (e) => { |
| | this.currentImage = e.target.result; |
| | this.uploadArea.innerHTML = ` |
| | <img src="${e.target.result}" alt="Uploaded image" style="width: 100%; height: 100%; object-fit: cover; border-radius: 8px;" /> |
| | `; |
| | }; |
| | reader.readAsDataURL(file); |
| | } |
| |
|
| | |
| | updateConfidenceValue() { |
| | this.confidenceValue.textContent = `${this.confidenceSlider.value}%`; |
| | } |
| |
|
| | updateIouValue() { |
| | this.iouValue.textContent = `${this.iouSlider.value}%`; |
| | } |
| |
|
| | |
| | async identifySpecies() { |
| | if (!this.currentImage || this.isProcessing) return; |
| |
|
| | this.isProcessing = true; |
| | window.MarineAPI.utils.setLoading(this.identifyBtn, true); |
| |
|
| | try { |
| | |
| | const requestData = { |
| | image: this.currentImage.split(',')[1], |
| | confidence_threshold: this.confidenceSlider.value / 100, |
| | iou_threshold: this.iouSlider.value / 100, |
| | image_size: 640, |
| | return_annotated_image: true |
| | }; |
| |
|
| | |
| | const response = await fetch('/api/v1/detect', { |
| | method: 'POST', |
| | headers: { |
| | 'Content-Type': 'application/json', |
| | }, |
| | body: JSON.stringify(requestData) |
| | }); |
| |
|
| | if (!response.ok) { |
| | throw new Error(`API request failed: ${response.status}`); |
| | } |
| |
|
| | const result = await response.json(); |
| | this.displayResults(result); |
| | window.MarineAPI.utils.showNotification('Species identification completed!', 'success'); |
| |
|
| | } catch (error) { |
| | console.error('Identification failed:', error); |
| | window.MarineAPI.utils.showNotification('Identification failed. Please try again.', 'error'); |
| | } finally { |
| | this.isProcessing = false; |
| | window.MarineAPI.utils.setLoading(this.identifyBtn, false); |
| | } |
| | } |
| |
|
| | |
| | displayResults(result) { |
| | const { detections, annotated_image, processing_time, image_dimensions } = result; |
| |
|
| | |
| | if (annotated_image) { |
| | this.annotatedImageContainer.innerHTML = ` |
| | <img src="data:image/jpeg;base64,${annotated_image}" alt="Annotated results" /> |
| | `; |
| | } |
| |
|
| | |
| | this.processingTime.textContent = `${processing_time.toFixed(3)}s`; |
| | this.speciesCount.textContent = detections.length; |
| | this.imageSize.textContent = `${image_dimensions.width}×${image_dimensions.height}`; |
| |
|
| | |
| | this.metadataSection.style.display = 'block'; |
| |
|
| | |
| | if (detections.length > 0) { |
| | this.speciesList.innerHTML = detections.map(detection => ` |
| | <div class="species-item"> |
| | <span class="species-name">${detection.class_name}</span> |
| | <span class="species-confidence">${(detection.confidence * 100).toFixed(1)}%</span> |
| | </div> |
| | `).join(''); |
| | this.speciesSection.style.display = 'block'; |
| | } else { |
| | this.speciesList.innerHTML = '<p class="no-detections">No marine species detected. Try adjusting the confidence threshold.</p>'; |
| | this.speciesSection.style.display = 'block'; |
| | } |
| | } |
| |
|
| | |
| | loadSampleImages() { |
| | const sampleImages = [ |
| | { name: 'crab.png', description: 'Crab Species' }, |
| | { name: 'fish.png', description: 'Fish Species' }, |
| | { name: 'fish_2.png', description: 'Fish Variety' }, |
| | { name: 'fish_3.png', description: 'Marine Fish' }, |
| | { name: 'fish_4.png', description: 'Ocean Fish' }, |
| | { name: 'fish_5.png', description: 'Deep Sea Fish' }, |
| | { name: 'flat_fish.png', description: 'Flatfish' }, |
| | { name: 'flat_red_fish.png', description: 'Red Flatfish' }, |
| | { name: 'jelly.png', description: 'Jellyfish' }, |
| | { name: 'jelly_2.png', description: 'Jellyfish Species' }, |
| | { name: 'jelly_3.png', description: 'Marine Jelly' }, |
| | { name: 'puff.png', description: 'Pufferfish' }, |
| | { name: 'red_fish.png', description: 'Red Fish' }, |
| | { name: 'red_fish_2.png', description: 'Red Fish Species' }, |
| | { name: 'scene.png', description: 'Marine Scene' }, |
| | { name: 'scene_2.png', description: 'Ocean Scene' }, |
| | { name: 'scene_3.png', description: 'Underwater Scene' }, |
| | { name: 'scene_4.png', description: 'Deep Sea Scene' }, |
| | { name: 'scene_5.png', description: 'Marine Habitat' }, |
| | { name: 'scene_6.png', description: 'Ocean Floor' }, |
| | { name: 'soft_coral.png', description: 'Soft Coral' }, |
| | { name: 'starfish.png', description: 'Starfish' }, |
| | { name: 'starfish_2.png', description: 'Sea Star' } |
| | ].map(img => ({ |
| | ...img, |
| | url: `https://huggingface.co/seamo-ai/marina-species-v1/resolve/main/images/${img.name}` |
| | })); |
| |
|
| | this.sampleImagesSlider.innerHTML = sampleImages.map(image => ` |
| | <div class="sample-image-item" onclick="dashboard.loadSampleImage('${image.url}', '${image.name}')"> |
| | <img src="${image.url}" alt="${image.description}" loading="lazy" /> |
| | <div class="sample-image-overlay">${image.description}</div> |
| | </div> |
| | `).join(''); |
| | } |
| |
|
| | |
| | async loadSampleImage(imageUrl, imageName) { |
| | try { |
| | |
| | window.MarineAPI.utils.showNotification('Loading sample image...', 'info'); |
| |
|
| | let finalUrl = imageUrl; |
| |
|
| | |
| | try { |
| | const testResponse = await fetch(imageUrl, { method: 'HEAD' }); |
| | if (!testResponse.ok) { |
| | finalUrl = `https://huggingface.co/seamo-ai/marina-species-v1/resolve/main/images/${imageName}`; |
| | } |
| | } catch (e) { |
| | finalUrl = `https://huggingface.co/seamo-ai/marina-species-v1/resolve/main/images/${imageName}`; |
| | } |
| |
|
| | |
| | const response = await fetch(finalUrl); |
| | if (!response.ok) { |
| | throw new Error(`Failed to fetch image: ${response.status}`); |
| | } |
| |
|
| | const blob = await response.blob(); |
| |
|
| | |
| | const file = new File([blob], imageName, { type: blob.type }); |
| |
|
| | |
| | this.processFile(file); |
| |
|
| | window.MarineAPI.utils.showNotification('Sample image loaded successfully!', 'success'); |
| | } catch (error) { |
| | console.error('Failed to load sample image:', error); |
| | window.MarineAPI.utils.showNotification('Failed to load sample image. Please try uploading your own image.', 'error'); |
| | } |
| | } |
| |
|
| | |
| | scrollSliderLeft() { |
| | const containerWidth = this.sampleImagesSlider.clientWidth; |
| | this.sampleImagesSlider.scrollBy({ |
| | left: -containerWidth, |
| | behavior: 'smooth' |
| | }); |
| | } |
| |
|
| | scrollSliderRight() { |
| | const containerWidth = this.sampleImagesSlider.clientWidth; |
| | this.sampleImagesSlider.scrollBy({ |
| | left: containerWidth, |
| | behavior: 'smooth' |
| | }); |
| | } |
| | } |
| |
|
| | |
| | window.copyToClipboard = function(elementId) { |
| | const element = document.getElementById(elementId); |
| | if (!element) { |
| | console.error('Element not found:', elementId); |
| | return; |
| | } |
| |
|
| | const text = element.textContent; |
| |
|
| | if (navigator.clipboard && navigator.clipboard.writeText) { |
| | navigator.clipboard.writeText(text).then(() => { |
| | window.MarineAPI.utils.showNotification('Code copied to clipboard!', 'success'); |
| | }).catch(err => { |
| | console.error('Failed to copy text: ', err); |
| | fallbackCopyTextToClipboard(text); |
| | }); |
| | } else { |
| | fallbackCopyTextToClipboard(text); |
| | } |
| | }; |
| |
|
| | |
| | function fallbackCopyTextToClipboard(text) { |
| | const textArea = document.createElement("textarea"); |
| | textArea.value = text; |
| |
|
| | |
| | textArea.style.top = "0"; |
| | textArea.style.left = "0"; |
| | textArea.style.position = "fixed"; |
| |
|
| | document.body.appendChild(textArea); |
| | textArea.focus(); |
| | textArea.select(); |
| |
|
| | try { |
| | const successful = document.execCommand('copy'); |
| | if (successful) { |
| | window.MarineAPI.utils.showNotification('Code copied to clipboard!', 'success'); |
| | } else { |
| | window.MarineAPI.utils.showNotification('Failed to copy code', 'error'); |
| | } |
| | } catch (err) { |
| | console.error('Fallback: Oops, unable to copy', err); |
| | window.MarineAPI.utils.showNotification('Failed to copy code', 'error'); |
| | } |
| |
|
| | document.body.removeChild(textArea); |
| | } |
| |
|
| | |
| | document.addEventListener('DOMContentLoaded', function() { |
| | const tabButtons = document.querySelectorAll('.tab-button'); |
| | const tabContents = document.querySelectorAll('.tab-content'); |
| |
|
| | tabButtons.forEach(button => { |
| | button.addEventListener('click', function() { |
| | const targetTab = this.getAttribute('data-tab'); |
| |
|
| | |
| | tabButtons.forEach(b => b.classList.remove('active')); |
| | tabContents.forEach(c => c.classList.remove('active')); |
| |
|
| | |
| | this.classList.add('active'); |
| |
|
| | |
| | const targetContent = document.getElementById(targetTab + '-tab'); |
| | if (targetContent) { |
| | targetContent.classList.add('active'); |
| | } |
| | }); |
| | }); |
| | }); |
| |
|
| | |
| | function copyCode(elementId) { |
| | const codeElement = document.getElementById(elementId); |
| | const text = codeElement.textContent; |
| |
|
| | navigator.clipboard.writeText(text).then(function() { |
| | |
| | const button = codeElement.parentElement.querySelector('.copy-button'); |
| | const originalText = button.textContent; |
| | button.textContent = 'Copied!'; |
| | button.classList.add('copied'); |
| |
|
| | setTimeout(() => { |
| | button.textContent = originalText; |
| | button.classList.remove('copied'); |
| | }, 2000); |
| | }).catch(function(err) { |
| | console.error('Failed to copy text: ', err); |
| | }); |
| | } |
| |
|
| | |
| | let dashboard; |
| |
|
| | |
| | document.addEventListener('DOMContentLoaded', () => { |
| | dashboard = new MarineDashboard(); |
| | }); |
| |
|