// Amazon Multimodal Assistant - Main JavaScript
// Connected to Python Backend via FastAPI
class MultimodalAssistant {
constructor() {
this.searchHistory = []; // Local session history
this.isSearching = false;
this.currentUploadFile = null;
// Configuration: Point to your local FastAPI server
this.API_ENDPOINT = '/api/search';
this.initializeEventListeners();
this.initializeAnimations();
}
initializeEventListeners() {
// Search functionality
const searchBtn = document.getElementById('search-button');
const searchText = document.getElementById('search-text');
if (searchBtn) searchBtn.addEventListener('click', () => this.handleSearch());
if (searchText) searchText.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) { // Prevent default enter behavior in textarea usually needs shift
e.preventDefault();
this.handleSearch();
}
});
// Image upload functionality
this.initializeImageUpload();
// Clear history
const clearBtn = document.getElementById('clear-history');
if (clearBtn) clearBtn.addEventListener('click', () => this.clearHistory());
// Search mode radio buttons
document.querySelectorAll('input[name="search-mode"]').forEach(radio => {
radio.addEventListener('change', (e) => this.updateSearchMode(e.target.value));
});
}
initializeImageUpload() {
const uploadArea = document.getElementById('upload-area');
const imageInput = document.getElementById('image-input');
const removeButton = document.getElementById('remove-image');
if (!uploadArea || !imageInput) return;
// Click to upload
uploadArea.addEventListener('click', (e) => {
if (e.target !== removeButton && !e.target.closest('#remove-image')) {
imageInput.click();
}
});
// Drag and drop visuals
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('dragover');
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('dragover');
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('dragover');
if (e.dataTransfer.files.length > 0) {
this.handleImageUpload(e.dataTransfer.files[0]);
}
});
// File input change
imageInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
this.handleImageUpload(e.target.files[0]);
}
});
// Remove image
if (removeButton) {
removeButton.addEventListener('click', (e) => {
e.stopPropagation();
this.removeImage();
});
}
}
handleImageUpload(file) {
if (!file.type.startsWith('image/')) {
this.showNotification('Please select a valid image file.', 'error');
return;
}
// Save file object to send to API later
this.currentUploadFile = file;
const reader = new FileReader();
reader.onload = (e) => {
this.displayImagePreview(e.target.result);
// Auto switch to multimodal if image uploaded
const multiRadio = document.querySelector('input[name="search-mode"][value="multimodal"]');
if(multiRadio) multiRadio.checked = true;
this.showNotification('Image uploaded successfully', 'success');
};
reader.readAsDataURL(file);
}
displayImagePreview(src) {
const uploadContent = document.getElementById('upload-content');
const imagePreview = document.getElementById('image-preview');
const previewImg = document.getElementById('preview-img');
if (uploadContent) uploadContent.classList.add('hidden');
if (imagePreview) imagePreview.classList.remove('hidden');
if (previewImg) previewImg.src = src;
}
removeImage() {
this.currentUploadFile = null;
document.getElementById('upload-content').classList.remove('hidden');
document.getElementById('image-preview').classList.add('hidden');
document.getElementById('image-input').value = '';
}
async handleSearch() {
if (this.isSearching) return;
const textInput = document.getElementById('search-text');
const textQuery = textInput ? textInput.value.trim() : "";
// Get Search Mode
const modeEl = document.querySelector('input[name="search-mode"]:checked');
const searchMode = modeEl ? modeEl.value : "multimodal";
const hasImage = !!this.currentUploadFile;
// Validation
if (!textQuery && !hasImage) {
this.showNotification('Please enter text or upload an image.', 'warning');
return;
}
this.isSearching = true;
this.showLoadingState(true);
// 1. Prepare Form Data for Backend
const formData = new FormData();
formData.append('query', textQuery);
formData.append('mode', searchMode);
// Pass history so LLM knows context
const historyPayload = this.searchHistory.map(h => ({
role: h.role,
content: h.content
}));
formData.append('history', JSON.stringify(historyPayload));
if (this.currentUploadFile) {
formData.append('image', this.currentUploadFile);
}
try {
// 2. Call API
const response = await fetch(this.API_ENDPOINT, {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error(`Server error: ${response.statusText}`);
}
const data = await response.json();
// 3. Process Response & Update History
this.addToHistory({
role: 'user',
content: textQuery || '[Image Query]',
timestamp: new Date()
});
this.addToHistory({
role: 'assistant',
content: data.answer,
timestamp: new Date()
});
// 4. Update UI
this.displayQuery({
text: textQuery,
image: hasImage ? document.getElementById('preview-img').src : null,
mode: data.retrieval_method
});
this.displayResults(data.products);
this.displayAnswer(data.answer, data.retrieval_method);
if (data.products && data.products.length > 0) {
this.highlightEvidence(data.products[0]);
} else {
document.getElementById('evidence-card').classList.add('hidden');
}
// Optional: clear text input
// if (textInput) textInput.value = '';
} catch (error) {
console.error('Search failed:', error);
this.showNotification('Search failed: ' + error.message, 'error');
} finally {
this.isSearching = false;
this.showLoadingState(false);
}
}
displayQuery(query) {
const queryCard = document.getElementById('query-card');
const queryContent = document.getElementById('query-content');
const queryImage = document.getElementById('query-image');
const retrievalMethod = document.getElementById('retrieval-method');
const methodLabels = {
'text_only': 'Text Search',
'image_only': 'Image Search',
'multimodal_fusion': 'Multimodal Fusion',
'multimodal': 'Multimodal'
};
if (retrievalMethod) retrievalMethod.textContent = methodLabels[query.mode] || query.mode;
if (queryContent) {
queryContent.innerHTML = query.text ? `Text: "${query.text}"` : 'Image-only query';
}
if (query.image && queryImage) {
const img = queryImage.querySelector('img');
if(img) img.src = query.image;
queryImage.classList.remove('hidden');
} else if (queryImage) {
queryImage.classList.add('hidden');
}
if (queryCard) {
queryCard.classList.remove('hidden');
// Re-trigger animation if possible
queryCard.classList.remove('fade-in');
void queryCard.offsetWidth; // trigger reflow
queryCard.classList.add('fade-in');
}
}
displayAnswer(content, mode) {
const answerCard = document.getElementById('answer-card');
const answerContent = document.getElementById('answer-content');
// Convert Markdown-like formatting to HTML
let formattedContent = content
.replace(/\n/g, '
') // Line breaks
.replace(/\*\*(.*?)\*\*/g, '$1') // Bold text
.replace(/\*(.*?)\*/g, '$1'); // Italic text
// Convert Markdown image syntax to HTML img tags
// Pattern: 
formattedContent = formattedContent.replace(
/!\[(.*?)\]\((.*?)\)/g,
(match, alt, url) => {
// 如果是 images/ 开头的路径,转换为 /product_images/
let correctedUrl = url;
if (url.startsWith('images/')) {
correctedUrl = url.replace('images/', '/product_images/');
}
return `
`;
}
);
if (answerContent) answerContent.innerHTML = formattedContent;
if (answerCard) answerCard.classList.remove('hidden');
}
highlightEvidence(product) {
const evidenceCard = document.getElementById('evidence-card');
const evidenceImage = document.getElementById('evidence-image');
const evidenceName = document.getElementById('evidence-name');
const evidenceCategory = document.getElementById('evidence-category');
const evidenceSimilarity = document.getElementById('evidence-similarity');
if (!evidenceCard) return;
// Use backend URL or fallback
const imgSrc = product.image || 'https://via.placeholder.com/150?text=No+Img';
if (evidenceImage) {
evidenceImage.src = imgSrc;
evidenceImage.onerror = () => { evidenceImage.src = 'https://via.placeholder.com/150?text=Error'; };
}
if (evidenceName) evidenceName.textContent = product.name;
if (evidenceCategory) evidenceCategory.textContent = product.category;
if (evidenceSimilarity) evidenceSimilarity.textContent = `${Math.round(product.similarity * 100)}% match`;
evidenceCard.classList.remove('hidden');
}
displayResults(products) {
const resultsContainer = document.getElementById('results-container');
const resultsCount = document.getElementById('results-count');
if (resultsCount) resultsCount.textContent = `${products.length} items`;
if (!resultsContainer) return;
resultsContainer.innerHTML = '';
if (!products || products.length === 0) {
resultsContainer.innerHTML = '
${product.category}