Compare commits
3 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d9226251e9 | ||
![]() |
a829770c7e | ||
![]() |
4b82d495b2 |
6 changed files with 2370 additions and 0 deletions
33
.env
Normal file
33
.env
Normal file
|
@ -0,0 +1,33 @@
|
|||
# === AI CONFIGURATION ===
|
||||
|
||||
# OpenRouter API key for text generation (DeepSeek)
|
||||
AI_API_KEY=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
|
||||
|
||||
# DeepSeek model for text/keyword generation
|
||||
AI_MODEL_NAME=deepseek/deepseek-chat-v3-0324:free
|
||||
|
||||
# OpenRouter API URL
|
||||
AI_API_URL=https://openrouter.ai/api/v1/chat/completions
|
||||
|
||||
# === GROK VISION API CONFIGURATION ===
|
||||
|
||||
# Grok API key for vision analysis (add your Grok API key here)
|
||||
GROK_API_KEY=sk-voidai-vVU2HHiq1txTNmXdZOP98LzARmi4HsptTixMqFSX4yBbw8ogvmKlJEPeKrH1hwEd6j6AnED9LsR6ztPtRMT7UzeLOyxQkasbwKow
|
||||
|
||||
# Grok-2-Vision model
|
||||
GROK_VISION_MODEL=grok-2-vision-1212
|
||||
|
||||
# Grok API URL
|
||||
GROK_API_URL=https://api.voidai.app/v1/
|
||||
|
||||
# Optional: AI task-specific configuration
|
||||
AI_TASK=keyword_generation
|
||||
AI_RENAME_STRATEGY=descriptive # Options: 'timestamped', 'uuid', 'descriptive'
|
||||
|
||||
# === GENERAL SETTINGS ===
|
||||
|
||||
# Environment type
|
||||
NODE_ENV=development
|
||||
|
||||
# Optional: Logging or debugging
|
||||
ENABLE_LOGGING=true
|
75
GROK_SETUP.md
Normal file
75
GROK_SETUP.md
Normal file
|
@ -0,0 +1,75 @@
|
|||
# Grok-2-Vision Integration Setup
|
||||
|
||||
## How to Configure Your Grok API Key
|
||||
|
||||
The SEO Image Renamer now supports Grok-2-Vision for enhanced image analysis. Follow these steps to set up your API key:
|
||||
|
||||
### Step 1: Get Your Grok API Key
|
||||
1. Visit [https://console.x.ai/](https://console.x.ai/)
|
||||
2. Sign up or log in to your account
|
||||
3. Navigate to API Keys section
|
||||
4. Create a new API key for your project
|
||||
5. Copy the generated API key
|
||||
|
||||
### Step 2: Update the .env File
|
||||
1. Open the `.env` file in your project directory
|
||||
2. Find the line that says: `GROK_API_KEY=your_grok_api_key_here`
|
||||
3. Replace `your_grok_api_key_here` with your actual Grok API key
|
||||
4. Save the file
|
||||
|
||||
Example:
|
||||
```env
|
||||
GROK_API_KEY=gsk-proj-abc123def456ghi789jkl012mno345pqr678stu901vwx234yz567
|
||||
```
|
||||
|
||||
### Step 3: Test the Integration
|
||||
1. Open your SEO Image Renamer website
|
||||
2. Upload some images
|
||||
3. Add keywords and click "Generate SEO Keywords"
|
||||
4. Check the browser console for the message: "Grok API Key configured: Yes"
|
||||
|
||||
## What's New with Grok-2-Vision
|
||||
|
||||
### Enhanced Image Analysis
|
||||
- **Better Keyword Extraction**: Grok-2-Vision provides more accurate and contextual keywords from your images
|
||||
- **SEO-Focused Analysis**: The AI is specifically prompted to identify keywords useful for SEO image naming
|
||||
- **Improved Accuracy**: Better object and action recognition compared to previous vision models
|
||||
|
||||
### Technical Improvements
|
||||
- **Dedicated Vision API**: Separate API endpoint optimized for vision tasks
|
||||
- **Enhanced Prompts**: Specialized prompts for extracting SEO-relevant keywords
|
||||
- **Better Error Handling**: Improved fallback mechanisms if vision analysis fails
|
||||
- **Cleaner Responses**: Better parsing and filtering of AI responses
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Issue**: "Grok API Key configured: No" in console
|
||||
**Solution**: Make sure you've replaced `sk-voidai-vVU2HHiq1txTNmXdZOP98LzARmi4HsptTixMqFSX4yBbw8ogvmKlJEPeKrH1hwEd6j6AnED9LsR6ztPtRMT7UzeLOyxQkasbwKow` with your actual API key in the .env file
|
||||
|
||||
**Issue**: Vision analysis fails or returns generic keywords
|
||||
**Solution**:
|
||||
1. Check that your API key is valid and has sufficient credits
|
||||
2. Ensure your API key has access to Grok-2-Vision model
|
||||
3. Check browser console for specific error messages
|
||||
|
||||
**Issue**: .env file not loading
|
||||
**Solution**:
|
||||
1. Make sure the .env file is in the same directory as index.html
|
||||
2. Ensure your web server allows access to .env files (may need to serve from a local server)
|
||||
3. For production, you may need to hardcode the API key in the JavaScript file
|
||||
|
||||
### API Limits
|
||||
- Grok-2-Vision has rate limits and usage quotas
|
||||
- If you hit limits, the system will fallback to generic keywords
|
||||
- Monitor your usage in the X.AI console
|
||||
|
||||
## Security Note
|
||||
Keep your API key secure and never commit it to public repositories. The .env file should be added to your .gitignore file.
|
||||
|
||||
## Support
|
||||
If you encounter issues with the Grok-2-Vision integration, check:
|
||||
1. X.AI API documentation: [https://docs.x.ai/](https://docs.x.ai/)
|
||||
2. Your API key permissions and quotas
|
||||
3. Browser console for detailed error messages
|
20
config.js
Normal file
20
config.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
// Configuration file for SEO Image Renamer
|
||||
// Update your API keys here
|
||||
|
||||
window.APP_CONFIG = {
|
||||
// OpenRouter API for text generation (DeepSeek) - already configured
|
||||
OPENROUTER_API_KEY: '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',
|
||||
DEEPSEEK_MODEL: 'deepseek/deepseek-chat-v3-0324:free',
|
||||
OPENROUTER_API_URL: 'https://openrouter.ai/api/v1/chat/completions',
|
||||
|
||||
// Grok-2-Vision API - ADD YOUR GROK API KEY HERE
|
||||
GROK_API_KEY: 'sk-voidai-vVU2HHiq1txTNmXdZOP98LzARmi4HsptTixMqFSX4yBbw8ogvmKlJEPeKrH1hwEd6j6AnED9LsR6ztPtRMT7UzeLOyxQkasbwKow'
|
||||
GROK_VISION_MODEL: 'grok-2-vision-1212',
|
||||
GROK_API_URL: 'https://api.x.ai/v1/chat/completions'
|
||||
};
|
||||
|
||||
// Instructions:
|
||||
// 1. Get your Grok API key from https://console.x.ai/
|
||||
// 2. Replace 'your_grok_api_key_here' above with your actual API key
|
||||
// 3. Save this file
|
||||
// 4. The website will automatically use your new API key
|
310
index.html
Normal file
310
index.html
Normal file
|
@ -0,0 +1,310 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SEO Image Renamer - AI-Powered Image SEO Tool</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="container">
|
||||
<div class="logo">
|
||||
<h1><i class="fas fa-image"></i> SEO Image Renamer</h1>
|
||||
</div>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="#features">Features</a></li>
|
||||
<li><a href="#how-it-works">How It Works</a></li>
|
||||
<li><a href="#pricing">Pricing</a></li>
|
||||
<li><a href="#" class="btn btn-primary">Sign In</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div class="mobile-menu">
|
||||
<i class="fas fa-bars"></i>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section class="hero">
|
||||
<div class="container">
|
||||
<div class="hero-grid">
|
||||
<div class="hero-content">
|
||||
<div class="hero-badge">
|
||||
<i class="fas fa-bolt"></i>
|
||||
<span>AI-Powered</span>
|
||||
</div>
|
||||
<h1>Save time! Bulk rename your images individually for better SEO performance</h1>
|
||||
<p>Transform your image SEO workflow with AI that analyzes content and generates perfect filenames automatically. No more manual renaming - just upload, enhance, and download.</p>
|
||||
|
||||
<div class="hero-features">
|
||||
<div class="mini-feature">
|
||||
<i class="fas fa-eye"></i>
|
||||
<span>AI Vision Analysis</span>
|
||||
</div>
|
||||
<div class="mini-feature">
|
||||
<i class="fas fa-magic"></i>
|
||||
<span>Smart Keyword Enhancement</span>
|
||||
</div>
|
||||
<div class="mini-feature">
|
||||
<i class="fas fa-download"></i>
|
||||
<span>Instant ZIP Download</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hero-stats">
|
||||
<div class="stat">
|
||||
<span class="stat-number">10k+</span>
|
||||
<span class="stat-label">Images Processed</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-number">95%</span>
|
||||
<span class="stat-label">Time Saved</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hero-upload">
|
||||
<div id="drop-area" class="drop-area">
|
||||
<div class="drop-area-content">
|
||||
<div class="upload-icon">
|
||||
<i class="fas fa-cloud-upload-alt"></i>
|
||||
</div>
|
||||
<h3>Drop your images here</h3>
|
||||
<p>or click to browse files</p>
|
||||
<button id="browse-btn" class="upload-btn">
|
||||
<i class="fas fa-folder-open"></i>
|
||||
<span>Choose Files</span>
|
||||
</button>
|
||||
<input type="file" id="file-input" accept="image/*" multiple style="display: none;">
|
||||
<div class="supported-formats">
|
||||
<span>Supports: JPG, PNG, WEBP, GIF</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="workflow-section" class="workflow-section" style="display: none;">
|
||||
<div class="container">
|
||||
<div id="keywords-section" class="keywords-section">
|
||||
<div class="workflow-step">
|
||||
<div class="step-header">
|
||||
<i class="fas fa-tags"></i>
|
||||
<h3>Step 1: Add Your Keywords</h3>
|
||||
<p>Help our AI understand your content better</p>
|
||||
</div>
|
||||
|
||||
<div class="keywords-input">
|
||||
<input type="text" id="keyword-input" placeholder="Enter keywords or phrases (e.g., african barbershop amsterdam, professional wedding photography)">
|
||||
<button id="enhance-btn" class="btn btn-primary" disabled>
|
||||
<i class="fas fa-magic"></i> Generate SEO Keywords
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="keywords-display" class="keywords-display">
|
||||
<!-- Keywords will be displayed here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="images-preview" class="images-preview">
|
||||
<div class="workflow-step">
|
||||
<div class="step-header">
|
||||
<i class="fas fa-images"></i>
|
||||
<h3>Step 2: Review & Download</h3>
|
||||
<p>Your AI-generated filenames are ready</p>
|
||||
</div>
|
||||
|
||||
<div id="images-container" class="images-container">
|
||||
<!-- Images will be displayed here -->
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button id="download-btn" class="btn btn-success btn-large" disabled>
|
||||
<i class="fas fa-download"></i> Download Renamed Images as ZIP
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="features" class="features">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2>Powerful Features for Better SEO</h2>
|
||||
<p>Everything you need to optimize your images for search engines</p>
|
||||
</div>
|
||||
|
||||
<div class="features-grid">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<i class="fas fa-robot"></i>
|
||||
</div>
|
||||
<h3>AI-Powered Naming</h3>
|
||||
<p>Advanced AI generates SEO-friendly filenames that help your images rank higher in search results.</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<i class="fas fa-eye"></i>
|
||||
</div>
|
||||
<h3>Image Recognition</h3>
|
||||
<p>AI analyzes your images to understand content and context for more accurate naming.</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<i class="fas fa-key"></i>
|
||||
</div>
|
||||
<h3>Keyword Enhancement</h3>
|
||||
<p>Enhance your keywords with AI-suggested synonyms for better SEO performance.</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<i class="fas fa-file-archive"></i>
|
||||
</div>
|
||||
<h3>Easy Download</h3>
|
||||
<p>Download all your renamed images in a single ZIP file for easy implementation.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="how-it-works" class="how-it-works">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2>How It Works</h2>
|
||||
<p>Get better SEO for your images in just three simple steps</p>
|
||||
</div>
|
||||
|
||||
<div class="steps">
|
||||
<div class="step">
|
||||
<div class="step-number">1</div>
|
||||
<h3>Upload Images</h3>
|
||||
<p>Drag and drop your images or browse your files to upload them to our platform.</p>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<div class="step-number">2</div>
|
||||
<h3>Add Keywords</h3>
|
||||
<p>Provide keywords that describe your images, or let our AI enhance them for better SEO.</p>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<div class="step-number">3</div>
|
||||
<h3>Download & Implement</h3>
|
||||
<p>Download your renamed images as a ZIP file and use them on your website.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="pricing" class="pricing">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2>Simple, Transparent Pricing</h2>
|
||||
<p>Choose the plan that works best for you</p>
|
||||
</div>
|
||||
|
||||
<div class="pricing-grid">
|
||||
<div class="pricing-card">
|
||||
<h3>Basic</h3>
|
||||
<div class="price">$0<span>/month</span></div>
|
||||
<ul>
|
||||
<li>50 images per month</li>
|
||||
<li>AI-powered naming</li>
|
||||
<li>Keyword enhancement</li>
|
||||
<li>ZIP download</li>
|
||||
</ul>
|
||||
<button class="btn btn-outline">Get Started</button>
|
||||
</div>
|
||||
|
||||
<div class="pricing-card featured">
|
||||
<div class="featured-badge">Most Popular</div>
|
||||
<h3>Pro</h3>
|
||||
<div class="price">$9<span>/month</span></div>
|
||||
<ul>
|
||||
<li>500 images per month</li>
|
||||
<li>AI-powered naming</li>
|
||||
<li>Keyword enhancement</li>
|
||||
<li>ZIP download</li>
|
||||
<li>Priority support</li>
|
||||
</ul>
|
||||
<button class="btn btn-primary">Get Started</button>
|
||||
</div>
|
||||
|
||||
<div class="pricing-card">
|
||||
<h3>Max</h3>
|
||||
<div class="price">$19<span>/month</span></div>
|
||||
<ul>
|
||||
<li>1000 images per month</li>
|
||||
<li>AI-powered naming</li>
|
||||
<li>Keyword enhancement</li>
|
||||
<li>ZIP download</li>
|
||||
<li>Priority support</li>
|
||||
<li>Advanced analytics</li>
|
||||
</ul>
|
||||
<button class="btn btn-outline">Get Started</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<div class="container">
|
||||
<div class="footer-content">
|
||||
<div class="footer-logo">
|
||||
<h2><i class="fas fa-image"></i> SEO Image Renamer</h2>
|
||||
<p>AI-powered image SEO optimization</p>
|
||||
</div>
|
||||
|
||||
<div class="footer-links">
|
||||
<div class="footer-column">
|
||||
<h4>Product</h4>
|
||||
<ul>
|
||||
<li><a href="#features">Features</a></li>
|
||||
<li><a href="#how-it-works">How It Works</a></li>
|
||||
<li><a href="#pricing">Pricing</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="footer-column">
|
||||
<h4>Company</h4>
|
||||
<ul>
|
||||
<li><a href="#">About Us</a></li>
|
||||
<li><a href="#">Blog</a></li>
|
||||
<li><a href="#">Contact</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="footer-column">
|
||||
<h4>Legal</h4>
|
||||
<ul>
|
||||
<li><a href="#">Privacy Policy</a></li>
|
||||
<li><a href="#">Terms of Service</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer-bottom">
|
||||
<p>© 2025 SEO Image Renamer. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Include JSZip library for ZIP functionality -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
|
||||
<!-- Include configuration file -->
|
||||
<script src="config.js"></script>
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
736
script.js
Normal file
736
script.js
Normal file
|
@ -0,0 +1,736 @@
|
|||
// Global variables
|
||||
let uploadedImages = [];
|
||||
let keywords = [];
|
||||
let generatedNames = [];
|
||||
|
||||
// DOM elements
|
||||
const dropArea = document.getElementById('drop-area');
|
||||
const fileInput = document.getElementById('file-input');
|
||||
const browseBtn = document.getElementById('browse-btn');
|
||||
const workflowSection = document.getElementById('workflow-section');
|
||||
const keywordsSection = document.getElementById('keywords-section');
|
||||
const keywordInput = document.getElementById('keyword-input');
|
||||
const enhanceBtn = document.getElementById('enhance-btn');
|
||||
const keywordsDisplay = document.getElementById('keywords-display');
|
||||
const imagesPreview = document.getElementById('images-preview');
|
||||
const imagesContainer = document.getElementById('images-container');
|
||||
const downloadBtn = document.getElementById('download-btn');
|
||||
|
||||
// AI Configuration - loaded from config.js
|
||||
let AI_CONFIG = {};
|
||||
|
||||
// Initialize configuration on page load
|
||||
function initializeConfig() {
|
||||
// Load configuration from window.APP_CONFIG (set by config.js)
|
||||
if (window.APP_CONFIG) {
|
||||
AI_CONFIG = {
|
||||
// OpenRouter API for text generation (DeepSeek)
|
||||
API_KEY: window.APP_CONFIG.OPENROUTER_API_KEY,
|
||||
DEEPSEEK_MODEL: window.APP_CONFIG.DEEPSEEK_MODEL,
|
||||
API_URL: window.APP_CONFIG.OPENROUTER_API_URL,
|
||||
|
||||
// Grok-2-Vision API for image analysis
|
||||
GROK_API_KEY: window.APP_CONFIG.GROK_API_KEY,
|
||||
GROK_VISION_MODEL: window.APP_CONFIG.GROK_VISION_MODEL,
|
||||
GROK_API_URL: window.APP_CONFIG.GROK_API_URL
|
||||
};
|
||||
|
||||
console.log('✅ Configuration loaded successfully from config.js');
|
||||
console.log('🔑 Grok API Key configured:', AI_CONFIG.GROK_API_KEY !== 'your_grok_api_key_here' ? '✅ Yes' : '❌ No - Please add your API key to config.js');
|
||||
|
||||
if (AI_CONFIG.GROK_API_KEY === 'your_grok_api_key_here') {
|
||||
console.warn('⚠️ SETUP REQUIRED: Please add your Grok API key to config.js file');
|
||||
console.log('📖 Instructions: Open config.js and replace "your_grok_api_key_here" with your actual Grok API key from https://console.x.ai/');
|
||||
}
|
||||
} else {
|
||||
console.error('❌ Configuration file (config.js) not loaded properly');
|
||||
|
||||
// Fallback configuration
|
||||
AI_CONFIG = {
|
||||
API_KEY: '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',
|
||||
DEEPSEEK_MODEL: 'deepseek/deepseek-chat-v3-0324:free',
|
||||
API_URL: 'https://openrouter.ai/api/v1/chat/completions',
|
||||
GROK_API_KEY: 'your_grok_api_key_here',
|
||||
GROK_VISION_MODEL: 'grok-2-vision-1212',
|
||||
GROK_API_URL: 'https://api.x.ai/v1/chat/completions'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Upload area event listeners
|
||||
dropArea.addEventListener('click', () => fileInput.click());
|
||||
browseBtn.addEventListener('click', () => fileInput.click());
|
||||
fileInput.addEventListener('change', handleFileSelect);
|
||||
|
||||
// Drag and drop events
|
||||
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
||||
dropArea.addEventListener(eventName, preventDefaults, false);
|
||||
});
|
||||
|
||||
['dragenter', 'dragover'].forEach(eventName => {
|
||||
dropArea.addEventListener(eventName, highlight, false);
|
||||
});
|
||||
|
||||
['dragleave', 'drop'].forEach(eventName => {
|
||||
dropArea.addEventListener(eventName, unhighlight, false);
|
||||
});
|
||||
|
||||
dropArea.addEventListener('drop', handleDrop, false);
|
||||
|
||||
// Keyword events
|
||||
keywordInput.addEventListener('input', toggleEnhanceButton);
|
||||
enhanceBtn.addEventListener('click', enhanceKeywords);
|
||||
|
||||
// Download button
|
||||
downloadBtn.addEventListener('click', downloadImages);
|
||||
});
|
||||
|
||||
// Prevent default drag behaviors
|
||||
function preventDefaults(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
// Highlight drop area when item is dragged over it
|
||||
function highlight() {
|
||||
dropArea.classList.add('dragover');
|
||||
}
|
||||
|
||||
// Remove highlight when item is dragged out of drop area
|
||||
function unhighlight() {
|
||||
dropArea.classList.remove('dragover');
|
||||
}
|
||||
|
||||
// Handle dropped files
|
||||
function handleDrop(e) {
|
||||
const dt = e.dataTransfer;
|
||||
const files = dt.files;
|
||||
handleFiles(files);
|
||||
}
|
||||
|
||||
// Handle file selection
|
||||
function handleFileSelect(e) {
|
||||
const files = e.target.files;
|
||||
handleFiles(files);
|
||||
}
|
||||
|
||||
// Process uploaded files
|
||||
function handleFiles(files) {
|
||||
if (files.length === 0) return;
|
||||
|
||||
// Convert FileList to Array
|
||||
const filesArray = Array.from(files);
|
||||
|
||||
// Filter only image files
|
||||
const imageFiles = filesArray.filter(file => file.type.startsWith('image/'));
|
||||
|
||||
if (imageFiles.length === 0) {
|
||||
alert('Please select image files only.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear previous images
|
||||
uploadedImages = [];
|
||||
|
||||
// Process each image file
|
||||
let processedCount = 0;
|
||||
imageFiles.forEach(file => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
uploadedImages.push({
|
||||
file: file,
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
src: e.target.result,
|
||||
newName: generateFileName(file.name),
|
||||
visionKeywords: []
|
||||
});
|
||||
|
||||
processedCount++;
|
||||
// Show workflow section after all files are processed
|
||||
if (processedCount === imageFiles.length) {
|
||||
workflowSection.style.display = 'block';
|
||||
keywordsSection.style.display = 'block';
|
||||
imagesPreview.style.display = 'block';
|
||||
updateImagesPreview();
|
||||
|
||||
// Smooth scroll to workflow section
|
||||
workflowSection.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
// Generate a simple filename based on original name
|
||||
function generateFileName(originalName) {
|
||||
// Remove extension
|
||||
const nameWithoutExt = originalName.substring(0, originalName.lastIndexOf('.'));
|
||||
// Replace non-alphanumeric characters with spaces
|
||||
const cleanName = nameWithoutExt.replace(/[^a-zA-Z0-9]/g, ' ');
|
||||
// Capitalize first letter and make it SEO friendly
|
||||
return cleanName.charAt(0).toUpperCase() + cleanName.slice(1);
|
||||
}
|
||||
|
||||
// Toggle enhance button based on keyword input
|
||||
function toggleEnhanceButton() {
|
||||
enhanceBtn.disabled = keywordInput.value.trim() === '';
|
||||
}
|
||||
|
||||
// Enhance keywords with AI
|
||||
async function enhanceKeywords() {
|
||||
const keywordText = keywordInput.value.trim();
|
||||
if (keywordText === '') return;
|
||||
|
||||
// Show loading state
|
||||
enhanceBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Generating SEO Keywords...';
|
||||
enhanceBtn.disabled = true;
|
||||
|
||||
try {
|
||||
// Call AI API to enhance keywords
|
||||
const enhancedKeywords = await callAIKeywordEnhancement(keywordText);
|
||||
|
||||
// Better handling for keyphrases - split by comma primarily, preserve phrases
|
||||
const newKeywords = enhancedKeywords.split(',').map(k => k.trim()).filter(k => k !== '');
|
||||
|
||||
// Add new keywords/keyphrases to the list
|
||||
newKeywords.forEach(keyword => {
|
||||
// Avoid duplicates by checking both exact match and similarity
|
||||
const lowercaseKeyword = keyword.toLowerCase();
|
||||
const isDuplicate = keywords.some(existing =>
|
||||
existing.toLowerCase() === lowercaseKeyword ||
|
||||
existing.toLowerCase().includes(lowercaseKeyword) ||
|
||||
lowercaseKeyword.includes(existing.toLowerCase())
|
||||
);
|
||||
|
||||
if (!isDuplicate) {
|
||||
keywords.push(keyword);
|
||||
}
|
||||
});
|
||||
|
||||
// Update keywords display
|
||||
updateKeywordsDisplay();
|
||||
|
||||
// Clear input
|
||||
keywordInput.value = '';
|
||||
|
||||
// Analyze images with vision AI and generate new filenames
|
||||
await analyzeImagesAndGenerateNames();
|
||||
} catch (error) {
|
||||
console.error('Error enhancing keywords:', error);
|
||||
alert('An error occurred while enhancing keywords. You may have hit rate limits. Please try again in a moment.');
|
||||
|
||||
// Fallback to simple keyword enhancement
|
||||
fallbackToSimpleEnhancement(keywordText);
|
||||
} finally {
|
||||
// Reset button
|
||||
enhanceBtn.innerHTML = '<i class="fas fa-magic"></i> Generate SEO Keywords';
|
||||
enhanceBtn.disabled = keywordInput.value.trim() === '';
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to simple keyword enhancement
|
||||
function fallbackToSimpleEnhancement(keywordText) {
|
||||
const newKeywords = keywordText.split(/[, ]+/).filter(k => k !== '');
|
||||
newKeywords.forEach(keyword => {
|
||||
if (!keywords.includes(keyword)) {
|
||||
keywords.push(keyword);
|
||||
}
|
||||
});
|
||||
|
||||
// Add some simulated AI-enhanced keywords
|
||||
const aiKeywords = [
|
||||
'SEO optimized',
|
||||
'high quality',
|
||||
'professional',
|
||||
'digital',
|
||||
'modern'
|
||||
];
|
||||
|
||||
aiKeywords.forEach(keyword => {
|
||||
if (!keywords.includes(keyword)) {
|
||||
keywords.push(keyword);
|
||||
}
|
||||
});
|
||||
|
||||
// Update keywords display
|
||||
updateKeywordsDisplay();
|
||||
|
||||
// Generate new filenames for images
|
||||
generateNewFileNames();
|
||||
}
|
||||
|
||||
// Detect language from provided keywords
|
||||
function detectLanguage(keywords) {
|
||||
const lowerKeywords = keywords.toLowerCase();
|
||||
|
||||
// Common patterns for different languages
|
||||
const languagePatterns = {
|
||||
dutch: /\b(de|het|en|in|van|met|bij|naar|voor|amsterdam|nederland|barbier|kapper|winkel|kapperszaak|amsterdam|rotterdam|den haag|utrecht)\b/,
|
||||
german: /\b(der|die|das|und|in|von|mit|bei|nach|für|berlin|münchen|deutschland|friseur|laden|geschäft|hamburg|köln)\b/,
|
||||
french: /\b(le|la|les|de|du|des|et|dans|avec|chez|vers|pour|paris|france|coiffeur|magasin|boutique|lyon|marseille)\b/,
|
||||
spanish: /\b(el|la|los|las|de|del|y|en|con|para|hacia|madrid|españa|peluquero|tienda|negocio|barcelona|valencia)\b/,
|
||||
italian: /\b(il|la|lo|gli|le|di|del|e|in|con|per|verso|roma|italia|parrucchiere|negozio|bottega|milano|napoli)\b/,
|
||||
};
|
||||
|
||||
for (const [lang, pattern] of Object.entries(languagePatterns)) {
|
||||
if (pattern.test(lowerKeywords)) {
|
||||
return lang;
|
||||
}
|
||||
}
|
||||
|
||||
return 'english'; // Default to English
|
||||
}
|
||||
|
||||
// Call AI API to enhance keywords
|
||||
async function callAIKeywordEnhancement(keywords) {
|
||||
const detectedLanguage = detectLanguage(keywords);
|
||||
const languageInstruction = detectedLanguage === 'english' ? '' : `Respond in ${detectedLanguage} language. `;
|
||||
|
||||
const prompt = `You are an expert SEO consultant specializing in high-performing search keywords and keyphrases that people actually search for on Google.
|
||||
|
||||
${languageInstruction}Based on these seed keywords: "${keywords}"
|
||||
|
||||
Generate 8-12 SEO-optimized keywords and keyphrases that:
|
||||
1. Are commonly searched terms people actually use on Google
|
||||
2. Include both short keywords (1-2 words) and longer keyphrases (3-5 words)
|
||||
3. Focus on commercial intent and local SEO when relevant
|
||||
4. Include location-specific terms if a location is mentioned
|
||||
5. Consider trending and high-volume search terms
|
||||
6. Think like a customer searching for these services/products
|
||||
|
||||
Examples of good SEO keyphrases:
|
||||
- "african barbershop amsterdam"
|
||||
- "professional hair cutting services"
|
||||
- "luxury wedding photography"
|
||||
- "modern interior design ideas"
|
||||
- "organic food delivery service"
|
||||
|
||||
Return only the keywords and keyphrases separated by commas, no explanations or extra text.
|
||||
|
||||
Seed keywords: ${keywords}`;
|
||||
|
||||
const response = await fetch(AI_CONFIG.API_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${AI_CONFIG.API_KEY}`,
|
||||
'Content-Type': 'application/json',
|
||||
'HTTP-Referer': window.location.href,
|
||||
'X-Title': 'SEO Image Renamer'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: AI_CONFIG.DEEPSEEK_MODEL,
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: prompt
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('API Error Response:', errorText);
|
||||
throw new Error(`API request failed with status ${response.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.choices[0].message.content.trim();
|
||||
}
|
||||
|
||||
// Analyze images with vision AI and generate new filenames
|
||||
async function analyzeImagesAndGenerateNames() {
|
||||
if (keywords.length === 0) return;
|
||||
|
||||
// Show loading state for each image
|
||||
document.querySelectorAll('.new-name-input').forEach(input => {
|
||||
input.disabled = true;
|
||||
input.placeholder = 'Analyzing image and generating filename...';
|
||||
});
|
||||
|
||||
try {
|
||||
// Analyze each image with vision AI and generate unique filenames
|
||||
const usedNames = new Set();
|
||||
|
||||
for (let i = 0; i < uploadedImages.length; i++) {
|
||||
const image = uploadedImages[i];
|
||||
|
||||
// Analyze image with vision AI
|
||||
const visionKeywords = await analyzeImageWithVisionAI(image.src);
|
||||
image.visionKeywords = visionKeywords;
|
||||
|
||||
// Generate unique filename
|
||||
let newName;
|
||||
let attempts = 0;
|
||||
do {
|
||||
newName = await generateUniqueFilename(image, visionKeywords, usedNames);
|
||||
attempts++;
|
||||
} while (usedNames.has(newName.toLowerCase()) && attempts < 10);
|
||||
|
||||
// Add to used names
|
||||
usedNames.add(newName.toLowerCase());
|
||||
|
||||
// Update the image with the new name
|
||||
const extension = image.name.substring(image.name.lastIndexOf('.'));
|
||||
image.newName = `${newName.substring(0, 50)}${extension}`;
|
||||
}
|
||||
|
||||
// Update images preview
|
||||
updateImagesPreview();
|
||||
|
||||
// Enable download button
|
||||
downloadBtn.disabled = false;
|
||||
} catch (error) {
|
||||
console.error('Error analyzing images:', error);
|
||||
alert('An error occurred while analyzing images. You may have hit rate limits or the AI service is temporarily unavailable. Please try again in a moment.');
|
||||
|
||||
// Revert to simple filename generation
|
||||
fallbackToSimpleNaming();
|
||||
} finally {
|
||||
// Re-enable inputs
|
||||
document.querySelectorAll('.new-name-input').forEach(input => {
|
||||
input.disabled = false;
|
||||
input.placeholder = '';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Analyze image with Grok-2-Vision AI
|
||||
async function analyzeImageWithVisionAI(imageSrc) {
|
||||
const prompt = "Analyze this image and provide exactly 2 descriptive keywords that describe the main subject, object, or action in the image. Focus on what would be useful for SEO image naming. Return only the two keywords separated by a space, nothing else.";
|
||||
|
||||
const response = await fetch(AI_CONFIG.GROK_API_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${AI_CONFIG.GROK_API_KEY}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: AI_CONFIG.GROK_VISION_MODEL,
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: prompt
|
||||
},
|
||||
{
|
||||
type: "image_url",
|
||||
image_url: {
|
||||
url: imageSrc
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
max_tokens: 50,
|
||||
temperature: 0.3
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('Grok Vision API Error Response:', errorText);
|
||||
throw new Error(`Grok Vision API request failed with status ${response.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const visionResponse = data.choices[0].message.content.trim();
|
||||
|
||||
// Clean up response and extract keywords
|
||||
const keywords = visionResponse.split(' ').filter(word =>
|
||||
word.length > 2 &&
|
||||
!word.includes('.') &&
|
||||
!word.includes(',') &&
|
||||
/^[a-zA-Z]+$/.test(word)
|
||||
).slice(0, 2);
|
||||
|
||||
return keywords.length >= 2 ? keywords : ['image', 'content']; // Fallback if extraction fails
|
||||
}
|
||||
|
||||
// Generate unique filename
|
||||
async function generateUniqueFilename(image, visionKeywords, usedNames) {
|
||||
const detectedLanguage = detectLanguage(keywords.join(' '));
|
||||
const languageInstruction = detectedLanguage === 'english' ? '' : `Generate the filename in ${detectedLanguage} language. `;
|
||||
|
||||
// Ensure we use ALL keywords, not just first 5
|
||||
const keywordString = keywords.join(', ');
|
||||
const visionString = visionKeywords.join(' ');
|
||||
|
||||
const prompt = `You are an SEO filename expert. Create an SEO-optimized filename for this image that will rank well in search engines.
|
||||
|
||||
${languageInstruction}REQUIREMENTS:
|
||||
1. MUST include at least 2-3 of these user keywords: ${keywordString}
|
||||
2. MUST incorporate image content: ${visionString}
|
||||
3. Create natural, searchable phrases people actually type into Google
|
||||
4. Use 4-8 words maximum
|
||||
5. Focus on commercial intent and local SEO when applicable
|
||||
6. Think like someone searching for this image online
|
||||
|
||||
EXAMPLES of good SEO filenames:
|
||||
- "professional african barbershop amsterdam"
|
||||
- "luxury wedding photography netherlands"
|
||||
- "modern restaurant interior design"
|
||||
- "organic food delivery service menu"
|
||||
- "vintage car classic automobile show"
|
||||
|
||||
Original filename reference: ${image.name}
|
||||
|
||||
Create ONE natural filename that combines the keywords strategically for maximum SEO impact.
|
||||
Return only the filename with spaces between words, no punctuation, no file extension.`;
|
||||
|
||||
const response = await fetch(AI_CONFIG.API_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${AI_CONFIG.API_KEY}`,
|
||||
'Content-Type': 'application/json',
|
||||
'HTTP-Referer': window.location.href,
|
||||
'X-Title': 'SEO Image Renamer'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: AI_CONFIG.DEEPSEEK_MODEL,
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: prompt
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('Filename generation API Error Response:', errorText);
|
||||
throw new Error(`Filename generation API request failed with status ${response.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.choices[0].message.content.trim();
|
||||
}
|
||||
|
||||
// Fallback to simple naming
|
||||
function fallbackToSimpleNaming() {
|
||||
if (keywords.length === 0) return;
|
||||
|
||||
const usedNames = new Set();
|
||||
|
||||
uploadedImages.forEach((image, index) => {
|
||||
// Use more keywords for better SEO - cycle through all keywords
|
||||
const keywordCount = Math.min(keywords.length, 4); // Use up to 4 keywords
|
||||
const startIndex = index % Math.max(1, keywords.length - 2); // Rotate starting point
|
||||
const selectedKeywords = keywords.slice(startIndex, startIndex + keywordCount);
|
||||
const keywordString = selectedKeywords.join(' ');
|
||||
|
||||
const nameWithoutExt = image.name.substring(0, image.name.lastIndexOf('.'));
|
||||
const extension = image.name.substring(image.name.lastIndexOf('.'));
|
||||
|
||||
// Create SEO-focused name - prioritize keywords over original filename
|
||||
let newName = keywordString.length > 0 ? keywordString : nameWithoutExt;
|
||||
newName = newName.substring(0, 45); // Leave room for counter
|
||||
|
||||
// Ensure uniqueness
|
||||
let counter = 1;
|
||||
let uniqueName = newName;
|
||||
while (usedNames.has(uniqueName.toLowerCase())) {
|
||||
uniqueName = `${newName} ${counter}`;
|
||||
counter++;
|
||||
}
|
||||
|
||||
usedNames.add(uniqueName.toLowerCase());
|
||||
image.newName = uniqueName + extension;
|
||||
});
|
||||
|
||||
// Update images preview
|
||||
updateImagesPreview();
|
||||
|
||||
// Enable download button
|
||||
downloadBtn.disabled = false;
|
||||
}
|
||||
|
||||
// Fallback function to generate new filenames without AI
|
||||
function generateNewFileNames() {
|
||||
if (keywords.length === 0) return;
|
||||
|
||||
const usedNames = new Set();
|
||||
|
||||
uploadedImages.forEach((image, index) => {
|
||||
// Use more keywords for better SEO - cycle through all keywords
|
||||
const keywordCount = Math.min(keywords.length, 4); // Use up to 4 keywords
|
||||
const startIndex = index % Math.max(1, keywords.length - 2); // Rotate starting point
|
||||
const selectedKeywords = keywords.slice(startIndex, startIndex + keywordCount);
|
||||
const keywordString = selectedKeywords.join(' ');
|
||||
|
||||
const nameWithoutExt = image.name.substring(0, image.name.lastIndexOf('.'));
|
||||
const extension = image.name.substring(image.name.lastIndexOf('.'));
|
||||
|
||||
// Create SEO-focused name - prioritize keywords over original filename
|
||||
let newName = keywordString.length > 0 ? keywordString : nameWithoutExt;
|
||||
newName = newName.substring(0, 45); // Leave room for counter
|
||||
|
||||
// Ensure uniqueness
|
||||
let counter = 1;
|
||||
let uniqueName = newName;
|
||||
while (usedNames.has(uniqueName.toLowerCase())) {
|
||||
uniqueName = `${newName} ${counter}`;
|
||||
counter++;
|
||||
}
|
||||
|
||||
usedNames.add(uniqueName.toLowerCase());
|
||||
image.newName = uniqueName + extension;
|
||||
});
|
||||
|
||||
// Update images preview
|
||||
updateImagesPreview();
|
||||
|
||||
// Enable download button
|
||||
downloadBtn.disabled = false;
|
||||
}
|
||||
|
||||
// Highlight vision keywords in filename
|
||||
function highlightVisionKeywords(filename, visionKeywords) {
|
||||
if (!visionKeywords || visionKeywords.length === 0) {
|
||||
return filename;
|
||||
}
|
||||
|
||||
let highlightedName = filename;
|
||||
visionKeywords.forEach(keyword => {
|
||||
if (keyword && keyword.trim()) {
|
||||
const regex = new RegExp(`\\b${keyword.trim()}\\b`, 'gi');
|
||||
highlightedName = highlightedName.replace(regex, `<span class="vision-highlight">${keyword}</span>`);
|
||||
}
|
||||
});
|
||||
|
||||
return highlightedName;
|
||||
}
|
||||
|
||||
// Update keywords display
|
||||
function updateKeywordsDisplay() {
|
||||
keywordsDisplay.innerHTML = '';
|
||||
|
||||
keywords.forEach((keyword, index) => {
|
||||
const keywordChip = document.createElement('div');
|
||||
keywordChip.className = 'keyword-chip';
|
||||
keywordChip.innerHTML = `
|
||||
<span>${keyword}</span>
|
||||
<button class="remove-keyword" data-index="${index}">×</button>
|
||||
`;
|
||||
keywordsDisplay.appendChild(keywordChip);
|
||||
});
|
||||
|
||||
// Add event listeners to remove buttons
|
||||
document.querySelectorAll('.remove-keyword').forEach(button => {
|
||||
button.addEventListener('click', (e) => {
|
||||
const index = parseInt(e.target.getAttribute('data-index'));
|
||||
keywords.splice(index, 1);
|
||||
updateKeywordsDisplay();
|
||||
generateNewFileNames();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Update images preview
|
||||
function updateImagesPreview() {
|
||||
imagesContainer.innerHTML = '';
|
||||
|
||||
uploadedImages.forEach((image, index) => {
|
||||
const imageCard = document.createElement('div');
|
||||
imageCard.className = 'image-card';
|
||||
|
||||
// Get filename without extension for highlighting
|
||||
const nameWithoutExt = image.newName.substring(0, image.newName.lastIndexOf('.'));
|
||||
const extension = image.newName.substring(image.newName.lastIndexOf('.'));
|
||||
const highlightedName = highlightVisionKeywords(nameWithoutExt, image.visionKeywords);
|
||||
|
||||
imageCard.innerHTML = `
|
||||
<img src="${image.src}" alt="${image.name}" class="image-thumbnail">
|
||||
<div class="image-info">
|
||||
<div class="original-name">Original: ${image.name}</div>
|
||||
${image.visionKeywords && image.visionKeywords.length > 0 ?
|
||||
`<div class="vision-keywords">Vision AI: <span class="vision-tags">${image.visionKeywords.join(', ')}</span></div>` : ''}
|
||||
<div class="new-name-container">
|
||||
<label>New name:</label>
|
||||
<div class="filename-display">${highlightedName}${extension}</div>
|
||||
<input type="text" class="new-name-input" value="${image.newName}" data-index="${index}">
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
imagesContainer.appendChild(imageCard);
|
||||
});
|
||||
|
||||
// Add event listeners to name inputs
|
||||
document.querySelectorAll('.new-name-input').forEach(input => {
|
||||
input.addEventListener('input', (e) => {
|
||||
const index = parseInt(e.target.getAttribute('data-index'));
|
||||
uploadedImages[index].newName = e.target.value;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Download images as ZIP
|
||||
async function downloadImages() {
|
||||
if (uploadedImages.length === 0) return;
|
||||
|
||||
// Show loading state
|
||||
downloadBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Preparing Download...';
|
||||
downloadBtn.disabled = true;
|
||||
|
||||
try {
|
||||
// Create a new ZIP file
|
||||
const zip = new JSZip();
|
||||
|
||||
// Add each image to the ZIP with its new name
|
||||
for (const image of uploadedImages) {
|
||||
// Convert data URL to blob
|
||||
const blob = await fetch(image.src).then(res => res.blob());
|
||||
zip.file(image.newName, blob);
|
||||
}
|
||||
|
||||
// Generate the ZIP file
|
||||
const content = await zip.generateAsync({type: "blob"});
|
||||
|
||||
// Create download link
|
||||
const url = URL.createObjectURL(content);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'renamed-images.zip';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
|
||||
// Clean up
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}, 100);
|
||||
|
||||
// Reset button
|
||||
downloadBtn.innerHTML = '<i class="fas fa-download"></i> Download Renamed Images as ZIP';
|
||||
downloadBtn.disabled = false;
|
||||
} catch (error) {
|
||||
console.error('Error creating ZIP file:', error);
|
||||
alert('An error occurred while creating the ZIP file. Please try again.');
|
||||
|
||||
// Reset button
|
||||
downloadBtn.innerHTML = '<i class="fas fa-download"></i> Download Renamed Images as ZIP';
|
||||
downloadBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the page
|
||||
function init() {
|
||||
// Initialize configuration first
|
||||
initializeConfig();
|
||||
|
||||
// Set up any initial state
|
||||
downloadBtn.disabled = true;
|
||||
}
|
||||
|
||||
// Call init when page loads
|
||||
init();
|
1196
styles.css
Normal file
1196
styles.css
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue