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
}