looood / src /lib /local-summarizer.ts
looda3131's picture
Clean push without any binary history
cc276cc
/**
* @fileoverview A more intelligent, local, rule-based algorithm for summarizing chats.
* This acts as a "primitive AI" for summarization, running entirely in the browser.
*/
import type { Message, User } from './types';
export interface LocalSummary {
participants: string[];
topic: string;
summaryPoints: string[];
}
// Simple list of common "stop words" to ignore when finding topics.
const STOP_WORDS_EN = new Set(['a', 'an', 'the', 'is', 'are', 'was', 'were', 'in', 'on', 'at', 'i', 'you', 'he', 'she', 'it', 'we', 'they', 'and', 'but', 'or', 'so', 'to', 'of', 'for', 'with', 'about', 'as', 'that', 'this', 'what', 'when', 'where', 'why', 'how', 'do', 'does', 'did']);
const STOP_WORDS_AR = new Set(['ุฃู†ุง', 'ุฃู†ุช', 'ู‡ูˆ', 'ู‡ูŠ', 'ู†ุญู†', 'ู‡ู…', 'ูˆ', 'ุฃูˆ', 'ููŠ', 'ุนู„ู‰', 'ู…ู†', 'ุนู†', 'ู…ุง', 'ู…ุงุฐุง', 'ูƒูŠู', 'ู…ุชู‰', 'ุฃูŠู†', 'ู‡ู„', 'ูƒุงู†', 'ูŠูƒูˆู†', 'ู‚ุงู„', 'ู‚ุงู„ุช', 'ู‡ุฐุง', 'ู‡ุฐู‡', 'ุฐู„ูƒ', 'ุชู„ูƒ']);
// Keywords that indicate an important message
const KEYWORD_SCORES = {
// English
'agree': 5, 'decision': 5, 'plan': 5, 'important': 5, 'because': 4, 'question': 4,
'next step': 5, 'finally': 4, 'conclusion': 5, 'i will': 3, 'we should': 3,
// Arabic
'ู…ูˆุงูู‚ุฉ': 5, 'ู‚ุฑุงุฑ': 5, 'ุฎุทุฉ': 5, 'ู…ู‡ู…': 5, 'ู„ุฃู†': 4, 'ุณุคุงู„': 4,
'ุงู„ุฎุทูˆุฉ ุงู„ุชุงู„ูŠุฉ': 5, 'ุฃุฎูŠุฑุง': 4, 'ุฎู„ุงุตุฉ': 5, 'ุณุฃู‚ูˆู…': 3, 'ูŠุฌุจ ุนู„ูŠู†ุง': 3, 'ุงุชูู‚ู†ุง': 5,
};
interface ScoredMessage {
message: Message;
score: number;
}
/**
* Scores a message based on its content to determine its importance.
* @param message The message to score.
* @param previousMessage The message that came before it.
* @returns A numerical score.
*/
function getMessageScore(message: Message, previousMessage?: Message): number {
if (!message.text || message.isSystemMessage) return 0;
let score = 0;
const text = message.text.toLowerCase();
// 1. Keyword scoring
for (const keyword in KEYWORD_SCORES) {
if (text.includes(keyword)) {
score += KEYWORD_SCORES[keyword as keyof typeof KEYWORD_SCORES];
}
}
// 2. Question scoring
if (text.includes('?')) {
score += 5; // Questions are very important
}
// 3. Answer scoring (if it follows a question)
if (previousMessage && previousMessage.text?.includes('?')) {
score += 6; // Answers are even more important
}
// 4. Length scoring (longer messages are likely more detailed)
if (text.length > 100) {
score += 3;
} else if (text.length > 50) {
score += 2;
}
return score;
}
/**
* Generates a more intelligent summary of a chat history locally.
* @param messages The array of messages in the chat.
* @param currentUser The currently logged-in user.
* @returns A LocalSummary object.
*/
export function generateLocalSummary(messages: Message[], currentUser: User): LocalSummary {
if (messages.length === 0) {
return {
participants: [],
topic: 'No conversation yet',
summaryPoints: ['The chat is empty.'],
};
}
// 1. Identify Participants
const participantSet = new Set<string>();
messages.forEach(msg => {
if (msg.sender === currentUser.uid) {
participantSet.add(currentUser.displayName);
} else {
participantSet.add(msg.senderDisplayName);
}
});
const participants = Array.from(participantSet);
// 2. Score all messages for importance
const scoredMessages: ScoredMessage[] = [];
for (let i = 0; i < messages.length; i++) {
const message = messages[i];
const previousMessage = i > 0 ? messages[i - 1] : undefined;
scoredMessages.push({
message,
score: getMessageScore(message, previousMessage),
});
}
// Sort messages by score to find the most important ones
const sortedScoredMessages = scoredMessages.sort((a, b) => b.score - a.score);
// 3. Determine Topic from the most important messages
const wordCounts = new Map<string, number>();
const stopWords = new Set([...STOP_WORDS_EN, ...STOP_WORDS_AR]);
// Analyze top 5 most important messages for topic words
sortedScoredMessages.slice(0, 5).forEach(({ message }) => {
if (!message.text) return;
const words = message.text.toLowerCase().split(/\s+/);
words.forEach(word => {
const cleanWord = word.replace(/[.,!?"'()ุŸ]/g, '');
if (cleanWord.length > 3 && !stopWords.has(cleanWord)) {
wordCounts.set(cleanWord, (wordCounts.get(cleanWord) || 0) + 1);
}
});
});
const sortedWords = Array.from(wordCounts.entries()).sort((a, b) => b[1] - a[1]);
const topic = sortedWords.length > 0 ? sortedWords.slice(0, 2).map(entry => entry[0]).join(' & ') : 'General discussion';
// 4. Generate Summary Points from the top scored messages
const summaryPoints: string[] = [];
const usedMessageIds = new Set<string>();
// Take top 3-4 most important messages to form the summary
for (const scoredMsg of sortedScoredMessages) {
if (summaryPoints.length >= 4) break;
if (scoredMsg.score > 0 && scoredMsg.message.id && !usedMessageIds.has(scoredMsg.message.id)) {
const point = `${scoredMsg.message.senderDisplayName} discussed: "${scoredMsg.message.text!.substring(0, 50)}...".`;
summaryPoints.push(point);
usedMessageIds.add(scoredMsg.message.id);
}
}
// Fallback if no "important" messages were found
if (summaryPoints.length === 0 && messages.length > 0) {
const firstMessage = messages[0];
const lastMessage = messages[messages.length - 1];
summaryPoints.push(`Conversation started by ${firstMessage.senderDisplayName}.`);
if (lastMessage.id !== firstMessage.id) {
summaryPoints.push(`The last message was from ${lastMessage.senderDisplayName}.`);
}
}
return {
participants,
topic,
summaryPoints,
};
}