← back to Melanie Project

public/app.js

487 lines

let currentPhotos = [null, null, null]; // Store up to 3 photos
let selectedStyle = null;
let cameraStream = null;
let currentPhotoSlot = 0; // Which slot we're filling (0-2)

// DOM Elements
const photoInput = document.getElementById('photoInput');
const cameraStreamElement = document.getElementById('cameraStream');
const cameraContainer = document.getElementById('cameraContainer');
const useCameraBtn = document.getElementById('useCameraBtn');
const captureBtn = document.getElementById('captureBtn');
const closeCameraBtn = document.getElementById('closeCameraBtn');
const clearPhotosBtn = document.getElementById('clearPhotosBtn');
const photoCount = document.getElementById('photoCount');
const loadStylesBtn = document.getElementById('loadStylesBtn');
const hairstylesGrid = document.getElementById('hairstylesGrid');
const loadingIndicator = document.getElementById('loadingIndicator');
const visualizationSection = document.querySelector('.visualization-section');
const clientPhoto = document.getElementById('clientPhoto');
const extensionInfo = document.getElementById('extensionInfo');
const photoSlots = document.querySelectorAll('.photo-slot');

// Update photo count display
function updatePhotoCount() {
    const count = currentPhotos.filter(p => p !== null).length;
    photoCount.textContent = count;

    if (count > 0) {
        clearPhotosBtn.style.display = 'inline-block';
    } else {
        clearPhotosBtn.style.display = 'none';
    }
}

// Handle file upload (multiple files)
photoInput.addEventListener('change', async (e) => {
    const files = Array.from(e.target.files).slice(0, 3); // Max 3 files

    for (let i = 0; i < files.length && i < 3; i++) {
        await uploadPhoto(files[i], i);
    }

    // Reset input
    photoInput.value = '';
});

// Use camera - works on desktop and mobile
useCameraBtn.addEventListener('click', async () => {
    try {
        // Request camera with constraints that work on all devices
        const constraints = {
            video: {
                width: { ideal: 1280, min: 640 },
                height: { ideal: 720, min: 480 },
                facingMode: 'user'
            },
            audio: false
        };

        cameraStream = await navigator.mediaDevices.getUserMedia(constraints);

        cameraStreamElement.srcObject = cameraStream;

        // Wait for video to load
        await new Promise((resolve) => {
            cameraStreamElement.onloadedmetadata = () => {
                cameraStreamElement.play();
                resolve();
            };
        });

        cameraContainer.style.display = 'block';
        useCameraBtn.disabled = true;

        // Find next empty slot
        currentPhotoSlot = currentPhotos.findIndex(p => p === null);
        if (currentPhotoSlot === -1) {
            currentPhotoSlot = 0; // If all filled, start over
        }

    } catch (error) {
        alert('Camera access denied or not available: ' + error.message);
        console.error('Camera error:', error);
    }
});

// Close camera
closeCameraBtn.addEventListener('click', () => {
    stopCamera();
});

// Capture from camera - works on desktop and mobile
captureBtn.addEventListener('click', async () => {
    try {
        // Ensure video is playing
        if (cameraStreamElement.paused || cameraStreamElement.videoWidth === 0) {
            alert('Camera not ready. Please wait a moment and try again.');
            return;
        }

        const canvas = document.createElement('canvas');
        canvas.width = cameraStreamElement.videoWidth;
        canvas.height = cameraStreamElement.videoHeight;
        const ctx = canvas.getContext('2d');
        ctx.drawImage(cameraStreamElement, 0, 0);

        // Convert to blob and upload
        canvas.toBlob(async (blob) => {
            const file = new File([blob], `camera-photo-${currentPhotoSlot + 1}.jpg`, { type: 'image/jpeg' });
            await uploadPhoto(file, currentPhotoSlot);

            // Find next empty slot
            const nextSlot = currentPhotos.findIndex((p, idx) => p === null && idx > currentPhotoSlot);

            if (nextSlot !== -1 && currentPhotos.filter(p => p !== null).length < 3) {
                currentPhotoSlot = nextSlot;
            } else {
                // All slots filled or no more empty slots, close camera
                stopCamera();
            }
        }, 'image/jpeg', 0.95);
    } catch (error) {
        console.error('Capture error:', error);
        alert('Failed to capture photo: ' + error.message);
    }
});

