onesearch / components /TrustScore.js
jessikat29's picture
OneSearch Technical Specification
11a16a2 verified
Raw
History Blame Contribute Delete
9.7 kB
// 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;
}