childYb / templates /step2_upload.html
rethinks's picture
Upload 6 files
cd40c5f verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Step 2: Upload Event Photos - Smart Photo Selection</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
background: #f5f7fa;
min-height: 100vh;
color: #333;
}
/* Header */
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 30px 20px;
text-align: center;
color: white;
}
.header h1 {
font-size: 28px;
font-weight: 600;
margin-bottom: 5px;
}
.header h1 .highlight {
color: #ffd700;
}
.header p {
opacity: 0.9;
font-size: 14px;
}
/* Container */
.container {
max-width: 900px;
margin: 0 auto;
padding: 30px 20px;
}
/* Steps */
.steps {
display: flex;
justify-content: center;
align-items: flex-start;
gap: 15px;
margin-bottom: 40px;
flex-wrap: wrap;
}
.step {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
min-width: 80px;
}
.step-circle {
width: 40px;
height: 40px;
border-radius: 50%;
background: #e0e0e0;
color: #999;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 16px;
transition: all 0.3s;
}
.step.active .step-circle {
background: #667eea;
color: white;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
}
.step.completed .step-circle {
background: #4CAF50;
color: white;
}
.step-label {
font-size: 12px;
color: #666;
text-align: center;
max-width: 80px;
}
.step.active .step-label {
color: #667eea;
font-weight: 500;
}
.step-connector {
width: 40px;
height: 2px;
background: #e0e0e0;
margin-top: 20px;
}
.step.completed + .step-connector {
background: #4CAF50;
}
/* Reference Status Banner */
.status-banner {
border-radius: 12px;
padding: 15px 20px;
margin-bottom: 25px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 15px;
}
.status-banner.has-ref {
background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%);
}
.status-banner.no-ref {
background: linear-gradient(135deg, #fff3e0 0%, #ffe0b2 100%);
}
.status-banner .info {
display: flex;
align-items: center;
gap: 12px;
}
.status-banner .icon {
font-size: 24px;
}
.status-banner .text {
font-size: 14px;
}
.status-banner .text strong {
color: #2e7d32;
}
.status-banner.no-ref .text strong {
color: #e65100;
}
.status-banner a {
color: #667eea;
text-decoration: none;
font-size: 14px;
font-weight: 500;
white-space: nowrap;
}
.status-banner a:hover {
text-decoration: underline;
}
/* Card */
.card {
background: white;
border-radius: 16px;
box-shadow: 0 2px 20px rgba(0,0,0,0.08);
padding: 40px;
margin-bottom: 20px;
}
.card-icon {
width: 60px;
height: 60px;
margin: 0 auto 20px;
background: #f0f4ff;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
}
.card-icon svg {
width: 30px;
height: 30px;
color: #667eea;
}
.card h2 {
text-align: center;
font-size: 22px;
font-weight: 600;
margin-bottom: 10px;
color: #333;
}
.card > p {
text-align: center;
color: #666;
margin-bottom: 30px;
}
/* Upload Method Tabs */
.upload-tabs {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.upload-tab {
flex: 1;
padding: 12px 20px;
border: 2px solid #e0e0e0;
border-radius: 8px;
background: white;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
font-family: inherit;
}
.upload-tab:hover {
border-color: #667eea;
}
.upload-tab.active {
border-color: #667eea;
background: #f0f4ff;
color: #667eea;
}
/* Folder Path Input */
.folder-input-area {
border: 2px dashed #d0d7e3;
border-radius: 12px;
padding: 50px 20px;
text-align: center;
background: #fafbfc;
}
.folder-input-area.hidden {
display: none;
}
.folder-input-area h3 {
font-size: 18px;
font-weight: 500;
margin-bottom: 8px;
color: #333;
}
.folder-input-area > p {
color: #888;
font-size: 14px;
margin-bottom: 20px;
}
.folder-path-input {
width: 100%;
max-width: 500px;
padding: 14px 18px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
font-family: 'Consolas', monospace;
transition: border-color 0.2s;
}
.folder-path-input:focus {
outline: none;
border-color: #667eea;
}
.folder-hint {
font-size: 12px;
color: #999;
margin-top: 10px;
}
/* Upload Area */
.upload-area {
border: 2px dashed #d0d7e3;
border-radius: 12px;
padding: 50px 20px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
background: #fafbfc;
}
.upload-area.hidden {
display: none;
}
.upload-area:hover {
border-color: #667eea;
background: #f0f4ff;
}
.upload-area.drag-over {
border-color: #667eea;
background: #e8edff;
transform: scale(1.01);
}
.upload-icon {
font-size: 56px;
margin-bottom: 15px;
}
.upload-area h3 {
font-size: 18px;
font-weight: 500;
margin-bottom: 8px;
color: #333;
}
.upload-area p {
color: #888;
font-size: 14px;
margin-bottom: 20px;
}
/* Buttons */
.btn {
padding: 12px 28px;
border: none;
border-radius: 8px;
font-size: 15px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
font-family: inherit;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover {
background: #5a6fd6;
transform: translateY(-1px);
}
.btn-success {
background: #4CAF50;
color: white;
}
.btn-success:hover {
background: #43a047;
}
.btn-secondary {
background: #f0f0f0;
color: #666;
}
.btn-secondary:hover {
background: #e0e0e0;
}
/* Settings Panel */
.settings-panel {
margin-top: 30px;
padding-top: 30px;
border-top: 1px solid #eee;
}
.settings-panel h3 {
font-size: 16px;
font-weight: 600;
margin-bottom: 8px;
color: #333;
}
.settings-panel > p {
color: #888;
font-size: 13px;
margin-bottom: 20px;
}
/* Quality Mode Buttons */
.quality-buttons {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-bottom: 25px;
}
.quality-btn {
padding: 15px;
border: 2px solid #e0e0e0;
border-radius: 12px;
background: white;
cursor: pointer;
text-align: center;
transition: all 0.2s;
}
.quality-btn:hover {
border-color: #667eea;
}
.quality-btn.active {
border-color: #667eea;
background: #f0f4ff;
}
.quality-btn .icon {
font-size: 28px;
margin-bottom: 8px;
display: block;
}
.quality-btn .label {
font-weight: 600;
font-size: 14px;
color: #333;
display: block;
margin-bottom: 4px;
}
.quality-btn .desc {
font-size: 11px;
color: #888;
display: block;
}
.quality-btn.active .label {
color: #667eea;
}
/* Slider */
.slider-group {
margin-top: 20px;
}
.slider-group label {
display: block;
font-size: 14px;
font-weight: 500;
margin-bottom: 10px;
color: #333;
}
.slider-group input[type="range"] {
width: 100%;
height: 6px;
border-radius: 3px;
background: #e0e0e0;
outline: none;
-webkit-appearance: none;
}
.slider-group input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: #667eea;
cursor: pointer;
}
.slider-labels {
display: flex;
justify-content: space-between;
margin-top: 8px;
font-size: 12px;
color: #888;
}
.slider-labels .value {
color: #667eea;
font-weight: 600;
}
/* File Preview */
.file-preview {
margin-top: 30px;
padding-top: 30px;
border-top: 1px solid #eee;
}
.file-preview.hidden {
display: none;
}
.file-preview-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.file-preview h3 {
font-size: 16px;
font-weight: 500;
color: #333;
}
.file-count {
background: #667eea;
color: white;
padding: 4px 12px;
border-radius: 20px;
font-size: 13px;
font-weight: 500;
}
.preview-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
gap: 10px;
max-height: 200px;
overflow-y: auto;
margin-bottom: 20px;
}
.preview-item {
aspect-ratio: 1;
border-radius: 8px;
overflow: hidden;
background: #f0f0f0;
}
.preview-item img {
width: 100%;
height: 100%;
object-fit: cover;
}
.more-indicator {
display: flex;
align-items: center;
justify-content: center;
background: #667eea;
color: white;
font-weight: 600;
font-size: 14px;
}
/* Processing Overlay */
.processing-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255,255,255,0.98);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 1000;
}
.processing-overlay.hidden {
display: none;
}
.spinner {
width: 60px;
height: 60px;
border: 4px solid #f0f0f0;
border-top-color: #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.processing-overlay h2 {
margin-top: 25px;
font-size: 20px;
color: #333;
}
.progress-container {
width: 300px;
margin-top: 20px;
}
.progress-bar {
height: 8px;
background: #e0e0e0;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
transition: width 0.3s;
border-radius: 4px;
}
.progress-text {
text-align: center;
margin-top: 10px;
font-size: 14px;
color: #666;
}
.progress-text span {
font-weight: 600;
color: #667eea;
}
/* How it works */
.how-it-works {
background: white;
border-radius: 16px;
box-shadow: 0 2px 20px rgba(0,0,0,0.08);
padding: 30px 40px;
}
.how-it-works h3 {
font-size: 16px;
font-weight: 600;
margin-bottom: 20px;
color: #333;
}
.how-it-works ol {
list-style: none;
padding: 0;
margin: 0;
}
.how-it-works li {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 10px 0;
color: #555;
font-size: 14px;
}
.how-it-works .step-num {
width: 24px;
height: 24px;
background: #f0f4ff;
color: #667eea;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 600;
flex-shrink: 0;
}
@media (max-width: 600px) {
.card {
padding: 25px 20px;
}
.quality-buttons {
grid-template-columns: 1fr;
}
.status-banner {
flex-direction: column;
text-align: center;
}
}
</style>
</head>
<body>
<!-- Header -->
<div class="header">
<h1>Create a Personalized Photo Book for <span class="highlight">Your Child</span>!</h1>
<p>AI-powered photo selection to find the best moments</p>
</div>
<div class="container">
<!-- Step Indicator -->
<div class="steps">
<div class="step completed">
<div class="step-circle">&#10003;</div>
<div class="step-label">Reference</div>
</div>
<div class="step-connector" style="background: #4CAF50;"></div>
<div class="step active">
<div class="step-circle">2</div>
<div class="step-label">Pre-processing</div>
</div>
<div class="step-connector"></div>
<div class="step">
<div class="step-circle">3</div>
<div class="step-label">Review</div>
</div>
<div class="step-connector"></div>
<div class="step">
<div class="step-circle">4</div>
<div class="step-label">Detecting Favorites</div>
</div>
</div>
<!-- Reference Status Banner -->
{% if reference_count > 0 %}
<div class="status-banner has-ref">
<div class="info">
<span class="icon">&#9989;</span>
<span class="text"><strong>{{ reference_count }} reference photo(s) loaded</strong> - AI will find your child in uploaded photos</span>
</div>
<a href="/step1">Change</a>
</div>
{% else %}
<div class="status-banner no-ref">
<div class="info">
<span class="icon">&#9888;</span>
<span class="text"><strong>No reference photos</strong> - Processing all photos without face filtering</span>
</div>
<a href="/step1">Add reference photos</a>
</div>
{% endif %}
<!-- Main Card -->
<div class="card">
<div class="card-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V5h14v14zm-5-7l-3 3.72L9 13l-3 4h12l-4-5z"/>
</svg>
</div>
<h2>Upload Your Event Photos</h2>
<p>Upload all photos from the event - we'll find the best ones of your child</p>
<!-- Google Drive Input -->
<div class="folder-input-area" id="drive-input-area">
<div class="upload-icon" style="font-size: 48px;">&#9729;</div>
<h3>Import from Google Drive</h3>
<p>Paste a Google Drive folder URL (folder must be shared with the service account)</p>
<input type="text" id="drive-folder-url" class="folder-path-input"
placeholder="https://drive.google.com/drive/folders/1AbCdEfGhIjKlMnOpQrS">
<p class="folder-hint">Paste the folder URL from your browser address bar</p>
<div id="drive-preview" style="margin-top: 15px; display: none;">
<div style="background: #e8f5e9; padding: 12px 16px; border-radius: 8px; text-align: left;">
<strong id="drive-folder-name" style="color: #2e7d32;"></strong>
<span id="drive-image-count" style="color: #666; margin-left: 10px;"></span>
</div>
</div>
<button class="btn btn-secondary" id="preview-drive-btn" onclick="previewDriveFolder()" style="margin-top: 15px;">
Preview Folder
</button>
</div>
<!-- Settings Panel -->
<div class="settings-panel">
<h3>Selection Settings</h3>
<p>Adjust how strict the quality filter should be</p>
<div class="quality-buttons">
<button class="quality-btn" data-mode="lenient" onclick="setQualityMode('lenient')">
<span class="icon">&#128247;</span>
<span class="label">Keep More</span>
<span class="desc">Lower standards, more photos</span>
</button>
<button class="quality-btn active" data-mode="balanced" onclick="setQualityMode('balanced')">
<span class="icon">&#10024;</span>
<span class="label">Balanced</span>
<span class="desc">Recommended setting</span>
</button>
<button class="quality-btn" data-mode="strict" onclick="setQualityMode('strict')">
<span class="icon">&#127942;</span>
<span class="label">Best Only</span>
<span class="desc">Highest quality only</span>
</button>
</div>
<div class="slider-group">
<label>Duplicate Detection</label>
<input type="range" id="similarity" min="0.80" max="0.98" step="0.02" value="0.92">
<div class="slider-labels">
<span>Keep similar</span>
<span class="value" id="similarity-value">92%</span>
<span>Remove similar</span>
</div>
</div>
</div>
<!-- File Preview -->
<div id="file-preview" class="file-preview hidden">
<div class="file-preview-header">
<h3>Selected Files</h3>
<span class="file-count" id="file-count">0 photos</span>
</div>
<div id="preview-grid" class="preview-grid"></div>
<button class="btn btn-success" id="process-btn" onclick="startProcessing()">
{% if reference_count > 0 %}
Find My Child &amp; Select Best Photos
{% else %}
Select Best Photos
{% endif %}
</button>
</div>
</div>
<!-- How it works -->
<div class="how-it-works">
<h3>How it works:</h3>
<ol>
<li><span class="step-num">1</span> Upload all your event/school photos</li>
<li><span class="step-num">2</span> AI scans every photo looking for your child's face</li>
<li><span class="step-num">3</span> Review the matches and confirm which ones to keep</li>
<li><span class="step-num">4</span> AI selects the best quality photos from your selection</li>
<li><span class="step-num">5</span> Download your curated photo collection</li>
</ol>
</div>
</div>
<!-- Processing Overlay -->
<div id="processing-overlay" class="processing-overlay hidden">
<div class="spinner"></div>
<h2 id="processing-message">Processing your photos...</h2>
<p id="files-checked" style="color: #667eea; font-size: 18px; font-weight: 600; margin-top: 10px;"></p>
<div class="progress-container">
<div class="progress-bar">
<div class="progress-fill" id="progress-fill" style="width: 0%"></div>
</div>
<p class="progress-text"><span id="progress-percent">0</span>%</p>
</div>
</div>
<script>
const hasReferences = {{ reference_count }} > 0;
let qualityMode = 'balanced';
let jobId = null;
let driveFolderVerified = false;
function updateDrivePreview() {
const driveUrl = document.getElementById('drive-folder-url').value.trim();
const preview = document.getElementById('file-preview');
if (driveUrl && driveFolderVerified) {
preview.classList.remove('hidden');
} else {
preview.classList.add('hidden');
}
}
async function previewDriveFolder() {
const driveUrl = document.getElementById('drive-folder-url').value.trim();
if (!driveUrl) {
alert('Please enter a Google Drive folder URL');
return;
}
const btn = document.getElementById('preview-drive-btn');
btn.textContent = 'Checking...';
btn.disabled = true;
try {
const response = await fetch('/preview_drive_folder', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ folder_url: driveUrl })
});
const data = await response.json();
if (data.error) {
alert('Error: ' + data.error);
driveFolderVerified = false;
} else {
// Show preview
document.getElementById('drive-folder-name').textContent = data.folder_name;
document.getElementById('drive-image-count').textContent = `(${data.image_count} images found)`;
document.getElementById('drive-preview').style.display = 'block';
driveFolderVerified = true;
// Show file preview section with process button
const preview = document.getElementById('file-preview');
preview.classList.remove('hidden');
document.getElementById('file-count').textContent = `${data.image_count} images`;
document.getElementById('preview-grid').innerHTML = '<div style="padding: 20px; color: #666; font-size: 14px;">Ready to import from: ' + data.folder_name + '</div>';
}
} catch (e) {
console.error('Preview error:', e);
alert('Failed to preview folder. Please check the URL.');
driveFolderVerified = false;
} finally {
btn.textContent = 'Preview Folder';
btn.disabled = false;
}
}
// Drive URL input handler - reset verification on change
document.getElementById('drive-folder-url').addEventListener('input', function() {
driveFolderVerified = false;
document.getElementById('drive-preview').style.display = 'none';
document.getElementById('file-preview').classList.add('hidden');
});
function setQualityMode(mode) {
qualityMode = mode;
document.querySelectorAll('.quality-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.mode === mode);
});
}
// Similarity slider
document.getElementById('similarity').addEventListener('input', function() {
document.getElementById('similarity-value').textContent = Math.round(this.value * 100) + '%';
});
async function startProcessing() {
// Show processing overlay
document.getElementById('processing-overlay').classList.remove('hidden');
// Google Drive import mode
const driveUrl = document.getElementById('drive-folder-url').value.trim();
if (!driveUrl) {
alert('Please enter a Google Drive folder URL');
document.getElementById('processing-overlay').classList.add('hidden');
return;
}
if (!driveFolderVerified) {
alert('Please click "Preview Folder" first to verify access');
document.getElementById('processing-overlay').classList.add('hidden');
return;
}
document.getElementById('processing-message').textContent = 'Connecting to Google Drive...';
try {
const response = await fetch('/import_from_drive', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
folder_url: driveUrl,
quality_mode: qualityMode,
similarity_threshold: parseFloat(document.getElementById('similarity').value)
})
});
const data = await response.json();
if (data.error) {
alert('Error: ' + data.error);
document.getElementById('processing-overlay').classList.add('hidden');
return;
}
jobId = data.job_id;
pollStatus();
} catch (error) {
console.error('Error:', error);
alert('Failed to import from Google Drive. Please try again.');
document.getElementById('processing-overlay').classList.add('hidden');
}
}
async function pollStatus() {
try {
const response = await fetch(`/status/${jobId}`);
const data = await response.json();
document.getElementById('processing-message').textContent = data.message;
document.getElementById('progress-fill').style.width = `${data.progress}%`;
document.getElementById('progress-percent').textContent = data.progress;
// Show files checked count
const filesCheckedEl = document.getElementById('files-checked');
if (data.total_photos > 0) {
filesCheckedEl.textContent = `${data.photos_checked || 0} / ${data.total_photos} files checked`;
} else {
filesCheckedEl.textContent = '';
}
if (data.status === 'review_pending') {
window.location.href = `/step3_review/${jobId}`;
} else if (data.status === 'complete') {
window.location.href = `/step4_results/${jobId}`;
} else if (data.status === 'error') {
alert('Error: ' + data.message);
document.getElementById('processing-overlay').classList.add('hidden');
} else {
setTimeout(pollStatus, 1000);
}
} catch (error) {
console.error('Poll error:', error);
setTimeout(pollStatus, 2000);
}
}
</script>
</body>
</html>