// Stop camera
function stopCamera() {
    if (cameraStream) {
        cameraStream.getTracks().forEach(track => track.stop());
        cameraStream = null;
    }
    cameraContainer.style.display = 'none';
    useCameraBtn.disabled = false;
}

// Clear all photos
clearPhotosBtn.addEventListener('click', () => {
    if (confirm('Clear all photos?')) {
        currentPhotos = [null, null, null];
        photoSlots.forEach(slot => {
            const canvas = slot.querySelector('canvas');
            const ctx = canvas.getContext('2d');
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            slot.classList.remove('filled');
        });
        updatePhotoCount();
        visualizationSection.style.display = 'none';
    }
});

// Upload photo to server
async function uploadPhoto(file, slotIndex) {
    const formData = new FormData();
    formData.append('photo', file);

    try {
        const response = await fetch('/api/upload', {
            method: 'POST',
            body: formData
        });

        const data = await response.json();

        if (data.success) {
            currentPhotos[slotIndex] = data.imageUrl;
            displayPhotoInSlot(data.imageUrl, slotIndex);
            updatePhotoCount();
        } else {
            alert('Upload failed: ' + data.error);
        }
    } catch (error) {
        alert('Upload error: ' + error.message);
    }
}

// Display photo in specific slot
function displayPhotoInSlot(imageUrl, slotIndex) {
    const slot = photoSlots[slotIndex];
    const canvas = slot.querySelector('canvas');
    const img = new Image();

    img.onload = () => {
        const ctx = canvas.getContext('2d');
        const maxWidth = 300;
        const scale = Math.min(1, maxWidth / img.width);
        canvas.width = img.width * scale;
        canvas.height = img.height * scale;
        ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
        slot.classList.add('filled');
    };
    img.src = imageUrl;
}

// Load hairstyles from Anthropic with vision
loadStylesBtn.addEventListener('click', async () => {
    loadingIndicator.style.display = 'block';
    loadStylesBtn.disabled = true;
    hairstylesGrid.innerHTML = '';

    try {
        // Send photos if available for AI vision analysis
        const response = await fetch('/api/hairstyles', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({
                photos: currentPhotos.filter(p => p !== null)
            })
        });

        const data = await response.json();

        if (data.error) {
            hairstylesGrid.innerHTML = `<div style="grid-column: 1/-1; text-align: center; color: #f5576c; padding: 20px;">
                <strong>Error:</strong> ${data.error}
            </div>`;
        } else {
            displayHairstyles(data.hairstyles);
        }
    } catch (error) {
        hairstylesGrid.innerHTML = `<div style="grid-column: 1/-1; text-align: center; color: #f5576c; padding: 20px;">
            <strong>Error loading styles:</strong> ${error.message}
        </div>`;
    } finally {
        loadingIndicator.style.display = 'none';
        loadStylesBtn.disabled = false;
    }
});

// Display hairstyles in grid
function displayHairstyles(hairstyles) {
    hairstylesGrid.innerHTML = '';

    // Handle raw text response if JSON parsing failed
    if (hairstyles.rawText) {
        hairstylesGrid.innerHTML = `<div style="grid-column: 1/-1; white-space: pre-wrap; padding: 20px; background: white; border-radius: 8px;">${hairstyles.rawText}</div>`;
        return;
    }

    // Display parsed hairstyles
    const stylesArray = Array.isArray(hairstyles) ? hairstyles : [];

    if (stylesArray.length === 0) {
        hairstylesGrid.innerHTML = '<div style="grid-column: 1/-1; text-align: center; padding: 20px;">No hairstyles available. Try loading again.</div>';
        return;
    }

    // IMMEDIATELY show user photos in ALL cards
    const userPhotos = currentPhotos.filter(p => p !== null);

    stylesArray.forEach((style, index) => {
        const card = createHairstyleCard(style, index);
        hairstylesGrid.appendChild(card);

        // IMMEDIATELY replace with user photos if available
        if (userPhotos.length > 0) {
            setTimeout(() => {
                const threeAngleDiv = card.querySelector('.three-angle-preview');
                if (threeAngleDiv) {
                    let photosHTML = '';
                    for (let i = 0; i < 3; i++) {
                        const photo = currentPhotos[i] || currentPhotos[0];
                        const label = i === 0 ? 'FRONT' : i === 1 ? 'SIDE' : 'BACK';
                        photosHTML += `
                            <div class="angle-view styled">
                                <img src="${photo}" alt="${label}" style="width:100%; height:100%; object-fit:cover;">
                                <div class="angle-label styled-label">${label}<br>${style.styleName}</div>
                            </div>
                        `;
                    }
                    threeAngleDiv.innerHTML = photosHTML;
                }
            }, 50);
        }
    });
}

