Spaces:
Running
Running
| // OneSearch Main Application Script | |
| // Application State Management | |
| class OneSearchApp { | |
| constructor() { | |
| this.user = null; | |
| this.currentSearch = null; | |
| this.searchResults = []; | |
| this.trustScores = new Map(); | |
| this.walletBalance = 47.23; | |
| this.isAuthenticated = false; | |
| this.init(); | |
| } | |
| init() { | |
| this.setupEventListeners(); | |
| this.initializeComponents(); | |
| this.loadInitialData(); | |
| this.setupWebSocketConnection(); | |
| console.log('OneSearch app initialized'); | |
| } | |
| setupEventListeners() { | |
| // Search functionality | |
| const searchBtn = document.getElementById('search-btn'); | |
| const problemInput = document.getElementById('problem-input'); | |
| if (searchBtn) { | |
| searchBtn.addEventListener('click', () => this.performSearch()); | |
| } | |
| if (problemInput) { | |
| problemInput.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') { | |
| this.performSearch(); | |
| } | |
| }); | |
| } | |
| // QR Generation | |
| const generateQrBtn = document.getElementById('generate-qr'); | |
| if (generateQrBtn) { | |
| generateQrBtn.addEventListener('click', () => this.showQRModal()); | |
| } | |
| const createQrBtn = document.getElementById('create-qr-btn'); | |
| if (createQrBtn) { | |
| createQrBtn.addEventListener('click', () => this.showQRModal()); | |
| } | |
| const closeQrModal = document.getElementById('close-qr-modal'); | |
| if (closeQrModal) { | |
| closeQrModal.addEventListener('click', () => this.hideQRModal()); | |
| } | |
| // Authentication | |
| const authButton = document.getElementById('auth-button'); | |
| if (authButton) { | |
| authButton.addEventListener('click', () => this.handleAuth()); | |
| } | |
| // Navigation | |
| this.setupNavigation(); | |
| } | |
| setupNavigation() { | |
| // Smooth scrolling for navigation links | |
| document.querySelectorAll('a[href^="#"]').forEach(link => { | |
| link.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| const target = document.querySelector(link.getAttribute('href')); | |
| if (target) { | |
| target.scrollIntoView({ | |
| behavior: 'smooth', | |
| block: 'start' | |
| }); | |
| } | |
| }); | |
| }); | |
| } | |
| async performSearch() { | |
| const problemInput = document.getElementById('problem-input'); | |
| const query = problemInput?.value.trim(); | |
| if (!query) { | |
| this.showNotification('Please enter a problem description', 'warning'); | |
| return; | |
| } | |
| try { | |
| this.setLoading(true); | |
| // Simulate AI-powered problem interpretation | |
| const searchResults = await this.interpretProblem(query); | |
| this.searchResults = searchResults; | |
| this.currentSearch = query; | |
| this.updateMindMap(searchResults); | |
| this.displaySearchResults(searchResults); | |
| this.showNotification('Search completed successfully', 'success'); | |
| } catch (error) { | |
| console.error('Search error:', error); | |
| this.showNotification('Search failed. Please try again.', 'error'); | |
| } finally { | |
| this.setLoading(false); | |
| } | |
| } | |
| async interpretProblem(query) { | |
| // Simulate AI/ML interpretation with mock data | |
| await new Promise(resolve => setTimeout(resolve, 1500)); // Simulate API call | |
| const mockProducts = [ | |
| { | |
| id: '1', | |
| title: 'Stackable Storage Containers Set', | |
| description: 'Apex Kitchen Solutions - Set of 6 BPA-free containers with airtight seals', | |
| merchant: 'Apex Kitchen Solutions', | |
| category: 'Storage Solutions', | |
| price: 32.99, | |
| currency: 'USD', | |
| score: 82.6, | |
| scoreBreakdown: { | |
| recommenderReputation: 0.68, | |
| provenance: 0.95, | |
| usageDuration: 0.6, | |
| conversionRate: 0.3, | |
| afterSales: 0.7, | |
| locality: 0.9, | |
| costSatisfaction: 0.64, | |
| qualityReviews: 0.92 | |
| }, | |
| reviewCount: 124, | |
| averageRating: 4.2, | |
| qrConversions: 23, | |
| wageTransparency: true, | |
| livingWageCompliant: true, | |
| tags: ['kitchen', 'storage', 'organization', 'space-saving'] | |
| }, | |
| { | |
| id: '2', | |
| title: 'Modular Cabinet System', | |
| description: 'FlexStorage Pro - Adjustable cabinet system for small spaces', | |
| merchant: 'FlexStorage Pro', | |
| category: 'Storage Solutions', | |
| price: 89.99, | |
| currency: 'USD', | |
| score: 78.3, | |
| scoreBreakdown: { | |
| recommenderReputation: 0.72, | |
| provenance: 0.88, | |
| usageDuration: 0.75, | |
| conversionRate: 0.25, | |
| afterSales: 0.65, | |
| locality: 0.85, | |
| costSatisfaction: 0.58, | |
| qualityReviews: 0.85 | |
| }, | |
| reviewCount: 89, | |
| averageRating: 4.0, | |
| qrConversions: 18, | |
| wageTransparency: false, | |
| livingWageCompliant: false, | |
| tags: ['kitchen', 'storage', 'cabinet', 'modular'] | |
| }, | |
| { | |
| id: '3', | |
| title: 'Magnetic Organization System', | |
| description: 'MagOrganize - Magnetic strips and containers for vertical storage', | |
| merchant: 'MagOrganize Co', | |
| category: 'Storage Solutions', | |
| price: 24.99, | |
| currency: 'USD', | |
| score: 75.8, | |
| scoreBreakdown: { | |
| recommenderReputation: 0.65, | |
| provenance: 0.82, | |
| usageDuration: 0.55, | |
| conversionRate: 0.4, | |
| afterSales: 0.72, | |
| locality: 0.9, | |
| costSatisfaction: 0.71, | |
| qualityReviews: 0.88 | |
| }, | |
| reviewCount: 156, | |
| averageRating: 4.1, | |
| qrConversions: 31, | |
| wageTransparency: true, | |
| livingWageCompliant: true, | |
| tags: ['kitchen', 'storage', 'magnetic', 'vertical'] | |
| } | |
| ]; | |
| return mockProducts; | |
| } | |
| updateMindMap(results) { | |
| const mindMapContainer = document.getElementById('mindmap-container'); | |
| if (!mindMapContainer || !window.MindMap) return; | |
| // Prepare mind map data | |
| const mindMapData = { | |
| nodes: [ | |
| { id: 'root', label: this.currentSearch, level: 0 }, | |
| { id: 'category', label: 'Storage Solutions', level: 1 }, | |
| { id: 'budget', label: '$25-$90', level: 1 }, | |
| { id: 'size', label: 'Small Space', level: 1 } | |
| ], | |
| edges: [ | |
| { from: 'root', to: 'category' }, | |
| { from: 'root', to: 'budget' }, | |
| { from: 'root', to: 'size' } | |
| ] | |
| }; | |
| // Add product nodes | |
| results.forEach((product, index) => { | |
| const nodeId = `product_${product.id}`; | |
| mindMapData.nodes.push({ | |
| id: nodeId, | |
| label: product.title, | |
| level: 2, | |
| score: product.score | |
| }); | |
| mindMapData.edges.push({ from: 'category', to: nodeId }); | |
| }); | |
| window.MindMap.render(mindMapContainer, mindMapData); | |
| } | |
| displaySearchResults(results) { | |
| const resultsContainer = document.getElementById('search-results'); | |
| if (!resultsContainer) return; | |
| resultsContainer.innerHTML = ''; | |
| results.forEach(product => { | |
| const resultElement = this.createResultElement(product); | |
| resultsContainer.appendChild(resultElement); | |
| }); | |
| } | |
| createResultElement(product) { | |
| const result = document.createElement('div'); | |
| result.className = 'search-result-item'; | |
| result.innerHTML = ` | |
| <div class="flex items-start justify-between"> | |
| <div class="flex-1"> | |
| <div class="flex items-center gap-3 mb-2"> | |
| <h4 class="text-lg font-semibold text-gray-900">${product.title}</h4> | |
| ${product.wageTransparency ? '<span class="px-2 py-1 bg-green-100 text-green-800 text-xs rounded-full">Wage Transparent</span>' : ''} | |
| ${product.livingWageCompliant ? '<span class="px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded-full">Living Wage</span>' : ''} | |
| </div> | |
| <p class="text-gray-600 mb-3">${product.description}</p> | |
| <div class="flex items-center gap-6 text-sm text-gray-500 mb-3"> | |
| <span>by ${product.merchant}</span> | |
| <span class="flex items-center gap-1"> | |
| <i data-feather="star" class="h-4 w-4 fill-current text-yellow-400"></i> | |
| ${product.averageRating} (${product.reviewCount} reviews) | |
| </span> | |
| <span class="flex items-center gap-1"> | |
| <i data-feather="share-2" class="h-4 w-4"></i> | |
| ${product.qrConversions} recommendations | |
| </span> | |
| </div> | |
| <div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-3"> | |
| <div class="trust-score"> | |
| <div class="text-xs text-gray-600">Recommender</div> | |
| <div class="font-medium">${Math.round(product.scoreBreakdown.recommenderReputation * 100)}%</div> | |
| <div class="trust-score-bar"> | |
| <div class="trust-score-fill trust-score-good" style="width: ${product.scoreBreakdown.recommenderReputation * 100}%"></div> | |
| </div> | |
| </div> | |
| <div class="trust-score"> | |
| <div class="text-xs text-gray-600">Provenance</div> | |
| <div class="font-medium">${Math.round(product.scoreBreakdown.provenance * 100)}%</div> | |
| <div class="trust-score-bar"> | |
| <div class="trust-score-fill trust-score-excellent" style="width: ${product.scoreBreakdown.provenance * 100}%"></div> | |
| </div> | |
| </div> | |
| <div class="trust-score"> | |
| <div class="text-xs text-gray-600">Reviews</div> | |
| <div class="font-medium">${Math.round(product.scoreBreakdown.qualityReviews * 100)}%</div> | |
| <div class="trust-score-bar"> | |
| <div class="trust-score-fill trust-score-excellent" style="width: ${product.scoreBreakdown.qualityReviews * 100}%"></div> | |
| </div> | |
| </div> | |
| <div class="trust-score"> | |
| <div class="text-xs text-gray-600">Cost Satisfaction</div> | |
| <div class="font-medium">${Math.round(product.scoreBreakdown.costSatisfaction * 100)}%</div> | |
| <div class="trust-score-bar"> | |
| <div class="trust-score-fill trust-score-good" style="width: ${product.scoreBreakdown.costSatisfaction * 100}%"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="text-right ml-6"> | |
| <div class="result-score mb-2"> | |
| <div class="score-circle ${this.getScoreColorClass(product.score)}">${product.score}</div> | |
| <span class="text-sm text-gray-600">Trust Score</span> | |
| </div> | |
| <div class="text-2xl font-bold text-gray-900 mb-4">$${product.price}</div> | |
| <div class="space-y-2"> | |
| <button class="w-full bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-md text-sm font-medium"> | |
| Learn More | |
| </button> | |
| <button class="w-full bg-gray-200 hover:bg-gray-300 text-gray-700 px-4 py-2 rounded-md text-sm font-medium"> | |
| Add to Compare | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| // Replace feather icons | |
| setTimeout(() => { | |
| if (window.feather) { | |
| window.feather.replace(); | |
| } | |
| }, 0); | |
| return result; | |
| } | |
| getScoreColorClass(score) { | |
| if (score >= 80) return 'trust-score-excellent'; | |
| if (score >= 70) return 'trust-score-good'; | |
| if (score >= 60) return 'trust-score-average'; | |
| return 'trust-score-poor'; | |
| } | |
| showQRModal() { | |
| const modal = document.getElementById('qr-modal'); | |
| if (modal) { | |
| modal.classList.remove('hidden'); | |
| modal.classList.add('fade-in'); | |
| } | |
| } | |
| hideQRModal() { | |
| const modal = document.getElementById('qr-modal'); | |
| if (modal) { | |
| modal.classList.add('hidden'); | |
| modal.classList.remove('fade-in'); | |
| } | |
| } | |
| handleAuth() { | |
| if (this.isAuthenticated) { | |
| this.logout(); | |
| } else { | |
| this.login(); | |
| } | |
| } | |
| async login() { | |
| // Simulate OAuth login | |
| try { | |
| this.setLoading(true); | |
| await new Promise(resolve => setTimeout(resolve, 1000)); | |
| this.user = { | |
| id: 'user_123', | |
| displayName: 'Demo User', | |
| profileTrust: 0.94, | |
| verifiedLevel: 'merchant-verified', | |
| walletAccountId: 'wallet_456' | |
| }; | |
| this.isAuthenticated = true; | |
| this.updateAuthUI(); | |
| this.showNotification('Successfully signed in', 'success'); | |
| } catch (error) { | |
| this.showNotification('Login failed', 'error'); | |
| } finally { | |
| this.setLoading(false); | |
| } | |
| } | |
| logout() { | |
| this.user = null; | |
| this.isAuthenticated = false; | |
| this.updateAuthUI(); | |
| this.showNotification('Signed out successfully', 'info'); | |
| } | |
| updateAuthUI() { | |
| const authButton = document.getElementById('auth-button'); | |
| const generateQrButton = document.getElementById('generate-qr'); | |
| if (this.isAuthenticated) { | |
| authButton.textContent = 'Sign Out'; | |
| authButton.className = 'bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded-md text-sm font-medium'; | |
| if (generateQrButton) { | |
| generateQrButton.classList.remove('hidden'); | |
| } | |
| } else { | |
| authButton.textContent = 'Sign In'; | |
| authButton.className = 'bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-md text-sm font-medium'; | |
| if (generateQrButton) { | |
| generateQrButton.classList.add('hidden'); | |
| } | |
| } | |
| } | |
| setupWebSocketConnection() { | |
| // Simulate WebSocket connection for real-time updates | |
| if ('WebSocket' in window) { | |
| // In a real implementation, this would connect to the actual WebSocket server | |
| console.log('WebSocket support detected'); | |
| } | |
| } | |
| initializeComponents() { | |
| // Initialize trust score component | |
| if (window.TrustScore) { | |
| window.TrustScore.init(); | |
| } | |
| // Initialize review system | |
| if (window.ReviewSystem) { | |
| window.ReviewSystem.init(); | |
| } | |
| // Initialize wallet component | |
| if (window.Wallet) { | |
| window.Wallet.init(); | |
| } | |
| } | |
| loadInitialData() { | |
| // Load initial sample data | |
| this.updateTrustScores(); | |
| this.loadSampleReviews(); | |
| } | |
| updateTrustScores() { | |
| // Simulate trust score updates | |
| const scores = [82.6, 78.3, 75.8, 89.2, 71.5]; | |
| scores.forEach((score, index) => { | |
| this.trustScores.set(`product_${index + 1}`, score); | |
| }); | |
| } | |
| loadSampleReviews() { | |
| // Sample reviews are already in the HTML | |
| // This method can be used to load dynamic review data | |
| } | |
| setLoading(loading) { | |
| const searchBtn = document.getElementById('search-btn'); | |
| if (loading) { | |
| searchBtn.disabled = true; | |
| searchBtn.innerHTML = '<span class="spinner"></span> Searching...'; | |
| } else { | |
| searchBtn.disabled = false; | |
| searchBtn.innerHTML = 'Search'; | |
| } | |
| } | |
| showNotification(message, type = 'info') { | |
| // Create notification element | |
| const notification = document.createElement('div'); | |
| notification.className = `fixed top-4 right-4 z-50 p-4 rounded-lg shadow-lg max-w-sm transform transition-all duration-300 translate-x-full`; | |
| const bgColor = { | |
| success: 'bg-green-500', | |
| error: 'bg-red-500', | |
| warning: 'bg-yellow-500', | |
| info: 'bg-blue-500' | |
| }[type]; | |
| notification.classList.add(bgColor); | |
| notification.innerHTML = ` | |
| <div class="flex items-center text-white"> | |
| <span class="flex-1">${message}</span> | |
| <button class="ml-4 text-white hover:text-gray-200" onclick="this.parentElement.parentElement.remove()"> | |
| <i data-feather="x" class="h-4 w-4"></i> | |
| </button> | |
| </div> | |
| `; | |
| document.body.appendChild(notification); | |
| // Trigger animation | |
| setTimeout(() => { | |
| notification.classList.remove('translate-x-full'); | |
| }, 100); | |
| // Auto remove after 5 seconds | |
| setTimeout(() => { | |
| notification.classList.add('translate-x-full'); | |
| setTimeout(() => { | |
| if (notification.parentElement) { | |
| notification.remove(); | |
| } | |
| }, 300); | |
| }, 5000); | |
| // Replace icons | |
| setTimeout(() => { | |
| if (window.feather) { | |
| window.feather.replace(); | |
| } | |
| }, 0); | |
| } | |
| } | |
| // Initialize application when DOM is loaded | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // Initialize the main app | |
| window.OneSearchApp = new OneSearchApp(); | |
| // Setup global error handling | |
| window.addEventListener('error', (event) => { | |
| console.error('Global error:', event.error); | |
| if (window.OneSearchApp) { | |
| window.OneSearchApp.showNotification('An unexpected error occurred', 'error'); | |
| } | |
| }); | |
| // Setup unhandled promise rejection handling | |
| window.addEventListener('unhandledrejection', (event) => { | |
| console.error('Unhandled promise rejection:', event.reason); | |
| if (window.OneSearchApp) { | |
| window.OneSearchApp.showNotification('A network error occurred', 'error'); | |
| } | |
| }); | |
| }); | |
| // Export for use in other components | |
| if (typeof module !== 'undefined' && module.exports) { | |
| module.exports = OneSearchApp; | |
| } |