agentic.email/frontend/src/utils/inboxSignals.js
2026-10-04 00:41:19 +05:30

274 lines
9.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { senderDisplayName } from "./senderAvatar";
const CATEGORY_ORDER = ["Primary", "Updates", "Promotions", "Social", "Finance"];
export function emailSearchText(email) {
const sender = typeof email.sender === "string"
? email.sender
: email.sender?.name || email.sender?.email || email.sender?.address || "";
return [
sender,
email.subject,
email.snippet,
email.preview,
email.body_text,
].filter(Boolean).join(" ").toLowerCase();
}
export function emailCategory(email) {
const text = emailSearchText(email);
if (/invoice|bill|payment|receipt|finance|bank|transfer|otp|wallet/.test(text)) return "Finance";
if (/linkedin|twitter|facebook|instagram|connection request|social/.test(text)) return "Social";
if (/unsubscribe|promo|offer|sale|newsletter|deal|discount/.test(text)) return "Promotions";
if (/notification|update|alert|noreply|no-reply|provisioned|activated|console|security/.test(text)) return "Updates";
return "Primary";
}
const PRIORITY_LABELS = new Set(["Important", "Clients", "Projects", "Finance", "HR"]);
const ACTION_LABELS = new Set(["Clients", "Important", "Projects", "Finance", "HR", "AI"]);
export function emailLabels(email) {
const text = emailSearchText(email);
const labels = [];
if (email.is_starred || /urgent|important|asap|priority/.test(text)) {
labels.push({ name: "Important", tone: "rose" });
}
if (/project|proposal|website|development|sow|contract/.test(text)) {
labels.push({ name: "Projects", tone: "violet" });
}
if (/client|customer|internship|candidate|revdau|desi clans/.test(text)) {
labels.push({ name: "Clients", tone: "blue" });
}
if (/hr|hiring|recruit|offer letter|onboarding/.test(text)) {
labels.push({ name: "HR", tone: "violet" });
}
if (/invoice|bill|payment|finance|receipt/.test(text)) {
labels.push({ name: "Finance", tone: "amber" });
}
if (/integration|api|webhook|connect/.test(text)) {
labels.push({ name: "Integrations", tone: "green" });
}
if (/ai|agent|llm|gpt|model/.test(text)) {
labels.push({ name: "AI", tone: "pink" });
}
if (/domain|dns|mx|spf|dkim|\.env|environment variable/.test(text)) {
labels.push({ name: "Domains", tone: "amber" });
}
if (/linkedin|twitter|facebook|instagram|social/.test(text)) {
labels.push({ name: "Social", tone: "green" });
}
if (/notification|update|alert|noreply|security/.test(text)) {
labels.push({ name: "Updates", tone: "blue" });
}
if (/personal|family/.test(text)) {
labels.push({ name: "Personal", tone: "green" });
}
const unique = [];
labels.forEach((label) => {
if (!unique.some((item) => item.name === label.name)) unique.push(label);
});
return unique.slice(0, 2);
}
export function matchesLens(email, lens) {
if (!lens || lens === "all") return true;
if (lens === "starred") return email.is_starred === true;
return emailLabels(email).some((label) => label.name === lens);
}
export function categoryCounts(emails) {
const counts = Object.fromEntries(CATEGORY_ORDER.map((name) => [name, 0]));
emails.forEach((email) => {
if (email.is_read === false) counts[emailCategory(email)] += 1;
});
return counts;
}
function hasPriorityLabel(email) {
return emailLabels(email).some((label) => PRIORITY_LABELS.has(label.name));
}
function hasActionLabel(email) {
return emailLabels(email).some((label) => ACTION_LABELS.has(label.name));
}
function looksActionable(email) {
const text = emailSearchText(email);
return /please|action|reply|review|attached|confirm|need|waiting|\?|follow up|response/.test(text)
|| attachmentCount(email) > 0
|| /https?:\/\//.test(text);
}
export function inboxIntelligence(emails) {
const unread = emails.filter((email) => email.is_read === false);
const important = emails.filter((email) =>
email.is_starred
|| email.is_read === false
|| hasPriorityLabel(email)
|| emailLabels(email).length > 0,
);
const actionItems = emails.filter((email) =>
looksActionable(email) || hasActionLabel(email) || email.is_read === false,
);
const meetings = emails.filter((email) =>
/meet|meeting|calendar|schedule|call|zoom|interview|sync|demo|standup/.test(emailSearchText(email)),
);
const invoices = emails.filter((email) =>
/invoice|bill|payment|receipt|purchase order|\bpo\b/.test(emailSearchText(email)),
);
return {
unread: unread.length,
important: important.length,
actionItems: actionItems.length,
meetings: meetings.length,
invoices: invoices.length,
importantFromClients: important.filter((email) =>
emailLabels(email).some((label) => label.name === "Clients"),
).length,
};
}
export function attachmentCount(email) {
if (Array.isArray(email.attachments)) return email.attachments.length;
return Number(email.attachment_count || 0);
}
function emailTimestamp(email) {
const value = email.received_at || email.sent_at || email.created_at || email.updated_at;
if (!value) return null;
const date = new Date(value);
return Number.isNaN(date.getTime()) ? null : date;
}
function formatRelativeAge(date) {
if (!date) return "";
const hours = Math.floor((Date.now() - date.getTime()) / 3600000);
if (hours < 1) return "<1h";
if (hours < 24) return `${hours}h`;
return `${Math.floor(hours / 24)}d`;
}
function extractMeetingTime(text) {
const match = text.match(/\b(\d{1,2}:\d{2})\s*(am|pm)?/i)
|| text.match(/\b(\d{1,2})\s*(am|pm)\b/i);
if (!match) return null;
const raw = match[0].replace(/\s+/g, " ").trim();
if (/am|pm/i.test(raw)) return raw.toUpperCase();
const [hours, minutes] = raw.split(":").map(Number);
if (Number.isNaN(hours)) return raw;
const period = hours >= 12 ? "PM" : "AM";
const hour12 = hours % 12 || 12;
return `${hour12}:${String(minutes || 0).padStart(2, "0")} ${period}`;
}
function extractVenue(text) {
if (/google meet|meet\.google/i.test(text)) return "Google Meet";
if (/zoom/i.test(text)) return "Zoom";
if (/teams|microsoft teams/i.test(text)) return "Teams";
if (/video call|video meeting/i.test(text)) return "Video call";
return "";
}
function extractReplyStatus(text) {
if (/confirm|confirmation|awaiting/.test(text)) return "Awaiting confirmation";
if (/proposal|quote/.test(text)) return "Proposal reply";
if (/invoice|payment|bill/.test(text)) return "Payment follow-up";
if (/review|feedback/.test(text)) return "Needs review";
if (/reply|respond/.test(text)) return "Needs reply";
return "Needs attention";
}
function extractDeadline(text) {
if (/tomorrow/i.test(text)) return "Tomorrow";
const weekday = text.match(/\b(monday|tuesday|wednesday|thursday|friday|saturday|sunday)\b/i);
if (weekday) {
return weekday[1].charAt(0).toUpperCase() + weekday[1].slice(1);
}
if (/today/i.test(text)) return "Today";
return null;
}
export function inboxContext(emails) {
const stats = inboxIntelligence(emails);
const needsReply = emails
.filter((email) =>
email.is_read === false
|| looksActionable(email)
|| hasActionLabel(email)
|| /please|reply|respond|confirm|review|attached|need your|waiting|action required/.test(
emailSearchText(email),
),
)
.sort((left, right) => {
const leftDate = emailTimestamp(left)?.getTime() || 0;
const rightDate = emailTimestamp(right)?.getTime() || 0;
return rightDate - leftDate;
})
.slice(0, 4)
.map((email) => ({
id: email.id,
name: senderDisplayName(email),
age: formatRelativeAge(emailTimestamp(email)),
status: extractReplyStatus(emailSearchText(email)),
}));
const today = [];
emails.forEach((email) => {
const text = emailSearchText(email);
if (!/meet|meeting|call|interview|sync|calendar/.test(text)) return;
const time = extractMeetingTime(text);
if (!time) return;
const label = (email.subject || "Meeting").trim().slice(0, 42);
const venue = extractVenue(text);
const tone = /invoice|payment|bill|purchase order|\bpo\b/.test(text)
? "amber"
: /sync|project/.test(text)
? "rose"
: /client|demo/.test(text)
? "blue"
: "green";
if (!today.some((item) => item.time === time && item.label === label)) {
today.push({ time, label, venue, tone });
}
});
const deadlines = [];
emails.forEach((email) => {
const text = emailSearchText(email);
if (!/invoice|proposal|payment|due|deadline|bill/.test(text)) return;
const due = extractDeadline(text);
if (!due) return;
const label = (email.subject || "Deadline")
.split(/[-–:|]/)
.map((part) => part.trim())
.find(Boolean)
?.slice(0, 28) || "Deadline";
if (!deadlines.some((item) => item.label === label && item.due === due)) {
deadlines.push({ label, due });
}
});
return {
stats,
needsReply,
today: today.slice(0, 4),
deadlines: deadlines.slice(0, 4),
};
}
export function labelCounts(emails) {
const counts = {
Important: 0,
Projects: 0,
Clients: 0,
Finance: 0,
Personal: 0,
};
emails.forEach((email) => {
emailLabels(email).forEach((label) => {
counts[label.name] = (counts[label.name] || 0) + 1;
});
});
return counts;
}