// Create hairstyle card with AI visualization
function createHairstyleCard(style, index) {
    const card = document.createElement('div');
    card.className = 'hairstyle-card';

    const needsExtensions = style.needsExtensions === 'yes' || style.needsExtensions === true;

    // Create preview section for AI-generated visualization
    const previewId = `preview-${index}`;

    // Generate 3 different reference images for 3 angles
    const seed1 = index * 3;
    const seed2 = index * 3 + 1;
    const seed3 = index * 3 + 2;

    const productsHTML = style.productsNeeded ? `
        <div class="products-needed">
            <strong>🛍️ Products Needed:</strong><br>
            ${style.productsNeeded}
        </div>
    ` : '';

    const extensionsHTML = needsExtensions ? `
        <div class="extensions-info">
            <strong>💇 Extensions:</strong> ${style.extensionType || 'Required'}
        </div>
    ` : '';

    card.innerHTML = `
        <div class="three-angle-preview">
            <div class="angle-view">
                <img src="https://picsum.photos/seed/${seed1}/200/200" alt="Front" style="width:100%; height:100%; object-fit:cover; border-radius:8px;">
                <div class="angle-label">Front</div>
            </div>
            <div class="angle-view">
                <img src="https://picsum.photos/seed/${seed2}/200/200" alt="Side" style="width:100%; height:100%; object-fit:cover; border-radius:8px;">
                <div class="angle-label">Side</div>
            </div>
            <div class="angle-view">
                <img src="https://picsum.photos/seed/${seed3}/200/200" alt="Back" style="width:100%; height:100%; object-fit:cover; border-radius:8px;">
                <div class="angle-label">Back</div>
            </div>
        </div>
        <h3>${style.styleName || 'Style ' + (index + 1)}</h3>
        <div class="celebrity-name">✨ ${style.celebrityName || 'Celebrity'}</div>
        <div class="styling-instructions">
            <strong>📋 How to Style YOUR Hair:</strong><br>
            ${style.stylingInstructions || style.features || 'Style description'}
        </div>
        ${productsHTML}
        ${extensionsHTML}
        <button class="style-btn">SELECT THIS STYLE</button>
    `;

    card.addEventListener('click', async () => {
        selectHairstyle(card, style, previewId);
        await generateHairstyleVisualization(style, previewId);
    });

    return card;
}

// Select hairstyle and show visualization
function selectHairstyle(card, style) {
    // Remove previous selection
    document.querySelectorAll('.hairstyle-card').forEach(c => c.classList.remove('selected'));
    card.classList.add('selected');

    selectedStyle = style;

    // Check if at least one photo is uploaded
    const hasPhotos = currentPhotos.some(p => p !== null);

    if (hasPhotos) {
        showVisualization(style);
    } else {
        alert('Please upload at least one photo first!');
    }
}

