736 lines
No EOL
26 KiB
JavaScript
736 lines
No EOL
26 KiB
JavaScript
// 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(); |