Spaces:
Configuration error
Configuration error
| /** | |
| * 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 | |
| } | |