329 lines
13 KiB
JavaScript
329 lines
13 KiB
JavaScript
// AI-powered image search functionality
|
|
|
|
// Initialize search form
|
|
function initializeSearchForm() {
|
|
const form = document.getElementById('searchForm');
|
|
const thresholdSlider = document.getElementById('similarityThreshold');
|
|
const thresholdValue = document.getElementById('thresholdValue');
|
|
|
|
// Update threshold display
|
|
thresholdSlider.addEventListener('input', (e) => {
|
|
thresholdValue.textContent = e.target.value;
|
|
});
|
|
|
|
// Handle form submission
|
|
form.addEventListener('submit', (e) => {
|
|
e.preventDefault();
|
|
performSearch();
|
|
});
|
|
}
|
|
|
|
// Perform search
|
|
async function performSearch() {
|
|
if (!config.isConfigured()) {
|
|
showAlert('Please configure your API settings first.', 'warning');
|
|
return;
|
|
}
|
|
|
|
const query = document.getElementById('searchQuery').value.trim();
|
|
const threshold = parseFloat(document.getElementById('similarityThreshold').value);
|
|
const maxResults = parseInt(document.getElementById('maxResults').value);
|
|
|
|
if (!query) {
|
|
showAlert('Please enter a search query', 'danger');
|
|
return;
|
|
}
|
|
|
|
const resultsContainer = document.getElementById('searchResults');
|
|
resultsContainer.innerHTML = `
|
|
<div class="text-center py-4">
|
|
<div class="loading-spinner"></div>
|
|
<p class="mt-2">Searching for "${escapeHtml(query)}"...</p>
|
|
</div>
|
|
`;
|
|
|
|
try {
|
|
const results = await apiClient.searchImages(query, threshold, maxResults);
|
|
await displaySearchResults(results, query);
|
|
} catch (error) {
|
|
handleApiError(error, 'searching images');
|
|
resultsContainer.innerHTML = `
|
|
<div class="alert alert-danger">
|
|
<i class="fas fa-exclamation-triangle me-2"></i>
|
|
Search failed. Please try again.
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
// Display search results
|
|
async function displaySearchResults(results, query) {
|
|
const container = document.getElementById('searchResults');
|
|
|
|
if (!results || results.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="text-center py-5">
|
|
<i class="fas fa-search fa-3x text-muted mb-3"></i>
|
|
<h4>No results found</h4>
|
|
<p class="text-muted">Try adjusting your search query or similarity threshold</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
// Create results HTML with placeholder images first
|
|
const resultsHtml = `
|
|
<div class="mb-4">
|
|
<h4>Search Results</h4>
|
|
<p class="text-muted">Found ${results.length} images matching "${escapeHtml(query)}"</p>
|
|
</div>
|
|
<div class="row">
|
|
${results.map(result => `
|
|
<div class="col-md-6 col-lg-4 mb-4">
|
|
<div class="card search-result h-100">
|
|
<div class="position-relative">
|
|
<div class="image-container" style="position: relative; height: 200px;">
|
|
<img id="search-img-${result.image.id}"
|
|
src="/placeholder-image.png"
|
|
class="card-img-top"
|
|
alt="${escapeHtml(result.image.description || 'Image')}"
|
|
style="height: 200px; object-fit: cover; cursor: pointer; opacity: 0.5;"
|
|
onclick="viewImage('${result.image.id}')">
|
|
<div class="loading-overlay" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);">
|
|
<div class="loading-spinner"></div>
|
|
</div>
|
|
</div>
|
|
<div class="position-absolute top-0 end-0 m-2">
|
|
<span class="badge bg-primary similarity-score">
|
|
${Math.round(result.similarity * 100)}% match
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<h6 class="card-title">${escapeHtml(truncateText(result.image.description || 'Untitled', 60))}</h6>
|
|
<p class="card-text small text-muted">
|
|
<i class="fas fa-calendar me-1"></i>${formatDate(result.image.upload_date)}
|
|
</p>
|
|
${result.image.tags && result.image.tags.length > 0 ? `
|
|
<div class="mb-2">
|
|
${result.image.tags.slice(0, 3).map(tag =>
|
|
`<span class="badge bg-secondary me-1">${escapeHtml(tag)}</span>`
|
|
).join('')}
|
|
${result.image.tags.length > 3 ?
|
|
`<span class="badge bg-light text-dark">+${result.image.tags.length - 3}</span>` : ''
|
|
}
|
|
</div>
|
|
` : ''}
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div class="btn-group" role="group">
|
|
<button class="btn btn-sm btn-outline-primary" onclick="viewImage('${result.image.id}')">
|
|
<i class="fas fa-eye"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-secondary" onclick="editImage('${result.image.id}')">
|
|
<i class="fas fa-edit"></i>
|
|
</button>
|
|
</div>
|
|
<small class="text-muted">
|
|
Similarity: ${(result.similarity * 100).toFixed(1)}%
|
|
</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
`;
|
|
|
|
container.innerHTML = resultsHtml;
|
|
|
|
// Load actual images asynchronously (need to import loadImageBlob from images.js)
|
|
for (const result of results) {
|
|
if (typeof loadImageBlob === 'function') {
|
|
loadImageBlob(result.image.id).then(blobUrl => {
|
|
const imgElement = document.getElementById(`search-img-${result.image.id}`);
|
|
const loadingOverlay = imgElement?.parentElement.querySelector('.loading-overlay');
|
|
|
|
if (imgElement) {
|
|
imgElement.src = blobUrl;
|
|
imgElement.style.opacity = '1';
|
|
if (loadingOverlay) {
|
|
loadingOverlay.style.display = 'none';
|
|
}
|
|
}
|
|
}).catch(error => {
|
|
console.error(`Failed to load search image ${result.image.id}:`, error);
|
|
const imgElement = document.getElementById(`search-img-${result.image.id}`);
|
|
const loadingOverlay = imgElement?.parentElement.querySelector('.loading-overlay');
|
|
|
|
if (imgElement) {
|
|
imgElement.src = '/placeholder-image.png';
|
|
imgElement.style.opacity = '1';
|
|
if (loadingOverlay) {
|
|
loadingOverlay.style.display = 'none';
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Add search refinement options
|
|
addSearchRefinementOptions(query, results);
|
|
}
|
|
|
|
// Add search refinement options
|
|
function addSearchRefinementOptions(query, results) {
|
|
const container = document.getElementById('searchResults');
|
|
|
|
// Extract common tags from results
|
|
const allTags = results.flatMap(result => result.image.tags || []);
|
|
const tagCounts = {};
|
|
allTags.forEach(tag => {
|
|
tagCounts[tag] = (tagCounts[tag] || 0) + 1;
|
|
});
|
|
|
|
const popularTags = Object.entries(tagCounts)
|
|
.sort(([,a], [,b]) => b - a)
|
|
.slice(0, 8)
|
|
.map(([tag]) => tag);
|
|
|
|
if (popularTags.length > 0) {
|
|
const refinementHtml = `
|
|
<div class="mt-4 p-3 bg-light rounded">
|
|
<h6><i class="fas fa-filter me-2"></i>Refine your search</h6>
|
|
<p class="small text-muted mb-2">Popular tags in these results:</p>
|
|
<div class="d-flex flex-wrap gap-2">
|
|
${popularTags.map(tag => `
|
|
<button class="btn btn-sm btn-outline-secondary"
|
|
onclick="refineSearchWithTag('${escapeHtml(tag)}')">
|
|
${escapeHtml(tag)}
|
|
</button>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
container.insertAdjacentHTML('beforeend', refinementHtml);
|
|
}
|
|
|
|
// Add export/share options
|
|
const actionsHtml = `
|
|
<div class="mt-4 text-center">
|
|
<div class="btn-group" role="group">
|
|
<button class="btn btn-outline-primary" onclick="exportSearchResults('${escapeHtml(query)}')">
|
|
<i class="fas fa-download me-1"></i>Export Results
|
|
</button>
|
|
<button class="btn btn-outline-secondary" onclick="shareSearchResults('${escapeHtml(query)}')">
|
|
<i class="fas fa-share me-1"></i>Share Search
|
|
</button>
|
|
<button class="btn btn-outline-info" onclick="saveSearch('${escapeHtml(query)}')">
|
|
<i class="fas fa-bookmark me-1"></i>Save Search
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
container.insertAdjacentHTML('beforeend', actionsHtml);
|
|
}
|
|
|
|
// Refine search with tag
|
|
function refineSearchWithTag(tag) {
|
|
const currentQuery = document.getElementById('searchQuery').value.trim();
|
|
const newQuery = currentQuery ? `${currentQuery} ${tag}` : tag;
|
|
|
|
document.getElementById('searchQuery').value = newQuery;
|
|
performSearch();
|
|
}
|
|
|
|
// Export search results
|
|
function exportSearchResults(query) {
|
|
// This would typically generate a CSV or JSON export
|
|
showAlert('Export functionality would be implemented here', 'info');
|
|
}
|
|
|
|
// Share search results
|
|
async function shareSearchResults(query) {
|
|
const url = `${window.location.origin}${window.location.pathname}#search`;
|
|
const text = `Check out these AI search results for "${query}"`;
|
|
|
|
if (navigator.share) {
|
|
try {
|
|
await navigator.share({
|
|
title: 'SeReact Search Results',
|
|
text: text,
|
|
url: url
|
|
});
|
|
} catch (error) {
|
|
console.log('Error sharing:', error);
|
|
copyToClipboard(url);
|
|
}
|
|
} else {
|
|
copyToClipboard(url);
|
|
}
|
|
}
|
|
|
|
// Save search
|
|
function saveSearch(query) {
|
|
const savedSearches = JSON.parse(localStorage.getItem('savedSearches') || '[]');
|
|
|
|
if (!savedSearches.includes(query)) {
|
|
savedSearches.push(query);
|
|
localStorage.setItem('savedSearches', JSON.stringify(savedSearches));
|
|
showAlert('Search saved successfully!', 'success');
|
|
updateSavedSearches();
|
|
} else {
|
|
showAlert('This search is already saved', 'info');
|
|
}
|
|
}
|
|
|
|
// Update saved searches display
|
|
function updateSavedSearches() {
|
|
const savedSearches = JSON.parse(localStorage.getItem('savedSearches') || '[]');
|
|
|
|
if (savedSearches.length === 0) return;
|
|
|
|
const searchForm = document.getElementById('searchForm');
|
|
const existingSaved = document.getElementById('savedSearches');
|
|
|
|
if (existingSaved) {
|
|
existingSaved.remove();
|
|
}
|
|
|
|
const savedHtml = `
|
|
<div id="savedSearches" class="mt-3">
|
|
<h6><i class="fas fa-bookmark me-2"></i>Saved Searches</h6>
|
|
<div class="d-flex flex-wrap gap-2">
|
|
${savedSearches.map(search => `
|
|
<button class="btn btn-sm btn-outline-primary"
|
|
onclick="loadSavedSearch('${escapeHtml(search)}')">
|
|
${escapeHtml(truncateText(search, 30))}
|
|
<button class="btn-close ms-2" style="font-size: 0.6em;"
|
|
onclick="event.stopPropagation(); removeSavedSearch('${escapeHtml(search)}')"></button>
|
|
</button>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
searchForm.insertAdjacentHTML('afterend', savedHtml);
|
|
}
|
|
|
|
// Load saved search
|
|
function loadSavedSearch(query) {
|
|
document.getElementById('searchQuery').value = query;
|
|
performSearch();
|
|
}
|
|
|
|
// Remove saved search
|
|
function removeSavedSearch(query) {
|
|
const savedSearches = JSON.parse(localStorage.getItem('savedSearches') || '[]');
|
|
const filtered = savedSearches.filter(search => search !== query);
|
|
localStorage.setItem('savedSearches', JSON.stringify(filtered));
|
|
updateSavedSearches();
|
|
showAlert('Saved search removed', 'info');
|
|
}
|
|
|
|
// Initialize saved searches on page load
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
// Add a small delay to ensure the search form is loaded
|
|
setTimeout(updateSavedSearches, 100);
|
|
});
|