Final front-end version for day 1 by Jeen.

This commit is contained in:
Jeen Koster 2025-08-05 00:15:36 +02:00
parent 4b82d495b2
commit a829770c7e
3 changed files with 1173 additions and 412 deletions

259
script.js
View file

@ -7,6 +7,7 @@ let generatedNames = [];
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');
@ -15,10 +16,11 @@ 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)
// AI Configuration
const AI_CONFIG = {
API_KEY: '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',
MODEL_NAME: 'deepseek/deepseek-chat-v3-0324:free',
DEEPSEEK_MODEL: 'deepseek/deepseek-chat-v3-0324:free',
VISION_MODEL: 'meta-llama/llama-3.2-11b-vision-instruct:free',
API_URL: 'https://openrouter.ai/api/v1/chat/completions'
};
@ -110,15 +112,23 @@ function handleFiles(files) {
size: file.size,
type: file.type,
src: e.target.result,
newName: generateFileName(file.name)
newName: generateFileName(file.name),
visionKeywords: []
});
processedCount++;
// Show keywords section after all files are processed
// 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);
@ -169,11 +179,14 @@ async function enhanceKeywords() {
// Clear input
keywordInput.value = '';
// Generate new filenames for images
await generateNewFileNamesWithAI();
// 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. Please try again.');
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> Enhance with AI';
@ -181,6 +194,37 @@ async function enhanceKeywords() {
}
}
// 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();
}
// 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}`;
@ -189,10 +233,12 @@ async function callAIKeywordEnhancement(keywords) {
method: 'POST',
headers: {
'Authorization': `Bearer ${AI_CONFIG.API_KEY}`,
'Content-Type': 'application/json'
'Content-Type': 'application/json',
'HTTP-Referer': window.location.href,
'X-Title': 'SEO Image Renamer'
},
body: JSON.stringify({
model: AI_CONFIG.MODEL_NAME,
model: AI_CONFIG.DEEPSEEK_MODEL,
messages: [
{
role: "user",
@ -203,37 +249,50 @@ async function callAIKeywordEnhancement(keywords) {
});
if (!response.ok) {
throw new Error(`API request failed with status ${response.status}`);
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();
}
// Generate new filenames for images based on keywords using AI
async function generateNewFileNamesWithAI() {
// 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 = 'Generating AI filename...';
input.placeholder = 'Analyzing image and generating filename...';
});
try {
// Generate new filename for each image
// 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];
const keywordString = keywords.slice(0, 5).join(', ');
// Call AI to generate a descriptive filename
const aiGeneratedName = await callAIFilenameGeneration(image.name, keywordString);
// 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 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;
image.newName = `${newName.substring(0, 50)}${extension}`;
}
// Update images preview
@ -242,11 +301,11 @@ async function generateNewFileNamesWithAI() {
// Enable download button
downloadBtn.disabled = false;
} catch (error) {
console.error('Error generating filenames:', error);
alert('An error occurred while generating filenames. Please try again.');
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
generateNewFileNames();
fallbackToSimpleNaming();
} finally {
// Re-enable inputs
document.querySelectorAll('.new-name-input').forEach(input => {
@ -256,18 +315,76 @@ async function generateNewFileNamesWithAI() {
}
}
// 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.`;
// Analyze image with vision AI
async function analyzeImageWithVisionAI(imageSrc) {
const prompt = "Analyze this image and provide exactly 2 keywords that describe the main subject or action in the image. Return only the two keywords separated by a space, nothing else.";
const response = await fetch(AI_CONFIG.API_URL, {
method: 'POST',
headers: {
'Authorization': `Bearer ${AI_CONFIG.API_KEY}`,
'Content-Type': 'application/json'
'Content-Type': 'application/json',
'HTTP-Referer': window.location.href,
'X-Title': 'SEO Image Renamer'
},
body: JSON.stringify({
model: AI_CONFIG.MODEL_NAME,
model: AI_CONFIG.VISION_MODEL,
messages: [
{
role: "user",
content: [
{
type: "text",
text: prompt
},
{
type: "image_url",
image_url: {
url: imageSrc
}
}
]
}
]
})
});
if (!response.ok) {
const errorText = await response.text();
console.error('Vision API Error Response:', errorText);
throw new Error(`Vision API request failed with status ${response.status}: ${errorText}`);
}
const data = await response.json();
const visionResponse = data.choices[0].message.content.trim();
return visionResponse.split(' ').slice(0, 2); // Get up to 2 keywords
}
// Generate unique filename
async function generateUniqueFilename(image, visionKeywords, usedNames) {
const keywordString = keywords.slice(0, 5).join(', ');
const visionString = visionKeywords.join(' ');
const prompt = `Create a natural, descriptive filename for an image.
User keywords: ${keywordString}
Image content keywords: ${visionString}
Original filename: ${image.name}
Create a human-readable phrase that combines these elements naturally, like "burning car at a carshow in france".
Use 3-7 words, start with a capital letter, and use spaces between words.
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',
'HTTP-Referer': window.location.href,
'X-Title': 'SEO Image Renamer'
},
body: JSON.stringify({
model: AI_CONFIG.DEEPSEEK_MODEL,
messages: [
{
role: "user",
@ -278,17 +395,21 @@ async function callAIFilenameGeneration(originalName, keywords) {
});
if (!response.ok) {
throw new Error(`API request failed with status ${response.status}`);
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().replace(/[^a-zA-Z0-9\- ]/g, '').replace(/\s+/g, '-');
return data.choices[0].message.content.trim();
}
// Fallback function to generate new filenames without AI
function generateNewFileNames() {
// Fallback to simple naming
function fallbackToSimpleNaming() {
if (keywords.length === 0) return;
const usedNames = new Set();
uploadedImages.forEach((image, index) => {
// Combine keywords with the original filename
const keywordString = keywords.slice(0, 3).join(' ');
@ -296,8 +417,18 @@ function generateNewFileNames() {
const extension = image.name.substring(image.name.lastIndexOf('.'));
// Create new name
const newName = `${keywordString} ${nameWithoutExt}`.substring(0, 50) + extension;
image.newName = newName;
let newName = `${keywordString} ${nameWithoutExt}`.substring(0, 50);
// 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
@ -307,6 +438,57 @@ function generateNewFileNames() {
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) => {
// 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
let newName = `${keywordString} ${nameWithoutExt}`.substring(0, 50);
// 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 = '';
@ -339,12 +521,21 @@ function updateImagesPreview() {
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>