420 lines
14 KiB
JavaScript
420 lines
14 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 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 (in a real app, this would be loaded from .env)
|
||
|
const AI_CONFIG = {
|
||
|
API_KEY: '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',
|
||
|
MODEL_NAME: 'deepseek/deepseek-chat-v3-0324:free',
|
||
|
API_URL: 'https://openrouter.ai/api/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)
|
||
|
});
|
||
|
|
||
|
processedCount++;
|
||
|
// Show keywords section after all files are processed
|
||
|
if (processedCount === imageFiles.length) {
|
||
|
keywordsSection.style.display = 'block';
|
||
|
imagesPreview.style.display = 'block';
|
||
|
updateImagesPreview();
|
||
|
}
|
||
|
};
|
||
|
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> Enhancing...';
|
||
|
enhanceBtn.disabled = true;
|
||
|
|
||
|
try {
|
||
|
// Call AI API to enhance keywords
|
||
|
const enhancedKeywords = await callAIKeywordEnhancement(keywordText);
|
||
|
|
||
|
// Split keywords by comma or space
|
||
|
const newKeywords = enhancedKeywords.split(/[, ]+/).filter(k => k !== '');
|
||
|
|
||
|
// Add new keywords to the list
|
||
|
newKeywords.forEach(keyword => {
|
||
|
if (!keywords.includes(keyword)) {
|
||
|
keywords.push(keyword);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// Update keywords display
|
||
|
updateKeywordsDisplay();
|
||
|
|
||
|
// Clear input
|
||
|
keywordInput.value = '';
|
||
|
|
||
|
// Generate new filenames for images
|
||
|
await generateNewFileNamesWithAI();
|
||
|
} catch (error) {
|
||
|
console.error('Error enhancing keywords:', error);
|
||
|
alert('An error occurred while enhancing keywords. Please try again.');
|
||
|
} finally {
|
||
|
// Reset button
|
||
|
enhanceBtn.innerHTML = '<i class="fas fa-magic"></i> Enhance with AI';
|
||
|
enhanceBtn.disabled = keywordInput.value.trim() === '';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Call AI API to enhance keywords
|
||
|
async function callAIKeywordEnhancement(keywords) {
|
||
|
const prompt = `Enhance these keywords for SEO image optimization. Provide 10 additional related keywords that would help images rank better in search engines. Return only the keywords separated by commas, nothing else. Keywords: ${keywords}`;
|
||
|
|
||
|
const response = await fetch(AI_CONFIG.API_URL, {
|
||
|
method: 'POST',
|
||
|
headers: {
|
||
|
'Authorization': `Bearer ${AI_CONFIG.API_KEY}`,
|
||
|
'Content-Type': 'application/json'
|
||
|
},
|
||
|
body: JSON.stringify({
|
||
|
model: AI_CONFIG.MODEL_NAME,
|
||
|
messages: [
|
||
|
{
|
||
|
role: "user",
|
||
|
content: prompt
|
||
|
}
|
||
|
]
|
||
|
})
|
||
|
});
|
||
|
|
||
|
if (!response.ok) {
|
||
|
throw new Error(`API request failed with status ${response.status}`);
|
||
|
}
|
||
|
|
||
|
const data = await response.json();
|
||
|
return data.choices[0].message.content.trim();
|
||
|
}
|
||
|
|
||
|
// Generate new filenames for images based on keywords using AI
|
||
|
async function generateNewFileNamesWithAI() {
|
||
|
if (keywords.length === 0) return;
|
||
|
|
||
|
// Show loading state for each image
|
||
|
document.querySelectorAll('.new-name-input').forEach(input => {
|
||
|
input.disabled = true;
|
||
|
input.placeholder = 'Generating AI filename...';
|
||
|
});
|
||
|
|
||
|
try {
|
||
|
// Generate new filename for each image
|
||
|
for (let i = 0; i < uploadedImages.length; i++) {
|
||
|
const image = uploadedImages[i];
|
||
|
const keywordString = keywords.slice(0, 5).join(', ');
|
||
|
|
||
|
// Call AI to generate a descriptive filename
|
||
|
const aiGeneratedName = await callAIFilenameGeneration(image.name, keywordString);
|
||
|
|
||
|
// Update the image with the new name
|
||
|
const nameWithoutExt = image.name.substring(0, image.name.lastIndexOf('.'));
|
||
|
const extension = image.name.substring(image.name.lastIndexOf('.'));
|
||
|
const newName = `${aiGeneratedName.substring(0, 50)}${extension}`;
|
||
|
image.newName = newName;
|
||
|
}
|
||
|
|
||
|
// Update images preview
|
||
|
updateImagesPreview();
|
||
|
|
||
|
// Enable download button
|
||
|
downloadBtn.disabled = false;
|
||
|
} catch (error) {
|
||
|
console.error('Error generating filenames:', error);
|
||
|
alert('An error occurred while generating filenames. Please try again.');
|
||
|
|
||
|
// Revert to simple filename generation
|
||
|
generateNewFileNames();
|
||
|
} finally {
|
||
|
// Re-enable inputs
|
||
|
document.querySelectorAll('.new-name-input').forEach(input => {
|
||
|
input.disabled = false;
|
||
|
input.placeholder = '';
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Call AI API to generate filename
|
||
|
async function callAIFilenameGeneration(originalName, keywords) {
|
||
|
const prompt = `Generate an SEO-optimized filename for an image. The original filename is "${originalName}" and the keywords are: ${keywords}. Create a descriptive, SEO-friendly filename that is 3-6 words long. Use only letters, numbers, and hyphens. Do not include the file extension. Return only the filename, nothing else.`;
|
||
|
|
||
|
const response = await fetch(AI_CONFIG.API_URL, {
|
||
|
method: 'POST',
|
||
|
headers: {
|
||
|
'Authorization': `Bearer ${AI_CONFIG.API_KEY}`,
|
||
|
'Content-Type': 'application/json'
|
||
|
},
|
||
|
body: JSON.stringify({
|
||
|
model: AI_CONFIG.MODEL_NAME,
|
||
|
messages: [
|
||
|
{
|
||
|
role: "user",
|
||
|
content: prompt
|
||
|
}
|
||
|
]
|
||
|
})
|
||
|
});
|
||
|
|
||
|
if (!response.ok) {
|
||
|
throw new Error(`API request failed with status ${response.status}`);
|
||
|
}
|
||
|
|
||
|
const data = await response.json();
|
||
|
return data.choices[0].message.content.trim().replace(/[^a-zA-Z0-9\- ]/g, '').replace(/\s+/g, '-');
|
||
|
}
|
||
|
|
||
|
// Fallback function to generate new filenames without AI
|
||
|
function generateNewFileNames() {
|
||
|
if (keywords.length === 0) return;
|
||
|
|
||
|
uploadedImages.forEach((image, index) => {
|
||
|
// Combine keywords with the original filename
|
||
|
const keywordString = keywords.slice(0, 3).join(' ');
|
||
|
const nameWithoutExt = image.name.substring(0, image.name.lastIndexOf('.'));
|
||
|
const extension = image.name.substring(image.name.lastIndexOf('.'));
|
||
|
|
||
|
// Create new name
|
||
|
const newName = `${keywordString} ${nameWithoutExt}`.substring(0, 50) + extension;
|
||
|
image.newName = newName;
|
||
|
});
|
||
|
|
||
|
// Update images preview
|
||
|
updateImagesPreview();
|
||
|
|
||
|
// Enable download button
|
||
|
downloadBtn.disabled = false;
|
||
|
}
|
||
|
|
||
|
// 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';
|
||
|
imageCard.innerHTML = `
|
||
|
<img src="${image.src}" alt="${image.name}" class="image-thumbnail">
|
||
|
<div class="image-info">
|
||
|
<div class="original-name">Original: ${image.name}</div>
|
||
|
<div class="new-name-container">
|
||
|
<label>New name:</label>
|
||
|
<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() {
|
||
|
// Set up any initial state
|
||
|
downloadBtn.disabled = true;
|
||
|
}
|
||
|
|
||
|
// Call init when page loads
|
||
|
init();
|