// Photo Selection App JavaScript
// Automatic Selection Mode - AI decides which photos to keep
let selectedFiles = [];
let currentJobId = null;
let pollInterval = null;
let resultsData = null;
let currentQualityMode = 'balanced';
// DOM Elements
const dropZone = document.getElementById('drop-zone');
const fileInput = document.getElementById('file-input');
const filePreview = document.getElementById('file-preview');
const previewGrid = document.getElementById('preview-grid');
const fileCount = document.getElementById('file-count');
const similaritySlider = document.getElementById('similarity');
const similarityValue = document.getElementById('similarity-value');
// Initialize
document.addEventListener('DOMContentLoaded', () => {
setupDropZone();
setupFileInput();
setupSimilaritySlider();
setupQualityButtons();
});
// Drop Zone Setup
function setupDropZone() {
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('dragover');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('dragover');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('dragover');
handleFiles(e.dataTransfer.files);
});
dropZone.addEventListener('click', () => {
fileInput.click();
});
}
// File Input Setup
function setupFileInput() {
fileInput.addEventListener('change', (e) => {
handleFiles(e.target.files);
});
}
// Similarity Slider
function setupSimilaritySlider() {
similaritySlider.addEventListener('input', (e) => {
const value = Math.round(parseFloat(e.target.value) * 100);
similarityValue.textContent = value + '%';
});
}
// Quality Mode Buttons Setup
function setupQualityButtons() {
// Set initial state
document.querySelectorAll('.quality-btn').forEach(btn => {
if (btn.dataset.mode === 'balanced') {
btn.classList.add('active');
}
});
}
// Set Quality Mode
function setQualityMode(mode) {
currentQualityMode = mode;
// Update button states
document.querySelectorAll('.quality-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.mode === mode);
});
}
// Handle Selected Files
function handleFiles(files) {
const validFiles = Array.from(files).filter(file => {
const ext = file.name.split('.').pop().toLowerCase();
return ['jpg', 'jpeg', 'png', 'heic', 'heif', 'webp'].includes(ext);
});
if (validFiles.length === 0) {
alert('No valid image files selected.');
return;
}
selectedFiles = [...selectedFiles, ...validFiles];
updateFilePreview();
}
// Update File Preview
function updateFilePreview() {
if (selectedFiles.length === 0) {
filePreview.classList.add('hidden');
return;
}
filePreview.classList.remove('hidden');
fileCount.textContent = selectedFiles.length;
previewGrid.innerHTML = '';
selectedFiles.forEach((file, index) => {
const item = document.createElement('div');
item.className = 'preview-item';
// Create thumbnail
if (file.type.startsWith('image/') && !file.name.toLowerCase().endsWith('.heic')) {
const img = document.createElement('img');
img.src = URL.createObjectURL(file);
item.appendChild(img);
} else {
// For HEIC or unknown types, show placeholder
item.innerHTML = `
photo
`;
}
// Remove button
const removeBtn = document.createElement('button');
removeBtn.className = 'remove-btn';
removeBtn.innerHTML = '×';
removeBtn.onclick = (e) => {
e.stopPropagation();
removeFile(index);
};
item.appendChild(removeBtn);
previewGrid.appendChild(item);
});
}
// Remove File
function removeFile(index) {
selectedFiles.splice(index, 1);
updateFilePreview();
}
// Start Processing
async function startProcessing() {
if (selectedFiles.length === 0) {
alert('Please select some photos first.');
return;
}
const similarity = parseFloat(document.getElementById('similarity').value);
// Show processing section
document.getElementById('upload-section').classList.add('hidden');
document.getElementById('processing-section').classList.remove('hidden');
// Create form data
const formData = new FormData();
selectedFiles.forEach(file => {
formData.append('files', file);
});
formData.append('quality_mode', currentQualityMode);
formData.append('similarity', similarity);
try {
// Upload files
updateProgress(5, 'Uploading files...');
const response = await fetch('/upload', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error('Upload failed');
}
const data = await response.json();
currentJobId = data.job_id;
// Start polling for status
pollInterval = setInterval(checkStatus, 1000);
} catch (error) {
console.error('Error:', error);
alert('Error uploading files: ' + error.message);
resetToUpload();
}
}
// Check Processing Status
async function checkStatus() {
if (!currentJobId) return;
try {
const response = await fetch(`/status/${currentJobId}`);
const data = await response.json();
updateProgress(data.progress, data.message);
updateProcessingSteps(data.progress);
if (data.status === 'complete') {
clearInterval(pollInterval);
await loadResults();
} else if (data.status === 'review_pending') {
// Face filtering complete - redirect to review page
clearInterval(pollInterval);
window.location.href = `/step3_review/${currentJobId}`;
} else if (data.status === 'error') {
clearInterval(pollInterval);
alert('Processing error: ' + data.message);
resetToUpload();
}
} catch (error) {
console.error('Status check error:', error);
}
}
// Update Progress
function updateProgress(percent, message) {
document.getElementById('progress-fill').style.width = `${percent}%`;
document.getElementById('progress-percent').textContent = Math.round(percent);
document.getElementById('processing-message').textContent = message || 'Processing...';
}
// Update Processing Steps
function updateProcessingSteps(progress) {
// Check if we're in face filtering mode (has step-0) or full processing mode
const hasFaceFiltering = document.getElementById('step-0') !== null;
let steps;
if (hasFaceFiltering) {
// Face filtering mode: 3 steps (step-0, step-1, step-2)
steps = [
{ id: 'step-0', threshold: 10 },
{ id: 'step-1', threshold: 70 },
{ id: 'step-2', threshold: 90 }
];
} else {
// Full processing mode: 5 steps (step-1 through step-5)
steps = [
{ id: 'step-1', threshold: 20 },
{ id: 'step-2', threshold: 40 },
{ id: 'step-3', threshold: 50 },
{ id: 'step-4', threshold: 60 },
{ id: 'step-5', threshold: 80 }
];
}
steps.forEach((step, index) => {
const el = document.getElementById(step.id);
if (!el) return; // Skip if element doesn't exist
if (progress >= step.threshold) {
el.classList.add('complete');
el.classList.remove('active');
} else if (index === 0 || progress >= steps[index - 1].threshold) {
el.classList.add('active');
el.classList.remove('complete');
} else {
el.classList.remove('active', 'complete');
}
});
}
// Load Results
async function loadResults() {
try {
const response = await fetch(`/results/${currentJobId}`);
resultsData = await response.json();
displayResults(resultsData);
document.getElementById('processing-section').classList.add('hidden');
document.getElementById('results-section').classList.remove('hidden');
} catch (error) {
console.error('Error loading results:', error);
alert('Error loading results');
resetToUpload();
}
}
// Display Results
function displayResults(data) {
// Update stats
const totalPhotos = data.summary.total_photos;
const selectedCount = data.summary.selected_count;
const rejectedCount = data.summary.rejected_count;
const selectionRate = data.summary.selection_rate;
document.getElementById('stat-total').textContent = totalPhotos;
document.getElementById('stat-selected').textContent = selectedCount;
document.getElementById('stat-rejected').textContent = rejectedCount;
// Handle optional elements
const statRate = document.getElementById('stat-rate');
if (statRate) statRate.textContent = selectionRate;
// Handle face filtering stats if available
const faceFiltering = data.summary.face_filtering;
const statFound = document.getElementById('stat-found');
if (statFound && faceFiltering) {
statFound.textContent = faceFiltering.after_face_filter || faceFiltering.user_confirmed || 0;
}
// Display rejection breakdown
displayRejectionBreakdown(data.rejection_breakdown);
// Display selected photos
const selectedGrid = document.getElementById('selected-grid');
selectedGrid.innerHTML = '';
if (data.selected.length === 0) {
selectedGrid.innerHTML = 'No photos selected. Try using "Keep More" mode.
';
} else {
data.selected.forEach(photo => {
selectedGrid.appendChild(createPhotoCard(photo, false));
});
}
// Display rejected photos
const rejectedGrid = document.getElementById('rejected-grid');
rejectedGrid.innerHTML = '';
if (data.rejected.length === 0) {
rejectedGrid.innerHTML = 'No photos filtered out.
';
} else {
data.rejected.forEach(photo => {
rejectedGrid.appendChild(createPhotoCard(photo, true));
});
}
}
// Display Rejection Breakdown
function displayRejectionBreakdown(breakdown) {
const container = document.getElementById('rejection-bars');
const breakdownSection = document.getElementById('rejection-breakdown');
if (!breakdown || Object.keys(breakdown).length === 0) {
breakdownSection.classList.add('hidden');
return;
}
breakdownSection.classList.remove('hidden');
container.innerHTML = '';
// Calculate total for percentages
const total = Object.values(breakdown).reduce((sum, count) => sum + count, 0);
// Sort by count descending
const sortedReasons = Object.entries(breakdown).sort((a, b) => b[1] - a[1]);
// Color mapping for reasons
const reasonColors = {
'Quality below threshold': '#ef4444',
'Too blurry': '#f59e0b',
'Poor face quality': '#8b5cf6',
'Too similar to another photo': '#3b82f6',
'Better version exists in same group': '#6366f1'
};
sortedReasons.forEach(([reason, count]) => {
const percent = Math.round((count / total) * 100);
const color = reasonColors[reason] || '#64748b';
const bar = document.createElement('div');
bar.className = 'rejection-bar-item';
bar.innerHTML = `
${reason}
${count} photos (${percent}%)
`;
container.appendChild(bar);
});
}
// Create Photo Card
function createPhotoCard(photo, isRejected) {
const card = document.createElement('div');
card.className = `photo-card ${isRejected ? 'rejected' : ''}`;
card.onclick = () => openModal(photo, isRejected);
const score = photo.score || 0;
const scorePercent = Math.round(score * 100);
let reasonHtml = '';
if (isRejected && photo.reason) {
reasonHtml = `${photo.reason}
`;
} else if (!isRejected && photo.selection_detail) {
// Show selection reason for selected photos
reasonHtml = `${photo.selection_detail}
`;
}
card.innerHTML = `
${photo.filename}
${reasonHtml}
`;
return card;
}
// Open Modal
function openModal(photo, isRejected = false) {
const modal = document.getElementById('photo-modal');
const modalImage = document.getElementById('modal-image');
const modalFilename = document.getElementById('modal-filename');
const modalReason = document.getElementById('modal-reason');
const modalScores = document.getElementById('modal-scores');
modalImage.src = `/photo/${currentJobId}/${photo.filename}`;
modalFilename.textContent = photo.filename;
// Show selection/rejection reason
if (isRejected && photo.reason) {
modalReason.innerHTML = `${photo.reason}
`;
if (photo.reason_detail) {
modalReason.innerHTML += `${photo.reason_detail}
`;
}
modalReason.classList.remove('hidden');
} else if (!isRejected && photo.selection_detail) {
// Show selection reason with detail
modalReason.innerHTML = `✓ Selected
`;
modalReason.innerHTML += `${photo.selection_detail}
`;
modalReason.classList.remove('hidden');
} else {
modalReason.innerHTML = '✓ Selected by AI
';
modalReason.classList.remove('hidden');
}
// Build score breakdown
const scores = [
{ label: 'Overall Score', value: photo.score, color: '#6366f1' },
{ label: 'Face Quality', value: photo.face_quality, color: '#10b981' },
{ label: 'Aesthetic', value: photo.aesthetic_quality, color: '#f59e0b' },
{ label: 'Emotional', value: photo.emotional_signal, color: '#ef4444' },
{ label: 'Uniqueness', value: photo.uniqueness, color: '#8b5cf6' }
];
modalScores.innerHTML = scores.map(s => {
const percent = Math.round((s.value || 0) * 100);
return `
`;
}).join('');
modal.classList.remove('hidden');
}
// Close Modal
function closeModal() {
document.getElementById('photo-modal').classList.add('hidden');
}
// Close modal on background click
document.getElementById('photo-modal').addEventListener('click', (e) => {
if (e.target.id === 'photo-modal') {
closeModal();
}
});
// Close modal on Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeModal();
}
});
// Switch Tab
function switchTab(tabName) {
// Update tab buttons
document.querySelectorAll('.tab').forEach(tab => {
tab.classList.toggle('active', tab.dataset.tab === tabName);
});
// Update tab content
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.toggle('active', content.id === `${tabName}-tab`);
});
}
// Download Selected Photos
async function downloadSelected() {
if (!currentJobId) return;
try {
window.location.href = `/download/${currentJobId}`;
} catch (error) {
alert('Error downloading files');
}
}
// Reset App
async function resetApp() {
if (currentJobId) {
try {
await fetch(`/cleanup/${currentJobId}`, { method: 'POST' });
} catch (error) {
console.error('Cleanup error:', error);
}
}
resetToUpload();
}
// Reset to Upload State
function resetToUpload() {
selectedFiles = [];
currentJobId = null;
resultsData = null;
currentQualityMode = 'balanced';
if (pollInterval) {
clearInterval(pollInterval);
pollInterval = null;
}
// Reset UI
document.getElementById('upload-section').classList.remove('hidden');
document.getElementById('processing-section').classList.add('hidden');
document.getElementById('results-section').classList.add('hidden');
// Reset file preview
filePreview.classList.add('hidden');
previewGrid.innerHTML = '';
fileCount.textContent = '0';
fileInput.value = '';
// Reset quality mode buttons
document.querySelectorAll('.quality-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.mode === 'balanced');
});
// Reset progress
updateProgress(0, '');
// Reset processing steps
document.querySelectorAll('.step').forEach(step => {
step.classList.remove('active', 'complete');
});
}