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