From 73de0918d85ea7cbf16d13313b7c0d5421073d7e Mon Sep 17 00:00:00 2001
From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com>
Date: Wed, 21 May 2025 16:01:53 +0300
Subject: [PATCH 1/8] Chat history
---
.../automation/AssistantTabs.tsx | 2 +-
apps/web/app/api/chat/route.ts | 180 ++++++++++++++++--
apps/web/app/api/chats/[chatId]/route.ts | 51 +++++
apps/web/app/api/chats/route.ts | 20 ++
apps/web/components/assistant-chat/chat.tsx | 71 +++++++
apps/web/hooks/useChat.ts | 6 +
apps/web/hooks/useChats.ts | 6 +
.../20250521104911_chat/migration.sql | 33 ++++
apps/web/prisma/schema.prisma | 49 ++++-
apps/web/utils/ai/assistant/chat.ts | 62 +-----
apps/web/utils/llms/index.ts | 9 +-
11 files changed, 409 insertions(+), 80 deletions(-)
create mode 100644 apps/web/app/api/chats/[chatId]/route.ts
create mode 100644 apps/web/app/api/chats/route.ts
create mode 100644 apps/web/hooks/useChat.ts
create mode 100644 apps/web/hooks/useChats.ts
create mode 100644 apps/web/prisma/migrations/20250521104911_chat/migration.sql
diff --git a/apps/web/app/(app)/[emailAccountId]/automation/AssistantTabs.tsx b/apps/web/app/(app)/[emailAccountId]/automation/AssistantTabs.tsx
index fc1dbe6006..3e446b912a 100644
--- a/apps/web/app/(app)/[emailAccountId]/automation/AssistantTabs.tsx
+++ b/apps/web/app/(app)/[emailAccountId]/automation/AssistantTabs.tsx
@@ -58,7 +58,7 @@ export function AssistantTabs() {
Select a tab or chat with your AI assistant to explain how it
- should handle your incoming emails
+ should handle incoming emails
diff --git a/apps/web/app/api/chat/route.ts b/apps/web/app/api/chat/route.ts
index 67201f1427..32b88b83f3 100644
--- a/apps/web/app/api/chat/route.ts
+++ b/apps/web/app/api/chat/route.ts
@@ -3,16 +3,37 @@ import { withEmailAccount } from "@/utils/middleware";
import { getEmailAccountWithAi } from "@/utils/user/get";
import { NextResponse } from "next/server";
import { aiProcessAssistantChat } from "@/utils/ai/assistant/chat";
+import { createScopedLogger } from "@/utils/logger";
+import prisma from "@/utils/prisma";
+import { appendClientMessage } from "ai";
export const maxDuration = 120;
-const assistantInputSchema = z.object({
- messages: z.array(
- z.object({
- role: z.enum(["user", "assistant"]),
- content: z.string(),
- }),
- ),
+const logger = createScopedLogger("api/chat");
+
+const textPartSchema = z.object({
+ text: z.string().min(1).max(2000),
+ type: z.enum(["text"]),
+});
+
+export const assistantInputSchema = z.object({
+ chatId: z.string().uuid(),
+ message: z.object({
+ id: z.string().uuid(),
+ createdAt: z.coerce.date(),
+ role: z.enum(["user"]),
+ content: z.string().min(1).max(2000),
+ parts: z.array(textPartSchema),
+ // experimental_attachments: z
+ // .array(
+ // z.object({
+ // url: z.string().url(),
+ // name: z.string().min(1).max(2000),
+ // contentType: z.enum(["image/png", "image/jpg", "image/jpeg"]),
+ // }),
+ // )
+ // .optional(),
+ }),
});
export const POST = withEmailAccount(async (request) => {
@@ -23,17 +44,154 @@ export const POST = withEmailAccount(async (request) => {
if (!user) return NextResponse.json({ error: "Not authenticated" });
const json = await request.json();
- const body = assistantInputSchema.safeParse(json);
+ const { data, error } = assistantInputSchema.safeParse(json);
+
+ if (error)
+ return NextResponse.json({ error: error.message }, { status: 400 });
+
+ const chat = data.chatId
+ ? await getChatById(data.chatId)
+ : await createNewChat(emailAccountId);
+
+ if (!chat) {
+ return NextResponse.json(
+ { error: "Failed to get or create chat" },
+ { status: 500 },
+ );
+ }
- if (body.error) {
- return NextResponse.json({ error: body.error.message }, { status: 400 });
+ if (chat.emailAccountId !== emailAccountId) {
+ return NextResponse.json(
+ { error: "You are not authorized to access this chat" },
+ { status: 403 },
+ );
}
+ const { message } = data;
+ const mappedDbMessages = chat.messages.map((dbMsg) => {
+ return {
+ ...dbMsg,
+ role: convertDbRoleToSdkRole(dbMsg.role),
+ };
+ });
+
+ const messages = appendClientMessage({
+ messages: mappedDbMessages,
+ message,
+ });
+
const result = await aiProcessAssistantChat({
- messages: body.data.messages,
+ messages,
emailAccountId,
user,
+ onFinish: (messages: any) => {
+ saveChatMessages({
+ messagesToSave: messages,
+ chatId: chat.id,
+ });
+ },
});
return result.toDataStreamResponse();
});
+
+async function createNewChat(emailAccountId: string) {
+ try {
+ const newChat = await prisma.chat.create({
+ data: { emailAccountId },
+ include: { messages: true },
+ });
+ logger.info("New chat created", { chatId: newChat.id, emailAccountId });
+ return newChat;
+ } catch (error) {
+ logger.error("Failed to create new chat", { error, emailAccountId });
+ return undefined;
+ }
+}
+
+async function saveChatMessages({
+ messagesToSave,
+ chatId,
+}: {
+ messagesToSave: {
+ role: "user" | "assistant" | "system" | "tool";
+ content: string;
+ attachments?: any[];
+ metadata?: Record;
+ }[];
+ chatId: string;
+}): Promise {
+ if (!chatId) {
+ logger.error("saveChatMessages called without a chatId.");
+ return false;
+ }
+ if (!messagesToSave || messagesToSave.length === 0) {
+ logger.warn("saveChatMessages called with no messages to save.", {
+ chatId,
+ });
+ return true; // No messages to save is not an error in this context
+ }
+
+ try {
+ const prismaMessagesData = messagesToSave.map((msg) => {
+ const mappedRole = msg.role.toUpperCase();
+
+ const attachmentsToSave =
+ msg.attachments && msg.attachments.length > 0
+ ? msg.attachments
+ : undefined;
+ const metadataToSave =
+ msg.metadata && Object.keys(msg.metadata).length > 0
+ ? msg.metadata
+ : undefined;
+
+ return {
+ chatId: chatId,
+ role: mappedRole,
+ content: msg.content,
+ attachments: attachmentsToSave,
+ metadata: metadataToSave,
+ };
+ });
+
+ await prisma.chatMessage.createMany({
+ data: prismaMessagesData,
+ skipDuplicates: false, // If a message might be re-processed, this could be true if unique IDs are part of input
+ });
+
+ logger.info("Successfully saved chat messages", { chatId });
+ return true;
+ } catch (error) {
+ logger.error("Failed to save chat messages to DB", {
+ error,
+ chatId: chatId,
+ numberOfMessages: messagesToSave.length,
+ });
+ return false;
+ }
+}
+
+async function getChatById(chatId: string) {
+ const chat = await prisma.chat.findUnique({
+ where: { id: chatId },
+ include: { messages: true },
+ });
+ return chat;
+}
+
+function convertDbRoleToSdkRole(
+ role: string,
+): "user" | "assistant" | "system" | "data" {
+ switch (role) {
+ case "user":
+ return "user";
+ case "assistant":
+ return "assistant";
+ case "system":
+ return "system";
+ case "data":
+ return "data";
+ default:
+ return "assistant";
+ }
+}
diff --git a/apps/web/app/api/chats/[chatId]/route.ts b/apps/web/app/api/chats/[chatId]/route.ts
new file mode 100644
index 0000000000..3b79690a6b
--- /dev/null
+++ b/apps/web/app/api/chats/[chatId]/route.ts
@@ -0,0 +1,51 @@
+import { NextResponse } from "next/server";
+import prisma from "@/utils/prisma";
+import { withEmailAccount } from "@/utils/middleware";
+
+export type GetChatResponse = Awaited>;
+
+export const GET = withEmailAccount(async (request, { params }) => {
+ const { emailAccountId } = request.auth;
+ const { chatId } = await params;
+
+ if (!chatId) {
+ return NextResponse.json(
+ { error: "Chat ID is required." },
+ { status: 400 },
+ );
+ }
+
+ const chat = await getChat({ chatId, emailAccountId });
+
+ if (!chat) {
+ return NextResponse.json(
+ { error: "Chat not found or access denied." },
+ { status: 404 },
+ );
+ }
+ return NextResponse.json(chat);
+});
+
+async function getChat({
+ chatId,
+ emailAccountId,
+}: {
+ chatId: string;
+ emailAccountId: string;
+}) {
+ const chat = await prisma.chat.findUnique({
+ where: {
+ id: chatId,
+ emailAccountId,
+ },
+ include: {
+ messages: {
+ orderBy: {
+ createdAt: "asc",
+ },
+ },
+ },
+ });
+
+ return chat;
+}
diff --git a/apps/web/app/api/chats/route.ts b/apps/web/app/api/chats/route.ts
new file mode 100644
index 0000000000..451ee0e713
--- /dev/null
+++ b/apps/web/app/api/chats/route.ts
@@ -0,0 +1,20 @@
+import { NextResponse } from "next/server";
+import prisma from "@/utils/prisma";
+import { withEmailAccount } from "@/utils/middleware";
+
+export type GetChatsResponse = Awaited>;
+
+export const GET = withEmailAccount(async (request) => {
+ const emailAccountId = request.auth.emailAccountId;
+ const result = await getChats({ emailAccountId });
+ return NextResponse.json(result);
+});
+
+async function getChats({ emailAccountId }: { emailAccountId: string }) {
+ const chats = await prisma.chat.findMany({
+ where: { emailAccountId },
+ orderBy: { updatedAt: "desc" },
+ });
+
+ return { chats };
+}
diff --git a/apps/web/components/assistant-chat/chat.tsx b/apps/web/components/assistant-chat/chat.tsx
index 5e1d2881cd..bd035fe0c8 100644
--- a/apps/web/components/assistant-chat/chat.tsx
+++ b/apps/web/components/assistant-chat/chat.tsx
@@ -1,10 +1,13 @@
"use client";
import type React from "react";
+import { useState } from "react";
import { type ScopedMutator, SWRConfig, useSWRConfig } from "swr";
import type { UIMessage } from "ai";
import { useChat } from "@ai-sdk/react";
import { toast } from "sonner";
+import { HistoryIcon, Loader2 } from "lucide-react";
+import { useQueryState } from "nuqs";
import { MultimodalInput } from "@/components/assistant-chat/multimodal-input";
import { Messages } from "./messages";
import { EMAIL_ACCOUNT_HEADER } from "@/utils/config";
@@ -14,6 +17,15 @@ import { ResizablePanel } from "@/components/ui/resizable";
import { AssistantTabs } from "@/app/(app)/[emailAccountId]/automation/AssistantTabs";
import { ChatProvider } from "./ChatContext";
import { SWRProvider } from "@/providers/SWRProvider";
+import { Button } from "@/components/ui/button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { useChats } from "@/hooks/useChats";
+import { LoadingContent } from "@/components/LoadingContent";
// Some mega hacky code used here to workaround AI SDK's use of SWR
// AI SDK uses SWR too and this messes with the global SWR config
@@ -108,6 +120,9 @@ function ChatUI({
return (
+
+
+
+
+ setShouldLoadChats(true)}
+ >
+
+ Chat History
+
+
+
+
+
+ Loading chats...
+
+ }
+ errorComponent={
+ Error loading chats
+ }
+ >
+ {data && data.chats.length > 0 ? (
+ data.chats.map((chatItem) => (
+ {
+ setChatId(chatItem.id);
+ }}
+ >
+ {`Chat from ${new Date(chatItem.createdAt).toLocaleString()}`}
+
+ ))
+ ) : (
+
+ No previous chats found
+
+ )}
+
+
+
+ );
+}
+
// NOTE: not sure why we don't just use the default from AI SDK
function generateUUID(): string {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
diff --git a/apps/web/hooks/useChat.ts b/apps/web/hooks/useChat.ts
new file mode 100644
index 0000000000..c9044d1608
--- /dev/null
+++ b/apps/web/hooks/useChat.ts
@@ -0,0 +1,6 @@
+import useSWR from "swr";
+import type { GetChatResponse } from "@/app/api/chats/[chatId]/route";
+
+export function useChat(chatId?: string) {
+ return useSWR(chatId ? `/api/chats/${chatId}` : null);
+}
diff --git a/apps/web/hooks/useChats.ts b/apps/web/hooks/useChats.ts
new file mode 100644
index 0000000000..81b4badaf3
--- /dev/null
+++ b/apps/web/hooks/useChats.ts
@@ -0,0 +1,6 @@
+import useSWR from "swr";
+import type { GetChatsResponse } from "@/app/api/chats/route";
+
+export function useChats(shouldFetch = true) {
+ return useSWR(shouldFetch ? "/api/chats" : null);
+}
diff --git a/apps/web/prisma/migrations/20250521104911_chat/migration.sql b/apps/web/prisma/migrations/20250521104911_chat/migration.sql
new file mode 100644
index 0000000000..c2273db331
--- /dev/null
+++ b/apps/web/prisma/migrations/20250521104911_chat/migration.sql
@@ -0,0 +1,33 @@
+-- CreateTable
+CREATE TABLE "Chat" (
+ "id" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "emailAccountId" TEXT NOT NULL,
+
+ CONSTRAINT "Chat_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "ChatMessage" (
+ "id" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "role" TEXT NOT NULL,
+ "content" TEXT NOT NULL,
+ "chatId" TEXT NOT NULL,
+
+ CONSTRAINT "ChatMessage_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE INDEX "Chat_emailAccountId_idx" ON "Chat"("emailAccountId");
+
+-- CreateIndex
+CREATE INDEX "ChatMessage_chatId_idx" ON "ChatMessage"("chatId");
+
+-- AddForeignKey
+ALTER TABLE "Chat" ADD CONSTRAINT "Chat_emailAccountId_fkey" FOREIGN KEY ("emailAccountId") REFERENCES "EmailAccount"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "ChatMessage" ADD CONSTRAINT "ChatMessage_chatId_fkey" FOREIGN KEY ("chatId") REFERENCES "Chat"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/apps/web/prisma/schema.prisma b/apps/web/prisma/schema.prisma
index cc4db7bbb0..bcc0c94218 100644
--- a/apps/web/prisma/schema.prisma
+++ b/apps/web/prisma/schema.prisma
@@ -52,18 +52,18 @@ model User {
sessions Session[]
// additional fields
- completedOnboardingAt DateTime? // questions about the user. e.g. their role
- completedAppOnboardingAt DateTime? // how to use the app
- onboardingAnswers Json?
- lastLogin DateTime?
- utms Json?
- errorMessages Json? // eg. user set incorrect AI API key
+ completedOnboardingAt DateTime? // questions about the user. e.g. their role
+ completedAppOnboardingAt DateTime? // how to use the app
+ onboardingAnswers Json?
+ lastLogin DateTime?
+ utms Json?
+ errorMessages Json? // eg. user set incorrect AI API key
// settings
- aiProvider String?
- aiModel String?
- aiApiKey String?
- webhookSecret String?
+ aiProvider String?
+ aiModel String?
+ aiApiKey String?
+ webhookSecret String?
// premium can be shared among multiple users
premiumId String?
@@ -121,6 +121,7 @@ model EmailAccount {
emailMessages EmailMessage[]
emailTokens EmailToken[]
knowledge Knowledge[]
+ chats Chat[]
@@index([lastSummaryEmailAt])
}
@@ -565,6 +566,34 @@ model DraftSendLog {
@@index([executedActionId])
}
+model Chat {
+ id String @id @default(cuid())
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ messages ChatMessage[]
+ emailAccountId String
+ emailAccount EmailAccount @relation(fields: [emailAccountId], references: [id], onDelete: Cascade)
+
+ @@index([emailAccountId])
+}
+
+model ChatMessage {
+ id String @id @default(cuid())
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ role String
+ content String
+ // attachments Json? // For storing message attachments if any
+ // metadata Json? // For any additional metadata, like tool calls
+
+ chatId String
+ chat Chat @relation(fields: [chatId], references: [id], onDelete: Cascade)
+
+ @@index([chatId])
+}
+
enum ActionType {
ARCHIVE
LABEL
diff --git a/apps/web/utils/ai/assistant/chat.ts b/apps/web/utils/ai/assistant/chat.ts
index e4021f98b7..b72c72d6a6 100644
--- a/apps/web/utils/ai/assistant/chat.ts
+++ b/apps/web/utils/ai/assistant/chat.ts
@@ -1,4 +1,4 @@
-import { tool } from "ai";
+import { type Message, tool } from "ai";
import { z } from "zod";
import { createScopedLogger } from "@/utils/logger";
import { createRuleSchema } from "@/utils/ai/rule/create-rule-schema";
@@ -95,59 +95,6 @@ export type UpdateLearnedPatternsSchema = z.infer<
typeof updateLearnedPatternsSchema
>;
-// // Keeping the original schema for backward compatibility
-// const updateRuleSchema = z.object({
-// ruleName: z.string().describe("The name of the rule to update"),
-// condition: z
-// .object({
-// aiInstructions: z.string(),
-// static: z.object({
-// from: z.string(),
-// to: z.string(),
-// subject: z.string(),
-// body: z.string(),
-// }),
-// conditionalOperator: z.enum([LogicalOperator.AND, LogicalOperator.OR]),
-// })
-// .optional(),
-// actions: z.array(
-// z
-// .object({
-// type: z.enum([
-// ActionType.ARCHIVE,
-// ActionType.LABEL,
-// ActionType.REPLY,
-// ActionType.SEND_EMAIL,
-// ActionType.FORWARD,
-// ActionType.MARK_READ,
-// ActionType.MARK_SPAM,
-// ActionType.CALL_WEBHOOK,
-// ]),
-// fields: z.object({
-// label: z.string().optional(),
-// content: z.string().optional(),
-// webhookUrl: z.string().optional(),
-// }),
-// })
-// .optional(),
-// ),
-// learnedPatterns: z
-// .array(
-// z.object({
-// include: z.object({
-// from: z.string(),
-// subject: z.string(),
-// }),
-// exclude: z.object({
-// from: z.string(),
-// subject: z.string(),
-// }),
-// }),
-// )
-// .optional(),
-// });
-// export type UpdateRuleSchema = z.infer;
-
const updateAboutSchema = z.object({ about: z.string() });
export type UpdateAboutSchema = z.infer;
@@ -161,10 +108,12 @@ export async function aiProcessAssistantChat({
messages,
emailAccountId,
user,
+ onFinish,
}: {
- messages: { role: "user" | "assistant"; content: string }[];
+ messages: Message[];
emailAccountId: string;
user: EmailAccountWithAI;
+ onFinish: (messages: any) => void;
}) {
const system = `You are an assistant that helps create and update rules to manage a user's inbox. Our platform is called Inbox Zero.
@@ -431,6 +380,9 @@ Examples:
onStepFinish: async ({ text, toolCalls }) => {
logger.trace("Step finished", { text, toolCalls });
},
+ onFinish: async (_text, result) => {
+ if (onFinish) onFinish(result);
+ },
maxSteps: 10,
tools: {
get_user_rules_and_settings: tool({
diff --git a/apps/web/utils/llms/index.ts b/apps/web/utils/llms/index.ts
index 1188163d1d..46e165e7d9 100644
--- a/apps/web/utils/llms/index.ts
+++ b/apps/web/utils/llms/index.ts
@@ -9,6 +9,8 @@ import {
RetryError,
streamText,
type StepResult,
+ smoothStream,
+ type Message,
} from "ai";
import { env } from "@/env";
import { saveAiUsage } from "@/utils/usage";
@@ -164,12 +166,12 @@ export async function chatCompletionStream({
useEconomyModel?: boolean;
system?: string;
prompt?: string;
- messages?: CoreMessage[];
+ messages?: Message[];
tools?: Record;
maxSteps?: number;
userEmail: string;
usageLabel: string;
- onFinish?: (text: string) => Promise;
+ onFinish?: (text: string, result: any) => Promise;
onStepFinish?: (
stepResult: StepResult>,
) => Promise;
@@ -188,6 +190,7 @@ export async function chatCompletionStream({
maxSteps,
providerOptions,
...commonOptions,
+ experimental_transform: smoothStream({ chunking: "word" }),
onStepFinish,
onFinish: async ({ usage, text }) => {
await saveAiUsage({
@@ -198,7 +201,7 @@ export async function chatCompletionStream({
label,
});
- if (onFinish) await onFinish(text);
+ if (onFinish) await onFinish(text, result);
},
});
From 5af54bb37e6382088d74a8c5b94f59164eaecde5 Mon Sep 17 00:00:00 2001
From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com>
Date: Wed, 21 May 2025 16:51:08 +0300
Subject: [PATCH 2/8] fix save message
---
apps/web/app/api/ai/summarise/controller.ts | 4 +-
apps/web/app/api/chat/route.ts | 125 +++++++++---------
.../migration.sql | 10 ++
apps/web/prisma/schema.prisma | 7 +-
apps/web/utils/ai/assistant/chat.ts | 13 +-
apps/web/utils/llms/index.ts | 10 +-
6 files changed, 88 insertions(+), 81 deletions(-)
create mode 100644 apps/web/prisma/migrations/20250521132820_message_parts/migration.sql
diff --git a/apps/web/app/api/ai/summarise/controller.ts b/apps/web/app/api/ai/summarise/controller.ts
index c05a8aaf32..0fd811b024 100644
--- a/apps/web/app/api/ai/summarise/controller.ts
+++ b/apps/web/app/api/ai/summarise/controller.ts
@@ -24,8 +24,8 @@ export async function summarise({
prompt,
userEmail,
usageLabel: "Summarise",
- onFinish: async (completion) => {
- await saveSummary(prompt, completion);
+ onFinish: async (result) => {
+ await saveSummary(prompt, result.text);
await expire(prompt, 60 * 60 * 24);
},
});
diff --git a/apps/web/app/api/chat/route.ts b/apps/web/app/api/chat/route.ts
index 32b88b83f3..afb34f03f8 100644
--- a/apps/web/app/api/chat/route.ts
+++ b/apps/web/app/api/chat/route.ts
@@ -1,3 +1,9 @@
+import {
+ appendClientMessage,
+ appendResponseMessages,
+ type CoreAssistantMessage,
+ type CoreToolMessage,
+} from "ai";
import { z } from "zod";
import { withEmailAccount } from "@/utils/middleware";
import { getEmailAccountWithAi } from "@/utils/user/get";
@@ -5,7 +11,7 @@ import { NextResponse } from "next/server";
import { aiProcessAssistantChat } from "@/utils/ai/assistant/chat";
import { createScopedLogger } from "@/utils/logger";
import prisma from "@/utils/prisma";
-import { appendClientMessage } from "ai";
+import { Prisma, type ChatMessage } from "@prisma/client";
export const maxDuration = 120;
@@ -68,10 +74,12 @@ export const POST = withEmailAccount(async (request) => {
}
const { message } = data;
- const mappedDbMessages = chat.messages.map((dbMsg) => {
+ const mappedDbMessages = chat.messages.map((dbMsg: ChatMessage) => {
return {
...dbMsg,
role: convertDbRoleToSdkRole(dbMsg.role),
+ content: "",
+ parts: dbMsg.parts as any,
};
});
@@ -80,14 +88,42 @@ export const POST = withEmailAccount(async (request) => {
message,
});
+ await saveChatMessage({
+ chat: { connect: { id: chat.id } },
+ id: message.id,
+ role: "user",
+ parts: message.parts,
+ // attachments: message.experimental_attachments ?? [],
+ });
+
const result = await aiProcessAssistantChat({
messages,
emailAccountId,
user,
- onFinish: (messages: any) => {
- saveChatMessages({
- messagesToSave: messages,
- chatId: chat.id,
+ onFinish: async ({ response }) => {
+ const assistantId = getTrailingMessageId({
+ messages: response.messages.filter(
+ (message: { role: string }) => message.role === "assistant",
+ ),
+ });
+
+ if (!assistantId) {
+ throw new Error("No assistant message found!");
+ }
+
+ const [, assistantMessage] = appendResponseMessages({
+ messages: [message],
+ responseMessages: response.messages,
+ });
+
+ await saveChatMessage({
+ id: assistantId,
+ chat: { connect: { id: chat.id } },
+ role: assistantMessage.role,
+ parts: assistantMessage.parts
+ ? (assistantMessage.parts as Prisma.InputJsonValue)
+ : Prisma.JsonNull,
+ // attachments: assistantMessage.experimental_attachments ?? [],
});
},
});
@@ -109,66 +145,8 @@ async function createNewChat(emailAccountId: string) {
}
}
-async function saveChatMessages({
- messagesToSave,
- chatId,
-}: {
- messagesToSave: {
- role: "user" | "assistant" | "system" | "tool";
- content: string;
- attachments?: any[];
- metadata?: Record;
- }[];
- chatId: string;
-}): Promise {
- if (!chatId) {
- logger.error("saveChatMessages called without a chatId.");
- return false;
- }
- if (!messagesToSave || messagesToSave.length === 0) {
- logger.warn("saveChatMessages called with no messages to save.", {
- chatId,
- });
- return true; // No messages to save is not an error in this context
- }
-
- try {
- const prismaMessagesData = messagesToSave.map((msg) => {
- const mappedRole = msg.role.toUpperCase();
-
- const attachmentsToSave =
- msg.attachments && msg.attachments.length > 0
- ? msg.attachments
- : undefined;
- const metadataToSave =
- msg.metadata && Object.keys(msg.metadata).length > 0
- ? msg.metadata
- : undefined;
-
- return {
- chatId: chatId,
- role: mappedRole,
- content: msg.content,
- attachments: attachmentsToSave,
- metadata: metadataToSave,
- };
- });
-
- await prisma.chatMessage.createMany({
- data: prismaMessagesData,
- skipDuplicates: false, // If a message might be re-processed, this could be true if unique IDs are part of input
- });
-
- logger.info("Successfully saved chat messages", { chatId });
- return true;
- } catch (error) {
- logger.error("Failed to save chat messages to DB", {
- error,
- chatId: chatId,
- numberOfMessages: messagesToSave.length,
- });
- return false;
- }
+async function saveChatMessage(message: Prisma.ChatMessageCreateInput) {
+ return prisma.chatMessage.create({ data: message });
}
async function getChatById(chatId: string) {
@@ -195,3 +173,18 @@ function convertDbRoleToSdkRole(
return "assistant";
}
}
+
+type ResponseMessageWithoutId = CoreToolMessage | CoreAssistantMessage;
+type ResponseMessage = ResponseMessageWithoutId & { id: string };
+
+function getTrailingMessageId({
+ messages,
+}: {
+ messages: Array;
+}): string | null {
+ const trailingMessage = messages.at(-1);
+
+ if (!trailingMessage) return null;
+
+ return trailingMessage.id;
+}
diff --git a/apps/web/prisma/migrations/20250521132820_message_parts/migration.sql b/apps/web/prisma/migrations/20250521132820_message_parts/migration.sql
new file mode 100644
index 0000000000..39562844b5
--- /dev/null
+++ b/apps/web/prisma/migrations/20250521132820_message_parts/migration.sql
@@ -0,0 +1,10 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `content` on the `ChatMessage` table. All the data in the column will be lost.
+ - Added the required column `parts` to the `ChatMessage` table without a default value. This is not possible if the table is not empty.
+
+*/
+-- AlterTable
+ALTER TABLE "ChatMessage" DROP COLUMN "content",
+ADD COLUMN "parts" JSONB NOT NULL;
diff --git a/apps/web/prisma/schema.prisma b/apps/web/prisma/schema.prisma
index bcc0c94218..dc9c4264d0 100644
--- a/apps/web/prisma/schema.prisma
+++ b/apps/web/prisma/schema.prisma
@@ -583,10 +583,9 @@ model ChatMessage {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
- role String
- content String
- // attachments Json? // For storing message attachments if any
- // metadata Json? // For any additional metadata, like tool calls
+ role String
+ parts Json
+ // attachments Json?
chatId String
chat Chat @relation(fields: [chatId], references: [id], onDelete: Cascade)
diff --git a/apps/web/utils/ai/assistant/chat.ts b/apps/web/utils/ai/assistant/chat.ts
index b72c72d6a6..4c27edd1ff 100644
--- a/apps/web/utils/ai/assistant/chat.ts
+++ b/apps/web/utils/ai/assistant/chat.ts
@@ -1,4 +1,4 @@
-import { type Message, tool } from "ai";
+import { type Message, type StepResult, type Tool, tool } from "ai";
import { z } from "zod";
import { createScopedLogger } from "@/utils/logger";
import { createRuleSchema } from "@/utils/ai/rule/create-rule-schema";
@@ -113,7 +113,12 @@ export async function aiProcessAssistantChat({
messages: Message[];
emailAccountId: string;
user: EmailAccountWithAI;
- onFinish: (messages: any) => void;
+ onFinish: (
+ response: Omit<
+ StepResult>,
+ "stepType" | "isContinued"
+ >,
+ ) => Promise;
}) {
const system = `You are an assistant that helps create and update rules to manage a user's inbox. Our platform is called Inbox Zero.
@@ -380,9 +385,7 @@ Examples:
onStepFinish: async ({ text, toolCalls }) => {
logger.trace("Step finished", { text, toolCalls });
},
- onFinish: async (_text, result) => {
- if (onFinish) onFinish(result);
- },
+ onFinish,
maxSteps: 10,
tools: {
get_user_rules_and_settings: tool({
diff --git a/apps/web/utils/llms/index.ts b/apps/web/utils/llms/index.ts
index 46e165e7d9..e52c306032 100644
--- a/apps/web/utils/llms/index.ts
+++ b/apps/web/utils/llms/index.ts
@@ -171,7 +171,9 @@ export async function chatCompletionStream({
maxSteps?: number;
userEmail: string;
usageLabel: string;
- onFinish?: (text: string, result: any) => Promise;
+ onFinish?: (
+ result: Omit>, "stepType" | "isContinued">,
+ ) => Promise;
onStepFinish?: (
stepResult: StepResult>,
) => Promise;
@@ -192,16 +194,16 @@ export async function chatCompletionStream({
...commonOptions,
experimental_transform: smoothStream({ chunking: "word" }),
onStepFinish,
- onFinish: async ({ usage, text }) => {
+ onFinish: async (result) => {
await saveAiUsage({
email: userEmail,
provider,
model,
- usage,
+ usage: result.usage,
label,
});
- if (onFinish) await onFinish(text, result);
+ if (onFinish) await onFinish(result);
},
});
From 19454814091c22864ef4466bd35978f1c166b69b Mon Sep 17 00:00:00 2001
From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com>
Date: Wed, 21 May 2025 16:52:19 +0300
Subject: [PATCH 3/8] fix schema
---
apps/web/app/api/chat/route.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/apps/web/app/api/chat/route.ts b/apps/web/app/api/chat/route.ts
index afb34f03f8..fe219389a0 100644
--- a/apps/web/app/api/chat/route.ts
+++ b/apps/web/app/api/chat/route.ts
@@ -22,7 +22,7 @@ const textPartSchema = z.object({
type: z.enum(["text"]),
});
-export const assistantInputSchema = z.object({
+const assistantInputSchema = z.object({
chatId: z.string().uuid(),
message: z.object({
id: z.string().uuid(),
@@ -111,6 +111,7 @@ export const POST = withEmailAccount(async (request) => {
throw new Error("No assistant message found!");
}
+ // handles all tool calls
const [, assistantMessage] = appendResponseMessages({
messages: [message],
responseMessages: response.messages,
From 25e3930f183af6176045a6034f022b5f54eb6f35 Mon Sep 17 00:00:00 2001
From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com>
Date: Thu, 22 May 2025 01:38:10 +0300
Subject: [PATCH 4/8] Chat fixes
---
.../(app)/[emailAccountId]/assistant/page.tsx | 6 +-----
apps/web/app/api/chat/route.ts | 11 +++++------
apps/web/components/assistant-chat/chat.tsx | 16 +++++++++++-----
.../assistant-chat/multimodal-input.tsx | 4 ++--
apps/web/hooks/useChats.ts | 2 +-
5 files changed, 20 insertions(+), 19 deletions(-)
diff --git a/apps/web/app/(app)/[emailAccountId]/assistant/page.tsx b/apps/web/app/(app)/[emailAccountId]/assistant/page.tsx
index 61c3482d71..d2301bf449 100644
--- a/apps/web/app/(app)/[emailAccountId]/assistant/page.tsx
+++ b/apps/web/app/(app)/[emailAccountId]/assistant/page.tsx
@@ -44,11 +44,7 @@ export default async function AssistantPage({
-
+
diff --git a/apps/web/app/api/chat/route.ts b/apps/web/app/api/chat/route.ts
index fe219389a0..2cc37124ab 100644
--- a/apps/web/app/api/chat/route.ts
+++ b/apps/web/app/api/chat/route.ts
@@ -23,9 +23,9 @@ const textPartSchema = z.object({
});
const assistantInputSchema = z.object({
- chatId: z.string().uuid(),
+ id: z.string().optional(),
message: z.object({
- id: z.string().uuid(),
+ id: z.string(),
createdAt: z.coerce.date(),
role: z.enum(["user"]),
content: z.string().min(1).max(2000),
@@ -52,11 +52,10 @@ export const POST = withEmailAccount(async (request) => {
const json = await request.json();
const { data, error } = assistantInputSchema.safeParse(json);
- if (error)
- return NextResponse.json({ error: error.message }, { status: 400 });
+ if (error) return NextResponse.json({ error: error.errors }, { status: 400 });
- const chat = data.chatId
- ? await getChatById(data.chatId)
+ const chat = data.id
+ ? await getChatById(data.id)
: await createNewChat(emailAccountId);
if (!chat) {
diff --git a/apps/web/components/assistant-chat/chat.tsx b/apps/web/components/assistant-chat/chat.tsx
index bd035fe0c8..ba8fa0ecd9 100644
--- a/apps/web/components/assistant-chat/chat.tsx
+++ b/apps/web/components/assistant-chat/chat.tsx
@@ -35,7 +35,7 @@ import { LoadingContent } from "@/components/LoadingContent";
// AI SDK v5 won't use SWR anymore so we can remove this workaround
type ChatProps = {
- id: string;
+ id?: string;
initialMessages: Array;
emailAccountId: string;
};
@@ -65,7 +65,11 @@ function ChatInner({
}) {
const chat = useChat({
id,
- body: { id },
+ api: "/api/chat",
+ experimental_prepareRequestBody: (body) => ({
+ id,
+ message: body.messages.at(-1),
+ }),
initialMessages,
experimental_throttle: 100,
sendExtraMessageFields: true,
@@ -78,7 +82,7 @@ function ChatInner({
},
onError: (error) => {
console.error(error);
- toast.error("An error occured, please try again!");
+ toast.error(`An error occured! ${error.message || ""}`);
},
});
@@ -105,7 +109,7 @@ function ChatUI({
chatId,
}: {
chat: ReturnType;
- chatId: string;
+ chatId?: string;
}) {
const {
messages,
@@ -121,7 +125,9 @@ function ChatUI({
return (
-
+
+
+
768) {
textareaRef.current?.focus();
}
- }, [handleSubmit, setLocalStorageInput, width, chatId]);
+ }, [handleSubmit, setLocalStorageInput, width]);
return (
diff --git a/apps/web/hooks/useChats.ts b/apps/web/hooks/useChats.ts
index 81b4badaf3..8d8a549f06 100644
--- a/apps/web/hooks/useChats.ts
+++ b/apps/web/hooks/useChats.ts
@@ -1,6 +1,6 @@
import useSWR from "swr";
import type { GetChatsResponse } from "@/app/api/chats/route";
-export function useChats(shouldFetch = true) {
+export function useChats(shouldFetch: boolean) {
return useSWR
(shouldFetch ? "/api/chats" : null);
}
From 9f1f99e20af3add35e825894b60610d67efbcbce Mon Sep 17 00:00:00 2001
From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com>
Date: Thu, 22 May 2025 01:43:27 +0300
Subject: [PATCH 5/8] reload chat list on click
---
apps/web/components/assistant-chat/chat.tsx | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/apps/web/components/assistant-chat/chat.tsx b/apps/web/components/assistant-chat/chat.tsx
index ba8fa0ecd9..25bb5bc5d3 100644
--- a/apps/web/components/assistant-chat/chat.tsx
+++ b/apps/web/components/assistant-chat/chat.tsx
@@ -175,7 +175,7 @@ function ChatUI({
function ChatHistoryDropdown() {
const [_chatId, setChatId] = useQueryState("chatId");
const [shouldLoadChats, setShouldLoadChats] = useState(false);
- const { data, error, isLoading } = useChats(shouldLoadChats);
+ const { data, error, isLoading, mutate } = useChats(shouldLoadChats);
return (
@@ -184,6 +184,7 @@ function ChatHistoryDropdown() {
variant="ghost"
size="icon"
onMouseEnter={() => setShouldLoadChats(true)}
+ onClick={() => mutate()}
>
Chat History
From b1eb0ec77ec0c3922a530506edf7e0d01a2ddff6 Mon Sep 17 00:00:00 2001
From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com>
Date: Thu, 22 May 2025 18:40:06 +0300
Subject: [PATCH 6/8] load old chats
---
.../(app)/[emailAccountId]/assistant/page.tsx | 3 +-
.../automation/AssistantTabs.tsx | 2 +-
apps/web/components/assistant-chat/chat.tsx | 38 +++++++++++++++----
.../hooks/{useChat.ts => useChatMessages.ts} | 2 +-
4 files changed, 35 insertions(+), 10 deletions(-)
rename apps/web/hooks/{useChat.ts => useChatMessages.ts} (77%)
diff --git a/apps/web/app/(app)/[emailAccountId]/assistant/page.tsx b/apps/web/app/(app)/[emailAccountId]/assistant/page.tsx
index d2301bf449..46c8b46907 100644
--- a/apps/web/app/(app)/[emailAccountId]/assistant/page.tsx
+++ b/apps/web/app/(app)/[emailAccountId]/assistant/page.tsx
@@ -33,6 +33,7 @@ export default async function AssistantPage({
}
}
+ // TODO:
// const hasPendingRule = prisma.rule.findFirst({
// where: { emailAccountId, automate: false },
// select: { id: true },
@@ -44,7 +45,7 @@ export default async function AssistantPage({
-
+
diff --git a/apps/web/app/(app)/[emailAccountId]/automation/AssistantTabs.tsx b/apps/web/app/(app)/[emailAccountId]/automation/AssistantTabs.tsx
index 3e446b912a..0157ea1894 100644
--- a/apps/web/app/(app)/[emailAccountId]/automation/AssistantTabs.tsx
+++ b/apps/web/app/(app)/[emailAccountId]/automation/AssistantTabs.tsx
@@ -56,7 +56,7 @@ export function AssistantTabs() {
-
+
Select a tab or chat with your AI assistant to explain how it
should handle incoming emails
diff --git a/apps/web/components/assistant-chat/chat.tsx b/apps/web/components/assistant-chat/chat.tsx
index 25bb5bc5d3..8955d16698 100644
--- a/apps/web/components/assistant-chat/chat.tsx
+++ b/apps/web/components/assistant-chat/chat.tsx
@@ -26,6 +26,8 @@ import {
} from "@/components/ui/dropdown-menu";
import { useChats } from "@/hooks/useChats";
import { LoadingContent } from "@/components/LoadingContent";
+import { useChatMessages } from "@/hooks/useChatMessages";
+import type { GetChatResponse } from "@/app/api/chats/[chatId]/route";
// Some mega hacky code used here to workaround AI SDK's use of SWR
// AI SDK uses SWR too and this messes with the global SWR config
@@ -35,8 +37,6 @@ import { LoadingContent } from "@/components/LoadingContent";
// AI SDK v5 won't use SWR anymore so we can remove this workaround
type ChatProps = {
- id?: string;
- initialMessages: Array;
emailAccountId: string;
};
@@ -44,30 +44,40 @@ export function Chat(props: ChatProps) {
// Use parent SWR config for mutate
const { mutate } = useSWRConfig();
+ const [chatId] = useQueryState("chatId");
+ const { data } = useChatMessages(chatId ?? undefined);
+
return (
-
+
);
}
function ChatInner({
- id,
+ chatId,
initialMessages,
emailAccountId,
mutate,
}: ChatProps & {
+ chatId?: string;
mutate: ScopedMutator;
+ initialMessages: Array;
}) {
const chat = useChat({
- id,
+ id: chatId,
api: "/api/chat",
experimental_prepareRequestBody: (body) => ({
- id,
+ id: chatId,
message: body.messages.at(-1),
}),
initialMessages,
@@ -90,7 +100,7 @@ function ChatInner({
-
+
@@ -237,3 +247,17 @@ function generateUUID(): string {
return v.toString(16);
});
}
+
+function convertToUIMessages(chat: GetChatResponse): Array {
+ return (
+ chat?.messages.map((message) => ({
+ id: message.id,
+ parts: message.parts as UIMessage["parts"],
+ role: message.role as UIMessage["role"],
+ // Note: content will soon be deprecated in @ai-sdk/react
+ content: "",
+ createdAt: message.createdAt,
+ // experimental_attachments: (message.attachments as Array) ?? [],
+ })) || []
+ );
+}
diff --git a/apps/web/hooks/useChat.ts b/apps/web/hooks/useChatMessages.ts
similarity index 77%
rename from apps/web/hooks/useChat.ts
rename to apps/web/hooks/useChatMessages.ts
index c9044d1608..d82f9adc8a 100644
--- a/apps/web/hooks/useChat.ts
+++ b/apps/web/hooks/useChatMessages.ts
@@ -1,6 +1,6 @@
import useSWR from "swr";
import type { GetChatResponse } from "@/app/api/chats/[chatId]/route";
-export function useChat(chatId?: string) {
+export function useChatMessages(chatId?: string) {
return useSWR(chatId ? `/api/chats/${chatId}` : null);
}
From 134766b54c3d3520135153b9479cea97e86714e8 Mon Sep 17 00:00:00 2001
From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com>
Date: Thu, 22 May 2025 18:53:43 +0300
Subject: [PATCH 7/8] Lazy load pending
---
.cursor/rules/get-api-route.mdc | 12 +++++----
.../(app)/[emailAccountId]/assistant/page.tsx | 6 -----
.../automation/AssistantTabs.tsx | 26 +++++++++++++------
.../[emailAccountId]/automation/page.tsx | 2 --
apps/web/app/api/rules/pending/route.ts | 23 ++++++++++++++++
5 files changed, 48 insertions(+), 21 deletions(-)
create mode 100644 apps/web/app/api/rules/pending/route.ts
diff --git a/.cursor/rules/get-api-route.mdc b/.cursor/rules/get-api-route.mdc
index 3c34a27a77..d61bde4755 100644
--- a/.cursor/rules/get-api-route.mdc
+++ b/.cursor/rules/get-api-route.mdc
@@ -14,12 +14,13 @@ import { NextResponse } from "next/server";
import prisma from "@/utils/prisma";
import { withAuth } from "@/utils/middleware";
+// Notice how we infer the response type. We don't need to manually type it out
export type GetExampleResponse = Awaited>;
-export const GET = withAuth(async () => {
+// The middleware does the error handling and authentication for us already
+export const GET = withEmailAccount(async () => {
const emailAccountId = request.auth.emailAccountId;
-
const result = getData({ email });
return NextResponse.json(result);
});
@@ -37,8 +38,9 @@ See [data-fetching.mdc](mdc:.cursor/rules/data-fetching.mdc) as to how this woul
Key Requirements:
- - Always wrap the handler with `withAuth` for consistent error handling and authentication.
- - We don't need try/catch as `withAuth` handles that.
- - Infer and export response type.
+ - Always wrap the handler with `withAuth` or `withEmailAccount` for consistent error handling and authentication.
+ - `withAuth` gets the user. `withEmailAccount` gets the currently active email account. A user can have multiple email accounts under it.
+ - We don't need try/catch as `withAuth` and `withEmailAccount` handles that.
+ - Infer and export response type as in the example.
- Use Prisma for database queries.
- Return responses using `NextResponse.json()`
diff --git a/apps/web/app/(app)/[emailAccountId]/assistant/page.tsx b/apps/web/app/(app)/[emailAccountId]/assistant/page.tsx
index 46c8b46907..e639d62e87 100644
--- a/apps/web/app/(app)/[emailAccountId]/assistant/page.tsx
+++ b/apps/web/app/(app)/[emailAccountId]/assistant/page.tsx
@@ -33,12 +33,6 @@ export default async function AssistantPage({
}
}
- // TODO:
- // const hasPendingRule = prisma.rule.findFirst({
- // where: { emailAccountId, automate: false },
- // select: { id: true },
- // });
-
return (
diff --git a/apps/web/app/(app)/[emailAccountId]/automation/AssistantTabs.tsx b/apps/web/app/(app)/[emailAccountId]/automation/AssistantTabs.tsx
index 0157ea1894..468e922ecc 100644
--- a/apps/web/app/(app)/[emailAccountId]/automation/AssistantTabs.tsx
+++ b/apps/web/app/(app)/[emailAccountId]/automation/AssistantTabs.tsx
@@ -1,3 +1,6 @@
+"use client";
+
+import useSWR from "swr";
import { History } from "@/app/(app)/[emailAccountId]/automation/History";
import { Pending } from "@/app/(app)/[emailAccountId]/automation/Pending";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
@@ -8,8 +11,14 @@ import { RulesPrompt } from "@/app/(app)/[emailAccountId]/automation/RulesPrompt
import { TabsToolbar } from "@/components/TabsToolbar";
import { TypographyP } from "@/components/Typography";
import { RuleTab } from "@/app/(app)/[emailAccountId]/automation/RuleTab";
+import type { GetPendingRulesResponse } from "@/app/api/rules/pending/route";
export function AssistantTabs() {
+ const { data: pendingData } =
+ useSWR("/api/rules/pending");
+
+ const hasPendingRule = pendingData?.hasPending ?? false;
+
return (
@@ -20,11 +29,9 @@ export function AssistantTabs() {
Rules
Test
History
- {/*
- {(await hasPendingRule) && (
- Pending
- )}
- */}
+ {hasPendingRule && (
+ Pending
+ )}
Knowledge Base
@@ -75,12 +82,15 @@ export function AssistantTabs() {
-
-
-
+ {hasPendingRule && (
+
+
+
+ )}
+ {/* Set via search params. Not a visible tab. */}
diff --git a/apps/web/app/(app)/[emailAccountId]/automation/page.tsx b/apps/web/app/(app)/[emailAccountId]/automation/page.tsx
index 7e53c1574b..754b54e6c7 100644
--- a/apps/web/app/(app)/[emailAccountId]/automation/page.tsx
+++ b/apps/web/app/(app)/[emailAccountId]/automation/page.tsx
@@ -1,5 +1,4 @@
import { Suspense } from "react";
-import Link from "next/link";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import prisma from "@/utils/prisma";
@@ -15,7 +14,6 @@ import { PermissionsCheck } from "@/app/(app)/[emailAccountId]/PermissionsCheck"
import { TabsToolbar } from "@/components/TabsToolbar";
import { GmailProvider } from "@/providers/GmailProvider";
import { ASSISTANT_ONBOARDING_COOKIE } from "@/utils/cookies";
-import { Button } from "@/components/ui/button";
import { prefixPath } from "@/utils/path";
export const maxDuration = 300; // Applies to the actions
diff --git a/apps/web/app/api/rules/pending/route.ts b/apps/web/app/api/rules/pending/route.ts
new file mode 100644
index 0000000000..5a7266a5be
--- /dev/null
+++ b/apps/web/app/api/rules/pending/route.ts
@@ -0,0 +1,23 @@
+import { NextResponse } from "next/server";
+import prisma from "@/utils/prisma";
+import { withEmailAccount } from "@/utils/middleware";
+import type { RequestWithEmailAccount } from "@/utils/middleware";
+
+export type GetPendingRulesResponse = Awaited<
+ ReturnType
+>;
+
+export const GET = withEmailAccount(async (req: RequestWithEmailAccount) => {
+ const { emailAccountId } = req.auth;
+ const data = await getPendingRules({ emailAccountId });
+ return NextResponse.json(data);
+});
+
+async function getPendingRules({ emailAccountId }: { emailAccountId: string }) {
+ const rule = await prisma.rule.findFirst({
+ where: { emailAccountId, automate: false },
+ select: { id: true },
+ });
+
+ return { hasPending: Boolean(rule) };
+}
From 132bdf9e7b9d101a3b072d83318cd774a72151aa Mon Sep 17 00:00:00 2001
From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com>
Date: Thu, 22 May 2025 18:54:11 +0300
Subject: [PATCH 8/8] delete old rule
---
.cursor/rules/memory.mdc | 69 ----------------------------------------
1 file changed, 69 deletions(-)
delete mode 100644 .cursor/rules/memory.mdc
diff --git a/.cursor/rules/memory.mdc b/.cursor/rules/memory.mdc
deleted file mode 100644
index 0e6f72ee9c..0000000000
--- a/.cursor/rules/memory.mdc
+++ /dev/null
@@ -1,69 +0,0 @@
----
-description:
-globs:
-alwaysApply: true
----
-# AI Memory Rule
-
-This rule defines how the AI should manage and utilize its "memory" regarding this specific project, including user preferences, learned facts, and project-specific conventions.
-
-## Purpose
-
-The AI's memory helps maintain consistency and adapt to specific project needs or user preferences discovered during interactions. It prevents the AI from repeatedly asking for the same information or making suggestions contrary to established patterns.
-
-## Storage
-
-All learned project-specific knowledge and preferences should be stored and referenced in the `learned-memories.mdc` file located in `.cursor/rules`.
-
-## Updating Memory
-
-When new information relevant to the project's conventions, user preferences, or specific technical details is learned (either explicitly told by the user or inferred through conversation), the AI should:
-
-1. **Identify Key Information:** Determine the core piece of knowledge to be stored.
-2. **Check Existing Memory:** Review `learned-memories.mdc` to see if this information contradicts or updates existing entries.
-3. **Propose Update:** Suggest an edit to `learned-memories.mdc` to add or modify the relevant information. Keep entries concise and clear.
-
-## Using Memory
-
-Before proposing solutions, code changes, or answering questions, the AI should consult `learned-memories.mdc` to ensure its response aligns with the recorded knowledge and preferences.
-
-## Example Scenario
-
-**User:** "We've decided to use Tailwind v4 for this project, not v3."
-
-**AI Action:**
-
-1. Recognize this as a project-specific technical decision.
-2. Check `learned-memories.mdc` for existing Tailwind version information.
-3. Propose adding or updating an entry in `learned-memories.mdc`:
- ```markdown
- ## Technical Decisions
-
- * **CSS Framework:** Tailwind v4 is used. Ensure usage aligns with v4 documentation and practices, noting differences from v3.
- ```
-4. In subsequent interactions involving Tailwind, the AI will refer to this entry and consult v4 documentation if necessary.
-
-## Memory File (`.cursor/rules/learned-memories.mdc`)
-
-The basic structure:
-
-```markdown
-# Project Memory
-
-This file stores project-specific knowledge, conventions, and user preferences learned by the AI assistant.
-
-## User Preferences
-
-- [Preference 1]
-- [Preference 2]
-
-## Technical Decisions
-
-- [Decision 1]
-- [Decision 2]
-
-## Project Conventions
-
-- [Convention 1]
-- [Convention 2]
-```