Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>PhotoShot Pro | Production Coordinator</title> | |
| <!-- Phosphor Icons for modern UI iconography --> | |
| <script src="https://unpkg.com/@phosphor-icons/web"></script> | |
| <style> | |
| :root { | |
| /* Color Palette - Modern & Professional */ | |
| --primary: #2563eb; | |
| --primary-dark: #1e40af; | |
| --secondary: #64748b; | |
| --bg-body: #f1f5f9; | |
| --bg-surface: #ffffff; | |
| --bg-panel: #ffffff; | |
| --text-main: #0f172a; | |
| --text-muted: #64748b; | |
| --border: #e2e8f0; | |
| --accent-success: #10b981; | |
| --accent-warning: #f59e0b; | |
| --accent-danger: #ef4444; | |
| --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); | |
| --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); | |
| --radius: 12px; | |
| --font-family: 'Inter', system-ui, -apple-system, sans-serif; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| body { | |
| font-family: var(--font-family); | |
| background-color: var(--bg-body); | |
| color: var(--text-main); | |
| line-height: 1.5; | |
| min-height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| /* Header */ | |
| header { | |
| background: var(--bg-surface); | |
| border-bottom: 1px solid var(--border); | |
| padding: 1rem 2rem; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| position: sticky; | |
| top: 0; | |
| z-index: 100; | |
| } | |
| .brand { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| font-weight: 700; | |
| font-size: 1.25rem; | |
| color: var(--primary); | |
| } | |
| .brand i { | |
| font-size: 1.5rem; | |
| } | |
| .anycoder-link { | |
| font-size: 0.875rem; | |
| color: var(--text-muted); | |
| text-decoration: none; | |
| background: #f8fafc; | |
| padding: 0.5rem 1rem; | |
| border-radius: var(--radius); | |
| border: 1px solid var(--border); | |
| transition: all 0.2s; | |
| } | |
| .anycoder-link:hover { | |
| background: var(--primary); | |
| color: white; | |
| border-color: var(--primary); | |
| } | |
| /* Main Layout */ | |
| main { | |
| flex: 1; | |
| display: grid; | |
| grid-template-columns: 350px 1fr; | |
| gap: 2rem; | |
| padding: 2rem; | |
| max-width: 1600px; | |
| margin: 0 auto; | |
| width: 100%; | |
| } | |
| @media (max-width: 900px) { | |
| main { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| /* Panels */ | |
| .panel { | |
| background: var(--bg-panel); | |
| border-radius: var(--radius); | |
| box-shadow: var(--shadow-sm); | |
| border: 1px solid var(--border); | |
| padding: 1.5rem; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 1.5rem; | |
| height: fit-content; | |
| } | |
| .panel-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| font-weight: 600; | |
| font-size: 1.1rem; | |
| color: var(--text-main); | |
| padding-bottom: 1rem; | |
| border-bottom: 1px solid var(--border); | |
| } | |
| /* Form Elements */ | |
| .form-group { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.5rem; | |
| } | |
| label { | |
| font-size: 0.875rem; | |
| font-weight: 500; | |
| color: var(--text-muted); | |
| } | |
| textarea { | |
| width: 100%; | |
| padding: 0.75rem; | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius); | |
| font-family: inherit; | |
| resize: vertical; | |
| min-height: 120px; | |
| font-size: 0.95rem; | |
| transition: border-color 0.2s; | |
| } | |
| textarea:focus { | |
| outline: none; | |
| border-color: var(--primary); | |
| box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); | |
| } | |
| .btn-primary { | |
| background: var(--primary); | |
| color: white; | |
| border: none; | |
| padding: 0.75rem 1.5rem; | |
| border-radius: var(--radius); | |
| font-weight: 600; | |
| cursor: pointer; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| gap: 0.5rem; | |
| transition: background 0.2s; | |
| } | |
| .btn-primary:hover { | |
| background: var(--primary-dark); | |
| } | |
| .btn-secondary { | |
| background: transparent; | |
| color: var(--text-muted); | |
| border: 1px solid var(--border); | |
| padding: 0.75rem 1.5rem; | |
| border-radius: var(--radius); | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| .btn-secondary:hover { | |
| background: #f8fafc; | |
| color: var(--text-main); | |
| } | |
| /* Results Area */ | |
| .results-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 1rem; | |
| } | |
| .shot-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); | |
| gap: 1.5rem; | |
| } | |
| /* Shot Card */ | |
| .shot-card { | |
| background: #fff; | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius); | |
| padding: 1.25rem; | |
| transition: transform 0.2s, box-shadow 0.2s; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .shot-card:hover { | |
| transform: translateY(-2px); | |
| box-shadow: var(--shadow-md); | |
| } | |
| .shot-card::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 4px; | |
| height: 100%; | |
| background: var(--secondary); | |
| } | |
| .shot-card.featured::before { | |
| background: var(--primary); | |
| } | |
| .shot-card.macro::before { | |
| background: var(--accent-warning); | |
| } | |
| .shot-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: flex-start; | |
| margin-bottom: 1rem; | |
| } | |
| .shot-type { | |
| font-weight: 700; | |
| font-size: 1rem; | |
| color: var(--text-main); | |
| } | |
| .shot-badge { | |
| font-size: 0.75rem; | |
| padding: 0.25rem 0.5rem; | |
| border-radius: 99px; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| } | |
| .badge-global { | |
| background: #eff6ff; | |
| color: var(--primary); | |
| } | |
| .badge-single { | |
| background: #f0fdf4; | |
| color: var(--accent-success); | |
| } | |
| .shot-details { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.75rem; | |
| } | |
| .detail-row { | |
| display: flex; | |
| align-items: flex-start; | |
| gap: 0.75rem; | |
| font-size: 0.875rem; | |
| } | |
| .detail-row i { | |
| color: var(--text-muted); | |
| margin-top: 2px; | |
| flex-shrink: 0; | |
| } | |
| .color-tag { | |
| display: inline-block; | |
| background: var(--bg-body); | |
| padding: 0.2rem 0.5rem; | |
| border-radius: 4px; | |
| font-size: 0.8rem; | |
| margin-right: 0.25rem; | |
| margin-bottom: 0.25rem; | |
| border: 1px solid var(--border); | |
| } | |
| .overlay-text { | |
| background: var(--text-main); | |
| color: white; | |
| padding: 0.5rem; | |
| border-radius: 6px; | |
| font-family: monospace; | |
| font-size: 0.8rem; | |
| margin-top: 0.5rem; | |
| } | |
| .overlay-text.empty { | |
| background: transparent; | |
| color: var(--text-muted); | |
| font-style: italic; | |
| border: 1px dashed var(--border); | |
| } | |
| /* Empty State */ | |
| .empty-state { | |
| text-align: center; | |
| padding: 4rem 2rem; | |
| color: var(--text-muted); | |
| } | |
| .empty-state i { | |
| font-size: 3rem; | |
| margin-bottom: 1rem; | |
| opacity: 0.5; | |
| } | |
| /* Summary Bar */ | |
| .summary-bar { | |
| background: var(--bg-surface); | |
| padding: 1rem 1.5rem; | |
| border-radius: var(--radius); | |
| margin-bottom: 1.5rem; | |
| border: 1px solid var(--border); | |
| display: flex; | |
| gap: 2rem; | |
| flex-wrap: wrap; | |
| } | |
| .summary-item { | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .summary-label { | |
| font-size: 0.75rem; | |
| text-transform: uppercase; | |
| color: var(--text-muted); | |
| font-weight: 600; | |
| } | |
| .summary-value { | |
| font-size: 1.1rem; | |
| font-weight: 700; | |
| color: var(--text-main); | |
| } | |
| /* Toast Notification */ | |
| .toast { | |
| position: fixed; | |
| bottom: 2rem; | |
| right: 2rem; | |
| background: var(--text-main); | |
| color: white; | |
| padding: 1rem 1.5rem; | |
| border-radius: var(--radius); | |
| box-shadow: var(--shadow-md); | |
| transform: translateY(150%); | |
| transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); | |
| z-index: 1000; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| } | |
| .toast.show { | |
| transform: translateY(0); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="brand"> | |
| <i class="ph ph-camera"></i> | |
| PhotoShot Pro | |
| </div> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link"> | |
| Built with anycoder | |
| </a> | |
| </header> | |
| <main> | |
| <!-- Left Panel: Input Controls --> | |
| <section class="panel"> | |
| <div class="panel-header"> | |
| <i class="ph ph-sliders-horizontal"></i> | |
| Production Input | |
| </div> | |
| <div class="form-group"> | |
| <label for="productInput">Product Name & Details</label> | |
| <textarea id="productInput" placeholder="e.g. Men's High-Waisted Slim Fit Chino / Navy, Beige, Black - Pay 2 Take 3 (Includes Free Belt)"></textarea> | |
| <small style="color: var(--text-muted); font-size: 0.75rem;">Include gender, features, colors (separated by / or ,), and promotions.</small> | |
| </div> | |
| <div style="display: flex; gap: 1rem;"> | |
| <button id="generateBtn" class="btn-primary" style="flex: 1;"> | |
| <i class="ph ph-magic-wand"></i> Generate List | |
| </button> | |
| <button id="clearBtn" class="btn-secondary"> | |
| <i class="ph ph-trash"></i> | |
| </button> | |
| </div> | |
| <div style="margin-top: auto; border-top: 1px solid var(--border); padding-top: 1rem;"> | |
| <label>Logic Preview</label> | |
| <div style="font-size: 0.8rem; color: var(--text-muted); display: flex; flex-direction: column; gap: 0.5rem;"> | |
| <div style="display: flex; justify-content: space-between;"> | |
| <span>Gender Detection:</span> | |
| <strong id="previewGender">-</strong> | |
| </div> | |
| <div style="display: flex; justify-content: space-between;"> | |
| <span>Colors Found:</span> | |
| <strong id="previewColors">-</strong> | |
| </div> | |
| <div style="display: flex; justify-content: space-between;"> | |
| <span>Promo Offer:</span> | |
| <strong id="previewPromo">-</strong> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Right Panel: Output Results --> | |
| <section class="panel" style="background: transparent; border: none; box-shadow: none; padding: 0;"> | |
| <div id="resultsArea"> | |
| <!-- Empty State --> | |
| <div class="empty-state"> | |
| <i class="ph ph-clipboard-text"></i> | |
| <h3>Ready to Coordinate</h3> | |
| <p>Enter a product name on the left to generate a photography shot list based on your production logic.</p> | |
| </div> | |
| </div> | |
| </section> | |
| </main> | |
| <div id="toast" class="toast"> | |
| <i class="ph ph-check-circle" style="font-size: 1.25rem;"></i> | |
| <span>Shot list generated successfully!</span> | |
| </div> | |
| <script> | |
| // --- DOM Elements --- | |
| const productInput = document.getElementById('productInput'); | |
| const generateBtn = document.getElementById('generateBtn'); | |
| const clearBtn = document.getElementById('clearBtn'); | |
| const resultsArea = document.getElementById('resultsArea'); | |
| const toast = document.getElementById('toast'); | |
| const previewGender = document.getElementById('previewGender'); | |
| const previewColors = document.getElementById('previewColors'); | |
| const previewPromo = document.getElementById('previewPromo'); | |
| // --- Logic Configuration --- | |
| const SHOT_DEFINITIONS = [ | |
| { id: 'mirror', name: 'Review Mirror', type: 'global', gender: true }, | |
| { id: 'bed', name: 'Review Bed (Pile)', type: 'global', gender: false }, | |
| { id: 'main', name: 'Main Product Photo', type: 'global', gender: false, overlay: true }, | |
| { id: 'iso', name: 'Isolated Product (PNG)', type: 'global', gender: false }, | |
| { id: 'macro', name: 'Detail Shot 2 (Macro)', type: 'single', gender: false, macroException: true }, | |
| { id: 'street', name: 'Model Shot 1 (Street)', type: 'single', gender: true }, | |
| { id: 'urban', name: 'Model Shot 2 (Urban)', type: 'single', gender: true }, | |
| { id: 'hand', name: 'Detail Shot 1 (Hand)', type: 'single', gender: false } | |
| ]; | |
| // --- Parsing Logic --- | |
| function parseProductDetails(input) { | |
| const text = input.trim(); | |
| if (!text) return null; | |
| // 1. Gender Detection | |
| let gender = 'Unspecified'; | |
| if (text.toLowerCase().includes("men's")) gender = 'Male'; | |
| else if (text.toLowerCase().includes("women's")) gender = 'Female'; | |
| // 2. Color Parsing (Split by "/" or ",") | |
| // We look for the color section usually after the product name, but regex is safer for specific delimiters | |
| // Removing everything inside parens first to avoid confusing "Pay X Take Y" logic with colors | |
| const cleanTextForColors = text.replace(/\([^)]*\)/g, ''); | |
| // Split by / or comma, trim whitespace, filter empty | |
| let colors = cleanTextForColors.split(/\/|,/).map(c => c.trim()).filter(c => c.length > 0); | |
| // If no delimiters found but we have text, we might assume the whole thing is one color or no color specified. | |
| // For this tool, we assume explicit delimiters are used for multi-color. | |
| // However, if the split returns 1 item that is the whole product name, we might be parsing wrong. | |
| // Let's try to isolate colors: usually the last part of the string before brackets. | |
| // For simplicity based on prompt: "Extract available colors by splitting..." | |
| // Filter out non-color words (heuristic) | |
| const stopWords = ['pants', 'shirt', 'jacket', 'dress', 'shoes', 'fit', 'style', 'collection', 'pay', 'take', 'men\'s', 'women\'s']; | |
| colors = colors.filter(c => !stopWords.includes(c.toLowerCase())); | |
| if (colors.length === 0) colors = ['Default']; | |
| // 3. Promo Detection ("Pay X Take Y") | |
| const promoRegex = /(pay \d+ take \d+)/i; | |
| const promoMatch = text.match(promoRegex); | |
| const promoText = promoMatch ? promoMatch[0] : null; | |
| // 4. Feature Adaptation (Structural Details) | |
| // Looking for specific keywords mentioned in prompt + common fashion terms | |
| const featureKeywords = [ | |
| 'high-waisted', 'vertical front seams', 'pleated', 'slim fit', | |
| 'relaxed fit', 'skinny', 'straight leg', 'tapered', 'cargo', | |
| 'pockets', 'zipper', 'buttons', 'elastic waist' | |
| ]; | |
| // Normalize text for checking | |
| const lowerText = text.toLowerCase(); | |
| const foundFeatures = featureKeywords.filter(feat => lowerText.includes(feat.toLowerCase())); | |
| const featureString = foundFeatures.length > 0 ? foundFeatures.join(', ') : null; | |
| // 5. Bonus/Gift Check (for Macro Exception) | |
| const hasBonusOrGift = /bonus|gift|free|includes/i.test(text); | |
| return { | |
| raw: text, | |
| gender, | |
| colors, | |
| promoText, | |
| featureString, | |
| hasBonusOrGift | |
| }; | |
| } | |
| // --- Rendering Logic --- | |
| function renderShotList(data) { | |
| let colorIndex = 0; | |
| const cardsHTML = SHOT_DEFINITIONS.map((shot, index) => { | |
| let assignedColors = []; | |
| let overlayText = ''; | |
| let description = data.raw; | |
| let badgeClass = shot.type === 'global' ? 'badge-global' : 'badge-single'; | |
| let badgeText = shot.type === 'global' ? 'All Colors' : 'Variant Focus'; | |
| let cardClass = ''; | |
| // Color Assignment | |
| if (shot.type === 'global') { | |
| assignedColors = [...data.colors]; | |
| cardClass = 'featured'; | |
| } else { | |
| // Individual: Rotate colors | |
| const color = data.colors[colorIndex % data.colors.length]; | |
| assignedColors = [color]; | |
| colorIndex++; | |
| } | |
| // Overlay Logic | |
| if (shot.overlay) { | |
| overlayText = data.promoText || ''; | |
| } | |
| // Macro Exception | |
| if (shot.macroException) { | |
| cardClass = 'macro'; | |
| if (data.hasBonusOrGift && data.promoText) { | |
| overlayText = ''; // Strip promo | |
| // Logic: "list only the product name and color" | |
| // We construct a clean description | |
| const cleanName = data.raw.replace(data.promoText, '').replace(/\s\s+/g, ' ').trim(); | |
| description = cleanName; | |
| } | |
| } | |
| // Set Exclusion for Individual (Prompt: "exclude multi-piece promotional logic... focus solely on color variant") | |
| if (shot.type === 'single' && data.promoText) { | |
| // Remove promo from description for individual shots | |
| description = description.replace(data.promoText, '').replace(/\s\s+/g, ' ').trim(); | |
| } | |
| // Append features if present | |
| if (data.featureString) { | |
| description += ` (${data.featureString})`; | |
| } | |
| // Render HTML | |
| return ` | |
| <div class="shot-card ${cardClass}"> | |
| <div class="shot-header"> | |
| <span class="shot-type">${shot.name}</span> | |
| <span class="shot-badge ${badgeClass}">${badgeText}</span> | |
| </div> | |
| <div class="shot-details"> | |
| ${shot.gender ? ` | |
| <div class="detail-row"> | |
| <i class="ph ph-gender-intersex"></i> | |
| <span><strong>Model:</strong> ${data.gender}</span> | |
| </div>` : ''} | |
| <div class="detail-row"> | |
| <i class="ph ph-palette"></i> | |
| <div> | |
| <strong>Color${assignedColors.length > 1 ? 's' : ''}:</strong><br> | |
| <div style="margin-top:4px;"> | |
| ${assignedColors.map(c => `<span class="color-tag">${c}</span>`).join('')} | |
| </div> | |
| </div> | |
| </div> | |
| <div class="detail-row"> | |
| <i class="ph ph-info"></i> | |
| <span>${description}</span> | |
| </div> | |
| ${shot.overlay || shot.macroException ? ` | |
| <div class="detail-row"> | |
| <i class="ph ph-text-aa"></i> | |
| <div style="width: 100%;"> | |
| <strong>Text Overlay:</strong> | |
| <div class="overlay-text ${!overlayText ? 'empty' : ''}"> | |
| ${overlayText ? overlayText : 'None'} | |
| </div> | |
| </div> | |
| </div>` : ''} | |
| </div> | |
| </div> | |
| `; | |
| }).join(''); | |
| // Summary Bar | |
| const summaryHTML = ` | |
| <div class="summary-bar"> | |
| <div class="summary-item"> | |
| <span class="summary-label">Total Shots</span> | |
| <span class="summary-value">${SHOT_DEFINITIONS.length}</span> | |
| </div> | |
| <div class="summary-item"> | |
| <span class="summary-label">Target Gender</span> | |
| <span class="summary-value">${data.gender}</span> | |
| </div> | |
| <div class="summary-item"> | |
| <span class="summary-label">Colors Detected</span> | |
| <span class="summary-value">${data.colors.length}</span> | |
| </div> | |
| <div class="summary-item"> | |
| <span class="summary-label">Promotion</span> | |
| <span class="summary-value" style="color: ${data.promoText ? 'var(--accent-success)' : 'var(--text-muted)'}"> | |
| ${data.promoText ? 'Active' : 'None'} | |
| </span> | |
| </div> | |
| </div> | |
| <div class="shot-grid"> | |
| ${cardsHTML} | |
| </div> | |
| `; | |
| resultsArea.innerHTML = summaryHTML; | |
| } | |
| // --- Event Listeners --- | |
| function updatePreview() { | |
| const data = parseProductDetails(productInput.value); | |
| if (data) { | |
| previewGender.textContent = data.gender; | |
| previewColors.textContent = data.colors.join(', '); | |
| previewPromo.textContent = data.promoText || 'None'; | |
| } else { | |
| previewGender.textContent = '-'; | |
| previewColors.textContent = '-'; | |
| previewPromo.textContent = '-'; | |
| } | |
| } | |
| function showToast() { | |
| toast.classList.add('show'); | |
| setTimeout(() => { | |
| toast.classList.remove('show'); | |
| }, 3000); | |
| } | |
| generateBtn.addEventListener('click', () => { | |
| const data = parseProductDetails(productInput.value); | |
| if (data) { | |
| renderShotList(data); | |
| showToast(); | |
| } else { | |
| resultsArea.innerHTML = ` | |
| <div class="empty-state"> | |
| <i class="ph ph-warning-circle" style="color: var(--accent-danger)"></i> | |
| <h3>Invalid Input</h3> | |
| <p>Please enter a product name to generate the shot list.</p> | |
| </div> | |
| `; | |
| } | |
| }); | |
| clearBtn.addEventListener('click', () => { | |
| productInput.value = ''; | |
| updatePreview(); | |
| resultsArea.innerHTML = ` | |
| <div class="empty-state"> | |
| <i class="ph ph-clipboard-text"></i> | |
| <h3>Ready to Coordinate</h3> | |
| <p>Enter a product name on the left to generate a photography shot list based on your production logic.</p> | |
| </div> | |
| `; | |
| }); | |
| productInput.addEventListener('input', updatePreview); | |
| // --- Initialization --- | |
| updatePreview(); | |
| </script> | |
| </body> | |
| </html> |