Spaces:
Running
Running
| // Trust Score Component | |
| class TrustScore { | |
| static init() { | |
| console.log('TrustScore component initialized'); | |
| this.setupScoreCalculation(); | |
| this.setupRealTimeUpdates(); | |
| } | |
| static setupScoreCalculation() { | |
| // Trust score calculation based on OneSearch specification | |
| this.weights = { | |
| recommenderReputation: 0.20, | |
| provenance: 0.20, | |
| usageDuration: 0.10, | |
| conversionRate: 0.10, | |
| afterSales: 0.10, | |
| locality: 0.05, | |
| costSatisfaction: 0.10, | |
| qualityReviews: 0.15 | |
| }; | |
| this.decayFactors = { | |
| reviews: 0.002, | |
| purchases: 0.001, | |
| tickets: 0.003 | |
| }; | |
| } | |
| static calculateScore(productId, evidence) { | |
| // Implement the OneSearch trust score formula | |
| let scoreRaw = 0; | |
| const breakdown = {}; | |
| // Calculate subscores with normalization and time decay | |
| Object.keys(this.weights).forEach(signal => { | |
| let value = evidence[signal] || 0; | |
| const weight = this.weights[signal]; | |
| // Apply time decay if timestamp is provided | |
| if (evidence.timestamp) { | |
| const daysSince = this.getDaysSince(evidence.timestamp); | |
| const decayRate = this.decayFactors[signal] || 0.001; | |
| const decayFactor = Math.exp(-decayRate * daysSince); | |
| value *= decayFactor; | |
| } | |
| // Normalize to 0-1 range | |
| value = this.normalizeSignal(signal, value); | |
| breakdown[signal] = value; | |
| scoreRaw += weight * value; | |
| }); | |
| // Apply penalties | |
| const penalty = this.calculatePenalties(evidence); | |
| const scoreNormalized = Math.max(0, Math.min(1, scoreRaw - penalty)); | |
| return { | |
| score: Math.round(scoreNormalized * 100 * 10) / 10, // Round to 1 decimal | |
| breakdown, | |
| penalty, | |
| evidence: evidence | |
| }; | |
| } | |
| static normalizeSignal(signal, value) { | |
| // Normalize different signal types to 0-1 range | |
| switch (signal) { | |
| case 'qualityReviews': | |
| // Reviews are in range [-1, 1], map to [0, 1] | |
| return Math.max(0, Math.min(1, (value + 1) / 2)); | |
| case 'costSatisfaction': | |
| // Cost satisfaction ratio, clamp to [0, 1.2] then normalize | |
| return Math.max(0, Math.min(1, value / 1.2)); | |
| case 'afterSales': | |
| // Convert issue rate to satisfaction score | |
| return Math.max(0, Math.min(1, 1 - (value || 0))); | |
| case 'usageDuration': | |
| // Normalize by expected category lifetime | |
| return Math.max(0, Math.min(1, value)); | |
| default: | |
| // Already normalized to 0-1 | |
| return Math.max(0, Math.min(1, value || 0)); | |
| } | |
| } | |
| static calculatePenalties(evidence) { | |
| let penalty = 0; | |
| // Dispute penalty | |
| if (evidence.disputeRate > 0) { | |
| penalty += Math.min(0.2, evidence.disputeRate * 0.1); | |
| } | |
| // Fraud penalty | |
| if (evidence.fraudFlags && evidence.fraudFlags.length > 0) { | |
| penalty += 0.15; | |
| } | |
| // Sybil/collusion penalty | |
| if (evidence.collusionRisk > 0.7) { | |
| penalty += 0.1; | |
| } | |
| return penalty; | |
| } | |
| static getDaysSince(timestamp) { | |
| const now = new Date(); | |
| const date = new Date(timestamp); | |
| return Math.floor((now - date) / (1000 * 60 * 60 * 24)); | |
| } | |
| static setupRealTimeUpdates() { | |
| // Setup WebSocket or polling for real-time score updates | |
| if (window.OneSearchApp && window.OneSearchApp.isAuthenticated) { | |
| this.startScorePolling(); | |
| } | |
| } | |
| static startScorePolling() { | |
| // Poll for score updates every 30 seconds | |
| setInterval(() => { | |
| this.updateVisibleScores(); | |
| }, 30000); | |
| } | |
| static updateVisibleScores() { | |
| // Update trust scores visible on the page | |
| const scoreElements = document.querySelectorAll('.trust-score'); | |
| scoreElements.forEach(element => { | |
| const productId = element.dataset.productId; | |
| if (productId) { | |
| this.updateScoreDisplay(element, productId); | |
| } | |
| }); | |
| } | |
| static updateScoreDisplay(element, productId) { | |
| // In a real implementation, this would fetch updated scores from the API | |
| // For now, we'll simulate small variations | |
| const currentScore = parseFloat(element.querySelector('.font-medium').textContent); | |
| const variation = (Math.random() - 0.5) * 2; // -1 to +1 | |
| const newScore = Math.max(0, Math.min(100, currentScore + variation)); | |
| element.querySelector('.font-medium').textContent = Math.round(newScore); | |
| // Update progress bar | |
| const progressBar = element.querySelector('.trust-score-fill'); | |
| if (progressBar) { | |
| progressBar.style.width = `${newScore}%`; | |
| } | |
| } | |
| static displayScoreBreakdown(productId, breakdown) { | |
| // Create detailed score breakdown modal or panel | |
| const modal = this.createBreakdownModal(productId, breakdown); | |
| document.body.appendChild(modal); | |
| // Show modal with animation | |
| setTimeout(() => { | |
| modal.classList.add('opacity-100'); | |
| }, 10); | |
| } | |
| static createBreakdownModal(productId, breakdown) { | |
| const modal = document.createElement('div'); | |
| modal.className = 'fixed inset-0 bg-gray-600 bg-opacity-50 flex items-center justify-center z-50 opacity-0 transition-opacity duration-300'; | |
| modal.innerHTML = ` | |
| <div class="bg-white rounded-lg shadow-xl max-w-2xl w-full mx-4 max-h-96 overflow-y-auto"> | |
| <div class="px-6 py-4 border-b"> | |
| <div class="flex justify-between items-center"> | |
| <h3 class="text-lg font-semibold">Trust Score Breakdown</h3> | |
| <button class="text-gray-400 hover:text-gray-600" onclick="this.closest('.fixed').remove()"> | |
| <i data-feather="x" class="h-6 w-6"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="p-6"> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| ${Object.entries(breakdown).map(([key, value]) => ` | |
| <div class="border rounded-lg p-4"> | |
| <div class="flex justify-between items-center mb-2"> | |
| <span class="text-sm font-medium text-gray-700">${this.formatSignalName(key)}</span> | |
| <span class="text-sm font-bold">${Math.round(value * 100)}%</span> | |
| </div> | |
| <div class="w-full bg-gray-200 rounded-full h-2"> | |
| <div class="bg-blue-500 h-2 rounded-full transition-all duration-500" style="width: ${value * 100}%"></div> | |
| </div> | |
| <div class="text-xs text-gray-500 mt-1"> | |
| Weight: ${this.weights[key] * 100}% | |
| </div> | |
| </div> | |
| `).join('')} | |
| </div> | |
| <div class="mt-6 p-4 bg-gray-50 rounded-lg"> | |
| <h4 class="font-medium mb-2">Evidence References</h4> | |
| <div class="text-sm text-gray-600"> | |
| <p>• 23 verified purchase receipts</p> | |
| <p>• 124 peer reviews with maturity status</p> | |
| <p>• 31 QR-based recommendations</p> | |
| <p>• 15 after-sales support tickets</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| // Replace icons | |
| setTimeout(() => { | |
| if (window.feather) { | |
| window.feather.replace(); | |
| } | |
| }, 0); | |
| return modal; | |
| } | |
| static formatSignalName(signal) { | |
| const names = { | |
| recommenderReputation: 'Recommender Reputation', | |
| provenance: 'Purchase Proof', | |
| usageDuration: 'Usage Duration', | |
| conversionRate: 'QR Conversions', | |
| afterSales: 'After-Sales Support', | |
| locality: 'Local Availability', | |
| costSatisfaction: 'Cost Satisfaction', | |
| qualityReviews: 'Quality Reviews' | |
| }; | |
| return names[signal] || signal; | |
| } | |
| static exportScoreData(productId) { | |
| // Export score data for auditing | |
| const scoreData = { | |
| productId, | |
| timestamp: new Date().toISOString(), | |
| calculationVersion: '1.0', | |
| weights: this.weights, | |
| breakdown: {}, // Would contain actual breakdown data | |
| previousHash: '0x4b32...', // Would contain actual hash | |
| dataHash: '0x9ad7...' // Would contain actual hash | |
| }; | |
| // In a real implementation, this would generate a signed JSON blob | |
| return JSON.stringify(scoreData, null, 2); | |
| } | |
| } | |
| // Export for use in main app | |
| if (typeof window !== 'undefined') { | |
| window.TrustScore = TrustScore; | |
| } | |
| if (typeof module !== 'undefined' && module.exports) { | |
| module.exports = TrustScore; | |
| } |