// 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 = `

Trust Score Breakdown

${Object.entries(breakdown).map(([key, value]) => `
${this.formatSignalName(key)} ${Math.round(value * 100)}%
Weight: ${this.weights[key] * 100}%
`).join('')}

Evidence References

• 23 verified purchase receipts

• 124 peer reviews with maturity status

• 31 QR-based recommendations

• 15 after-sales support tickets

`; // 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; }