@@ -203,12 +204,7 @@ function ExtraActions({ emailAccountId }: { emailAccountId: string }) {
buttonProps={{ size: "sm", variant: "ghost" }}
/>
-
+
);
}
diff --git a/apps/web/app/(app)/[emailAccountId]/simple/Summary.tsx b/apps/web/app/(app)/[emailAccountId]/simple/Summary.tsx
index 074494fd2c..ae51dc1e3e 100644
--- a/apps/web/app/(app)/[emailAccountId]/simple/Summary.tsx
+++ b/apps/web/app/(app)/[emailAccountId]/simple/Summary.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useCompletion } from "ai/react";
+import { useCompletion } from "@ai-sdk/react";
import { useEffect } from "react";
import { ButtonLoader } from "@/components/Loading";
import { ViewMoreButton } from "@/app/(app)/[emailAccountId]/simple/ViewMoreButton";
diff --git a/apps/web/app/(app)/[emailAccountId]/usage/usage.tsx b/apps/web/app/(app)/[emailAccountId]/usage/usage.tsx
index 939efe7fbe..6f29089cb0 100644
--- a/apps/web/app/(app)/[emailAccountId]/usage/usage.tsx
+++ b/apps/web/app/(app)/[emailAccountId]/usage/usage.tsx
@@ -7,13 +7,9 @@ import { usePremium } from "@/components/PremiumAlert";
import { LoadingContent } from "@/components/LoadingContent";
import { env } from "@/env";
import { isPremium } from "@/utils/premium";
+import type { RedisUsage } from "@/utils/redis/usage";
-export function Usage(props: {
- usage?: {
- openaiCalls: number;
- openaiTokensUsed: number;
- } | null;
-}) {
+export function Usage(props: { usage: RedisUsage | null }) {
const { premium, isLoading, error } = usePremium();
return (
diff --git a/apps/web/app/(app)/layout.tsx b/apps/web/app/(app)/layout.tsx
index 4ad8131c2f..364dabf19e 100644
--- a/apps/web/app/(app)/layout.tsx
+++ b/apps/web/app/(app)/layout.tsx
@@ -2,7 +2,6 @@ import "../../styles/globals.css";
import type React from "react";
import { cookies } from "next/headers";
import { Suspense } from "react";
-import dynamic from "next/dynamic";
import { redirect } from "next/navigation";
import { SideNavWithTopNav } from "@/components/SideNavWithTopNav";
import { TokenCheck } from "@/components/TokenCheck";
@@ -40,7 +39,7 @@ export default async function AppLayout({
if (!session?.user.email) redirect("/login");
const cookieStore = await cookies();
- const isClosed = cookieStore.get("sidebar_state")?.value === "false";
+ const isClosed = cookieStore.get("left-sidebar:state")?.value === "false";
return (
@@ -59,12 +58,12 @@ export default async function AppLayout({
-
+ {/*
-
+ */}
);
}
-const CrispWithNoSSR = dynamic(() => import("@/components/CrispChat"));
+// const CrispWithNoSSR = dynamic(() => import("@/components/CrispChat"));
diff --git a/apps/web/app/(marketing) b/apps/web/app/(marketing)
index 2f5ed3aa7e..dc789dc5a0 160000
--- a/apps/web/app/(marketing)
+++ b/apps/web/app/(marketing)
@@ -1 +1 @@
-Subproject commit 2f5ed3aa7e6fe6c51233910e7ff27b98819a620c
+Subproject commit dc789dc5a0a7b772c61eb22da7383ad9cd5b839a
diff --git a/apps/web/app/api/chat/route.ts b/apps/web/app/api/chat/route.ts
index 2e31a23131..b95868994d 100644
--- a/apps/web/app/api/chat/route.ts
+++ b/apps/web/app/api/chat/route.ts
@@ -1,4 +1,4 @@
-import { appendClientMessage, appendResponseMessages } from "ai";
+import { convertToModelMessages, type UIMessage } from "ai";
import { z } from "zod";
import { withEmailAccount } from "@/utils/middleware";
import { getEmailAccountWithAi } from "@/utils/user/get";
@@ -6,7 +6,9 @@ import { NextResponse } from "next/server";
import { aiProcessAssistantChat } from "@/utils/ai/assistant/chat";
import { createScopedLogger } from "@/utils/logger";
import prisma from "@/utils/prisma";
-import { Prisma, type ChatMessage } from "@prisma/client";
+import type { Prisma } from "@prisma/client";
+import { convertToUIMessages } from "@/components/assistant-chat/helpers";
+import { captureException } from "@/utils/error";
export const maxDuration = 120;
@@ -21,19 +23,8 @@ const assistantInputSchema = z.object({
id: z.string(),
message: z.object({
id: z.string(),
- createdAt: z.coerce.date(),
role: z.enum(["user"]),
- content: z.string().min(1).max(3000),
parts: z.array(textPartSchema),
- // experimental_attachments: z
- // .array(
- // z.object({
- // url: z.string().url(),
- // name: z.string().min(1).max(100),
- // contentType: z.enum(["image/png", "image/jpg", "image/jpeg"]),
- // }),
- // )
- // .optional(),
}),
});
@@ -49,7 +40,6 @@ export const POST = withEmailAccount(async (request) => {
if (error) return NextResponse.json({ error: error.errors }, { status: 400 });
- // create chat if it doesn't exist
const chat =
(await getChatById(data.id)) ||
(await createNewChat({ emailAccountId, chatId: data.id }));
@@ -69,63 +59,27 @@ export const POST = withEmailAccount(async (request) => {
}
const { message } = data;
- const mappedDbMessages = chat.messages.map((dbMsg: ChatMessage) => {
- return {
- ...dbMsg,
- role: convertDbRoleToSdkRole(dbMsg.role),
- content: "",
- parts: dbMsg.parts as any,
- };
- });
-
- const messages = appendClientMessage({
- messages: mappedDbMessages,
- message,
- });
+ const uiMessages = [...convertToUIMessages(chat), message];
await saveChatMessage({
chat: { connect: { id: chat.id } },
id: message.id,
role: "user",
parts: message.parts,
- // attachments: message.experimental_attachments ?? [],
});
try {
const result = await aiProcessAssistantChat({
- messages,
+ messages: convertToModelMessages(uiMessages),
emailAccountId,
user,
- onFinish: async ({ response }) => {
- const assistantMessages = response.messages.filter(
- (message) => message.role === "assistant",
- );
- const assistantId = getTrailingMessageId(assistantMessages);
-
- if (!assistantId) {
- logger.error("No assistant message found!", { response });
- throw new Error("No assistant message found!");
- }
-
- // handles all tool calls
- 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 ?? [],
- });
- },
});
- return result.toDataStreamResponse();
+ return result.toUIMessageStreamResponse({
+ onFinish: async ({ messages }) => {
+ await saveChatMessages(messages, chat.id);
+ },
+ });
} catch (error) {
logger.error("Error in assistant chat", { error });
return NextResponse.json(
@@ -155,10 +109,6 @@ async function createNewChat({
}
}
-async function saveChatMessage(message: Prisma.ChatMessageCreateInput) {
- return prisma.chatMessage.create({ data: message });
-}
-
async function getChatById(chatId: string) {
const chat = await prisma.chat.findUnique({
where: { id: chatId },
@@ -167,29 +117,22 @@ async function getChatById(chatId: string) {
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";
- }
+async function saveChatMessage(message: Prisma.ChatMessageCreateInput) {
+ return prisma.chatMessage.create({ data: message });
}
-function getTrailingMessageId
(
- messages: Array,
-): string | null {
- const trailingMessage = messages.at(-1);
-
- if (!trailingMessage) return null;
-
- return trailingMessage.id;
+async function saveChatMessages(messages: UIMessage[], chatId: string) {
+ try {
+ return prisma.chatMessage.createMany({
+ data: messages.map((message) => ({
+ chatId,
+ role: message.role,
+ parts: message.parts as Prisma.InputJsonValue,
+ })),
+ });
+ } catch (error) {
+ logger.error("Failed to save chat messages", { error, chatId });
+ captureException(error, { extra: { chatId } });
+ throw error;
+ }
}
diff --git a/apps/web/app/api/lemon-squeezy/webhook/route.ts b/apps/web/app/api/lemon-squeezy/webhook/route.ts
index 5929272659..8707d4d03a 100644
--- a/apps/web/app/api/lemon-squeezy/webhook/route.ts
+++ b/apps/web/app/api/lemon-squeezy/webhook/route.ts
@@ -286,7 +286,7 @@ async function subscriptionUpdated({
}
async function subscriptionCancelled({
- payload,
+ payload: _payload,
premiumId,
endsAt,
variantId,
diff --git a/apps/web/components/AccountSwitcher.tsx b/apps/web/components/AccountSwitcher.tsx
index 7d8bb11068..54849f3d65 100644
--- a/apps/web/components/AccountSwitcher.tsx
+++ b/apps/web/components/AccountSwitcher.tsx
@@ -20,7 +20,6 @@ import {
} from "@/components/ui/sidebar";
import { useAccounts } from "@/hooks/useAccounts";
import type { GetEmailAccountsResponse } from "@/app/api/user/email-accounts/route";
-import { useModifierKey } from "@/hooks/useModifierKey";
import { useAccount } from "@/providers/EmailAccountProvider";
import { ProfileImage } from "@/components/ProfileImage";
@@ -38,7 +37,6 @@ export function AccountSwitcherInternal({
emailAccounts: GetEmailAccountsResponse["emailAccounts"];
}) {
const { isMobile } = useSidebar();
- const { symbol: modifierSymbol } = useModifierKey();
const {
emailAccountId: activeEmailAccountId,
@@ -76,6 +74,7 @@ export function AccountSwitcherInternal({
{activeEmailAccount ? (
<>
diff --git a/apps/web/components/Combobox.tsx b/apps/web/components/Combobox.tsx
index 49d604f930..e4b9d6863a 100644
--- a/apps/web/components/Combobox.tsx
+++ b/apps/web/components/Combobox.tsx
@@ -38,7 +38,6 @@ export function Combobox(props: {