Spaces:
Configuration error
Configuration error
File size: 5,430 Bytes
bcce530 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 | /**
* Trending algorithm for OpenPrompt
* Calculates "hot score" based on recency and engagement
*/
interface TrendingPrompt {
id: string
totalRuns: number
starsCount: number
remixesCount: number
createdAt: Date
updatedAt: Date
}
/**
* Calculate trending score (Hot Score)
* Based on Reddit's "hot" algorithm with OpenPrompt-specific weights
*
* Score = (engagement) / (age_hours + 2)^1.5
*
* Weights:
* - Recent activity >> old activity
* - Runs: 1 point (high volume, lower signal)
* - Stars: 3 points (medium signal — user took action)
* - Remixes: 5 points (strongest signal — user remixed the work)
*/
export function calculateHotScore(prompt: TrendingPrompt): number {
const now = new Date()
// Calculate age in hours since creation
const ageMs = now.getTime() - prompt.createdAt.getTime()
const ageHours = ageMs / (1000 * 60 * 60)
// Engagement score with weighted components
const engagement =
prompt.totalRuns * 1 +
prompt.starsCount * 3 +
prompt.remixesCount * 5
// Hot score formula (prevents division by zero with +2)
const score = engagement / Math.pow(ageHours + 2, 1.5)
return score
}
/**
* Calculate trending score for today/this week
* Only considers recent activity with decay for older prompts
*/
export function calculateRecentTrendingScore(
prompt: TrendingPrompt,
timeWindow: 'day' | 'week' = 'week'
): number {
const now = new Date()
const cutoff = new Date()
if (timeWindow === 'day') {
cutoff.setDate(now.getDate() - 1)
} else {
cutoff.setDate(now.getDate() - 7)
}
// If prompt is older than cutoff, use a decay factor
if (prompt.createdAt < cutoff) {
const score = calculateHotScore(prompt)
// Apply decay (older = lower score)
const daysSinceCreation = (now.getTime() - prompt.createdAt.getTime()) / (1000 * 60 * 60 * 24)
const decay = Math.exp(-daysSinceCreation / 30) // 30-day half-life
return score * decay * 0.5 // Additional penalty for old content
}
return calculateHotScore(prompt)
}
/**
* Sort prompts by trending score
*/
export function sortByTrending<T extends TrendingPrompt>(
prompts: T[],
timeWindow?: 'day' | 'week'
): T[] {
return [...prompts].sort((a, b) => {
const scoreA = timeWindow
? calculateRecentTrendingScore(a, timeWindow)
: calculateHotScore(a)
const scoreB = timeWindow
? calculateRecentTrendingScore(b, timeWindow)
: calculateHotScore(b)
return scoreB - scoreA
})
}
/**
* Check if a prompt is currently trending using dynamic percentile thresholds.
*
* Instead of hardcoded magic numbers, we pass the p75 threshold from the caller
* (computed from a Redis-cached or DB-derived percentile of recent scores).
* Falls back to a conservative static threshold when no data is available.
*/
export function isTrending(
prompt: TrendingPrompt,
threshold?: number,
): boolean {
// Must be created within last 7 days to be "trending"
const weekAgo = new Date()
weekAgo.setDate(weekAgo.getDate() - 7)
if (prompt.createdAt < weekAgo) {
return false
}
const promptScore = calculateHotScore(prompt)
// If threshold was provided (e.g., from a percentile query), use it
if (threshold !== undefined) {
return promptScore >= threshold
}
// Conservative fallback: engagement must exceed "1 star OR 3 runs in <7 days"
const minEngagement = prompt.starsCount * 3 + prompt.totalRuns * 1
return promptScore > 0 && minEngagement >= 3
}
/**
* Compute the hot-score threshold for the p75 of recent prompts.
* Call this from the API route and cache the result for 5 minutes.
*
* @param recentPrompts - array of prompts from the last 7 days
* @param percentile - 0-100 (default: 75 = top 25%)
*/
export function computeTrendingThreshold(
recentPrompts: TrendingPrompt[],
percentile: number = 75,
): number {
if (recentPrompts.length === 0) return 0
const scores = recentPrompts
.map((p) => calculateHotScore(p))
.sort((a, b) => a - b)
const index = Math.floor((percentile / 100) * scores.length)
return scores[Math.min(index, scores.length - 1)]
}
/**
* Get trending badge text based on computed scores
*/
export function getTrendingBadge(
prompt: TrendingPrompt,
dayThreshold?: number,
weekThreshold?: number,
): { text: string; icon: string } | null {
const dayScore = calculateRecentTrendingScore(prompt, 'day')
const weekScore = calculateRecentTrendingScore(prompt, 'week')
// Use dynamic thresholds if provided, otherwise use relative checks
const dayThr = dayThreshold ?? Infinity
const weekThr = weekThreshold ?? Infinity
if (dayScore > dayThr) {
return { text: 'Trending Now', icon: 'Flame' }
}
if (weekScore > weekThr) {
return { text: 'Trending This Week', icon: 'TrendingUp' }
}
// Fallback: badge based on raw engagement if no thresholds given
if (!dayThreshold && !weekThreshold) {
const engagement = prompt.totalRuns + prompt.starsCount * 3 + prompt.remixesCount * 5
if (engagement > 100) return { text: 'Trending Now', icon: 'Flame' }
if (engagement > 30) return { text: 'Trending This Week', icon: 'TrendingUp' }
}
return null
}
|