open-prompt / src /lib /trending.ts
GitHub Action
Automated sync to Hugging Face
bcce530
/**
* 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
}