diff --git a/apps/web/components/CrispChat.tsx b/apps/web/components/CrispChat.tsx
index daff62f50e..67d2d30c96 100644
--- a/apps/web/components/CrispChat.tsx
+++ b/apps/web/components/CrispChat.tsx
@@ -8,6 +8,7 @@ const CrispChat = ({ email }: { email?: string }) => {
useEffect(() => {
if (env.NEXT_PUBLIC_CRISP_WEBSITE_ID) {
Crisp.configure(env.NEXT_PUBLIC_CRISP_WEBSITE_ID);
+ Crisp.setHideOnMobile(true);
if (email) Crisp.user.setEmail(email);
}
}, [email]);
diff --git a/apps/web/components/Toggle.tsx b/apps/web/components/Toggle.tsx
index 5823312034..9869e22a85 100644
--- a/apps/web/components/Toggle.tsx
+++ b/apps/web/components/Toggle.tsx
@@ -1,4 +1,4 @@
-import { Switch, SwitchGroup } from "@headlessui/react";
+import { Switch, Field } from "@headlessui/react";
import clsx from "clsx";
import type { FieldError } from "react-hook-form";
import { ErrorMessage, ExplainText, Label } from "./Input";
@@ -6,6 +6,7 @@ import { ErrorMessage, ExplainText, Label } from "./Input";
export interface ToggleProps {
name: string;
label?: string;
+ labelRight?: string;
enabled: boolean;
explainText?: string;
error?: FieldError;
@@ -13,11 +14,11 @@ export interface ToggleProps {
}
export const Toggle = (props: ToggleProps) => {
- const { label, enabled, onChange } = props;
+ const { label, labelRight, enabled, onChange } = props;
return (
-
+
{label && (
@@ -40,7 +41,12 @@ export const Toggle = (props: ToggleProps) => {
)}
/>
-
+ {labelRight && (
+
+
+
+ )}
+
{props.explainText ? (
{props.explainText}
) : null}
diff --git a/apps/web/utils/actions/ai-rule.ts b/apps/web/utils/actions/ai-rule.ts
index d7be451af3..34a17cfbfc 100644
--- a/apps/web/utils/actions/ai-rule.ts
+++ b/apps/web/utils/actions/ai-rule.ts
@@ -13,7 +13,7 @@ import { getGmailClient } from "@/utils/gmail/client";
import { aiCreateRule } from "@/utils/ai/rule/create-rule";
import {
runRulesOnMessage,
- testRulesOnMessage,
+ type RunRulesResult,
} from "@/utils/ai/choose-rule/run-rules";
import { emailToContent, parseMessage } from "@/utils/mail";
import { getMessage, getMessages } from "@/utils/gmail/message";
@@ -22,18 +22,17 @@ import {
createReceiptGroupAction,
} from "@/utils/actions/group";
import { GroupName } from "@/utils/config";
-import type { EmailForAction } from "@/utils/ai/actions";
import { executeAct } from "@/utils/ai/choose-rule/execute";
import { isDefined, type ParsedMessage } from "@/utils/types";
import { getSessionAndGmailClient } from "@/utils/actions/helpers";
-import { isActionError } from "@/utils/error";
+import { type ActionError, isActionError } from "@/utils/error";
import {
reportAiMistakeBody,
type ReportAiMistakeBody,
saveRulesPromptBody,
type SaveRulesPromptBody,
- testAiBody,
- type TestAiBody,
+ runRulesBody,
+ type RunRulesBody,
} from "@/utils/actions/validation";
import { aiPromptToRules } from "@/utils/ai/rule/prompt-to-rules";
import { aiDiffRules } from "@/utils/ai/rule/diff-rules";
@@ -50,7 +49,12 @@ const logger = createScopedLogger("ai-rule");
export const runRulesAction = withActionInstrumentation(
"runRules",
- async ({ email, force }: { email: EmailForAction; force: boolean }) => {
+ async (unsafeBody: RunRulesBody): Promise => {
+ const { success, data, error } = runRulesBody.safeParse(unsafeBody);
+ if (!success) return { error: error.message };
+
+ const { messageId, threadId, rerun, isTest } = data;
+
const sessionResult = await getSessionAndGmailClient();
if (isActionError(sessionResult)) return sessionResult;
const { gmail, user: u } = sessionResult;
@@ -72,75 +76,49 @@ export const runRulesAction = withActionInstrumentation(
});
if (!user?.email) return { error: "User email not found" };
- const [gmailMessage, hasExistingRule] = await Promise.all([
- getMessage(email.messageId, gmail, "full"),
- prisma.executedRule.findUnique({
- where: {
- unique_user_thread_message: {
- userId: user.id,
- threadId: email.threadId,
- messageId: email.messageId,
- },
- },
- select: { id: true },
- }),
+ const fetchExecutedRule = !isTest && !rerun;
+
+ const [gmailMessage, executedRule] = await Promise.all([
+ getMessage(messageId, gmail, "full"),
+ fetchExecutedRule
+ ? prisma.executedRule.findUnique({
+ where: {
+ unique_user_thread_message: {
+ userId: user.id,
+ threadId,
+ messageId,
+ },
+ },
+ select: {
+ id: true,
+ reason: true,
+ actionItems: true,
+ rule: true,
+ },
+ })
+ : null,
]);
- if (hasExistingRule && !force) {
+ if (executedRule) {
logger.info("Skipping. Rule already exists.", {
email: user.email,
- messageId: email.messageId,
- threadId: email.threadId,
+ messageId,
+ threadId,
});
- return;
- }
-
- const message = parseMessage(gmailMessage);
-
- await runRulesOnMessage({
- gmail,
- message,
- rules: user.rules,
- user: { ...user, email: user.email },
- isTest: false,
- });
- },
-);
-
-export const testAiAction = withActionInstrumentation(
- "testAi",
- async (unsafeBody: TestAiBody) => {
- const sessionResult = await getSessionAndGmailClient();
- if (isActionError(sessionResult)) return sessionResult;
-
- const { success, data, error } = testAiBody.safeParse(unsafeBody);
- if (!success) return { error: error.message };
- const { messageId } = data;
- const { gmail, user: u } = sessionResult;
-
- const user = await prisma.user.findUnique({
- where: { id: u.id },
- select: {
- id: true,
- email: true,
- about: true,
- aiProvider: true,
- aiModel: true,
- aiApiKey: true,
- rules: {
- where: { enabled: true },
- include: { actions: true, categoryFilters: true },
- },
- },
- });
- if (!user) return { error: "User not found" };
-
- const gmailMessage = await getMessage(messageId, gmail, "full");
+ return {
+ rule: executedRule.rule,
+ actionItems: executedRule.actionItems,
+ reason: executedRule.reason,
+ existing: true,
+ error: undefined,
+ };
+ }
const message = parseMessage(gmailMessage);
- const result = await testRulesOnMessage({
+ const result = await runRulesOnMessage({
+ isTest,
gmail,
message,
rules: user.rules,
@@ -175,7 +153,8 @@ export const testAiCustomContentAction = withActionInstrumentation(
});
if (!user) return { error: "User not found" };
- const result = await testRulesOnMessage({
+ const result = await runRulesOnMessage({
+ isTest: true,
gmail,
message: {
id: "testMessageId",
@@ -542,7 +521,10 @@ export const saveRulesPromptAction = withActionInstrumentation(
const { data, success, error } = saveRulesPromptBody.safeParse(unsafeData);
if (!success) {
- console.error("Input validation failed:", error.message);
+ logger.error("Input validation failed", {
+ email: session.user.email,
+ error: error.message,
+ });
return { error: error.message };
}
@@ -558,11 +540,11 @@ export const saveRulesPromptAction = withActionInstrumentation(
});
if (!user) {
- console.error("User not found");
+ logger.error("User not found");
return { error: "User not found" };
}
if (!user.email) {
- console.error("User email not found");
+ logger.error("User email not found");
return { error: "User email not found" };
}
@@ -706,7 +688,10 @@ export const saveRulesPromptAction = withActionInstrumentation(
for (const rule of editedRules) {
if (!rule.ruleId) {
- console.error(`Rule ID not found for rule. Prompt: ${rule.name}`);
+ logger.error("Rule ID not found for rule", {
+ email: user.email,
+ promptRule: rule.name,
+ });
continue;
}
diff --git a/apps/web/utils/actions/group.ts b/apps/web/utils/actions/group.ts
index 0300263e86..3219fc7cc8 100644
--- a/apps/web/utils/actions/group.ts
+++ b/apps/web/utils/actions/group.ts
@@ -104,23 +104,25 @@ async function generateGroupItemsFromPrompt(
});
await prisma.$transaction([
- ...result.senders.map((sender) =>
- prisma.groupItem.upsert({
- where: {
- groupId_type_value: {
- groupId,
+ ...result.senders
+ .filter((sender) => !sender.includes(user.email!))
+ .map((sender) =>
+ prisma.groupItem.upsert({
+ where: {
+ groupId_type_value: {
+ groupId,
+ type: GroupItemType.FROM,
+ value: sender,
+ },
+ },
+ update: {}, // No update needed if it exists
+ create: {
type: GroupItemType.FROM,
value: sender,
+ groupId,
},
- },
- update: {}, // No update needed if it exists
- create: {
- type: GroupItemType.FROM,
- value: sender,
- groupId,
- },
- }),
- ),
+ }),
+ ),
...result.subjects.map((subject) =>
prisma.groupItem.upsert({
where: {
@@ -159,7 +161,7 @@ export const createNewsletterGroupAction = withActionInstrumentation(
"createNewsletterGroup",
async () => {
const session = await auth();
- if (!session?.user.id) return { error: "Not logged in" };
+ if (!session?.user.email) return { error: "Not logged in" };
const name = GroupName.NEWSLETTER;
const existingGroup = await prisma.group.findFirst({
@@ -173,7 +175,11 @@ export const createNewsletterGroupAction = withActionInstrumentation(
if (!token.token) return { error: "No access token" };
- const newsletters = await findNewsletters(gmail, token.token);
+ const newsletters = await findNewsletters(
+ gmail,
+ token.token,
+ session.user.email,
+ );
const group = await prisma.group.create({
data: {
@@ -198,7 +204,7 @@ export const createReceiptGroupAction = withActionInstrumentation(
"createReceiptGroup",
async () => {
const session = await auth();
- if (!session?.user.id) return { error: "Not logged in" };
+ if (!session?.user.email) return { error: "Not logged in" };
const name = GroupName.RECEIPT;
const existingGroup = await prisma.group.findFirst({
@@ -212,7 +218,7 @@ export const createReceiptGroupAction = withActionInstrumentation(
if (!token.token) return { error: "No access token" };
- const receipts = await findReceipts(gmail, token.token);
+ const receipts = await findReceipts(gmail, token.token, session.user.email);
const group = await prisma.group.create({
data: {
@@ -241,7 +247,7 @@ export const regenerateGroupAction = withActionInstrumentation(
"regenerateGroup",
async (groupId: string) => {
const session = await auth();
- if (!session?.user.id) return { error: "Not logged in" };
+ if (!session?.user.email) return { error: "Not logged in" };
const existingGroup = await prisma.group.findUnique({
where: { id: groupId, userId: session.user.id },
@@ -260,9 +266,19 @@ export const regenerateGroupAction = withActionInstrumentation(
if (!token.token) return { error: "No access token" };
if (existingGroup.name === GroupName.NEWSLETTER) {
- await regenerateNewsletterGroup(existingGroup, gmail, token.token);
+ await regenerateNewsletterGroup(
+ existingGroup,
+ gmail,
+ token.token,
+ session.user.email,
+ );
} else if (existingGroup.name === GroupName.RECEIPT) {
- await regenerateReceiptGroup(existingGroup, gmail, token.token);
+ await regenerateReceiptGroup(
+ existingGroup,
+ gmail,
+ token.token,
+ session.user.email,
+ );
} else if (existingGroup.prompt) {
const user = await prisma.user.findUnique({
where: { id: session.user.id },
@@ -295,8 +311,9 @@ async function regenerateNewsletterGroup(
existingGroup: ExistingGroup,
gmail: gmail_v1.Gmail,
token: string,
+ userEmail: string,
) {
- const newsletters = await findNewsletters(gmail, token);
+ const newsletters = await findNewsletters(gmail, token, userEmail);
const items = newsletters.map((item) => ({
type: GroupItemType.FROM,
@@ -314,8 +331,9 @@ async function regenerateReceiptGroup(
existingGroup: ExistingGroup,
gmail: gmail_v1.Gmail,
token: string,
+ userEmail: string,
) {
- const receipts = await findReceipts(gmail, token);
+ const receipts = await findReceipts(gmail, token, userEmail);
const newItems = filterOutExisting(receipts, existingGroup.items);
await createGroupItems(
diff --git a/apps/web/utils/actions/validation.ts b/apps/web/utils/actions/validation.ts
index 3df5f30e49..4de8390381 100644
--- a/apps/web/utils/actions/validation.ts
+++ b/apps/web/utils/actions/validation.ts
@@ -159,6 +159,14 @@ export type RulesExamplesBody = z.infer;
export const testAiBody = z.object({ messageId: z.string() });
export type TestAiBody = z.infer;
+export const runRulesBody = z.object({
+ messageId: z.string(),
+ threadId: z.string(),
+ rerun: z.boolean().nullish(),
+ isTest: z.boolean(),
+});
+export type RunRulesBody = z.infer;
+
export const reportAiMistakeBody = z
.object({
email: z.object({
diff --git a/apps/web/utils/ai/choose-rule/run-rules.ts b/apps/web/utils/ai/choose-rule/run-rules.ts
index 3aeb017a27..861ed15de0 100644
--- a/apps/web/utils/ai/choose-rule/run-rules.ts
+++ b/apps/web/utils/ai/choose-rule/run-rules.ts
@@ -20,10 +20,11 @@ import { createScopedLogger } from "@/utils/logger";
const logger = createScopedLogger("ai-run-rules");
-export type TestResult = {
+export type RunRulesResult = {
rule?: Rule | null;
actionItems?: ActionItem[];
reason?: string | null;
+ existing?: boolean;
};
export async function runRulesOnMessage({
@@ -38,7 +39,7 @@ export async function runRulesOnMessage({
rules: RuleWithActionsAndCategories[];
user: Pick & UserAIFields;
isTest: boolean;
-}) {
+}): Promise {
const result = await findMatchingRule(rules, message, user);
if (result.rule) {
return await runRule(
@@ -135,28 +136,6 @@ async function saveSkippedExecutedRule({
});
}
-export async function testRulesOnMessage({
- gmail,
- message,
- rules,
- user,
-}: {
- gmail: gmail_v1.Gmail;
- message: ParsedMessage;
- rules: RuleWithActionsAndCategories[];
- user: Pick & UserAIFields;
-}): Promise {
- const result = await runRulesOnMessage({
- gmail,
- message,
- rules,
- user,
- isTest: true,
- });
-
- return result;
-}
-
async function saveExecutedRule(
{
userId,
diff --git a/apps/web/utils/ai/group/find-newsletters.ts b/apps/web/utils/ai/group/find-newsletters.ts
index c842dd0095..5f9bd6df7f 100644
--- a/apps/web/utils/ai/group/find-newsletters.ts
+++ b/apps/web/utils/ai/group/find-newsletters.ts
@@ -12,6 +12,7 @@ const ignoreList = ["@github.com", "@google.com", "@gmail.com", "@slack.com"];
export async function findNewsletters(
gmail: gmail_v1.Gmail,
accessToken: string,
+ userEmail: string,
) {
const messages = await queryBatchMessagesPages(gmail, accessToken, {
query: "newsletter",
@@ -25,7 +26,11 @@ export async function findNewsletters(
return uniq(
[...messages, ...messages2]
.map((message) => message.headers.from)
- .filter((from) => !ignoreList.find((ignore) => from.includes(ignore))),
+ .filter(
+ (from) =>
+ !ignoreList.find((ignore) => from.includes(ignore)) &&
+ !from.includes(userEmail),
+ ),
);
}
diff --git a/apps/web/utils/ai/group/find-receipts.ts b/apps/web/utils/ai/group/find-receipts.ts
index 272a4aacbd..11127e99ae 100644
--- a/apps/web/utils/ai/group/find-receipts.ts
+++ b/apps/web/utils/ai/group/find-receipts.ts
@@ -36,7 +36,11 @@ const defaultReceiptSubjects = [
];
// Find additional receipts from the user's inbox that don't match the predefined lists
-export async function findReceipts(gmail: gmail_v1.Gmail, accessToken: string) {
+export async function findReceipts(
+ gmail: gmail_v1.Gmail,
+ accessToken: string,
+ userEmail: string,
+) {
const senders = await findReceiptSenders(gmail, accessToken);
const subjects = await findReceiptSubjects(gmail, accessToken);
@@ -49,7 +53,7 @@ export async function findReceipts(gmail: gmail_v1.Gmail, accessToken: string) {
type: GroupItemType.FROM,
value: sender,
})),
- ),
+ ) && !sender.includes(userEmail),
);
const sendersList = uniq([...filteredSenders, ...defaultReceiptSenders]);
diff --git a/apps/web/utils/queue/email-actions.ts b/apps/web/utils/queue/email-actions.ts
index c188e70ecc..ca69b8a190 100644
--- a/apps/web/utils/queue/email-actions.ts
+++ b/apps/web/utils/queue/email-actions.ts
@@ -1,39 +1,27 @@
"use client";
import { runRulesAction } from "@/utils/actions/ai-rule";
-import type { EmailForAction } from "@/utils/ai/actions";
import { pushToAiQueueAtom, removeFromAiQueueAtom } from "@/store/ai-queue";
import type { Thread } from "@/components/email-list/types";
import { isDefined } from "@/utils/types";
import { aiQueue } from "@/utils/queue/ai-queue";
-export const runAiRules = async (threadsArray: Thread[], force: boolean) => {
+export const runAiRules = async (threadsArray: Thread[], rerun: boolean) => {
const threads = threadsArray.filter(isDefined);
const threadIds = threads.map((t) => t.id);
pushToAiQueueAtom(threadIds);
aiQueue.addAll(
threads.map((thread) => async () => {
- const message = threadToRunRulesEmail(thread);
+ const message = thread.messages?.[thread.messages.length - 1];
if (!message) return;
- await runRulesAction({ email: message, force });
+ await runRulesAction({
+ messageId: message.id,
+ threadId: thread.id,
+ rerun,
+ isTest: false,
+ });
removeFromAiQueueAtom(thread.id);
}),
);
};
-
-function threadToRunRulesEmail(thread: Thread): EmailForAction | undefined {
- const message = thread.messages?.[thread.messages.length - 1];
- if (!message) return;
- const email: EmailForAction = {
- from: message.headers.from,
- replyTo: message.headers["reply-to"],
- subject: message.headers.subject,
- threadId: message.threadId || "",
- messageId: message.id || "",
- headerMessageId: message.headers["message-id"] || "",
- references: message.headers.references,
- };
-
- return email;
-}