← back to Handbag Authentication

public/mobile-camera.html

712 lines

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <meta name="apple-mobile-web-app-capable" content="yes">
    <title>Handbag Camera Upload</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            background: #000;
            color: white;
            overflow: hidden;
        }

        .camera-container {
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            display: flex;
            flex-direction: column;
        }

        /* Camera View */
        .camera-view {
            flex: 1;
            position: relative;
            background: #000;
            overflow: hidden;
        }

        #camera-feed {
            width: 100%;
            height: 100%;
            object-fit: cover;
        }

        #canvas {
            display: none;
        }

        /* Camera Overlay */
        .camera-overlay {
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            pointer-events: none;
        }

        .guide-box {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            width: 80%;
            height: 60%;
            border: 2px dashed rgba(255, 255, 255, 0.5);
            border-radius: 16px;
        }

        .guide-text {
            position: absolute;
            top: 20%;
            left: 50%;
            transform: translateX(-50%);
            background: rgba(0, 0, 0, 0.7);
            padding: 12px 24px;
            border-radius: 20px;
            font-size: 14px;
            text-align: center;
        }

        /* Top Bar */
        .top-bar {
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            padding: env(safe-area-inset-top) 16px 16px;
            background: linear-gradient(to bottom, rgba(0,0,0,0.8), transparent);
            z-index: 10;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }

        .back-btn, .flip-btn {
            background: rgba(255, 255, 255, 0.2);
            border: none;
            color: white;
            width: 44px;
            height: 44px;
            border-radius: 50%;
            font-size: 20px;
            cursor: pointer;
            backdrop-filter: blur(10px);
        }

        /* Controls */
        .camera-controls {
            padding: 20px calc(env(safe-area-inset-bottom) + 20px) 20px;
            background: linear-gradient(to top, rgba(0,0,0,0.8), transparent);
            display: flex;
            align-items: center;
            justify-content: space-around;
        }

        .capture-btn {
            width: 70px;
            height: 70px;
            border-radius: 50%;
            background: white;
            border: 4px solid rgba(255, 255, 255, 0.3);
            cursor: pointer;
            transition: transform 0.1s;
        }

        .capture-btn:active {
            transform: scale(0.95);
        }

        .gallery-btn, .flash-btn {
            width: 44px;
            height: 44px;
            background: rgba(255, 255, 255, 0.2);
            border: none;
            color: white;
            border-radius: 50%;
            font-size: 20px;
            cursor: pointer;
            backdrop-filter: blur(10px);
        }

        /* Preview Mode */
        .preview-container {
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: #000;
            display: none;
            flex-direction: column;
            z-index: 100;
        }

        .preview-container.active {
            display: flex;
        }

        .preview-image {
            flex: 1;
            display: flex;
            align-items: center;
            justify-content: center;
            padding: 20px;
        }

        .preview-image img {
            max-width: 100%;
            max-height: 100%;
            object-fit: contain;
            border-radius: 12px;
        }

        .preview-actions {
            padding: 20px;
            display: flex;
            gap: 12px;
            padding-bottom: calc(env(safe-area-inset-bottom) + 20px);
        }

        .preview-actions button {
            flex: 1;
            padding: 16px;
            border-radius: 12px;
            border: none;
            font-size: 16px;
            font-weight: 600;
            cursor: pointer;
        }

        .retake-btn {
            background: rgba(255, 255, 255, 0.1);
            color: white;
            border: 1px solid rgba(255, 255, 255, 0.3);
        }

        .upload-btn {
            background: white;
            color: #000;
        }

        /* Upload Progress */
        .upload-progress {
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: rgba(0, 0, 0, 0.95);
            display: none;
            align-items: center;
            justify-content: center;
            flex-direction: column;
            z-index: 200;
        }

        .upload-progress.active {
            display: flex;
        }

        .progress-spinner {
            width: 60px;
            height: 60px;
            border: 4px solid rgba(255, 255, 255, 0.2);
            border-top-color: white;
            border-radius: 50%;
            animation: spin 1s linear infinite;
            margin-bottom: 24px;
        }

        @keyframes spin {
            to { transform: rotate(360deg); }
        }

        .progress-text {
            font-size: 18px;
            margin-bottom: 12px;
        }

        .progress-detail {
            font-size: 14px;
            opacity: 0.7;
        }

        /* Results */
        .results-container {
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: #f5f5f7;
            display: none;
            flex-direction: column;
            z-index: 300;
        }

        .results-container.active {
            display: flex;
        }

        .results-header {
            background: white;
            padding: calc(env(safe-area-inset-top) + 16px) 16px 16px;
            border-bottom: 1px solid #e0e0e0;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }

        .results-content {
            flex: 1;
            overflow-y: auto;
            padding: 20px;
        }

        .result-card {
            background: white;
            border-radius: 16px;
            padding: 20px;
            margin-bottom: 16px;
        }

        .result-title {
            font-size: 18px;
            font-weight: 700;
            margin-bottom: 12px;
            color: #1d1d1f;
        }

        .result-item {
            padding: 12px 0;
            border-bottom: 1px solid #f0f0f0;
            display: flex;
            justify-content: space-between;
        }

        .result-item:last-child {
            border-bottom: none;
        }

        .result-label {
            color: #86868b;
            font-size: 14px;
        }

        .result-value {
            color: #1d1d1f;
            font-size: 14px;
            font-weight: 600;
        }

        .confidence-bar {
            width: 100%;
            height: 8px;
            background: #e0e0e0;
            border-radius: 4px;
            overflow: hidden;
            margin-top: 8px;
        }

        .confidence-fill {
            height: 100%;
            background: linear-gradient(90deg, #34c759, #30d158);
            transition: width 0.3s;
        }
    </style>
</head>
<body>
    <!-- Camera Container -->
    <div class="camera-container">
        <div class="camera-view">
            <video id="camera-feed" autoplay playsinline></video>
            <canvas id="canvas"></canvas>

            <!-- Overlay -->
            <div class="camera-overlay">
                <div class="guide-box"></div>
                <div class="guide-text">
                    📸 Position handbag in frame<br>
                    <small>Include all corners and details</small>
                </div>
            </div>

            <!-- Top Bar -->
            <div class="top-bar">
                <button class="back-btn" onclick="goBack()">✕</button>
                <button class="flip-btn" onclick="flipCamera()">🔄</button>
            </div>
        </div>

        <!-- Controls -->
        <div class="camera-controls">
            <button class="gallery-btn" onclick="openGallery()">
                🖼️
                <input type="file" accept="image/*" id="gallery-input" style="display:none" onchange="handleGallerySelect(event)">
            </button>
            <button class="capture-btn" onclick="capturePhoto()"></button>
            <button class="flash-btn" onclick="toggleFlash()">⚡</button>
        </div>
    </div>

    <!-- Preview Container -->
    <div class="preview-container" id="preview-container">
        <div class="top-bar">
            <button class="back-btn" onclick="closePreview()">✕</button>
            <span></span>
        </div>
        <div class="preview-image">
            <img id="preview-img" src="" alt="Captured photo">
        </div>
        <div class="preview-actions">
            <button class="retake-btn" onclick="retakePhoto()">Retake</button>
            <button class="upload-btn" onclick="uploadPhoto()">Upload & Analyze</button>
        </div>
    </div>

    <!-- Upload Progress -->
    <div class="upload-progress" id="upload-progress">
        <div class="progress-spinner"></div>
        <div class="progress-text">Analyzing handbag...</div>
        <div class="progress-detail">AI processing in progress</div>
    </div>

    <!-- Results Container -->
    <div class="results-container" id="results-container">
        <div class="results-header">
            <button class="back-btn" onclick="closeResults()" style="position:static;background:transparent;color:#1d1d1f;">✕</button>
            <h2 style="color:#1d1d1f;font-size:18px;">Analysis Results</h2>
            <span style="width:44px"></span>
        </div>
        <div class="results-content" id="results-content">
            <!-- Results will be populated here -->
        </div>
    </div>

    <script>
        let stream = null;
        let capturedImage = null;
        let currentCamera = 'user'; // 'user' or 'environment'
        let flashEnabled = false;

        // Initialize camera
        async function initCamera() {
            try {
                const constraints = {
                    video: {
                        facingMode: currentCamera,
                        width: { ideal: 1920 },
                        height: { ideal: 1080 }
                    }
                };

                stream = await navigator.mediaDevices.getUserMedia(constraints);
                document.getElementById('camera-feed').srcObject = stream;
            } catch (error) {
                console.error('Camera access error:', error);
                alert('Cannot access camera. Please check permissions.');
            }
        }

        // Capture photo
        function capturePhoto() {
            const video = document.getElementById('camera-feed');
            const canvas = document.getElementById('canvas');
            const context = canvas.getContext('2d');

            // Set canvas size to match video
            canvas.width = video.videoWidth;
            canvas.height = video.videoHeight;

            // Draw video frame to canvas
            context.drawImage(video, 0, 0);

            // Get image data
            capturedImage = canvas.toDataURL('image/jpeg', 0.9);

            // Show preview
            document.getElementById('preview-img').src = capturedImage;
            document.getElementById('preview-container').classList.add('active');
        }

        // Flip camera
        async function flipCamera() {
            currentCamera = currentCamera === 'user' ? 'environment' : 'user';

            if (stream) {
                stream.getTracks().forEach(track => track.stop());
            }

            await initCamera();
        }

        // Toggle flash
        function toggleFlash() {
            flashEnabled = !flashEnabled;
            const track = stream.getVideoTracks()[0];

            if (track.getCapabilities().torch) {
                track.applyConstraints({
                    advanced: [{ torch: flashEnabled }]
                });
            } else {
                alert('Flash not supported on this device');
            }
        }

        // Gallery select
        function openGallery() {
            document.getElementById('gallery-input').click();
        }

        function handleGallerySelect(event) {
            const file = event.target.files[0];
            if (file) {
                const reader = new FileReader();
                reader.onload = (e) => {
                    capturedImage = e.target.result;
                    document.getElementById('preview-img').src = capturedImage;
                    document.getElementById('preview-container').classList.add('active');
                };
                reader.readAsDataURL(file);
            }
        }

        // Preview actions
        function closePreview() {
            document.getElementById('preview-container').classList.remove('active');
        }

        function retakePhoto() {
            closePreview();
            capturedImage = null;
        }

        // Upload and analyze
        async function uploadPhoto() {
            if (!capturedImage) return;

            // Show progress
            document.getElementById('upload-progress').classList.add('active');
            closePreview();

            try {
                // Convert base64 to blob
                const blob = await fetch(capturedImage).then(r => r.blob());

                // Create form data
                const formData = new FormData();
                formData.append('image', blob, 'handbag.jpg');

                // Upload to server
                const response = await fetch('/api/analyze/image', {
                    method: 'POST',
                    body: formData
                });

                const result = await response.json();

                // Hide progress
                document.getElementById('upload-progress').classList.remove('active');

                // Show results
                displayResults(result);

            } catch (error) {
                console.error('Upload error:', error);
                document.getElementById('upload-progress').classList.remove('active');
                alert('Upload failed. Please try again.');
            }
        }

        // Display results
        function displayResults(data) {
            const container = document.getElementById('results-content');

            if (!data.success) {
                container.innerHTML = `
                    <div class="result-card">
                        <div class="result-title">❌ Error</div>
                        <p>${data.error || 'Analysis failed'}</p>
                    </div>
                `;
                document.getElementById('results-container').classList.add('active');
                return;
            }

            const analysis = data.analysis || {};
            const matches = data.matches || {};
            const priceComps = data.priceComparisons || [];

            // Calculate confidence and authenticity
            const confidence = Math.round((analysis.confidence || 0.5) * 100);
            const authenticity = confidence;

            // Format price range
            let estimatedValue = 'Unknown';
            if (analysis.priceRange && analysis.priceRange.max > 0) {
                estimatedValue = `$${analysis.priceRange.min.toLocaleString()} - $${analysis.priceRange.max.toLocaleString()}`;
            }

            const results = {
                brand: `${analysis.brand || 'Unknown'} (${confidence}% confidence)`,
                model: analysis.model || 'Unknown',
                material: analysis.material || 'Unknown',
                color: analysis.color || 'Unknown',
                condition: analysis.condition || 'Unknown',
                authenticity: authenticity,
                estimatedValue: estimatedValue,
                features: analysis.features || []
            };

            container.innerHTML = `
                <div class="result-card">
                    <div class="result-title">🔍 Identification</div>
                    <div class="result-item">
                        <span class="result-label">Brand</span>
                        <span class="result-value">${results.brand}</span>
                    </div>
                    <div class="result-item">
                        <span class="result-label">Model</span>
                        <span class="result-value">${results.model}</span>
                    </div>
                    <div class="result-item">
                        <span class="result-label">Material</span>
                        <span class="result-value">${results.material}</span>
                    </div>
                    <div class="result-item">
                        <span class="result-label">Color</span>
                        <span class="result-value">${results.color}</span>
                    </div>
                </div>

                <div class="result-card">
                    <div class="result-title">✅ Authenticity Check</div>
                    <div class="result-item">
                        <span class="result-label">Confidence Score</span>
                        <span class="result-value">${results.authenticity}%</span>
                    </div>
                    <div class="confidence-bar">
                        <div class="confidence-fill" style="width: ${results.authenticity}%"></div>
                    </div>
                </div>

                <div class="result-card">
                    <div class="result-title">💰 Estimated Value</div>
                    <div class="result-item">
                        <span class="result-label">Market Value</span>
                        <span class="result-value">${results.estimatedValue}</span>
                    </div>
                    <div class="result-item">
                        <span class="result-label">Condition</span>
                        <span class="result-value">${results.condition}</span>
                    </div>
                </div>

                <div class="result-card">
                    <div class="result-title">📋 Key Features</div>
                    ${results.features.length > 0 ? results.features.map(f => `
                        <div class="result-item">
                            <span class="result-value" style="font-weight:normal">• ${f}</span>
                        </div>
                    `).join('') : '<p style="color:#888;padding:10px">No features detected</p>'}
                </div>

                <div class="result-card">
                    <div class="result-title">🗄️ Database Matches</div>
                    <div class="result-item">
                        <span class="result-label">Total Found</span>
                        <span class="result-value">${matches.total || 0} items</span>
                    </div>
                    <div class="result-item">
                        <span class="result-label">Retailer DB</span>
                        <span class="result-value">${(matches.retailer || []).length} items</span>
                    </div>
                    <div class="result-item">
                        <span class="result-label">Luxury DB</span>
                        <span class="result-value">${(matches.luxury || []).length} items</span>
                    </div>
                    <div class="result-item">
                        <span class="result-label">Museum DB</span>
                        <span class="result-value">${(matches.museum || []).length} items</span>
                    </div>
                    <div class="result-item">
                        <span class="result-label">Live Auctions</span>
                        <span class="result-value">${(matches.liveAuctions || []).length} items</span>
                    </div>
                    <div class="result-item">
                        <span class="result-label">360° Images</span>
                        <span class="result-value">${(matches.abo360 || []).length} items</span>
                    </div>
                </div>

                ${priceComps.length > 0 ? `
                <div class="result-card">
                    <div class="result-title">💵 Price Comparisons</div>
                    ${priceComps.map(comp => `
                        <div class="result-item">
                            <span class="result-label">${comp.market}</span>
                            <span class="result-value">
                                ${comp.priceRange.min ? `$${comp.priceRange.min.toLocaleString()}` : ''}
                                ${comp.priceRange.avg ? `- $${comp.priceRange.avg.toLocaleString()}` : ''}
                                ${comp.count ? ` (${comp.count} items)` : ''}
                            </span>
                        </div>
                    `).join('')}
                </div>
                ` : ''}

                ${matches.liveAuctions && matches.liveAuctions.length > 0 ? `
                <div class="result-card">
                    <div class="result-title">🔥 Live Auctions (Japan)</div>
                    ${matches.liveAuctions.slice(0, 5).map(item => `
                        <div style="border-bottom: 1px solid #eee; padding: 12px 0;">
                            <div style="font-weight: 600; margin-bottom: 4px;">${item.title}</div>
                            <div style="color: #667eea; font-size: 1.2rem; font-weight: 700;">
                                $${item.priceUSD ? item.priceUSD.toFixed(0) : 'N/A'}
                            </div>
                            <div style="color: #888; font-size: 0.875rem;">
                                ¥${item.priceJPY ? item.priceJPY.toLocaleString() : 'N/A'} • ${item.condition || 'Used'}
                            </div>
                            <a href="${item.url}" target="_blank" rel="noopener noreferrer" style="color: #667eea; text-decoration: none; font-size: 0.875rem;">
                                View on ${item.source} →
                            </a>
                        </div>
                    `).join('')}
                </div>
                ` : ''}
            `;

            document.getElementById('results-container').classList.add('active');
        }

        function closeResults() {
            document.getElementById('results-container').classList.remove('active');
        }

        function goBack() {
            if (stream) {
                stream.getTracks().forEach(track => track.stop());
            }
            window.history.back();
        }

        // Initialize on load
        window.addEventListener('load', initCamera);
    </script>
</body>
</html>