a a vibe-coded abomination experiment of a fragrance review platform built on the atmosphere. drydown.social
at main 97 lines 3.5 kB view raw
1import { AtpBaseClient } from '../client/index' 2import type { UserPreferencesForScoring } from './reviewUtils' 3import { cache, TTL } from '../services/cache' 4import { DEFAULT_PREFERENCES } from '../data/preferenceDefinitions' 5 6/** 7 * Fetches another user's preference settings from their PDS. 8 * Returns null if the fetch fails (private settings, not found, etc.) 9 * Falls back to cache if available. 10 */ 11export async function fetchUserPreferences( 12 did: string, 13 pdsUrl: string 14): Promise<UserPreferencesForScoring | null> { 15 const cacheKey = `preferences:${did}` 16 17 // Check cache first 18 const cached = cache.get<UserPreferencesForScoring>(cacheKey) 19 if (cached) { 20 return cached 21 } 22 23 try { 24 // Create unauthenticated PDS client for public reads 25 const pdsClient = new AtpBaseClient({ service: pdsUrl }) 26 27 // Fetch the settings record (rkey is always 'self' for user settings) 28 const response = await pdsClient.social.drydown.settings.get({ 29 repo: did, 30 rkey: 'self', 31 }) 32 33 if (!response.value) { 34 return null 35 } 36 37 const settings = response.value 38 const preferences: UserPreferencesForScoring = { 39 presenceStyle: settings.presenceStyle, 40 longevityPriority: settings.longevityPriority, 41 complexityPreference: settings.complexityPreference, 42 scoringApproach: settings.scoringApproach, 43 } 44 45 // Cache the result 46 cache.set(cacheKey, preferences, TTL.SETTINGS) 47 48 return preferences 49 } catch (error) { 50 // Settings not found, private, or network error - return null 51 console.debug('Could not fetch user preferences:', error) 52 return null 53 } 54} 55 56/** 57 * Calculates compatibility score between two users' preferences. 58 * Returns a percentage from 0-100 indicating how aligned their preferences are. 59 * 60 * Algorithm: 61 * 1. Calculate absolute distance for each of 4 preference values (each on 1-5 scale) 62 * 2. Average the distances (max possible avg = 4) 63 * 3. Convert to percentage: ((4 - avgDistance) / 4) * 100 64 * 65 * Examples: 66 * - Identical preferences: avgDistance = 0 → 100% 67 * - Opposite preferences: avgDistance = 4 → 0% 68 * - Mixed preferences: avgDistance = 0.75 → 81% 69 */ 70export function calculateCompatibilityScore( 71 userPrefs: UserPreferencesForScoring, 72 otherPrefs: UserPreferencesForScoring 73): number { 74 // Use DEFAULT_PREFERENCES if any preference is undefined 75 const distances = [ 76 Math.abs((userPrefs.presenceStyle ?? DEFAULT_PREFERENCES.presenceStyle) - (otherPrefs.presenceStyle ?? DEFAULT_PREFERENCES.presenceStyle)), 77 Math.abs((userPrefs.longevityPriority ?? DEFAULT_PREFERENCES.longevityPriority) - (otherPrefs.longevityPriority ?? DEFAULT_PREFERENCES.longevityPriority)), 78 Math.abs((userPrefs.complexityPreference ?? DEFAULT_PREFERENCES.complexityPreference) - (otherPrefs.complexityPreference ?? DEFAULT_PREFERENCES.complexityPreference)), 79 Math.abs((userPrefs.scoringApproach ?? DEFAULT_PREFERENCES.scoringApproach) - (otherPrefs.scoringApproach ?? DEFAULT_PREFERENCES.scoringApproach)), 80 ] 81 82 const avgDistance = distances.reduce((sum, d) => sum + d, 0) / 4 83 const compatibility = ((4 - avgDistance) / 4) * 100 84 85 return Math.round(compatibility) 86} 87 88/** 89 * Returns a friendly label describing the compatibility level. 90 */ 91export function getCompatibilityLabel(score: number): string { 92 if (score >= 90) return 'Highly Similar' 93 if (score >= 75) return 'Similar Tastes' 94 if (score >= 50) return 'Somewhat Different' 95 if (score >= 25) return 'Different Tastes' 96 return 'Very Different' 97}