-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathalgorithm.js
More file actions
326 lines (297 loc) · 9.67 KB
/
Copy pathalgorithm.js
File metadata and controls
326 lines (297 loc) · 9.67 KB
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
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
/**
* X Algorithm Weights and Scoring Module
* Based on publicly available X algorithm source code insights
*
* Sources:
* - X Algorithm (formerly Twitter) open source release
* - https://github.com/twitter/the-algorithm
*/
// Engagement signal weights (based on X algorithm)
export const ENGAGEMENT_WEIGHTS = {
reply: 11, // Replies are worth 11x a like - drives conversation
follow: 10, // New follows are highly weighted
repost: 4, // Retweets/reposts spread content
quote: 4, // Quote tweets add value
share: 4, // Direct shares
like: 1, // Baseline engagement
click: 0.5, // Link clicks (lower because often bot traffic)
dwell: 0.5, // Time spent viewing
bookmark: 2, // Saves for later
report: -100, // Reports are severely punished
block: -50, // Blocks hurt significantly
mute: -10, // Mutes are a soft negative
spam_flag: -75, // Spam detection
unlike: -0.5, // Removing a like
};
// Content type multipliers
export const CONTENT_MULTIPLIERS = {
thread: 1.3, // Threads get boosted
original: 1.2, // Original content > reposts
reply: 1.0, // Replies are neutral
repost: 0.8, // Pure reposts are slightly demoted
quote_tweet: 1.1, // Quotes with commentary
with_media: 1.1, // Posts with images/video
external_link: 0.95, // External links slightly demoted (keeps users on platform)
verified_author: 1.1, // Verified accounts get small boost
};
// Media type boosts
export const MEDIA_BOOSTS = {
image: 1.2, // Images boost engagement
video: 1.5, // Video is heavily favored
gif: 1.1, // GIFs are good
poll: 1.3, // Polls drive interaction
space: 1.4, // Spaces/audio rooms
none: 1.0, // Text only (baseline)
};
// Best practices derived from algorithm
export const BEST_PRACTICES = {
optimal_length: {
min: 50,
sweet_spot: 100,
max_before_thread: 200,
},
posting_rhythm: {
min_gap_minutes: 30, // Avoid flood penalty
ideal_gap_hours: 2,
max_per_day: 5, // Diminishing returns after this
},
high_performing_content: [
'personal_stories',
'hot_takes',
'tutorials',
'behind_the_scenes',
'data_visualizations',
'curated_threads',
'questions',
'polls',
],
effective_hooks: [
'I spent X hours/days/years...',
"Here's what nobody tells you about...",
'The biggest mistake I see...',
'Unpopular opinion:',
'X things I learned from...',
'Stop doing X. Start doing Y.',
'This changed everything for me:',
'Most people get this wrong:',
"Here's the truth about...",
'I was today years old when...',
],
recency_boost: {
under_1_hour: 1.5,
under_6_hours: 1.2,
under_24_hours: 1.0,
older: 0.7,
},
};
/**
* Analyze engagement patterns from a set of tweets
* @param {Array} tweets - Array of tweet objects with text, likes, replies, reposts
* @returns {Object} Analysis results
*/
export function analyzeEngagementPatterns(tweets) {
if (!tweets || tweets.length === 0) {
return {
avgEngagement: { likes: 0, replies: 0, reposts: 0 },
bestPerforming: [],
patterns: {},
};
}
// Calculate averages
const totals = tweets.reduce(
(acc, t) => ({
likes: acc.likes + (t.likes || 0),
replies: acc.replies + (t.replies || 0),
reposts: acc.reposts + (t.reposts || 0),
}),
{ likes: 0, replies: 0, reposts: 0 }
);
const avgEngagement = {
likes: Math.round(totals.likes / tweets.length),
replies: Math.round(totals.replies / tweets.length),
reposts: Math.round(totals.reposts / tweets.length),
};
// Calculate weighted score for each tweet
const scoredTweets = tweets.map(t => ({
...t,
weightedScore:
(t.likes || 0) * ENGAGEMENT_WEIGHTS.like +
(t.replies || 0) * ENGAGEMENT_WEIGHTS.reply +
(t.reposts || 0) * ENGAGEMENT_WEIGHTS.repost,
}));
// Sort by weighted score
const bestPerforming = scoredTweets
.sort((a, b) => b.weightedScore - a.weightedScore)
.slice(0, 10);
// Analyze content patterns
const patterns = {
avgLength: Math.round(
tweets.reduce((sum, t) => sum + (t.text?.length || 0), 0) / tweets.length
),
withQuestions: tweets.filter(t => t.text?.includes('?')).length,
withLinks: tweets.filter(t => t.text?.includes('http')).length,
withHashtags: tweets.filter(t => t.text?.includes('#')).length,
};
return {
avgEngagement,
bestPerforming,
patterns,
};
}
/**
* Score a potential post's engagement potential
* @param {Object} post - Post object with text and optional hasMedia
* @returns {Object} Score and analysis
*/
export function scorePostPotential(post) {
const text = post.text || '';
let score = 50; // Start at neutral
const analysis = {
strengths: [],
weaknesses: [],
suggestions: [],
};
// Length analysis
const length = text.length;
if (length >= 50 && length <= 150) {
score += 10;
analysis.strengths.push('Good length (50-150 chars) for engagement');
} else if (length < 30) {
score -= 10;
analysis.weaknesses.push('Too short - add more substance');
} else if (length > 250) {
score -= 5;
analysis.suggestions.push('Consider making this a thread');
}
// Question detection (drives replies = 11x weight)
if (text.includes('?')) {
score += 15;
analysis.strengths.push('Contains a question - drives replies (11x value!)');
} else {
analysis.suggestions.push('Add a question to encourage replies');
}
// Hook detection
const hasHook = BEST_PRACTICES.effective_hooks.some(hook => {
const hookStart = hook.split('...')[0].toLowerCase();
return text.toLowerCase().includes(hookStart.replace(' x ', ' '));
});
if (hasHook) {
score += 10;
analysis.strengths.push('Uses an effective hook pattern');
}
// Personal elements
if (text.toLowerCase().includes(' i ') || text.toLowerCase().startsWith('i ')) {
score += 5;
analysis.strengths.push('Personal perspective increases relatability');
}
// Numbers and specifics
if (/\d+/.test(text)) {
score += 5;
analysis.strengths.push('Includes specific numbers - adds credibility');
}
// Contrarian/opinion markers
const opinionMarkers = ['unpopular opinion', 'hot take', 'controversial', 'actually', 'most people'];
if (opinionMarkers.some(m => text.toLowerCase().includes(m))) {
score += 8;
analysis.strengths.push('Contrarian framing drives engagement');
}
// Negative patterns
if (text.toLowerCase().includes('check out') && text.includes('http')) {
score -= 15;
analysis.weaknesses.push('Generic link sharing - add value/context');
}
if (text.includes('http')) {
score -= 5;
analysis.weaknesses.push('External links get ~0.95x multiplier');
analysis.suggestions.push('Add compelling commentary before the link');
}
// Too many hashtags
const hashtagCount = (text.match(/#/g) || []).length;
if (hashtagCount > 3) {
score -= 10;
analysis.weaknesses.push('Too many hashtags looks spammy');
} else if (hashtagCount === 1 || hashtagCount === 2) {
score += 3;
analysis.strengths.push('Good hashtag usage (1-2 is ideal)');
}
// Media boost
if (post.hasMedia) {
score += 12;
analysis.strengths.push('Media (image/video) boosts engagement significantly');
} else {
analysis.suggestions.push('Consider adding an image or video');
}
// Thread indicator
if (text.includes('🧵') || text.toLowerCase().includes('thread')) {
score += 8;
analysis.strengths.push('Thread format gets 1.3x algorithm boost');
}
// Call to action
const ctaPatterns = ['what do you think', 'thoughts?', 'agree or disagree', 'let me know', 'share your'];
if (ctaPatterns.some(cta => text.toLowerCase().includes(cta))) {
score += 10;
analysis.strengths.push('Has call-to-action - encourages engagement');
}
// Cap score at 0-100
score = Math.max(0, Math.min(100, score));
return {
score,
analysis,
};
}
/**
* Generate post suggestions for a topic
* @param {string} topic - Topic to generate suggestions for
* @returns {Array} Array of suggestion objects
*/
export function generatePostSuggestions(topic) {
const suggestions = [
{
type: 'question',
template: `What's your experience with ${topic}? I'll share my thoughts in the replies.`,
tip: 'Questions drive replies (11x engagement weight)',
potentialScore: 75,
},
{
type: 'hot_take',
template: `Unpopular opinion: Most advice about ${topic} is wrong. Here's why:`,
tip: 'Contrarian views spark debate and engagement',
potentialScore: 72,
},
{
type: 'personal_story',
template: `I spent 6 months learning ${topic}. Here's what actually worked:`,
tip: 'Personal transformation stories perform well',
potentialScore: 70,
},
{
type: 'thread_opener',
template: `Everything I know about ${topic} (thread) 🧵`,
tip: 'Threads get 1.3x algorithm boost',
potentialScore: 73,
},
{
type: 'tutorial',
template: `The simplest way to get started with ${topic}:\n\nStep 1:`,
tip: 'Tutorials are highly shareable and saveable',
potentialScore: 68,
},
{
type: 'behind_the_scenes',
template: `Here's how I actually use ${topic} day-to-day:\n\n(It's not what you'd expect)`,
tip: 'Behind-the-scenes content feels authentic',
potentialScore: 65,
},
];
return suggestions;
}
// Default export for the module
export default {
ENGAGEMENT_WEIGHTS,
CONTENT_MULTIPLIERS,
MEDIA_BOOSTS,
BEST_PRACTICES,
analyzeEngagementPatterns,
scorePostPotential,
generatePostSuggestions,
};