|
|
<!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> |