import type { SentimentResult } from '../types.js';
// AFINN-inspired word list (abridged, covering common social media terms)
const POSITIVE_WORDS: Record<string, number> = {
'love': 3, 'loved': 3, 'loving': 3, 'amazing': 4, 'awesome': 4, 'excellent': 4,
'great': 3, 'good': 2, 'best': 4, 'beautiful': 3, 'brilliant': 4, 'fantastic': 4,
'happy': 3, 'wonderful': 4, 'perfect': 4, 'incredible': 4, 'outstanding': 4,
'recommend': 2, 'impressive': 3, 'excited': 3, 'exciting': 3, 'thank': 2,
'thanks': 2, 'helpful': 2, 'enjoy': 2, 'enjoyed': 2, 'fun': 2, 'nice': 2,
'cool': 2, 'wow': 3, 'bravo': 3, 'superb': 4, 'win': 2, 'winning': 2,
'success': 3, 'successful': 3, 'inspire': 3, 'inspired': 3, 'inspiring': 3,
'innovative': 3, 'genius': 4, 'valuable': 2, 'positive': 2, 'exceptional': 4,
'lol': 1, 'haha': 1, 'like': 1, 'agree': 1, 'support': 2, 'celebrate': 3,
'congrats': 3, 'congratulations': 3, 'legendary': 4, 'fire': 2, 'lit': 2,
'goat': 3, 'clutch': 2, 'iconic': 3, 'blessed': 3, 'wholesome': 3,
};
const NEGATIVE_WORDS: Record<string, number> = {
'hate': -4, 'hated': -4, 'hating': -4, 'terrible': -4, 'awful': -4, 'horrible': -4,
'bad': -2, 'worst': -4, 'ugly': -3, 'stupid': -3, 'boring': -2, 'disappointed': -3,
'disappointing': -3, 'fail': -3, 'failed': -3, 'failure': -3, 'wrong': -2,
'poor': -2, 'waste': -3, 'sucks': -3, 'suck': -3, 'annoying': -2, 'annoyed': -2,
'angry': -3, 'scam': -4, 'fraud': -4, 'broken': -2, 'useless': -3, 'trash': -3,
'garbage': -3, 'disgusting': -4, 'pathetic': -3, 'ridiculous': -2, 'toxic': -3,
'overrated': -2, 'mediocre': -2, 'cringe': -2, 'disaster': -4, 'nightmare': -3,
'unfortunately': -2, 'sad': -2, 'worse': -3, 'problem': -2, 'problems': -2,
'bug': -1, 'bugs': -1, 'crash': -3, 'crashed': -3, 'dead': -2, 'rip': -2,
'flop': -3, 'mid': -1, 'cap': -1, 'fake': -3, 'ratio': -2, 'L': -2,
};
const EMOJI_SENTIMENT: Record<string, number> = {
'๐': 2, '๐': 3, 'โค๏ธ': 3, '๐ฅฐ': 3, '๐': 2, '๐คฃ': 2, '๐': 2, '๐': 3,
'๐ฅ': 2, '๐ฏ': 3, 'โจ': 2, '๐ช': 2, '๐': 2, '๐': 2, '๐': 3, '๐': 3,
'๐ข': -2, '๐ก': -3, '๐ค': -2, '๐': -3, '๐': -2, '๐': -2, '๐คฎ': -3,
'๐ญ': -1, '๐ฑ': -1, '๐': -2, '๐': -1, '๐คก': -2, '๐ฉ': -2,
};
const NEGATION_WORDS = new Set(['not', "don't", "doesn't", "didn't", "won't", "can't", "isn't", "aren't", 'never', 'no', 'neither', 'nor']);
export function analyzeSentiment(text: string): SentimentResult {
const words = text.toLowerCase().split(/\s+/);
let totalScore = 0;
let positiveCount = 0;
let negativeCount = 0;
let neutralCount = 0;
let wordCount = 0;
for (let i = 0; i < words.length; i++) {
const word = words[i].replace(/[^a-z']/g, '');
if (!word) continue;
const isNegated = i > 0 && NEGATION_WORDS.has(words[i - 1].replace(/[^a-z']/g, ''));
let score = 0;
if (POSITIVE_WORDS[word] !== undefined) {
score = POSITIVE_WORDS[word];
} else if (NEGATIVE_WORDS[word] !== undefined) {
score = NEGATIVE_WORDS[word];
}
if (score !== 0) {
if (isNegated) score = -score * 0.5;
totalScore += score;
wordCount++;
if (score > 0) positiveCount++;
else if (score < 0) negativeCount++;
} else {
neutralCount++;
}
}
// Check emojis
for (const [emoji, score] of Object.entries(EMOJI_SENTIMENT)) {
const count = (text.match(new RegExp(emoji, 'g')) || []).length;
if (count > 0) {
totalScore += score * count;
wordCount += count;
if (score > 0) positiveCount += count;
else negativeCount += count;
}
}
// ALL CAPS detection (amplifier)
const capsWords = text.match(/\b[A-Z]{3,}\b/g);
if (capsWords) {
totalScore *= 1 + (capsWords.length * 0.1);
}
// Normalize score to -5 to 5
const normalizedScore = wordCount > 0
? Math.max(-5, Math.min(5, totalScore / Math.sqrt(wordCount)))
: 0;
const total = positiveCount + negativeCount + neutralCount || 1;
let label: SentimentResult['label'];
if (Math.abs(normalizedScore) < 0.5) label = 'neutral';
else if (positiveCount > 0 && negativeCount > 0 && Math.abs(positiveCount - negativeCount) <= 2) label = 'mixed';
else if (normalizedScore > 0) label = 'positive';
else label = 'negative';
return {
score: Math.round(normalizedScore * 100) / 100,
label,
breakdown: {
positive: positiveCount / total,
negative: negativeCount / total,
neutral: neutralCount / total,
},
};
}
export function analyzeSentimentBatch(texts: string[]): SentimentResult {
if (texts.length === 0) {
return { score: 0, label: 'neutral', breakdown: { positive: 0, negative: 0, neutral: 1 } };
}
const results = texts.map(analyzeSentiment);
const avgScore = results.reduce((s, r) => s + r.score, 0) / results.length;
const avgBreakdown = {
positive: results.reduce((s, r) => s + r.breakdown.positive, 0) / results.length,
negative: results.reduce((s, r) => s + r.breakdown.negative, 0) / results.length,
neutral: results.reduce((s, r) => s + r.breakdown.neutral, 0) / results.length,
};
let label: SentimentResult['label'];
if (Math.abs(avgScore) < 0.5) label = 'neutral';
else if (avgBreakdown.positive > 0.3 && avgBreakdown.negative > 0.3) label = 'mixed';
else if (avgScore > 0) label = 'positive';
else label = 'negative';
return { score: Math.round(avgScore * 100) / 100, label, breakdown: avgBreakdown };
}