2025-08-04 22:20:30 +02:00
// 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' ) ;
2025-08-05 00:15:36 +02:00
const workflowSection = document . getElementById ( 'workflow-section' ) ;
2025-08-04 22:20:30 +02:00
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' ) ;
2025-08-05 21:46:06 +02:00
// 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'
} ;
}
}
2025-08-04 22:20:30 +02:00
// 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 ,
2025-08-05 00:15:36 +02:00
newName : generateFileName ( file . name ) ,
visionKeywords : [ ]
2025-08-04 22:20:30 +02:00
} ) ;
processedCount ++ ;
2025-08-05 00:15:36 +02:00
// Show workflow section after all files are processed
2025-08-04 22:20:30 +02:00
if ( processedCount === imageFiles . length ) {
2025-08-05 00:15:36 +02:00
workflowSection . style . display = 'block' ;
2025-08-04 22:20:30 +02:00
keywordsSection . style . display = 'block' ;
imagesPreview . style . display = 'block' ;
updateImagesPreview ( ) ;
2025-08-05 00:15:36 +02:00
// Smooth scroll to workflow section
workflowSection . scrollIntoView ( {
behavior : 'smooth' ,
block : 'start'
} ) ;
2025-08-04 22:20:30 +02:00
}
} ;
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
2025-08-05 21:46:06 +02:00
enhanceBtn . innerHTML = '<i class="fas fa-spinner fa-spin"></i> Generating SEO Keywords...' ;
2025-08-04 22:20:30 +02:00
enhanceBtn . disabled = true ;
try {
// Call AI API to enhance keywords
const enhancedKeywords = await callAIKeywordEnhancement ( keywordText ) ;
2025-08-05 21:46:06 +02:00
// Better handling for keyphrases - split by comma primarily, preserve phrases
const newKeywords = enhancedKeywords . split ( ',' ) . map ( k => k . trim ( ) ) . filter ( k => k !== '' ) ;
2025-08-04 22:20:30 +02:00
2025-08-05 21:46:06 +02:00
// Add new keywords/keyphrases to the list
2025-08-04 22:20:30 +02:00
newKeywords . forEach ( keyword => {
2025-08-05 21:46:06 +02:00
// 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 ) {
2025-08-04 22:20:30 +02:00
keywords . push ( keyword ) ;
}
} ) ;
// Update keywords display
updateKeywordsDisplay ( ) ;
// Clear input
keywordInput . value = '' ;
2025-08-05 00:15:36 +02:00
// Analyze images with vision AI and generate new filenames
await analyzeImagesAndGenerateNames ( ) ;
2025-08-04 22:20:30 +02:00
} catch ( error ) {
console . error ( 'Error enhancing keywords:' , error ) ;
2025-08-05 00:15:36 +02:00
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 ) ;
2025-08-04 22:20:30 +02:00
} finally {
// Reset button
2025-08-05 21:46:06 +02:00
enhanceBtn . innerHTML = '<i class="fas fa-magic"></i> Generate SEO Keywords' ;
2025-08-04 22:20:30 +02:00
enhanceBtn . disabled = keywordInput . value . trim ( ) === '' ;
}
}
2025-08-05 00:15:36 +02:00
// 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 ( ) ;
}
2025-08-05 21:46:06 +02:00
// 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
}
2025-08-04 22:20:30 +02:00
// Call AI API to enhance keywords
async function callAIKeywordEnhancement ( keywords ) {
2025-08-05 21:46:06 +02:00
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 } ` ;
2025-08-04 22:20:30 +02:00
const response = await fetch ( AI _CONFIG . API _URL , {
method : 'POST' ,
headers : {
'Authorization' : ` Bearer ${ AI _CONFIG . API _KEY } ` ,
2025-08-05 00:15:36 +02:00
'Content-Type' : 'application/json' ,
'HTTP-Referer' : window . location . href ,
'X-Title' : 'SEO Image Renamer'
2025-08-04 22:20:30 +02:00
} ,
body : JSON . stringify ( {
2025-08-05 00:15:36 +02:00
model : AI _CONFIG . DEEPSEEK _MODEL ,
2025-08-04 22:20:30 +02:00
messages : [
{
role : "user" ,
content : prompt
}
]
} )
} ) ;
if ( ! response . ok ) {
2025-08-05 00:15:36 +02:00
const errorText = await response . text ( ) ;
console . error ( 'API Error Response:' , errorText ) ;
throw new Error ( ` API request failed with status ${ response . status } : ${ errorText } ` ) ;
2025-08-04 22:20:30 +02:00
}
const data = await response . json ( ) ;
return data . choices [ 0 ] . message . content . trim ( ) ;
}
2025-08-05 00:15:36 +02:00
// Analyze images with vision AI and generate new filenames
async function analyzeImagesAndGenerateNames ( ) {
2025-08-04 22:20:30 +02:00
if ( keywords . length === 0 ) return ;
// Show loading state for each image
document . querySelectorAll ( '.new-name-input' ) . forEach ( input => {
input . disabled = true ;
2025-08-05 00:15:36 +02:00
input . placeholder = 'Analyzing image and generating filename...' ;
2025-08-04 22:20:30 +02:00
} ) ;
try {
2025-08-05 00:15:36 +02:00
// Analyze each image with vision AI and generate unique filenames
const usedNames = new Set ( ) ;
2025-08-04 22:20:30 +02:00
for ( let i = 0 ; i < uploadedImages . length ; i ++ ) {
const image = uploadedImages [ i ] ;
2025-08-05 00:15:36 +02:00
// 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 ( ) ) ;
2025-08-04 22:20:30 +02:00
// Update the image with the new name
const extension = image . name . substring ( image . name . lastIndexOf ( '.' ) ) ;
2025-08-05 00:15:36 +02:00
image . newName = ` ${ newName . substring ( 0 , 50 ) } ${ extension } ` ;
2025-08-04 22:20:30 +02:00
}
// Update images preview
updateImagesPreview ( ) ;
// Enable download button
downloadBtn . disabled = false ;
} catch ( error ) {
2025-08-05 00:15:36 +02:00
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.' ) ;
2025-08-04 22:20:30 +02:00
// Revert to simple filename generation
2025-08-05 00:15:36 +02:00
fallbackToSimpleNaming ( ) ;
2025-08-04 22:20:30 +02:00
} finally {
// Re-enable inputs
document . querySelectorAll ( '.new-name-input' ) . forEach ( input => {
input . disabled = false ;
input . placeholder = '' ;
} ) ;
}
}
2025-08-05 21:46:06 +02:00
// Analyze image with Grok-2-Vision AI
2025-08-05 00:15:36 +02:00
async function analyzeImageWithVisionAI ( imageSrc ) {
2025-08-05 21:46:06 +02:00
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." ;
2025-08-05 00:15:36 +02:00
2025-08-05 21:46:06 +02:00
const response = await fetch ( AI _CONFIG . GROK _API _URL , {
2025-08-05 00:15:36 +02:00
method : 'POST' ,
headers : {
2025-08-05 21:46:06 +02:00
'Authorization' : ` Bearer ${ AI _CONFIG . GROK _API _KEY } ` ,
'Content-Type' : 'application/json'
2025-08-05 00:15:36 +02:00
} ,
body : JSON . stringify ( {
2025-08-05 21:46:06 +02:00
model : AI _CONFIG . GROK _VISION _MODEL ,
2025-08-05 00:15:36 +02:00
messages : [
{
role : "user" ,
content : [
{
type : "text" ,
text : prompt
} ,
{
type : "image_url" ,
image _url : {
url : imageSrc
}
}
]
}
2025-08-05 21:46:06 +02:00
] ,
max _tokens : 50 ,
temperature : 0.3
2025-08-05 00:15:36 +02:00
} )
} ) ;
if ( ! response . ok ) {
const errorText = await response . text ( ) ;
2025-08-05 21:46:06 +02:00
console . error ( 'Grok Vision API Error Response:' , errorText ) ;
throw new Error ( ` Grok Vision API request failed with status ${ response . status } : ${ errorText } ` ) ;
2025-08-05 00:15:36 +02:00
}
const data = await response . json ( ) ;
const visionResponse = data . choices [ 0 ] . message . content . trim ( ) ;
2025-08-05 21:46:06 +02:00
// 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
2025-08-05 00:15:36 +02:00
}
// Generate unique filename
async function generateUniqueFilename ( image , visionKeywords , usedNames ) {
2025-08-05 21:46:06 +02:00
const detectedLanguage = detectLanguage ( keywords . join ( ' ' ) ) ;
const languageInstruction = detectedLanguage === 'english' ? '' : ` Generate the filename in ${ detectedLanguage } language. ` ;
2025-08-05 00:15:36 +02:00
2025-08-05 21:46:06 +02:00
// Ensure we use ALL keywords, not just first 5
const keywordString = keywords . join ( ', ' ) ;
const visionString = visionKeywords . join ( ' ' ) ;
2025-08-05 00:15:36 +02:00
2025-08-05 21:46:06 +02:00
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 . ` ;
2025-08-04 22:20:30 +02:00
const response = await fetch ( AI _CONFIG . API _URL , {
method : 'POST' ,
headers : {
'Authorization' : ` Bearer ${ AI _CONFIG . API _KEY } ` ,
2025-08-05 00:15:36 +02:00
'Content-Type' : 'application/json' ,
'HTTP-Referer' : window . location . href ,
'X-Title' : 'SEO Image Renamer'
2025-08-04 22:20:30 +02:00
} ,
body : JSON . stringify ( {
2025-08-05 00:15:36 +02:00
model : AI _CONFIG . DEEPSEEK _MODEL ,
2025-08-04 22:20:30 +02:00
messages : [
{
role : "user" ,
content : prompt
}
]
} )
} ) ;
if ( ! response . ok ) {
2025-08-05 00:15:36 +02:00
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 } ` ) ;
2025-08-04 22:20:30 +02:00
}
const data = await response . json ( ) ;
2025-08-05 00:15:36 +02:00
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 ) => {
2025-08-05 21:46:06 +02:00
// 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 ( ' ' ) ;
2025-08-05 00:15:36 +02:00
const nameWithoutExt = image . name . substring ( 0 , image . name . lastIndexOf ( '.' ) ) ;
const extension = image . name . substring ( image . name . lastIndexOf ( '.' ) ) ;
2025-08-05 21:46:06 +02:00
// 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
2025-08-05 00:15:36 +02:00
// 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 ;
2025-08-04 22:20:30 +02:00
}
// Fallback function to generate new filenames without AI
function generateNewFileNames ( ) {
if ( keywords . length === 0 ) return ;
2025-08-05 00:15:36 +02:00
const usedNames = new Set ( ) ;
2025-08-04 22:20:30 +02:00
uploadedImages . forEach ( ( image , index ) => {
2025-08-05 21:46:06 +02:00
// 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 ( ' ' ) ;
2025-08-04 22:20:30 +02:00
const nameWithoutExt = image . name . substring ( 0 , image . name . lastIndexOf ( '.' ) ) ;
const extension = image . name . substring ( image . name . lastIndexOf ( '.' ) ) ;
2025-08-05 21:46:06 +02:00
// 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
2025-08-05 00:15:36 +02:00
// 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 ;
2025-08-04 22:20:30 +02:00
} ) ;
// Update images preview
updateImagesPreview ( ) ;
// Enable download button
downloadBtn . disabled = false ;
}
2025-08-05 00:15:36 +02:00
// 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 ;
}
2025-08-04 22:20:30 +02:00
// Update keywords display
function updateKeywordsDisplay ( ) {
keywordsDisplay . innerHTML = '' ;
keywords . forEach ( ( keyword , index ) => {
const keywordChip = document . createElement ( 'div' ) ;
keywordChip . className = 'keyword-chip' ;
keywordChip . innerHTML = `
< span > $ { keyword } < / s p a n >
< button class = "remove-keyword" data - index = "${index}" > & times ; < / b u t t o n >
` ;
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' ;
2025-08-05 00:15:36 +02:00
// 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 ) ;
2025-08-04 22:20:30 +02:00
imageCard . innerHTML = `
< img src = "${image.src}" alt = "${image.name}" class = "image-thumbnail" >
< div class = "image-info" >
< div class = "original-name" > Original : $ { image . name } < / d i v >
2025-08-05 00:15:36 +02:00
$ { image . visionKeywords && image . visionKeywords . length > 0 ?
` <div class="vision-keywords">Vision AI: <span class="vision-tags"> ${ image . visionKeywords . join ( ', ' ) } </span></div> ` : '' }
2025-08-04 22:20:30 +02:00
< div class = "new-name-container" >
< label > New name : < / l a b e l >
2025-08-05 00:15:36 +02:00
< div class = "filename-display" > $ { highlightedName } $ { extension } < / d i v >
2025-08-04 22:20:30 +02:00
< input type = "text" class = "new-name-input" value = "${image.newName}" data - index = "${index}" >
< / d i v >
< / d i v >
` ;
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 ( ) {
2025-08-05 21:46:06 +02:00
// Initialize configuration first
initializeConfig ( ) ;
2025-08-04 22:20:30 +02:00
// Set up any initial state
downloadBtn . disabled = true ;
}
// Call init when page loads
init ( ) ;