| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Microstock Image Analyzer | Gemini-Powered SEO Metadata</title> |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
| <style> |
| :root { |
| --primary: #4a6bff; |
| --secondary: #f8f9fa; |
| --dark: #343a40; |
| --light: #ffffff; |
| --success: #28a745; |
| --info: #17a2b8; |
| --warning: #ffc107; |
| --danger: #dc3545; |
| --gemini-color: #0468d7; |
| } |
| |
| * { |
| margin: 0; |
| padding: 0; |
| box-sizing: border-box; |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
| } |
| |
| body { |
| background-color: #f5f7ff; |
| color: var(--dark); |
| line-height: 1.6; |
| } |
| |
| .container { |
| max-width: 1200px; |
| margin: 0 auto; |
| padding: 2rem; |
| } |
| |
| header { |
| text-align: center; |
| margin-bottom: 2rem; |
| position: relative; |
| } |
| |
| h1 { |
| color: var(--primary); |
| margin-bottom: 0.5rem; |
| font-weight: 700; |
| } |
| |
| .subtitle { |
| color: #6c757d; |
| font-size: 1.1rem; |
| margin-bottom: 1.5rem; |
| } |
| |
| .upload-container { |
| background-color: var(--light); |
| border-radius: 10px; |
| padding: 2rem; |
| box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); |
| margin-bottom: 2rem; |
| } |
| |
| .upload-area { |
| border: 2px dashed #d3d3d3; |
| border-radius: 8px; |
| padding: 3rem 2rem; |
| text-align: center; |
| cursor: pointer; |
| transition: all 0.3s ease; |
| margin-bottom: 1.5rem; |
| } |
| |
| .upload-area:hover { |
| border-color: var(--primary); |
| background-color: rgba(74, 107, 255, 0.05); |
| } |
| |
| .upload-area i { |
| font-size: 3rem; |
| color: var(--primary); |
| margin-bottom: 1rem; |
| } |
| |
| .upload-area h3 { |
| margin-bottom: 0.5rem; |
| } |
| |
| .upload-area p { |
| color: #6c757d; |
| margin-bottom: 1rem; |
| } |
| |
| #file-input { |
| display: none; |
| } |
| |
| .btn { |
| display: inline-block; |
| padding: 0.6rem 1.2rem; |
| background-color: var(--primary); |
| color: white; |
| border: none; |
| border-radius: 5px; |
| cursor: pointer; |
| font-size: 1rem; |
| font-weight: 500; |
| transition: all 0.3s ease; |
| text-align: center; |
| } |
| |
| .btn:hover { |
| background-color: #3a56d4; |
| transform: translateY(-2px); |
| box-shadow: 0 4px 12px rgba(74, 107, 255, 0.3); |
| } |
| |
| .btn-secondary { |
| background-color: #6c757d; |
| } |
| |
| .btn-secondary:hover { |
| background-color: #5a6268; |
| } |
| |
| .btn-warning { |
| background-color: var(--warning); |
| color: var(--dark); |
| } |
| |
| .btn-warning:hover { |
| background-color: #e0a800; |
| } |
| |
| .btn-success { |
| background-color: var(--success); |
| } |
| |
| .btn-success:hover { |
| background-color: #218838; |
| } |
| |
| .btn-gemini { |
| background-color: var(--gemini-color); |
| } |
| |
| .btn-gemini:hover { |
| background-color: #0352a8; |
| } |
| |
| .btn-block { |
| display: block; |
| width: 100%; |
| } |
| |
| .settings-panel { |
| background-color: var(--secondary); |
| border-radius: 8px; |
| padding: 1.5rem; |
| margin-bottom: 1.5rem; |
| } |
| |
| .settings-grid { |
| display: grid; |
| grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); |
| gap: 1rem; |
| } |
| |
| .form-group { |
| margin-bottom: 1rem; |
| } |
| |
| label { |
| display: block; |
| margin-bottom: 0.5rem; |
| font-weight: 500; |
| } |
| |
| select, input[type="text"], textarea { |
| width: 100%; |
| padding: 0.6rem; |
| border: 1px solid #ced4da; |
| border-radius: 5px; |
| background-color: var(--light); |
| } |
| |
| textarea { |
| min-height: 100px; |
| resize: vertical; |
| } |
| |
| .preview-container { |
| background-color: var(--light); |
| border-radius: 10px; |
| padding: 2rem; |
| box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); |
| margin-bottom: 2rem; |
| } |
| |
| .result-grid { |
| display: grid; |
| grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); |
| gap: 2rem; |
| } |
| |
| .image-card { |
| background-color: var(--secondary); |
| border-radius: 8px; |
| overflow: hidden; |
| box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); |
| transition: transform 0.3s ease; |
| } |
| |
| .image-card:hover { |
| transform: translateY(-5px); |
| box-shadow: 0 6px 15px rgba(0, 0, 0, 0.1); |
| } |
| |
| .image-preview { |
| width: 100%; |
| height: 200px; |
| object-fit: contain; |
| background-color: #e9ecef; |
| } |
| |
| .card-body { |
| padding: 1.5rem; |
| } |
| |
| .card-title { |
| font-size: 1.25rem; |
| margin-bottom: 0.75rem; |
| color: var(--dark); |
| } |
| |
| .card-text { |
| color: #6c757d; |
| margin-bottom: 1rem; |
| font-size: 0.9rem; |
| } |
| |
| .card-keywords { |
| display: flex; |
| flex-wrap: wrap; |
| gap: 0.5rem; |
| margin-bottom: 1rem; |
| } |
| |
| .keyword-chip { |
| background-color: var(--primary); |
| color: white; |
| padding: 0.3rem 0.6rem; |
| border-radius: 20px; |
| font-size: 0.75rem; |
| display: inline-block; |
| } |
| |
| .card-actions { |
| display: flex; |
| gap: 0.75rem; |
| margin-top: 1rem; |
| } |
| |
| .copy-btn { |
| background-color: var(--success); |
| padding: 0.5rem 1rem; |
| border-radius: 5px; |
| color: white; |
| border: none; |
| cursor: pointer; |
| display: flex; |
| align-items: center; |
| gap: 0.5rem; |
| font-size: 0.85rem; |
| } |
| |
| .regenerate-btn { |
| background-color: var(--info); |
| padding: 0.5rem 1rem; |
| border-radius: 5px; |
| color: white; |
| border: none; |
| cursor: pointer; |
| display: flex; |
| align-items: center; |
| gap: 0.5rem; |
| font-size: 0.85rem; |
| } |
| |
| .action-bar { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| margin-bottom: 1.5rem; |
| } |
| |
| .loading { |
| display: none; |
| text-align: center; |
| padding: 2rem; |
| } |
| |
| .spinner { |
| width: 50px; |
| height: 50px; |
| border: 5px solid rgba(74, 107, 255, 0.2); |
| border-radius: 50%; |
| border-top-color: var(--primary); |
| animation: spin 1s ease-in-out infinite; |
| margin: 0 auto 1rem; |
| } |
| |
| @keyframes spin { |
| to { transform: rotate(360deg); } |
| } |
| |
| .download-all { |
| background-color: var(--success); |
| margin-bottom: 2rem; |
| } |
| |
| .file-list { |
| list-style-type: none; |
| margin-bottom: 1rem; |
| max-height: 200px; |
| overflow-y: auto; |
| border: 1px solid #eee; |
| border-radius: 5px; |
| padding: 0.5rem; |
| } |
| |
| .file-list li { |
| padding: 0.5rem; |
| border-bottom: 1px solid #eee; |
| display: flex; |
| justify-content: space-between; |
| } |
| |
| .file-list li:last-child { |
| border-bottom: none; |
| } |
| |
| .file-name { |
| white-space: nowrap; |
| overflow: hidden; |
| text-overflow: ellipsis; |
| max-width: 80%; |
| } |
| |
| .file-size { |
| color: #6c757d; |
| font-size: 0.8rem; |
| } |
| |
| .remove-file { |
| color: #dc3545; |
| cursor: pointer; |
| margin-left: 0.5rem; |
| } |
| |
| .api-settings { |
| background-color: #fff8e1; |
| border: 1px solid #ffe0b2; |
| border-radius: 8px; |
| padding: 1.5rem; |
| margin-bottom: 1.5rem; |
| } |
| |
| .api-toggle { |
| display: flex; |
| align-items: center; |
| margin-bottom: 1rem; |
| } |
| |
| .api-toggle label { |
| margin-bottom: 0; |
| margin-left: 0.5rem; |
| cursor: pointer; |
| } |
| |
| .api-credentials { |
| display: none; |
| margin-top: 1rem; |
| } |
| |
| .api-credentials.active { |
| display: block; |
| } |
| |
| .tab-container { |
| margin-bottom: 1.5rem; |
| } |
| |
| .tabs { |
| display: flex; |
| border-bottom: 1px solid #ddd; |
| margin-bottom: 1rem; |
| } |
| |
| .tab { |
| padding: 0.75rem 1.5rem; |
| cursor: pointer; |
| border-bottom: 3px solid transparent; |
| transition: all 0.2s ease; |
| } |
| |
| .tab.active { |
| border-bottom: 3px solid var(--primary); |
| color: var(--primary); |
| font-weight: 500; |
| } |
| |
| .tab-content { |
| display: none; |
| } |
| |
| .tab-content.active { |
| display: block; |
| } |
| |
| .bulk-edit-container { |
| margin-top: 1.5rem; |
| } |
| |
| .alert { |
| padding: 1rem; |
| border-radius: 5px; |
| margin-bottom: 1rem; |
| display: flex; |
| align-items: flex-start; |
| gap: 0.75rem; |
| } |
| |
| .alert-info { |
| background-color: #e7f5ff; |
| border-left: 3px solid var(--info); |
| color: var(--dark); |
| } |
| |
| .alert-warning { |
| background-color: #fff4e6; |
| border-left: 3px solid var(--warning); |
| color: var(--dark); |
| } |
| |
| .alert-success { |
| background-color: #e6f5ea; |
| border-left: 3px solid var(--success); |
| color: var(--dark); |
| } |
| |
| .alert-error { |
| background-color: #fee; |
| border-left: 3px solid var(--danger); |
| color: var(--dark); |
| } |
| |
| .badge { |
| display: inline-block; |
| padding: 0.25rem 0.5rem; |
| border-radius: 20px; |
| font-size: 0.75rem; |
| font-weight: 500; |
| } |
| |
| .badge-gemini { |
| background-color: var(--gemini-color); |
| color: white; |
| } |
| |
| .badge-basic { |
| background-color: #6c757d; |
| color: white; |
| } |
| |
| .powered-by { |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| gap: 0.5rem; |
| margin-top: 1rem; |
| color: var(--gemini-color); |
| font-weight: 500; |
| } |
| |
| footer { |
| text-align: center; |
| margin-top: 3rem; |
| color: #6c757d; |
| font-size: 0.9rem; |
| } |
| |
| @media (max-width: 768px) { |
| .container { |
| padding: 1rem; |
| } |
| |
| .settings-grid { |
| grid-template-columns: 1fr; |
| } |
| |
| .result-grid { |
| grid-template-columns: 1fr; |
| } |
| |
| .action-bar { |
| flex-direction: column; |
| align-items: flex-start; |
| gap: 1rem; |
| } |
| } |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <header> |
| <h1>AI Microstock Image Analyzer</h1> |
| <p class="subtitle">Generate SEO-optimized metadata with Google Gemini AI for your stock photos</p> |
| </header> |
| |
| <div class="api-settings"> |
| <h3> |
| <i class="fas fa-key" style="color: var(--gemini-color);"></i> |
| Gemini API Configuration |
| </h3> |
| <div class="api-toggle"> |
| <input type="checkbox" id="use-gemini-api" checked> |
| <label for="use-gemini-api">Enable Google Gemini API Image Analysis</label> |
| </div> |
| |
| <div class="api-credentials active" id="api-credentials"> |
| <div class="form-group"> |
| <label for="api-key">Gemini API Key</label> |
| <input type="text" id="api-key" placeholder="Enter your Google Gemini API key (required)"> |
| <small>Get your API key from <a href="https://ai.google.dev/" target="_blank" style="color: var(--gemini-color);">Google AI Studio</a></small> |
| </div> |
| <button id="test-api-btn" class="btn btn-gemini"> |
| <i class="fas fa-plug"></i> Test API Connection |
| </button> |
| <button id="save-api-btn" class="btn"> |
| <i class="fas fa-save"></i> Save Settings |
| </button> |
| </div> |
| </div> |
| |
| <div class="tab-container"> |
| <div class="tabs"> |
| <div class="tab active" data-tab="upload">Upload Images</div> |
| <div class="tab" data-tab="bulk-edit">Bulk Edit</div> |
| </div> |
| |
| <div class="tab-content active" id="upload-tab"> |
| <div class="upload-container"> |
| <div class="upload-area" id="upload-area"> |
| <i class="fas fa-cloud-upload-alt"></i> |
| <h3>Upload Your Microstock Images</h3> |
| <p>Drag & drop your files here or click to browse</p> |
| <button class="btn">Select Files</button> |
| <input type="file" id="file-input" accept="image/*" multiple> |
| </div> |
| |
| <div class="file-list-container" id="file-list-container" style="display: none;"> |
| <h4>Selected Files (<span id="file-count">0</span>)</h4> |
| <ul class="file-list" id="file-list"></ul> |
| </div> |
| </div> |
| |
| <div class="settings-panel"> |
| <h3>Generation Settings</h3> |
| <div class="settings-grid"> |
| <div class="form-group"> |
| <label for="image-category">Image Category</label> |
| <select id="image-category"> |
| <option value="general">General</option> |
| <option value="business">Business</option> |
| <option value="technology">Technology</option> |
| <option value="nature">Nature</option> |
| <option value="people">People</option> |
| <option value="food">Food</option> |
| <option value="travel">Travel</option> |
| <option value="health">Health</option> |
| <option value="education">Education</option> |
| <option value="holiday">Holiday</option> |
| </select> |
| </div> |
| |
| <div class="form-group"> |
| <label for="image-style">Image Style</label> |
| <select id="image-style"> |
| <option value="realistic">Realistic</option> |
| <option value="flat">Flat Design</option> |
| <option value="3d">3D Rendering</option> |
| <option value="illustration">Illustration</option> |
| <option value="hand-drawn">Hand Drawn</option> |
| <option value="vector">Vector</option> |
| <option value="minimal">Minimal</option> |
| <option value="isometric">Isometric</option> |
| </select> |
| </div> |
| |
| <div class="form-group"> |
| <label for="language">Content Language</label> |
| <select id="language"> |
| <option value="en">English</option> |
| <option value="es">Spanish</option> |
| <option value="fr">French</option> |
| <option value="de">German</option> |
| <option value="pt">Portuguese</option> |
| </select> |
| </div> |
| |
| <div class="form-group"> |
| <label for="target-platform">Target Platform</label> |
| <select id="target-platform"> |
| <option value="all">All Platforms</option> |
| <option value="freepik">Freepik</option> |
| <option value="shutterstock">Shutterstock</option> |
| <option value="adobe">Adobe Stock</option> |
| <option value="iconscout">IconScout</option> |
| </select> |
| </div> |
| </div> |
| </div> |
| |
| <button id="generate-btn" class="btn btn-block btn-gemini"> |
| <i class="fas fa-magic"></i> Generate SEO Metadata with Gemini |
| </button> |
| |
| <div class="alert alert-info"> |
| <i class="fas fa-info-circle"></i> |
| <div> |
| <strong>Gemini AI Tip:</strong> For best results, describe the image category, style, and target platform above. |
| Gemini will generate highly relevant metadata based on your visual content and these specifications. |
| </div> |
| </div> |
| |
| <div class="loading" id="loading"> |
| <div class="spinner"></div> |
| <p id="loading-text">Gemini is analyzing your images and generating SEO metadata...</p> |
| <div class="powered-by"> |
| <img src="https://www.gstatic.com/lamda/images/gemini_sparkle_v002_d4735304ff6292a690345.svg" alt="Gemini" width="20"> |
| Powered by Google Gemini AI |
| </div> |
| </div> |
| |
| <div class="preview-container" id="results-container" style="display: none;"> |
| <div class="action-bar"> |
| <h3>Generated Results</h3> |
| <div> |
| <button id="download-all" class="btn download-all"> |
| <i class="fas fa-download"></i> Download All as CSV |
| </button> |
| <button id="regenerate-all" class="btn btn-warning"> |
| <i class="fas fa-sync-alt"></i> Regenerate All |
| </button> |
| </div> |
| </div> |
| |
| <div class="result-grid" id="result-grid"></div> |
| </div> |
| </div> |
| |
| <div class="tab-content" id="bulk-edit-tab"> |
| <div class="alert alert-warning"> |
| <i class="fas fa-exclamation-triangle"></i> |
| <div> |
| <strong>Notice:</strong> Please upload files first, then you can edit all metadata in bulk here. |
| Bulk edits will override existing metadata. |
| </div> |
| </div> |
| |
| <div class="bulk-edit-container" id="bulk-edit-fields" style="display: none;"> |
| <div class="form-group"> |
| <label for="bulk-title">Bulk Edit Title</label> |
| <input type="text" id="bulk-title" placeholder="Will be applied to all images"> |
| </div> |
| |
| <div class="form-group"> |
| <label for="bulk-description">Bulk Edit Description</label> |
| <textarea id="bulk-description" placeholder="Will be applied to all images"></textarea> |
| </div> |
| |
| <div class="form-group"> |
| <label for="bulk-keywords">Bulk Edit Keywords (comma separated)</label> |
| <textarea id="bulk-keywords" placeholder="Will replace all keywords for all images"></textarea> |
| </div> |
| |
| <button id="apply-bulk-edit" class="btn">Apply Bulk Changes to All Images</button> |
| </div> |
| </div> |
| </div> |
| |
| <div class="alert alert-info" id="api-status" style="display: none;"> |
| <i class="fas fa-info-circle"></i> |
| <div id="api-status-text"></div> |
| </div> |
| </div> |
| |
| <footer> |
| <p>AI Microstock Image Analyzer © 2023 | Powered by Google Gemini AI</p> |
| </footer> |
| |
| <script> |
| |
| const uploadArea = document.getElementById('upload-area'); |
| const fileInput = document.getElementById('file-input'); |
| const fileListContainer = document.getElementById('file-list-container'); |
| const fileList = document.getElementById('file-list'); |
| const fileCount = document.getElementById('file-count'); |
| const generateBtn = document.getElementById('generate-btn'); |
| const loadingIndicator = document.getElementById('loading'); |
| const loadingText = document.getElementById('loading-text'); |
| const resultsContainer = document.getElementById('results-container'); |
| const resultGrid = document.getElementById('result-grid'); |
| const downloadAllBtn = document.getElementById('download-all'); |
| const regenerateAllBtn = document.getElementById('regenerate-all'); |
| const applyBulkEditBtn = document.getElementById('apply-bulk-edit'); |
| const useGeminiApiCheckbox = document.getElementById('use-gemini-api'); |
| const apiCredentialsSection = document.getElementById('api-credentials'); |
| const saveApiBtn = document.getElementById('save-api-btn'); |
| const testApiBtn = document.getElementById('test-api-btn'); |
| const apiKeyInput = document.getElementById('api-key'); |
| const tabs = document.querySelectorAll('.tab'); |
| const tabContents = document.querySelectorAll('.tab-content'); |
| const bulkEditFields = document.getElementById('bulk-edit-fields'); |
| const apiStatusEl = document.getElementById('api-status'); |
| const apiStatusText = document.getElementById('api-status-text'); |
| |
| |
| let uploadedFiles = []; |
| let geminiApiKey = ''; |
| let useGeminiApi = true; |
| let apiConnected = false; |
| |
| |
| function init() { |
| |
| const savedApiKey = localStorage.getItem('geminiApiKey'); |
| const savedApiEnabled = localStorage.getItem('useGeminiApi'); |
| |
| if (savedApiKey) { |
| apiKeyInput.value = savedApiKey; |
| geminiApiKey = savedApiKey; |
| } |
| |
| if (savedApiEnabled !== null) { |
| useGeminiApi = savedApiEnabled === 'true'; |
| useGeminiApiCheckbox.checked = useGeminiApi; |
| apiCredentialsSection.classList.toggle('active', useGeminiApi); |
| } |
| |
| |
| if (geminiApiKey) { |
| showApiStatus('Using saved Gemini API key. Click "Test Connection" to verify.', 'info'); |
| } |
| } |
| |
| |
| uploadArea.addEventListener('click', () => fileInput.click()); |
| uploadArea.addEventListener('dragover', (e) => { |
| e.preventDefault(); |
| uploadArea.style.borderColor = 'var(--primary)'; |
| uploadArea.style.backgroundColor = 'rgba(74, 107, 255, 0.05)'; |
| }); |
| |
| uploadArea.addEventListener('dragleave', () => { |
| uploadArea.style.borderColor = '#d3d3d3'; |
| uploadArea.style.backgroundColor = 'transparent'; |
| }); |
| |
| uploadArea.addEventListener('drop', (e) => { |
| e.preventDefault(); |
| uploadArea.style.borderColor = '#d3d3d3'; |
| uploadArea.style.backgroundColor = 'transparent'; |
| |
| if (e.dataTransfer.files.length) { |
| handleFiles(e.dataTransfer.files); |
| } |
| }); |
| |
| fileInput.addEventListener('change', () => { |
| if (fileInput.files.length) { |
| handleFiles(fileInput.files); |
| } |
| }); |
| |
| generateBtn.addEventListener('click', generateMetadata); |
| downloadAllBtn.addEventListener('click', downloadAllAsCSV); |
| regenerateAllBtn.addEventListener('click', regenerateAllMetadata); |
| applyBulkEditBtn.addEventListener('click', applyBulkEdit); |
| |
| useGeminiApiCheckbox.addEventListener('change', toggleGeminiApi); |
| saveApiBtn.addEventListener('click', saveApiSettings); |
| testApiBtn.addEventListener('click', testApiConnection); |
| |
| |
| tabs.forEach(tab => { |
| tab.addEventListener('click', () => { |
| const tabId = tab.getAttribute('data-tab'); |
| |
| |
| tabs.forEach(t => t.classList.remove('active')); |
| tab.classList.add('active'); |
| |
| |
| tabContents.forEach(content => content.classList.remove('active')); |
| document.getElementById(`${tabId}-tab`).classList.add('active'); |
| |
| |
| if (tabId === 'bulk-edit' && uploadedFiles.length > 0) { |
| bulkEditFields.style.display = 'block'; |
| } |
| }); |
| }); |
| |
| |
| function showApiStatus(message, type = 'info') { |
| apiStatusEl.style.display = 'flex'; |
| apiStatusText.innerHTML = message; |
| |
| |
| apiStatusEl.className = 'alert'; |
| if (type === 'info') apiStatusEl.classList.add('alert-info'); |
| else if (type === 'error') apiStatusEl.classList.add('alert-error'); |
| else if (type === 'success') apiStatusEl.classList.add('alert-success'); |
| } |
| |
| |
| function hideApiStatus() { |
| apiStatusEl.style.display = 'none'; |
| } |
| |
| |
| function handleFiles(files) { |
| |
| if (files.length > 20) { |
| alert('For demo purposes, please upload up to 20 files at a time.'); |
| return; |
| } |
| |
| uploadedFiles = Array.from(files).filter(file => file.type.startsWith('image/')); |
| updateFileList(); |
| |
| if (uploadedFiles.length > 0) { |
| fileListContainer.style.display = 'block'; |
| |
| |
| if (document.querySelector('.tab.active').dataset.tab === 'bulk-edit') { |
| bulkEditFields.style.display = 'block'; |
| } |
| } else { |
| fileListContainer.style.display = 'none'; |
| bulkEditFields.style.display = 'none'; |
| } |
| } |
| |
| |
| function updateFileList() { |
| fileList.innerHTML = ''; |
| fileCount.textContent = uploadedFiles.length; |
| |
| uploadedFiles.forEach((file, index) => { |
| const li = document.createElement('li'); |
| |
| const nameSpan = document.createElement('span'); |
| nameSpan.className = 'file-name'; |
| nameSpan.textContent = file.name; |
| |
| const sizeSpan = document.createElement('span'); |
| sizeSpan.className = 'file-size'; |
| sizeSpan.textContent = formatFileSize(file.size); |
| |
| const removeSpan = document.createElement('span'); |
| removeSpan.className = 'remove-file'; |
| removeSpan.innerHTML = '<i class="fas fa-times"></i>'; |
| removeSpan.addEventListener('click', () => removeFile(index)); |
| |
| li.appendChild(nameSpan); |
| li.appendChild(sizeSpan); |
| li.appendChild(removeSpan); |
| fileList.appendChild(li); |
| }); |
| } |
| |
| |
| function removeFile(index) { |
| uploadedFiles.splice(index, 1); |
| updateFileList(); |
| |
| if (uploadedFiles.length === 0) { |
| fileListContainer.style.display = 'none'; |
| bulkEditFields.style.display = 'none'; |
| resultsContainer.style.display = 'none'; |
| } |
| } |
| |
| |
| function formatFileSize(bytes) { |
| if (bytes === 0) return '0 Bytes'; |
| |
| const k = 1024; |
| const sizes = ['Bytes', 'KB', 'MB', 'GB']; |
| const i = Math.floor(Math.log(bytes) / Math.log(k)); |
| |
| return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; |
| } |
| |
| |
| function toggleGeminiApi() { |
| useGeminiApi = useGeminiApiCheckbox.checked; |
| apiCredentialsSection.classList.toggle('active', useGeminiApi); |
| |
| if (!useGeminiApi) { |
| showApiStatus('Gemini API is disabled. Using basic image analysis.', 'warning'); |
| } else { |
| if (geminiApiKey) { |
| showApiStatus('Gemini API is enabled but not verified. Click "Test Connection" to verify.', 'info'); |
| } else { |
| showApiStatus('Gemini API is not configured. Please enter your API key.', 'error'); |
| } |
| } |
| } |
| |
| |
| function saveApiSettings() { |
| const apiKey = apiKeyInput.value.trim(); |
| |
| if (useGeminiApi && !apiKey) { |
| showApiStatus('Please enter your Gemini API key', 'error'); |
| return; |
| } |
| |
| geminiApiKey = apiKey; |
| localStorage.setItem('geminiApiKey', geminiApiKey); |
| localStorage.setItem('useGeminiApi', useGeminiApi); |
| |
| showApiStatus('API settings saved successfully!', 'success'); |
| setTimeout(hideApiStatus, 3000); |
| |
| |
| if (!apiConnected && useGeminiApi && geminiApiKey) { |
| showApiStatus('Settings saved. Click "Test Connection" to verify the API key.', 'info'); |
| } |
| } |
| |
| |
| async function testApiConnection() { |
| const apiKey = apiKeyInput.value.trim(); |
| |
| if (!apiKey) { |
| showApiStatus('Please enter your Gemini API key first', 'error'); |
| return; |
| } |
| |
| testApiBtn.disabled = true; |
| testApiBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Testing...'; |
| |
| try { |
| |
| await callGeminiAPI( |
| apiKey, |
| "What is 2 + 2?", |
| false |
| ); |
| |
| apiConnected = true; |
| showApiStatus('✅ Gemini API connection successful! You can now analyze images.', 'success'); |
| testApiBtn.innerHTML = '<i class="fas fa-check-circle"></i> Connection Verified'; |
| testApiBtn.className = 'btn btn-success'; |
| |
| |
| setTimeout(() => { |
| testApiBtn.innerHTML = '<i class="fas fa-plug"></i> Test API Connection'; |
| testApiBtn.className = 'btn btn-gemini'; |
| testApiBtn.disabled = false; |
| }, 5000); |
| |
| } catch (error) { |
| apiConnected = false; |
| console.error('API test failed:', error); |
| testApiBtn.innerHTML = '<i class="fas fa-exclamation-circle"></i> Connection Failed'; |
| testApiBtn.className = 'btn btn-secondary'; |
| |
| let errorMessage = 'Failed to connect to Gemini API. '; |
| if (error.message.includes('API_KEY_INVALID')) { |
| errorMessage += 'The API key is invalid. Please check and try again.'; |
| } else if (error.message.includes('quota')) { |
| errorMessage += 'You may have exceeded your quota or the API may not be enabled.'; |
| } else { |
| errorMessage += 'Please check your network connection and try again.'; |
| } |
| |
| showApiStatus(errorMessage, 'error'); |
| |
| |
| setTimeout(() => { |
| testApiBtn.innerHTML = '<i class="fas fa-plug"></i> Test API Connection'; |
| testApiBtn.className = 'btn btn-gemini'; |
| testApiBtn.disabled = false; |
| }, 5000); |
| } |
| } |
| |
| |
| async function callGeminiAPI(apiKey, prompt, includeImage, imageBase64 = null) { |
| const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-pro-vision:generateContent?key=${apiKey}`; |
| |
| |
| let requestBody = { |
| contents: [{ |
| parts: [ |
| { text: prompt } |
| ] |
| }] |
| }; |
| |
| if (includeImage && imageBase64) { |
| requestBody.contents[0].parts.push({ |
| inlineData: { |
| mimeType: "image/jpeg", |
| data: imageBase64.split(',')[1] |
| } |
| }); |
| } |
| |
| const response = await fetch(apiUrl, { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json' |
| }, |
| body: JSON.stringify(requestBody) |
| }); |
| |
| if (!response.ok) { |
| const errorData = await response.json(); |
| throw new Error(errorData.error?.message || 'Unknown API error'); |
| } |
| |
| const data = await response.json(); |
| return data; |
| } |
| |
| |
| async function generateMetadata() { |
| if (uploadedFiles.length === 0) { |
| showApiStatus('Please upload at least one image first.', 'error'); |
| return; |
| } |
| |
| if (useGeminiApi && !geminiApiKey) { |
| showApiStatus('Gemini API is enabled but no API key is provided. Please enter your API key.', 'error'); |
| return; |
| } |
| |
| loadingIndicator.style.display = 'block'; |
| resultsContainer.style.display = 'none'; |
| generateBtn.disabled = true; |
| |
| |
| resultGrid.innerHTML = ''; |
| |
| |
| for (let i = 0; i < uploadedFiles.length; i++) { |
| loadingText.textContent = `Analyzing images with ${useGeminiApi ? 'Gemini AI' : 'basic analysis'} (${i+1}/${uploadedFiles.length})...`; |
| |
| try { |
| await processSingleImage(uploadedFiles[i], i); |
| } catch (error) { |
| console.error(`Error processing image ${i}:`, error); |
| |
| |
| if (useGeminiApi) { |
| loadingText.textContent = `Gemini API failed, falling back to basic analysis (${i+1}/${uploadedFiles.length})...`; |
| uploadedFiles[i].metadata = generateDummyMetadata(uploadedFiles[i]); |
| createImageCard(uploadedFiles[i], i); |
| } |
| } |
| } |
| |
| loadingIndicator.style.display = 'none'; |
| resultsContainer.style.display = 'block'; |
| generateBtn.disabled = false; |
| |
| |
| window.scrollTo({ |
| top: resultsContainer.offsetTop - 20, |
| behavior: 'smooth' |
| }); |
| } |
| |
| |
| async function processSingleImage(file, index) { |
| return new Promise((resolve) => { |
| const reader = new FileReader(); |
| |
| reader.onload = async function(e) { |
| const imageDataUrl = e.target.result; |
| |
| |
| let metadata; |
| let source = ''; |
| |
| if (useGeminiApi && geminiApiKey) { |
| try { |
| metadata = await generateMetadataWithGemini(imageDataUrl, file); |
| source = 'gemini'; |
| } catch (error) { |
| console.error('Gemini API error, falling back to basic analysis:', error); |
| metadata = generateDummyMetadata(file); |
| source = 'basic'; |
| } |
| } else { |
| metadata = generateDummyMetadata(file); |
| source = 'basic'; |
| } |
| |
| |
| uploadedFiles[index].metadata = metadata; |
| uploadedFiles[index].source = source; |
| |
| createImageCard(uploadedFiles[index], index); |
| resolve(); |
| }; |
| |
| reader.readAsDataURL(file); |
| }); |
| } |
| |
| |
| function createImageCard(file, index) { |
| const metadata = file.metadata; |
| const imageDataUrl = URL.createObjectURL(file); |
| |
| const card = document.createElement('div'); |
| card.className = 'image-card'; |
| card.innerHTML = ` |
| <img src="${imageDataUrl}" alt="Preview" class="image-preview"> |
| <div class="card-body"> |
| <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;"> |
| <h4 class="card-title">${metadata.title}</h4> |
| <span class="badge ${file.source === 'gemini' ? 'badge-gemini' : 'badge-basic'}"> |
| ${file.source === 'gemini' ? 'Gemini AI' : 'Basic'} |
| </span> |
| </div> |
| <p class="card-text">${metadata.description}</p> |
| |
| <div class="card-keywords"> |
| ${metadata.keywords.slice(0, 5).map(keyword => |
| `<span class="keyword-chip">${keyword.trim()}</span>` |
| ).join('')} |
| ${metadata.keywords.length > 5 ? '<span class="keyword-chip">+'+ (metadata.keywords.length - 5) +'</span>' : ''} |
| </div> |
| |
| <div class="card-actions"> |
| <button class="copy-btn" onclick="copyMetadata(${index})"> |
| <i class="fas fa-copy"></i> Copy |
| </button> |
| <button class="regenerate-btn" onclick="regenerateMetadata(${index})"> |
| <i class="fas fa-sync-alt"></i> Regenerate |
| </button> |
| </div> |
| </div> |
| `; |
| |
| resultGrid.appendChild(card); |
| } |
| |
| |
| async function generateMetadataWithGemini(imageDataUrl, file) { |
| const category = document.getElementById('image-category').value; |
| const style = document.getElementById('image-style').value; |
| const platform = document.getElementById('target-platform').value; |
| const language = document.getElementById('language').value; |
| |
| const prompt = ` |
| Act as a professional microstock image analyst. Analyze this image and generate comprehensive metadata. |
| |
| Please provide: |
| 1. A title (60-100 characters) that is SEO-friendly, contains relevant keywords, and accurately describes the image |
| 2. A concise description (70-150 characters) that highlights key visual elements and potential use cases |
| 3. A list of 50 relevant keywords (comma-separated) that would help this image be discovered on stock platforms |
| |
| Important considerations: |
| - Image category: ${category} |
| - Visual style: ${style} |
| - Target platform: ${platform === 'all' ? 'general stock platforms' : platform} |
| - Preferred language: ${language} |
| - Keywords should be in English but relevant for the target language |
| |
| Format your response as a valid JSON object with these fields: |
| - "title": string |
| - "description": string |
| - "keywords": array of strings (maximum 50) |
| `; |
| |
| |
| const response = await callGeminiAPI(geminiApiKey, prompt, true, imageDataUrl); |
| |
| |
| if (!response.candidates || !response.candidates[0].content.parts[0].text) { |
| throw new Error('Invalid response from Gemini API'); |
| } |
| |
| |
| const responseText = response.candidates[0].content.parts[0].text; |
| let metadata; |
| |
| try { |
| |
| const jsonMatch = responseText.match(/\{[\s\S]*\}/); |
| if (jsonMatch) { |
| metadata = JSON.parse(jsonMatch[0]); |
| } else { |
| metadata = JSON.parse(responseText); |
| } |
| |
| |
| if (!metadata.title || !metadata.description || !metadata.keywords) { |
| throw new Error('Missing required fields in Gemini response'); |
| } |
| |
| |
| if (Array.isArray(metadata.keywords)) { |
| metadata.keywords = metadata.keywords.map(k => k.trim()); |
| } else if (typeof metadata.keywords === 'string') { |
| metadata.keywords = metadata.keywords.split(',').map(k => k.trim()); |
| } |
| |
| |
| if (metadata.keywords.length > 50) { |
| metadata.keywords = metadata.keywords.slice(0, 50); |
| } |
| |
| return metadata; |
| |
| } catch (error) { |
| console.error('Error parsing Gemini response:', error); |
| console.log('Original response:', responseText); |
| throw new Error('Failed to parse metadata from Gemini response'); |
| } |
| } |
| |
| |
| function generateDummyMetadata(file) { |
| |
| |
| const category = document.getElementById('image-category').value; |
| const style = document.getElementById('image-style').value; |
| const platform = document.getElementById('target-platform').value; |
| |
| const baseName = file.name.replace(/\.[^/.]+$/, "").replace(/[^a-zA-Z0-9]/g, ' ').trim(); |
| |
| |
| const title = `${capitalizeWords(baseName)} ${style} ${category} image for ${platform} stock`.substring(0, 75); |
| |
| |
| const description = `High-quality ${style} ${category} stock image featuring ${baseName}. Perfect for ${platform} and commercial use in ${category} projects.`; |
| |
| |
| let keywords = [ |
| ...baseName.toLowerCase().split(' '), |
| category, style, platform, |
| 'stock photo', 'royalty free', 'commercial use', |
| 'high resolution', 'premium', 'content', 'resource' |
| ]; |
| |
| |
| for (let i = keywords.length; i < 50; i++) { |
| keywords.push(`keyword${i + 1}`); |
| } |
| |
| |
| keywords = [...new Set(keywords)].slice(0, 50); |
| |
| return { |
| title: title, |
| description: description, |
| keywords: keywords |
| }; |
| } |
| |
| |
| function copyMetadata(index) { |
| const file = uploadedFiles[index]; |
| const metadata = file.metadata; |
| |
| const textToCopy = `Title: ${metadata.title}\n\n` + |
| `Description: ${metadata.description}\n\n` + |
| `Keywords: ${metadata.keywords.join(', ')}`; |
| |
| navigator.clipboard.writeText(textToCopy).then(() => { |
| showApiStatus('Metadata copied to clipboard!', 'success'); |
| setTimeout(hideApiStatus, 3000); |
| }).catch(err => { |
| console.error('Failed to copy: ', err); |
| showApiStatus('Failed to copy metadata', 'error'); |
| }); |
| } |
| |
| |
| async function regenerateMetadata(index) { |
| const category = document.getElementById('image-category').value; |
| const style = document.getElementById('image-style').value; |
| const platform = document.getElementById('target-platform').value; |
| const language = document.getElementById('language').value; |
| |
| if (useGeminiApi && !geminiApiKey) { |
| showApiStatus('Gemini API is enabled but no API key is provided.', 'error'); |
| return; |
| } |
| |
| let newMetadata; |
| let source = ''; |
| |
| if (useGeminiApi && geminiApiKey) { |
| try { |
| const reader = new FileReader(); |
| |
| await new Promise((resolve) => { |
| reader.onload = async function(e) { |
| const imageDataUrl = e.target.result; |
| newMetadata = await generateMetadataWithGemini(imageDataUrl, uploadedFiles[index]); |
| source = 'gemini'; |
| resolve(); |
| }; |
| |
| reader.readAsDataURL(uploadedFiles[index]); |
| }); |
| } catch (error) { |
| console.error('Gemini API error, falling back to basic analysis:', error); |
| newMetadata = generateDummyMetadata(uploadedFiles[index]); |
| source = 'basic'; |
| } |
| } else { |
| newMetadata = generateDummyMetadata(uploadedFiles[index]); |
| source = 'basic'; |
| } |
| |
| uploadedFiles[index].metadata = newMetadata; |
| uploadedFiles[index].source = source; |
| |
| |
| const cards = document.querySelectorAll('.image-card'); |
| if (cards[index]) { |
| const cardBody = cards[index].querySelector('.card-body'); |
| if (cardBody) { |
| cardBody.querySelector('.card-title').textContent = newMetadata.title; |
| cardBody.querySelector('.card-text').textContent = newMetadata.description; |
| cardBody.querySelector('.badge').className = `badge ${source === 'gemini' ? 'badge-gemini' : 'badge-basic'}`; |
| cardBody.querySelector('.badge').textContent = source === 'gemini' ? 'Gemini AI' : 'Basic'; |
| |
| const keywordsContainer = cardBody.querySelector('.card-keywords'); |
| keywordsContainer.innerHTML = ` |
| ${newMetadata.keywords.slice(0, 5).map(keyword => |
| `<span class="keyword-chip">${keyword.trim()}</span>` |
| ).join('')} |
| ${newMetadata.keywords.length > 5 ? '<span class="keyword-chip">+'+ (newMetadata.keywords.length - 5) +'</span>' : ''} |
| `; |
| } |
| } |
| |
| showApiStatus(`${source === 'gemini' ? 'Gemini' : 'Basic'} metadata regenerated for this image.`, 'success'); |
| setTimeout(hideApiStatus, 3000); |
| } |
| |
| |
| async function regenerateAllMetadata() { |
| if (uploadedFiles.length === 0) return; |
| |
| if (useGeminiApi && !geminiApiKey) { |
| showApiStatus('Gemini API is enabled but no API key is provided.', 'error'); |
| return; |
| } |
| |
| loadingIndicator.style.display = 'block'; |
| loadingText.textContent = 'Regenerating metadata for all images...'; |
| generateBtn.disabled = true; |
| regenerateAllBtn.disabled = true; |
| |
| |
| for (let i = 0; i < uploadedFiles.length; i++) { |
| await regenerateMetadata(i); |
| } |
| |
| loadingIndicator.style.display = 'none'; |
| generateBtn.disabled = false; |
| regenerateAllBtn.disabled = false; |
| } |
| |
| |
| function applyBulkEdit() { |
| const bulkTitle = document.getElementById('bulk-title').value.trim(); |
| const bulkDescription = document.getElementById('bulk-description').value.trim(); |
| const bulkKeywords = document.getElementById('bulk-keywords').value.trim(); |
| |
| if (!bulkTitle && !bulkDescription && !bulkKeywords) { |
| showApiStatus('Please enter at least one field to apply bulk changes', 'error'); |
| return; |
| } |
| |
| uploadedFiles.forEach((file, index) => { |
| if (!file.metadata) { |
| file.metadata = generateDummyMetadata(file); |
| } |
| |
| if (bulkTitle) { |
| file.metadata.title = bulkTitle; |
| } |
| |
| if (bulkDescription) { |
| file.metadata.description = bulkDescription; |
| } |
| |
| if (bulkKeywords) { |
| file.metadata.keywords = bulkKeywords.split(',').map(k => k.trim()).filter(k => k); |
| } |
| |
| |
| file.source = 'manual'; |
| |
| |
| const cards = document.querySelectorAll('.image-card'); |
| if (cards[index]) { |
| const cardBody = cards[index].querySelector('.card-body'); |
| if (cardBody) { |
| if (bulkTitle) { |
| cardBody.querySelector('.card-title').textContent = file.metadata.title; |
| } |
| if (bulkDescription) { |
| cardBody.querySelector('.card-text').textContent = file.metadata.description; |
| } |
| if (bulkKeywords) { |
| const keywordsContainer = cardBody.querySelector('.card-keywords'); |
| keywordsContainer.innerHTML = ` |
| ${file.metadata.keywords.slice(0, 5).map(keyword => |
| `<span class="keyword-chip">${keyword.trim()}</span>` |
| ).join('')} |
| ${file.metadata.keywords.length > 5 ? '<span class="keyword-chip">+'+ (file.metadata.keywords.length - 5) +'</span>' : ''} |
| `; |
| } |
| cardBody.querySelector('.badge').className = 'badge badge-basic'; |
| cardBody.querySelector('.badge').textContent = 'Manual'; |
| } |
| } |
| }); |
| |
| showApiStatus('Bulk changes applied to all images!', 'success'); |
| setTimeout(hideApiStatus, 3000); |
| } |
| |
| |
| function downloadAllAsCSV() { |
| let csvContent = "data:text/csv;charset=utf-8,"; |
| |
| |
| csvContent += "Filename,Title,Description,Keywords,Source\n"; |
| |
| |
| uploadedFiles.forEach(file => { |
| if (!file.metadata) { |
| file.metadata = generateDummyMetadata(file); |
| } |
| |
| const metadata = file.metadata; |
| const row = [ |
| file.name, |
| `"${metadata.title}"`, |
| `"${metadata.description}"`, |
| `"${metadata.keywords.join(', ')}"`, |
| file.source || 'basic' |
| ].join(','); |
| |
| csvContent += row + "\n"; |
| }); |
| |
| |
| const encodedUri = encodeURI(csvContent); |
| const link = document.createElement("a"); |
| link.setAttribute("href", encodedUri); |
| link.setAttribute("download", "microstock_metadata.csv"); |
| document.body.appendChild(link); |
| |
| |
| link.click(); |
| document.body.removeChild(link); |
| } |
| |
| |
| function capitalizeWords(str) { |
| return str.split(' ').map(word => |
| word.charAt(0).toUpperCase() + word.slice(1) |
| ).join(' '); |
| } |
| |
| |
| init(); |
| </script> |
| </body> |
| </html> |