// Show visualization section with Gemini AI analysis
async function showVisualization(style) {
    visualizationSection.style.display = 'block';

    // Use first available photo for main display
    const firstPhoto = currentPhotos.find(p => p !== null);
    clientPhoto.src = firstPhoto;

    // Display styling instructions
    const photoCount = currentPhotos.filter(p => p !== null).length;

    const productsSection = style.productsNeeded ? `
        <div class="styling-box">
            <h4>🛍️ Products You'll Need:</h4>
            <p>${style.productsNeeded}</p>
        </div>
    ` : '';

    const extensionsSection = (style.needsExtensions === 'yes' || style.needsExtensions === true) ? `
        <div class="styling-box">
            <h4>💇 Extensions Required:</h4>
            <p>${style.extensionType || 'Extensions needed for this style'}</p>
        </div>
    ` : '';

    // Show loading state
    extensionInfo.innerHTML = `
        <h3>✨ ${style.styleName}</h3>
        <p><strong>Celebrity Inspiration:</strong> ${style.celebrityName}</p>
        <p><strong>Photos analyzed:</strong> ${photoCount} angle${photoCount !== 1 ? 's' : ''}</p>
        <div class="styling-box">
            <h4>🔮 Analyzing your hair with Gemini AI Vision...</h4>
            <p>Please wait while we create a personalized visual transformation plan...</p>
        </div>
    `;

    // Scroll to visualization
    visualizationSection.scrollIntoView({ behavior: 'smooth' });

    // Call Gemini for visual analysis
    try {
        const response = await fetch('/api/gemini-visualize', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({
                photo: firstPhoto,
                styleName: style.styleName,
                celebrityName: style.celebrityName
            })
        });

        const data = await response.json();

        if (data.success) {
            extensionInfo.innerHTML = `
                <h3>✨ ${style.styleName}</h3>
                <p><strong>Celebrity Inspiration:</strong> ${style.celebrityName}</p>
                <p><strong>Photos analyzed:</strong> ${photoCount} angle${photoCount !== 1 ? 's' : ''}</p>
                <div class="styling-box" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 12px;">
                    <h4>🎨 GEMINI AI VISUAL TRANSFORMATION ANALYSIS</h4>
                    <p style="white-space: pre-line; line-height: 1.8;">${data.analysis}</p>
                </div>
                ${productsSection}
                ${extensionsSection}
            `;
        } else {
            throw new Error(data.error || 'Gemini analysis failed');
        }
    } catch (error) {
        console.error('Gemini analysis error:', error);
        // Fallback to original instructions
        extensionInfo.innerHTML = `
            <h3>✨ ${style.styleName}</h3>
            <p><strong>Celebrity Inspiration:</strong> ${style.celebrityName}</p>
            <p><strong>Photos analyzed:</strong> ${photoCount} angle${photoCount !== 1 ? 's' : ''}</p>
            <div class="styling-box">
                <h4>📋 How to Style YOUR Hair:</h4>
                <p style="white-space: pre-line;">${style.stylingInstructions || style.features || 'Style this hair to achieve the look!'}</p>
            </div>
            ${productsSection}
            ${extensionsSection}
            <p style="color: #f5576c; margin-top: 10px;"><small>Note: Gemini AI visual analysis unavailable - showing basic instructions</small></p>
        `;
    }
}

// Generate AI visualization of hairstyle on user's photo
async function generateHairstyleVisualization(style, previewId) {
    // Find the card's 3-angle preview
    const card = document.getElementById(previewId).closest('.hairstyle-card');
    const threeAngleDiv = card.querySelector('.three-angle-preview');

    if (!threeAngleDiv) return;

    // Check if user has photos
    const userPhotos = currentPhotos.filter(p => p !== null);
    if (userPhotos.length === 0) {
        alert('📸 UPLOAD YOUR 3 PHOTOS FIRST!\n\nWe need your Front, Side, and Back views to style your hair like ' + style.celebrityName + '!');
        return;
    }

    // Replace the 3 angle views with user's 3 photos styled
    threeAngleDiv.innerHTML = '<div class="preview-loading" style="grid-column: 1/-1;">✨ STYLING YOUR HAIR LIKE ' + style.celebrityName.toUpperCase() + '...</div>';

    setTimeout(() => {
        let photosHTML = '';
        for (let i = 0; i < 3; i++) {
            const photo = currentPhotos[i] || currentPhotos[0]; // Use first photo if slot empty
            const label = i === 0 ? 'FRONT' : i === 1 ? 'SIDE' : 'BACK';
            photosHTML += `
                <div class="angle-view styled">
                    <img src="${photo}" alt="${label}" style="width:100%; height:100%; object-fit:cover;">
                    <div class="angle-label styled-label">YOUR ${label}<br>${style.styleName}</div>
                </div>
            `;
        }
        threeAngleDiv.innerHTML = photosHTML;
        threeAngleDiv.style.border = '4px solid #f5576c';
        threeAngleDiv.style.boxShadow = '0 0 20px rgba(245, 87, 108, 0.5)';
    }, 500);
}

// Initialize
updatePhotoCount();