fishapi / static /js /dashboard.js
kamau1's picture
feat: api tabs
95cf7f2 verified
// Dashboard functionality for Marine Species API
class MarineDashboard {
constructor() {
this.currentImage = null;
this.currentImageFile = null;
this.isProcessing = false;
this.initializeElements();
this.bindEvents();
this.checkAPIStatus();
this.loadSampleImages();
}
initializeElements() {
// Upload elements
this.uploadArea = document.getElementById('uploadArea');
this.fileInput = document.getElementById('fileInput');
this.identifyBtn = document.getElementById('identifyBtn');
// Settings elements
this.confidenceSlider = document.getElementById('confidenceSlider');
this.confidenceValue = document.getElementById('confidenceValue');
this.iouSlider = document.getElementById('iouSlider');
this.iouValue = document.getElementById('iouValue');
// Display elements
this.annotatedImageContainer = document.getElementById('annotatedImageContainer');
// Results elements
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');
// Status elements
this.statusDot = document.getElementById('statusDot');
this.statusText = document.getElementById('statusText');
this.modelInfo = document.getElementById('modelInfo');
// Model details elements
this.totalSpecies = document.getElementById('totalSpecies');
this.deviceInfo = document.getElementById('deviceInfo');
// Sample images
this.sampleImagesSlider = document.getElementById('sampleImagesSlider');
this.sliderPrev = document.getElementById('sliderPrev');
this.sliderNext = document.getElementById('sliderNext');
}
bindEvents() {
// Upload area events
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));
// File input change
this.fileInput.addEventListener('change', this.handleFileSelect.bind(this));
// Settings sliders
this.confidenceSlider.addEventListener('input', this.updateConfidenceValue.bind(this));
this.iouSlider.addEventListener('input', this.updateIouValue.bind(this));
// Identify button
this.identifyBtn.addEventListener('click', this.identifySpecies.bind(this));
// Slider controls
this.sliderPrev.addEventListener('click', this.scrollSliderLeft.bind(this));
this.sliderNext.addEventListener('click', this.scrollSliderRight.bind(this));
}
// API Status Check
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);
}
}
// Drag and Drop Handlers
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]);
}
}
// File Selection Handler
handleFileSelect(e) {
const file = e.target.files[0];
if (file) {
this.processFile(file);
}
}
// Process Selected File
processFile(file) {
// Validate file type
if (!file.type.startsWith('image/')) {
window.MarineAPI.utils.showNotification('Please select an image file', 'error');
return;
}
// Validate file size (10MB limit)
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;
// Update upload area
this.uploadArea.classList.add('has-image');
}
// Display Uploaded Image in Upload Area
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);
}
// Settings Handlers
updateConfidenceValue() {
this.confidenceValue.textContent = `${this.confidenceSlider.value}%`;
}
updateIouValue() {
this.iouValue.textContent = `${this.iouSlider.value}%`;
}
// Main Identification Function
async identifySpecies() {
if (!this.currentImage || this.isProcessing) return;
this.isProcessing = true;
window.MarineAPI.utils.setLoading(this.identifyBtn, true);
try {
// Prepare request data
const requestData = {
image: this.currentImage.split(',')[1], // Remove data:image/jpeg;base64, prefix
confidence_threshold: this.confidenceSlider.value / 100,
iou_threshold: this.iouSlider.value / 100,
image_size: 640,
return_annotated_image: true
};
// Make API request
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);
}
}
// Display Results
displayResults(result) {
const { detections, annotated_image, processing_time, image_dimensions } = result;
// Display annotated image
if (annotated_image) {
this.annotatedImageContainer.innerHTML = `
<img src="data:image/jpeg;base64,${annotated_image}" alt="Annotated results" />
`;
}
// Update metadata
this.processingTime.textContent = `${processing_time.toFixed(3)}s`;
this.speciesCount.textContent = detections.length;
this.imageSize.textContent = `${image_dimensions.width}×${image_dimensions.height}`;
// Show metadata section
this.metadataSection.style.display = 'block';
// Display species list
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';
}
}
// Load Sample Images
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('');
}
// Load Sample Image
async loadSampleImage(imageUrl, imageName) {
try {
// Show loading state
window.MarineAPI.utils.showNotification('Loading sample image...', 'info');
let finalUrl = imageUrl;
// Try local URL first, fallback to HuggingFace if needed
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}`;
}
// Fetch the image
const response = await fetch(finalUrl);
if (!response.ok) {
throw new Error(`Failed to fetch image: ${response.status}`);
}
const blob = await response.blob();
// Convert to File object
const file = new File([blob], imageName, { type: blob.type });
// Process the file
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');
}
}
// Slider Control Functions
scrollSliderLeft() {
const containerWidth = this.sampleImagesSlider.clientWidth;
this.sampleImagesSlider.scrollBy({
left: -containerWidth, // Scroll by full container width (2 images)
behavior: 'smooth'
});
}
scrollSliderRight() {
const containerWidth = this.sampleImagesSlider.clientWidth;
this.sampleImagesSlider.scrollBy({
left: containerWidth, // Scroll by full container width (2 images)
behavior: 'smooth'
});
}
}
// Global function for copy to clipboard (used by API docs)
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);
}
};
// Fallback copy function for older browsers
function fallbackCopyTextToClipboard(text) {
const textArea = document.createElement("textarea");
textArea.value = text;
// Avoid scrolling to bottom
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);
}
// API Documentation Tabs
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');
// Remove active class from all buttons and contents
tabButtons.forEach(b => b.classList.remove('active'));
tabContents.forEach(c => c.classList.remove('active'));
// Add active class to clicked button
this.classList.add('active');
// Show corresponding tab content
const targetContent = document.getElementById(targetTab + '-tab');
if (targetContent) {
targetContent.classList.add('active');
}
});
});
});
// Copy code functionality
function copyCode(elementId) {
const codeElement = document.getElementById(elementId);
const text = codeElement.textContent;
navigator.clipboard.writeText(text).then(function() {
// Find the copy button and show feedback
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);
});
}
// Make dashboard instance globally available for sample image clicks
let dashboard;
// Initialize dashboard when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
dashboard = new MarineDashboard();
});